package run

import (
	"context"
	"log/slog"
	"path/filepath"
	"strings"
	"time"

	"github.com/google/uuid"

	"github.com/yan1h/agent-coding-workflow/internal/audit"
	"github.com/yan1h/agent-coding-workflow/internal/infra/crypto"
	"github.com/yan1h/agent-coding-workflow/internal/infra/errs"
	procgrp "github.com/yan1h/agent-coding-workflow/internal/infra/proc"
	"github.com/yan1h/agent-coding-workflow/internal/workspace"
)

// ExecConfig 是 ExecService 的运行参数（派生自 config.RunConfig）。
type ExecConfig struct {
	EnvWhitelist    []string
	DefaultTimeout  time.Duration
	MaxTimeout      time.Duration
	MaxOutputBytes  int
	KillGrace       time.Duration
	AllowedCommands []string // 空 = 显式不限制命令；生产环境应保持非空白名单
}

// ExecService 提供 agent 自验证用的一次性 exec 原语：RunCommand / RunTests。
// 与 run profile（长驻进程）共享 workspace 鉴权（要求 WRITE，因为 exec 会修改工作树）、
// env 白名单（绝不泄漏 APP_MASTER_KEY / DB DSN / git 凭据）、命令白名单与 proc 树终止。
//
// 无状态、同步：每次调用执行一条命令直至退出/超时后返回，不持有运行态，
// 因此无需 ShutdownAll 注册——超时与 ctx 取消由 proc.RunOnce 内部处理。
type ExecService struct {
	wsRepo workspace.Repository
	wtSvc  workspace.WorktreeService
	pa     workspace.ProjectAccess
	rec    audit.Recorder
	enc    *crypto.Encryptor
	cfg    ExecConfig
	log    *slog.Logger
}

// NewExecService 构造 ExecService。enc 当前未用于 exec（请求 env 覆盖是临时明文），
// 保留参数以与 run.Service 装配签名一致并便于将来落库加密。
func NewExecService(
	wsRepo workspace.Repository,
	wtSvc workspace.WorktreeService,
	pa workspace.ProjectAccess,
	rec audit.Recorder,
	enc *crypto.Encryptor,
	cfg ExecConfig,
	log *slog.Logger,
) *ExecService {
	if log == nil {
		log = slog.Default()
	}
	if cfg.DefaultTimeout <= 0 {
		cfg.DefaultTimeout = 60 * time.Second
	}
	if cfg.MaxTimeout <= 0 {
		cfg.MaxTimeout = 600 * time.Second
	}
	if cfg.MaxOutputBytes <= 0 {
		cfg.MaxOutputBytes = 1 << 20
	}
	return &ExecService{wsRepo: wsRepo, wtSvc: wtSvc, pa: pa, rec: rec, enc: enc, cfg: cfg, log: log}
}

// RunCommand 在指定 workspace（main_path 或某 worktree）执行一条命令并返回结构化结果。
func (s *ExecService) RunCommand(ctx context.Context, c workspace.Caller, req ExecCommandReq) (ExecCommandResp, error) {
	spec, _, err := s.prepare(ctx, c, req.WorkspaceID, req.WorktreeID, req.Command, req.Args, req.Env, req.TimeoutSec)
	if err != nil {
		return ExecCommandResp{}, err
	}
	res, err := s.run(ctx, c, "exec.run_command", req.WorkspaceID, req.WorktreeID, req.Command, spec)
	if err != nil {
		return ExecCommandResp{}, err
	}
	return res, nil
}

// RunTests 执行测试命令并把 stdout 解析为结构化摘要。解析失败时填 ParseError 而非报错，
// 始终返回原始输出与退出码——agent 永远能拿到可用信息。
func (s *ExecService) RunTests(ctx context.Context, c workspace.Caller, req ExecTestsReq) (ExecTestsResp, error) {
	spec, _, err := s.prepare(ctx, c, req.WorkspaceID, req.WorktreeID, req.Command, req.Args, req.Env, req.TimeoutSec)
	if err != nil {
		return ExecTestsResp{}, err
	}
	cmdRes, err := s.run(ctx, c, "exec.run_tests", req.WorkspaceID, req.WorktreeID, req.Command, spec)
	if err != nil {
		return ExecTestsResp{}, err
	}
	resp := ExecTestsResp{ExecCommandResp: cmdRes}
	summary, perr := parseTestOutput(req.Format, cmdRes.Stdout, cmdRes.Stderr)
	if perr != nil {
		resp.ParseError = perr.Error()
	}
	resp.Summary = summary
	return resp, nil
}

// prepare 做命令/超时校验、鉴权、cwd 与 env 解析，产出可直接交给 proc.RunOnce 的 spec。
func (s *ExecService) prepare(
	ctx context.Context,
	c workspace.Caller,
	workspaceID, worktreeID, command string,
	args []string,
	envOverrides map[string]string,
	timeoutSec int,
) (procgrp.ExecSpec, *workspace.Workspace, error) {
	if strings.TrimSpace(command) == "" {
		return procgrp.ExecSpec{}, nil, errs.New(errs.CodeRunCommandInvalid, "command must not be empty")
	}
	if err := s.checkAllowed(command); err != nil {
		return procgrp.ExecSpec{}, nil, err
	}
	wsID, err := uuid.Parse(workspaceID)
	if err != nil {
		return procgrp.ExecSpec{}, nil, errs.Wrap(err, errs.CodeInvalidInput, "workspace_id parse")
	}

	ws, err := s.wsRepo.GetWorkspaceByID(ctx, wsID)
	if err != nil {
		return procgrp.ExecSpec{}, nil, err
	}
	_, canWrite, err := s.pa.ResolveByID(ctx, c.UserID, c.IsAdmin, ws.ProjectID)
	if err != nil {
		return procgrp.ExecSpec{}, nil, err
	}
	if !canWrite {
		return procgrp.ExecSpec{}, nil, errs.New(errs.CodeForbidden, "no write access to this workspace")
	}

	dir, err := s.resolveCwd(ctx, c, wsID, ws, worktreeID)
	if err != nil {
		return procgrp.ExecSpec{}, nil, err
	}

	spec := procgrp.ExecSpec{
		Command:        command,
		Args:           args,
		Dir:            dir,
		Env:            buildEnvFromWhitelist(s.cfg.EnvWhitelist, envOverrides),
		Timeout:        s.clampTimeout(timeoutSec),
		KillGrace:      s.cfg.KillGrace,
		MaxOutputBytes: s.cfg.MaxOutputBytes,
	}
	return spec, ws, nil
}

// run 执行 spec 并写审计，把 proc 结果折成 ExecCommandResp。超时映射为 run.exec_timeout。
func (s *ExecService) run(
	ctx context.Context,
	c workspace.Caller,
	action, workspaceID, worktreeID, command string,
	spec procgrp.ExecSpec,
) (ExecCommandResp, error) {
	res, err := procgrp.RunOnce(ctx, spec)
	if err != nil {
		return ExecCommandResp{}, errs.Wrap(err, errs.CodeRunExecFailed, "exec command")
	}
	resp := ExecCommandResp{
		ExitCode:   res.ExitCode,
		Stdout:     res.Stdout,
		Stderr:     res.Stderr,
		DurationMs: res.Duration.Milliseconds(),
		TimedOut:   res.TimedOut,
		Truncated:  res.Truncated,
	}
	s.audit(ctx, c, action, workspaceID, map[string]any{
		"workspace_id": workspaceID,
		"worktree_id":  worktreeID,
		"command":      command,
		"exit_code":    res.ExitCode,
		"duration_ms":  resp.DurationMs,
		"timed_out":    res.TimedOut,
	})
	return resp, nil
}

// checkAllowed 在 AllowedCommands 非空时要求命令 base name（去掉路径与 .exe 后缀）在白名单内。
func (s *ExecService) checkAllowed(command string) error {
	if len(s.cfg.AllowedCommands) == 0 {
		return nil
	}
	base := strings.ToLower(filepath.Base(command))
	base = strings.TrimSuffix(base, ".exe")
	for _, a := range s.cfg.AllowedCommands {
		if strings.ToLower(strings.TrimSuffix(a, ".exe")) == base {
			return nil
		}
	}
	return errs.New(errs.CodeRunExecCommandNotAllowed, "command not in allowed list: "+base)
}

// clampTimeout 把请求超时夹到 [1s, MaxTimeout]；0/负 → DefaultTimeout。
func (s *ExecService) clampTimeout(sec int) time.Duration {
	if sec <= 0 {
		return s.cfg.DefaultTimeout
	}
	d := time.Duration(sec) * time.Second
	if d < time.Second {
		d = time.Second
	}
	if d > s.cfg.MaxTimeout {
		d = s.cfg.MaxTimeout
	}
	return d
}

// resolveCwd 解析执行目录：worktreeID 非空 → 校验归属并返回其 Path；否则 main_path。
func (s *ExecService) resolveCwd(ctx context.Context, c workspace.Caller, wsID uuid.UUID, ws *workspace.Workspace, worktreeID string) (string, error) {
	if strings.TrimSpace(worktreeID) == "" {
		return ws.MainPath, nil
	}
	wtID, err := uuid.Parse(worktreeID)
	if err != nil {
		return "", errs.Wrap(err, errs.CodeInvalidInput, "worktree_id parse")
	}
	wts, err := s.wtSvc.List(ctx, c, wsID)
	if err != nil {
		return "", err
	}
	for _, wt := range wts {
		if wt.ID == wtID {
			return wt.Path, nil
		}
	}
	return "", errs.New(errs.CodeNotFound, "worktree not found in workspace")
}

func (s *ExecService) audit(ctx context.Context, c workspace.Caller, action, targetID string, meta map[string]any) {
	if s.rec == nil {
		return
	}
	uid := c.UserID
	_ = s.rec.Record(ctx, audit.Entry{
		UserID:     &uid,
		Action:     action,
		TargetType: "workspace",
		TargetID:   targetID,
		Metadata:   meta,
	})
}
