package mcp

import (
	"context"

	"github.com/google/uuid"
	mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp"

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

// RunService 是 run profile 工具依赖的窄接口，*run.Service 直接满足；
// 接口化便于单测注入 fake。
type RunService interface {
	List(ctx context.Context, c workspace.Caller, wsID uuid.UUID) ([]run.RunProfileResp, error)
	Create(ctx context.Context, c workspace.Caller, wsID uuid.UUID, req run.CreateRunProfileReq) (run.RunProfileResp, error)
	Update(ctx context.Context, c workspace.Caller, profileID uuid.UUID, req run.UpdateRunProfileReq) (run.RunProfileResp, error)
	Delete(ctx context.Context, c workspace.Caller, profileID uuid.UUID) error
	Start(ctx context.Context, c workspace.Caller, profileID uuid.UUID) (run.RunStatusResp, error)
	Stop(ctx context.Context, c workspace.Caller, profileID uuid.UUID) (run.RunStatusResp, error)
	Restart(ctx context.Context, c workspace.Caller, profileID uuid.UUID) (run.RunStatusResp, error)
	Status(ctx context.Context, c workspace.Caller, profileID uuid.UUID) (run.RunStatusResp, error)
	Logs(ctx context.Context, c workspace.Caller, profileID uuid.UUID, since int64, limit int) ([]run.LogLineResp, error)
}

// ExecService 是一次性 exec 工具依赖的窄接口，*run.ExecService 直接满足。
type ExecService interface {
	RunCommand(ctx context.Context, c workspace.Caller, req run.ExecCommandReq) (run.ExecCommandResp, error)
	RunTests(ctx context.Context, c workspace.Caller, req run.ExecTestsReq) (run.ExecTestsResp, error)
}

// registerRunTools 注册 run profile 工具。日志只提供快照式 get_run_logs，
// 流式订阅（WebSocket）不经 MCP 暴露。
//
// 项目 scope 校验策略与 worktree 工具一致：profile 级操作要求同时给
// workspace_id，先按 workspace 做 scope 校验，再用 List 校验 profile 归属。
func registerRunTools(srv *mcpsdk.Server, deps ServerDeps) {
	registerListRunProfiles(srv, deps)
	registerCreateRunProfile(srv, deps)
	registerUpdateRunProfile(srv, deps)
	registerDeleteRunProfile(srv, deps)
	registerStartRunProfile(srv, deps)
	registerStopRunProfile(srv, deps)
	registerRestartRunProfile(srv, deps)
	registerGetRunStatus(srv, deps)
	registerGetRunLogs(srv, deps)
	registerRunCommand(srv, deps)
	registerRunTests(srv, deps)
}

// runProfileInWorkspace 校验 profile 归属于给定 workspace 并返回其 UUID。
func runProfileInWorkspace(ctx context.Context, deps ServerDeps, wc workspace.Caller, wsID uuid.UUID, profileID string) (uuid.UUID, error) {
	pid, err := uuid.Parse(profileID)
	if err != nil {
		return uuid.Nil, errs.Wrap(err, errs.CodeInvalidInput, "profile_id parse")
	}
	ps, err := deps.Runs.List(ctx, wc, wsID)
	if err != nil {
		return uuid.Nil, err
	}
	for _, p := range ps {
		if p.ID == pid.String() {
			return pid, nil
		}
	}
	return uuid.Nil, errs.New(errs.CodeNotFound, "run profile not found in workspace")
}

// ===== list_run_profiles =====

type listRunProfilesIn struct {
	WorkspaceID string `json:"workspace_id" jsonschema:"workspace UUID"`
}
type listRunProfilesOut struct {
	Profiles []run.RunProfileResp `json:"profiles"`
}

func registerListRunProfiles(srv *mcpsdk.Server, deps ServerDeps) {
	mcpsdk.AddTool(srv,
		&mcpsdk.Tool{Name: "list_run_profiles", Description: "List run profiles of a workspace."},
		func(ctx context.Context, _ *mcpsdk.CallToolRequest, in listRunProfilesIn) (*mcpsdk.CallToolResult, listRunProfilesOut, error) {
			if err := CheckToolFromCtx(ctx, "list_run_profiles"); err != nil {
				return nil, listRunProfilesOut{}, err
			}
			c, err := deps.Caller.Resolve(ctx)
			if err != nil {
				return nil, listRunProfilesOut{}, err
			}
			w, wc, err := scopedWorkspace(ctx, deps, c, in.WorkspaceID)
			if err != nil {
				return nil, listRunProfilesOut{}, err
			}
			ps, err := deps.Runs.List(ctx, wc, w.ID)
			if err != nil {
				return nil, listRunProfilesOut{}, err
			}
			return nil, listRunProfilesOut{Profiles: ps}, nil
		})
}

// ===== create_run_profile =====

type createRunProfileIn struct {
	WorkspaceID string            `json:"workspace_id" jsonschema:"workspace UUID"`
	Slug        string            `json:"slug" jsonschema:"profile slug (non-empty)"`
	Name        string            `json:"name" jsonschema:"display name (non-empty)"`
	Description string            `json:"description,omitempty"`
	Command     string            `json:"command" jsonschema:"command line to run (non-empty)"`
	Args        []string          `json:"args,omitempty"`
	Env         map[string]string `json:"env,omitempty" jsonschema:"environment overrides (encrypted at rest, never returned)"`
	Enabled     *bool             `json:"enabled,omitempty" jsonschema:"default true"`
}
type runProfileOut struct {
	OK      bool               `json:"ok"`
	Profile run.RunProfileResp `json:"profile"`
}

func registerCreateRunProfile(srv *mcpsdk.Server, deps ServerDeps) {
	mcpsdk.AddTool(srv,
		&mcpsdk.Tool{Name: "create_run_profile", Description: "Create a run profile in a workspace."},
		func(ctx context.Context, _ *mcpsdk.CallToolRequest, in createRunProfileIn) (*mcpsdk.CallToolResult, runProfileOut, error) {
			if err := CheckToolFromCtx(ctx, "create_run_profile"); err != nil {
				return nil, runProfileOut{}, err
			}
			c, err := deps.Caller.Resolve(ctx)
			if err != nil {
				return nil, runProfileOut{}, err
			}
			w, wc, err := scopedWorkspace(ctx, deps, c, in.WorkspaceID)
			if err != nil {
				return nil, runProfileOut{}, err
			}
			p, err := deps.Runs.Create(ctx, wc, w.ID, run.CreateRunProfileReq{
				Slug: in.Slug, Name: in.Name, Description: in.Description,
				Command: in.Command, Args: in.Args, Env: in.Env, Enabled: in.Enabled,
			})
			if err != nil {
				return nil, runProfileOut{}, err
			}
			return nil, runProfileOut{OK: true, Profile: p}, nil
		})
}

// ===== update_run_profile =====

type updateRunProfileIn struct {
	WorkspaceID string             `json:"workspace_id" jsonschema:"workspace UUID"`
	ProfileID   string             `json:"profile_id" jsonschema:"run profile UUID"`
	Slug        *string            `json:"slug,omitempty"`
	Name        *string            `json:"name,omitempty"`
	Description *string            `json:"description,omitempty"`
	Command     *string            `json:"command,omitempty"`
	Args        *[]string          `json:"args,omitempty"`
	Env         *map[string]string `json:"env,omitempty" jsonschema:"full replacement of env overrides"`
	Enabled     *bool              `json:"enabled,omitempty"`
}

func registerUpdateRunProfile(srv *mcpsdk.Server, deps ServerDeps) {
	mcpsdk.AddTool(srv,
		&mcpsdk.Tool{Name: "update_run_profile", Description: "Update a run profile (patch semantics: omitted fields keep current values)."},
		func(ctx context.Context, _ *mcpsdk.CallToolRequest, in updateRunProfileIn) (*mcpsdk.CallToolResult, runProfileOut, error) {
			if err := CheckToolFromCtx(ctx, "update_run_profile"); err != nil {
				return nil, runProfileOut{}, err
			}
			c, err := deps.Caller.Resolve(ctx)
			if err != nil {
				return nil, runProfileOut{}, err
			}
			w, wc, err := scopedWorkspace(ctx, deps, c, in.WorkspaceID)
			if err != nil {
				return nil, runProfileOut{}, err
			}
			pid, err := runProfileInWorkspace(ctx, deps, wc, w.ID, in.ProfileID)
			if err != nil {
				return nil, runProfileOut{}, err
			}
			p, err := deps.Runs.Update(ctx, wc, pid, run.UpdateRunProfileReq{
				Slug: in.Slug, Name: in.Name, Description: in.Description,
				Command: in.Command, Args: in.Args, Env: in.Env, Enabled: in.Enabled,
			})
			if err != nil {
				return nil, runProfileOut{}, err
			}
			return nil, runProfileOut{OK: true, Profile: p}, nil
		})
}

// ===== delete_run_profile =====

type runProfileTargetIn struct {
	WorkspaceID string `json:"workspace_id" jsonschema:"workspace UUID"`
	ProfileID   string `json:"profile_id" jsonschema:"run profile UUID"`
}

func registerDeleteRunProfile(srv *mcpsdk.Server, deps ServerDeps) {
	mcpsdk.AddTool(srv,
		&mcpsdk.Tool{Name: "delete_run_profile", Description: "Delete a run profile. Irreversible."},
		func(ctx context.Context, _ *mcpsdk.CallToolRequest, in runProfileTargetIn) (*mcpsdk.CallToolResult, okOut, error) {
			if err := CheckToolFromCtx(ctx, "delete_run_profile"); err != nil {
				return nil, okOut{}, err
			}
			c, err := deps.Caller.Resolve(ctx)
			if err != nil {
				return nil, okOut{}, err
			}
			w, wc, err := scopedWorkspace(ctx, deps, c, in.WorkspaceID)
			if err != nil {
				return nil, okOut{}, err
			}
			pid, err := runProfileInWorkspace(ctx, deps, wc, w.ID, in.ProfileID)
			if err != nil {
				return nil, okOut{}, err
			}
			if err := deps.Runs.Delete(ctx, wc, pid); err != nil {
				return nil, okOut{}, err
			}
			return nil, okOut{OK: true}, nil
		})
}

// ===== start/stop/restart/status =====

type runStatusOut struct {
	OK     bool              `json:"ok"`
	Status run.RunStatusResp `json:"status"`
}

// registerRunLifecycleTool 抽取 start/stop/restart/status 四个同构工具的注册逻辑。
func registerRunLifecycleTool(srv *mcpsdk.Server, deps ServerDeps, name, desc string,
	op func(context.Context, workspace.Caller, uuid.UUID) (run.RunStatusResp, error)) {
	mcpsdk.AddTool(srv,
		&mcpsdk.Tool{Name: name, Description: desc},
		func(ctx context.Context, _ *mcpsdk.CallToolRequest, in runProfileTargetIn) (*mcpsdk.CallToolResult, runStatusOut, error) {
			if err := CheckToolFromCtx(ctx, name); err != nil {
				return nil, runStatusOut{}, err
			}
			c, err := deps.Caller.Resolve(ctx)
			if err != nil {
				return nil, runStatusOut{}, err
			}
			w, wc, err := scopedWorkspace(ctx, deps, c, in.WorkspaceID)
			if err != nil {
				return nil, runStatusOut{}, err
			}
			pid, err := runProfileInWorkspace(ctx, deps, wc, w.ID, in.ProfileID)
			if err != nil {
				return nil, runStatusOut{}, err
			}
			st, err := op(ctx, wc, pid)
			if err != nil {
				return nil, runStatusOut{}, err
			}
			return nil, runStatusOut{OK: true, Status: st}, nil
		})
}

// 注意：op 不能写成 deps.Runs.Start 这类 method value——注册期对 nil 接口取
// 方法值会 panic（deps.Runs 在部分单测装配里为空），必须用闭包延迟到调用期。

func registerStartRunProfile(srv *mcpsdk.Server, deps ServerDeps) {
	registerRunLifecycleTool(srv, deps, "start_run_profile", "Start the process of a run profile.",
		func(ctx context.Context, wc workspace.Caller, pid uuid.UUID) (run.RunStatusResp, error) {
			return deps.Runs.Start(ctx, wc, pid)
		})
}

func registerStopRunProfile(srv *mcpsdk.Server, deps ServerDeps) {
	registerRunLifecycleTool(srv, deps, "stop_run_profile", "Stop the process of a run profile.",
		func(ctx context.Context, wc workspace.Caller, pid uuid.UUID) (run.RunStatusResp, error) {
			return deps.Runs.Stop(ctx, wc, pid)
		})
}

func registerRestartRunProfile(srv *mcpsdk.Server, deps ServerDeps) {
	registerRunLifecycleTool(srv, deps, "restart_run_profile", "Restart the process of a run profile.",
		func(ctx context.Context, wc workspace.Caller, pid uuid.UUID) (run.RunStatusResp, error) {
			return deps.Runs.Restart(ctx, wc, pid)
		})
}

func registerGetRunStatus(srv *mcpsdk.Server, deps ServerDeps) {
	registerRunLifecycleTool(srv, deps, "get_run_status", "Get the live status of a run profile.",
		func(ctx context.Context, wc workspace.Caller, pid uuid.UUID) (run.RunStatusResp, error) {
			return deps.Runs.Status(ctx, wc, pid)
		})
}

// ===== get_run_logs =====

type getRunLogsIn struct {
	WorkspaceID string `json:"workspace_id" jsonschema:"workspace UUID"`
	ProfileID   string `json:"profile_id" jsonschema:"run profile UUID"`
	Since       int64  `json:"since,omitempty" jsonschema:"return log lines with ID > this value"`
	Limit       int    `json:"limit,omitempty" jsonschema:"max lines to return (default 200, max 1000)"`
}
type getRunLogsOut struct {
	Lines []run.LogLineResp `json:"lines"`
}

func registerGetRunLogs(srv *mcpsdk.Server, deps ServerDeps) {
	mcpsdk.AddTool(srv,
		&mcpsdk.Tool{Name: "get_run_logs", Description: "Get a snapshot of recent log lines of a run profile (no streaming)."},
		func(ctx context.Context, _ *mcpsdk.CallToolRequest, in getRunLogsIn) (*mcpsdk.CallToolResult, getRunLogsOut, error) {
			if err := CheckToolFromCtx(ctx, "get_run_logs"); err != nil {
				return nil, getRunLogsOut{}, err
			}
			c, err := deps.Caller.Resolve(ctx)
			if err != nil {
				return nil, getRunLogsOut{}, err
			}
			w, wc, err := scopedWorkspace(ctx, deps, c, in.WorkspaceID)
			if err != nil {
				return nil, getRunLogsOut{}, err
			}
			pid, err := runProfileInWorkspace(ctx, deps, wc, w.ID, in.ProfileID)
			if err != nil {
				return nil, getRunLogsOut{}, err
			}
			limit := in.Limit
			if limit <= 0 {
				limit = 200
			}
			if limit > 1000 {
				limit = 1000
			}
			lines, err := deps.Runs.Logs(ctx, wc, pid, in.Since, limit)
			if err != nil {
				return nil, getRunLogsOut{}, err
			}
			return nil, getRunLogsOut{Lines: lines}, nil
		})
}

// ===== run_command =====

type runCommandIn struct {
	WorkspaceID string            `json:"workspace_id" jsonschema:"workspace UUID"`
	WorktreeID  string            `json:"worktree_id,omitempty" jsonschema:"worktree UUID; empty runs in workspace main_path"`
	Command     string            `json:"command" jsonschema:"executable to run, e.g. go / npm (non-empty)"`
	Args        []string          `json:"args,omitempty" jsonschema:"command arguments"`
	Env         map[string]string `json:"env,omitempty" jsonschema:"ephemeral env overrides (plaintext, not persisted)"`
	TimeoutSec  int               `json:"timeout_sec,omitempty" jsonschema:"timeout in seconds; clamped to server max"`
}

func registerRunCommand(srv *mcpsdk.Server, deps ServerDeps) {
	mcpsdk.AddTool(srv,
		&mcpsdk.Tool{Name: "run_command", Description: "Run one command to completion in the workspace (main_path or a worktree) and return exit code, stdout, stderr, duration. Sandboxed: only whitelisted env vars are passed; requires project write access."},
		func(ctx context.Context, _ *mcpsdk.CallToolRequest, in runCommandIn) (*mcpsdk.CallToolResult, run.ExecCommandResp, error) {
			if err := CheckToolFromCtx(ctx, "run_command"); err != nil {
				return nil, run.ExecCommandResp{}, err
			}
			c, err := deps.Caller.Resolve(ctx)
			if err != nil {
				return nil, run.ExecCommandResp{}, err
			}
			w, wc, err := scopedWorkspace(ctx, deps, c, in.WorkspaceID)
			if err != nil {
				return nil, run.ExecCommandResp{}, err
			}
			if in.WorktreeID != "" {
				if _, err := worktreeInWorkspace(ctx, deps, wc, w.ID, in.WorktreeID); err != nil {
					return nil, run.ExecCommandResp{}, err
				}
			}
			resp, err := deps.Execs.RunCommand(ctx, wc, run.ExecCommandReq{
				WorkspaceID: w.ID.String(),
				WorktreeID:  in.WorktreeID,
				Command:     in.Command,
				Args:        in.Args,
				Env:         in.Env,
				TimeoutSec:  in.TimeoutSec,
			})
			if err != nil {
				return nil, run.ExecCommandResp{}, err
			}
			return nil, resp, nil
		})
}

// ===== run_tests =====

type runTestsIn struct {
	WorkspaceID string            `json:"workspace_id" jsonschema:"workspace UUID"`
	WorktreeID  string            `json:"worktree_id,omitempty" jsonschema:"worktree UUID; empty runs in workspace main_path"`
	Command     string            `json:"command" jsonschema:"test runner, e.g. go (non-empty)"`
	Args        []string          `json:"args,omitempty" jsonschema:"args, e.g. ['test','-json','./...']"`
	Format      string            `json:"format,omitempty" jsonschema:"output format: gotest|junit|vitest|auto (default auto)"`
	Env         map[string]string `json:"env,omitempty" jsonschema:"ephemeral env overrides (plaintext, not persisted)"`
	TimeoutSec  int               `json:"timeout_sec,omitempty" jsonschema:"timeout in seconds; clamped to server max"`
}

func registerRunTests(srv *mcpsdk.Server, deps ServerDeps) {
	mcpsdk.AddTool(srv,
		&mcpsdk.Tool{Name: "run_tests", Description: "Run a test command and parse its output (go test -json / JUnit XML / vitest JSON) into a structured pass/fail summary. Returns raw output plus a summary; never fails on parse errors."},
		func(ctx context.Context, _ *mcpsdk.CallToolRequest, in runTestsIn) (*mcpsdk.CallToolResult, run.ExecTestsResp, error) {
			if err := CheckToolFromCtx(ctx, "run_tests"); err != nil {
				return nil, run.ExecTestsResp{}, err
			}
			c, err := deps.Caller.Resolve(ctx)
			if err != nil {
				return nil, run.ExecTestsResp{}, err
			}
			w, wc, err := scopedWorkspace(ctx, deps, c, in.WorkspaceID)
			if err != nil {
				return nil, run.ExecTestsResp{}, err
			}
			if in.WorktreeID != "" {
				if _, err := worktreeInWorkspace(ctx, deps, wc, w.ID, in.WorktreeID); err != nil {
					return nil, run.ExecTestsResp{}, err
				}
			}
			resp, err := deps.Execs.RunTests(ctx, wc, run.ExecTestsReq{
				WorkspaceID: w.ID.String(),
				WorktreeID:  in.WorktreeID,
				Command:     in.Command,
				Args:        in.Args,
				Format:      in.Format,
				Env:         in.Env,
				TimeoutSec:  in.TimeoutSec,
			})
			if err != nil {
				return nil, run.ExecTestsResp{}, err
			}
			return nil, resp, nil
		})
}
