// agentkind_configfiles.go 实现 AgentKind 配置文件管理（admin only）。
//
// 配置文件是 agent CLI 自身的配置（如 .claude/settings.json、.codex/config.toml），
// rel_path 相对受管 home 目录，spawn 前由 supervisor 物化到
// <agent_homes_dir>/<agent_kind_id>/ 并通过 CLAUDE_CONFIG_DIR / CODEX_HOME /
// GEMINI_CLI_HOME / HOME 指过去。
//
// 与 env 的安全模型差异（有意为之）：env 加密后永不出 API；配置文件内容同样
// 加密存储（可能含 auth token），但明文返回给 admin —— 否则无法在 Web 端编辑。
package acp

import (
	"context"
	"encoding/json"
	"path"
	"strings"

	"github.com/google/uuid"
	toml "github.com/pelletier/go-toml/v2"

	"github.com/yan1h/agent-coding-workflow/internal/infra/errs"
)

// maxConfigFileSize 是单个配置文件明文上限（字节）。
const maxConfigFileSize = 256 << 10

func (s *agentKindService) ListConfigFiles(ctx context.Context, c Caller, kindID uuid.UUID) ([]*ConfigFileContent, error) {
	if !c.IsAdmin {
		return nil, errs.New(errs.CodeForbidden, "admin only")
	}
	if _, err := s.repo.GetAgentKindByID(ctx, kindID); err != nil {
		return nil, err
	}
	files, err := s.repo.ListConfigFiles(ctx, kindID)
	if err != nil {
		return nil, err
	}
	out := make([]*ConfigFileContent, 0, len(files))
	for _, f := range files {
		plain, derr := s.enc.Decrypt(f.EncryptedContent)
		if derr != nil {
			return nil, errs.Wrap(derr, errs.CodeInternal, "decrypt config file")
		}
		out = append(out, &ConfigFileContent{RelPath: f.RelPath, Content: string(plain), UpdatedAt: f.UpdatedAt})
	}
	return out, nil
}

func (s *agentKindService) UpsertConfigFile(ctx context.Context, c Caller, kindID uuid.UUID, relPath, content string) (*ConfigFileContent, error) {
	if !c.IsAdmin {
		return nil, errs.New(errs.CodeForbidden, "admin only")
	}
	if _, err := s.repo.GetAgentKindByID(ctx, kindID); err != nil {
		return nil, err
	}
	clean, err := normalizeConfigRelPath(relPath)
	if err != nil {
		return nil, err
	}
	if len(content) > maxConfigFileSize {
		return nil, errs.New(errs.CodeInvalidInput, "config file too large (max 256KB)")
	}
	if err := validateConfigSyntax(clean, content); err != nil {
		return nil, err
	}
	encrypted, err := s.enc.Encrypt([]byte(content))
	if err != nil {
		return nil, errs.Wrap(err, errs.CodeInternal, "encrypt config file")
	}
	out, err := s.repo.UpsertConfigFile(ctx, &ConfigFile{
		ID: uuid.New(), AgentKindID: kindID, RelPath: clean,
		EncryptedContent: encrypted, UpdatedBy: c.UserID,
	})
	if err != nil {
		return nil, err
	}
	s.recordAudit(ctx, c, "acp.agent_kind.config_file.upsert", kindID.String(),
		map[string]any{"rel_path": clean, "size": len(content)})
	return &ConfigFileContent{RelPath: out.RelPath, Content: content, UpdatedAt: out.UpdatedAt}, nil
}

func (s *agentKindService) DeleteConfigFile(ctx context.Context, c Caller, kindID uuid.UUID, relPath string) error {
	if !c.IsAdmin {
		return errs.New(errs.CodeForbidden, "admin only")
	}
	clean, err := normalizeConfigRelPath(relPath)
	if err != nil {
		return err
	}
	found, err := s.repo.DeleteConfigFile(ctx, kindID, clean)
	if err != nil {
		return err
	}
	if !found {
		return errs.New(errs.CodeNotFound, "config file not found")
	}
	s.recordAudit(ctx, c, "acp.agent_kind.config_file.delete", kindID.String(),
		map[string]any{"rel_path": clean})
	return nil
}

// normalizeConfigRelPath 归一并校验 rel_path：统一正斜杠、path.Clean 后必须仍是
// 受管目录内的相对路径（拒绝绝对路径、盘符与 ".." 逃逸）。返回归一形式作为存储键。
func normalizeConfigRelPath(p string) (string, error) {
	p = strings.TrimSpace(strings.ReplaceAll(p, "\\", "/"))
	if p == "" || len(p) > 512 {
		return "", errs.New(errs.CodeInvalidInput, "rel_path required (max 512 chars)")
	}
	if strings.HasPrefix(p, "/") || strings.Contains(p, ":") {
		return "", errs.New(errs.CodeInvalidInput, "rel_path must be relative")
	}
	clean := path.Clean(p)
	if clean == "." || clean == ".." || strings.HasPrefix(clean, "../") {
		return "", errs.New(errs.CodeInvalidInput, "rel_path escapes managed directory")
	}
	return clean, nil
}

// validateConfigSyntax 按扩展名做语法校验。未识别的扩展名不校验。
func validateConfigSyntax(relPath, content string) error {
	switch strings.ToLower(path.Ext(relPath)) {
	case ".json":
		if !json.Valid([]byte(content)) {
			return errs.New(errs.CodeInvalidInput, "invalid JSON content")
		}
	case ".toml":
		var v any
		if err := toml.Unmarshal([]byte(content), &v); err != nil {
			return errs.New(errs.CodeInvalidInput, "invalid TOML content: "+err.Error())
		}
	}
	return nil
}
