package mcp

import (
	"context"
	"fmt"

	"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/project"
	"github.com/yan1h/agent-coding-workflow/internal/workspace"
)

func registerPMTools(srv *mcpsdk.Server, deps ServerDeps) {
	registerListProjects(srv, deps)
	registerCreateProject(srv, deps)
	registerUpdateProject(srv, deps)
	registerArchiveProject(srv, deps)
	registerUnarchiveProject(srv, deps)
	registerListWorkspaces(srv, deps)
	registerListRequirements(srv, deps)
	registerListIssues(srv, deps)
	registerGetIssue(srv, deps)
	registerGetRequirement(srv, deps)
	registerCreateIssue(srv, deps)
	registerCreateSubtask(srv, deps)
	registerAddDependency(srv, deps)
	registerRemoveDependency(srv, deps)
	registerListDependencies(srv, deps)
	registerSubmitDecomposition(srv, deps)
	registerUpdateIssue(srv, deps)
	registerCloseIssue(srv, deps)
	registerReopenIssue(srv, deps)
	registerAssignIssue(srv, deps)
	registerCreateRequirement(srv, deps)
	registerUpdateRequirement(srv, deps)
	registerCloseRequirement(srv, deps)
	registerReopenRequirement(srv, deps)
	registerSetRequirementPhase(srv, deps)
	registerAddRequirementArtifact(srv, deps)
	registerApproveRequirementArtifact(srv, deps)
	registerListRequirementArtifacts(srv, deps)
}

// ===== list_projects =====

type listProjectsIn struct {
	Archived bool `json:"archived,omitempty" jsonschema:"include archived projects"`
}
type projectSummary struct {
	Slug       string `json:"slug"`
	Name       string `json:"name"`
	Visibility string `json:"visibility"`
}
type listProjectsOut struct {
	Projects []projectSummary `json:"projects"`
}

func registerListProjects(srv *mcpsdk.Server, deps ServerDeps) {
	mcpsdk.AddTool(srv,
		&mcpsdk.Tool{Name: "list_projects", Description: "List projects visible to the caller."},
		func(ctx context.Context, _ *mcpsdk.CallToolRequest, in listProjectsIn) (*mcpsdk.CallToolResult, listProjectsOut, error) {
			if err := CheckToolFromCtx(ctx, "list_projects"); err != nil {
				return nil, listProjectsOut{}, err
			}
			c, err := deps.Caller.Resolve(ctx)
			if err != nil {
				return nil, listProjectsOut{}, err
			}
			ps, err := deps.Projects.List(ctx, c, in.Archived)
			if err != nil {
				return nil, listProjectsOut{}, err
			}
			out := listProjectsOut{Projects: make([]projectSummary, 0, len(ps))}
			for _, p := range ps {
				if err := CheckProjectFromCtx(ctx, p.ID); err != nil {
					continue
				}
				out.Projects = append(out.Projects, projectSummary{
					Slug: p.Slug, Name: p.Name, Visibility: string(p.Visibility),
				})
			}
			return nil, out, nil
		})
}

// ===== list_workspaces =====

type listWorkspacesIn struct {
	ProjectSlug string `json:"project_slug" jsonschema:"project slug"`
}
type workspaceSummary struct {
	ID            string `json:"id"`
	Slug          string `json:"slug"`
	Name          string `json:"name"`
	DefaultBranch string `json:"default_branch"`
	SyncStatus    string `json:"sync_status"`
}
type listWorkspacesOut struct {
	Workspaces []workspaceSummary `json:"workspaces"`
}

func registerListWorkspaces(srv *mcpsdk.Server, deps ServerDeps) {
	mcpsdk.AddTool(srv,
		&mcpsdk.Tool{Name: "list_workspaces", Description: "List workspaces in a project."},
		func(ctx context.Context, _ *mcpsdk.CallToolRequest, in listWorkspacesIn) (*mcpsdk.CallToolResult, listWorkspacesOut, error) {
			if err := CheckToolFromCtx(ctx, "list_workspaces"); err != nil {
				return nil, listWorkspacesOut{}, err
			}
			c, err := deps.Caller.Resolve(ctx)
			if err != nil {
				return nil, listWorkspacesOut{}, err
			}
			p, err := deps.Projects.Get(ctx, c, in.ProjectSlug)
			if err != nil {
				return nil, listWorkspacesOut{}, err
			}
			if err := CheckProjectFromCtx(ctx, p.ID); err != nil {
				return nil, listWorkspacesOut{}, err
			}
			wsCaller := workspace.Caller{UserID: c.UserID, IsAdmin: c.IsAdmin}
			ws, err := deps.Workspaces.List(ctx, wsCaller, in.ProjectSlug)
			if err != nil {
				return nil, listWorkspacesOut{}, err
			}
			out := listWorkspacesOut{Workspaces: make([]workspaceSummary, 0, len(ws))}
			for _, w := range ws {
				out.Workspaces = append(out.Workspaces, workspaceSummary{
					ID: w.ID.String(), Slug: w.Slug, Name: w.Name,
					DefaultBranch: w.DefaultBranch, SyncStatus: string(w.SyncStatus),
				})
			}
			return nil, out, nil
		})
}

// ===== list_requirements =====

type listRequirementsIn struct {
	ProjectSlug string `json:"project_slug" jsonschema:"required field"`
	Phase       string `json:"phase,omitempty"`
	Status      string `json:"status,omitempty"`
}
type requirementSummary struct {
	Number int    `json:"number"`
	Title  string `json:"title"`
	Phase  string `json:"phase"`
	Status string `json:"status"`
}
type listRequirementsOut struct {
	Requirements []requirementSummary `json:"requirements"`
}

func registerListRequirements(srv *mcpsdk.Server, deps ServerDeps) {
	mcpsdk.AddTool(srv,
		&mcpsdk.Tool{Name: "list_requirements", Description: "List requirements in a project."},
		func(ctx context.Context, _ *mcpsdk.CallToolRequest, in listRequirementsIn) (*mcpsdk.CallToolResult, listRequirementsOut, error) {
			if err := CheckToolFromCtx(ctx, "list_requirements"); err != nil {
				return nil, listRequirementsOut{}, err
			}
			c, err := deps.Caller.Resolve(ctx)
			if err != nil {
				return nil, listRequirementsOut{}, err
			}
			p, err := deps.Projects.Get(ctx, c, in.ProjectSlug)
			if err != nil {
				return nil, listRequirementsOut{}, err
			}
			if err := CheckProjectFromCtx(ctx, p.ID); err != nil {
				return nil, listRequirementsOut{}, err
			}
			rs, err := deps.Reqs.List(ctx, c, in.ProjectSlug, project.RequirementFilter{
				Phase: project.Phase(in.Phase), Status: project.Status(in.Status),
			})
			if err != nil {
				return nil, listRequirementsOut{}, err
			}
			out := listRequirementsOut{Requirements: make([]requirementSummary, 0, len(rs))}
			for _, r := range rs {
				out.Requirements = append(out.Requirements, requirementSummary{
					Number: r.Number, Title: r.Title,
					Phase: string(r.Phase), Status: string(r.Status),
				})
			}
			return nil, out, nil
		})
}

// ===== list_issues =====

type listIssuesIn struct {
	ProjectSlug       string `json:"project_slug" jsonschema:"required field"`
	RequirementNumber any    `json:"requirement_number,omitempty"`
	Status            string `json:"status,omitempty"`
	AssigneeID        string `json:"assignee_id,omitempty"`
}
type issueSummary struct {
	Number int    `json:"number"`
	Title  string `json:"title"`
	Status string `json:"status"`
}
type listIssuesOut struct {
	Issues []issueSummary `json:"issues"`
}

func registerListIssues(srv *mcpsdk.Server, deps ServerDeps) {
	mcpsdk.AddTool(srv,
		&mcpsdk.Tool{Name: "list_issues", Description: "List issues in a project."},
		func(ctx context.Context, _ *mcpsdk.CallToolRequest, in listIssuesIn) (*mcpsdk.CallToolResult, listIssuesOut, error) {
			if err := CheckToolFromCtx(ctx, "list_issues"); err != nil {
				return nil, listIssuesOut{}, err
			}
			c, err := deps.Caller.Resolve(ctx)
			if err != nil {
				return nil, listIssuesOut{}, err
			}
			p, err := deps.Projects.Get(ctx, c, in.ProjectSlug)
			if err != nil {
				return nil, listIssuesOut{}, err
			}
			if err := CheckProjectFromCtx(ctx, p.ID); err != nil {
				return nil, listIssuesOut{}, err
			}
			f := project.IssueFilter{Status: project.Status(in.Status)}
			switch v := in.RequirementNumber.(type) {
			case nil:
			case string:
				if v == "none" {
					zero := 0
					f.Requirement = &zero
				}
			case float64:
				n := int(v)
				f.Requirement = &n
			}
			if in.AssigneeID != "" {
				aid, err := uuid.Parse(in.AssigneeID)
				if err != nil {
					return nil, listIssuesOut{}, errs.Wrap(err, errs.CodeInvalidInput, "assignee_id parse")
				}
				f.AssigneeID = &aid
			}
			is, err := deps.Issues.List(ctx, c, in.ProjectSlug, f)
			if err != nil {
				return nil, listIssuesOut{}, err
			}
			out := listIssuesOut{Issues: make([]issueSummary, 0, len(is))}
			for _, i := range is {
				out.Issues = append(out.Issues, issueSummary{
					Number: i.Number, Title: i.Title, Status: string(i.Status),
				})
			}
			return nil, out, nil
		})
}

// ===== get_issue =====

type getIssueIn struct {
	ProjectSlug string `json:"project_slug" jsonschema:"required field"`
	Number      int    `json:"number" jsonschema:"issue number (>=1)"`
}
type issueDetail struct {
	Number      int    `json:"number"`
	Title       string `json:"title"`
	Description string `json:"description,omitempty"`
	Status      string `json:"status"`
	CreatedAt   string `json:"created_at"`
}

func registerGetIssue(srv *mcpsdk.Server, deps ServerDeps) {
	mcpsdk.AddTool(srv,
		&mcpsdk.Tool{Name: "get_issue", Description: "Get a single issue by project_slug + number."},
		func(ctx context.Context, _ *mcpsdk.CallToolRequest, in getIssueIn) (*mcpsdk.CallToolResult, issueDetail, error) {
			if err := CheckToolFromCtx(ctx, "get_issue"); err != nil {
				return nil, issueDetail{}, err
			}
			c, err := deps.Caller.Resolve(ctx)
			if err != nil {
				return nil, issueDetail{}, err
			}
			p, err := deps.Projects.Get(ctx, c, in.ProjectSlug)
			if err != nil {
				return nil, issueDetail{}, err
			}
			if err := CheckProjectFromCtx(ctx, p.ID); err != nil {
				return nil, issueDetail{}, err
			}
			i, err := deps.Issues.Get(ctx, c, in.ProjectSlug, in.Number)
			if err != nil {
				return nil, issueDetail{}, err
			}
			return nil, issueDetail{
				Number: i.Number, Title: i.Title, Description: i.Description,
				Status: string(i.Status), CreatedAt: i.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
			}, nil
		})
}

// ===== get_requirement =====

type getRequirementIn struct {
	ProjectSlug string `json:"project_slug" jsonschema:"required field"`
	Number      int    `json:"number" jsonschema:"issue number (>=1)"`
}
type requirementDetail struct {
	Number      int    `json:"number"`
	Title       string `json:"title"`
	Description string `json:"description,omitempty"`
	Phase       string `json:"phase"`
	Status      string `json:"status"`
	CreatedAt   string `json:"created_at"`
}

func registerGetRequirement(srv *mcpsdk.Server, deps ServerDeps) {
	mcpsdk.AddTool(srv,
		&mcpsdk.Tool{Name: "get_requirement", Description: "Get a single requirement by project_slug + number."},
		func(ctx context.Context, _ *mcpsdk.CallToolRequest, in getRequirementIn) (*mcpsdk.CallToolResult, requirementDetail, error) {
			if err := CheckToolFromCtx(ctx, "get_requirement"); err != nil {
				return nil, requirementDetail{}, err
			}
			c, err := deps.Caller.Resolve(ctx)
			if err != nil {
				return nil, requirementDetail{}, err
			}
			p, err := deps.Projects.Get(ctx, c, in.ProjectSlug)
			if err != nil {
				return nil, requirementDetail{}, err
			}
			if err := CheckProjectFromCtx(ctx, p.ID); err != nil {
				return nil, requirementDetail{}, err
			}
			r, err := deps.Reqs.Get(ctx, c, in.ProjectSlug, in.Number)
			if err != nil {
				return nil, requirementDetail{}, err
			}
			return nil, requirementDetail{
				Number: r.Number, Title: r.Title, Description: r.Description,
				Phase: string(r.Phase), Status: string(r.Status),
				CreatedAt: r.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
			}, nil
		})
}

// ===== create_issue =====

type createIssueIn struct {
	ProjectSlug string `json:"project_slug" jsonschema:"required field"`
	Title       string `json:"title" jsonschema:"title (non-empty)"`
	Description string `json:"description,omitempty"`
}
type issueCreateOut struct {
	OK    bool        `json:"ok"`
	Issue issueDetail `json:"issue"`
}

func registerCreateIssue(srv *mcpsdk.Server, deps ServerDeps) {
	mcpsdk.AddTool(srv,
		&mcpsdk.Tool{Name: "create_issue", Description: "Create a new issue in a project."},
		func(ctx context.Context, _ *mcpsdk.CallToolRequest, in createIssueIn) (*mcpsdk.CallToolResult, issueCreateOut, error) {
			if err := CheckToolFromCtx(ctx, "create_issue"); err != nil {
				return nil, issueCreateOut{}, err
			}
			c, err := deps.Caller.Resolve(ctx)
			if err != nil {
				return nil, issueCreateOut{}, err
			}
			p, err := deps.Projects.Get(ctx, c, in.ProjectSlug)
			if err != nil {
				return nil, issueCreateOut{}, err
			}
			if err := CheckProjectFromCtx(ctx, p.ID); err != nil {
				return nil, issueCreateOut{}, err
			}
			i, err := deps.Issues.Create(ctx, c, in.ProjectSlug, project.CreateIssueInput{
				Title: in.Title, Description: in.Description,
			})
			if err != nil {
				return nil, issueCreateOut{}, err
			}
			return nil, issueCreateOut{OK: true, Issue: issueDetail{
				Number: i.Number, Title: i.Title, Description: i.Description,
				Status: string(i.Status), CreatedAt: i.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
			}}, nil
		})
}

// ===== create_subtask =====

type createSubtaskIn struct {
	ProjectSlug  string `json:"project_slug" jsonschema:"required field"`
	ParentNumber int    `json:"parent_number" jsonschema:"parent issue number (>=1)"`
	Title        string `json:"title" jsonschema:"title (non-empty)"`
	Description  string `json:"description,omitempty"`
	Priority     int    `json:"priority,omitempty" jsonschema:"scheduling priority 0..3 (higher = sooner)"`
}

func registerCreateSubtask(srv *mcpsdk.Server, deps ServerDeps) {
	mcpsdk.AddTool(srv,
		&mcpsdk.Tool{Name: "create_subtask", Description: "Create a subtask under a parent issue (task decomposition). Sets parent_id + priority."},
		func(ctx context.Context, _ *mcpsdk.CallToolRequest, in createSubtaskIn) (*mcpsdk.CallToolResult, issueCreateOut, error) {
			if err := CheckToolFromCtx(ctx, "create_subtask"); err != nil {
				return nil, issueCreateOut{}, err
			}
			c, err := deps.Caller.Resolve(ctx)
			if err != nil {
				return nil, issueCreateOut{}, err
			}
			p, err := deps.Projects.Get(ctx, c, in.ProjectSlug)
			if err != nil {
				return nil, issueCreateOut{}, err
			}
			if err := CheckProjectFromCtx(ctx, p.ID); err != nil {
				return nil, issueCreateOut{}, err
			}
			prio := in.Priority
			i, err := deps.Issues.CreateSubtask(ctx, c, in.ProjectSlug, in.ParentNumber, project.CreateIssueInput{
				Title: in.Title, Description: in.Description, Priority: &prio,
			})
			if err != nil {
				return nil, issueCreateOut{}, err
			}
			return nil, issueCreateOut{OK: true, Issue: issueDetail{
				Number: i.Number, Title: i.Title, Description: i.Description,
				Status: string(i.Status), CreatedAt: i.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
			}}, nil
		})
}

// ===== add_dependency =====

type addDependencyIn struct {
	ProjectSlug   string `json:"project_slug" jsonschema:"required field"`
	BlockedNumber int    `json:"blocked_number" jsonschema:"issue blocked-by blocker_number"`
	BlockerNumber int    `json:"blocker_number" jsonschema:"blocking issue number"`
}
type dependencyOut struct {
	OK            bool `json:"ok"`
	BlockedNumber int  `json:"blocked_number"`
	BlockerNumber int  `json:"blocker_number"`
}

func registerAddDependency(srv *mcpsdk.Server, deps ServerDeps) {
	mcpsdk.AddTool(srv,
		&mcpsdk.Tool{Name: "add_dependency", Description: "Add a dependency edge: blocked_number is blocked-by blocker_number. Rejects cycles and cross-project edges."},
		func(ctx context.Context, _ *mcpsdk.CallToolRequest, in addDependencyIn) (*mcpsdk.CallToolResult, dependencyOut, error) {
			if err := CheckToolFromCtx(ctx, "add_dependency"); err != nil {
				return nil, dependencyOut{}, err
			}
			c, err := deps.Caller.Resolve(ctx)
			if err != nil {
				return nil, dependencyOut{}, err
			}
			p, err := deps.Projects.Get(ctx, c, in.ProjectSlug)
			if err != nil {
				return nil, dependencyOut{}, err
			}
			if err := CheckProjectFromCtx(ctx, p.ID); err != nil {
				return nil, dependencyOut{}, err
			}
			if _, err := deps.Issues.AddDependency(ctx, c, in.ProjectSlug, project.AddDependencyInput{
				BlockedNumber: in.BlockedNumber, BlockerNumber: in.BlockerNumber,
			}); err != nil {
				return nil, dependencyOut{}, err
			}
			return nil, dependencyOut{OK: true, BlockedNumber: in.BlockedNumber, BlockerNumber: in.BlockerNumber}, nil
		})
}

// ===== remove_dependency =====

func registerRemoveDependency(srv *mcpsdk.Server, deps ServerDeps) {
	mcpsdk.AddTool(srv,
		&mcpsdk.Tool{Name: "remove_dependency", Description: "Remove a dependency edge (plan correction)."},
		func(ctx context.Context, _ *mcpsdk.CallToolRequest, in addDependencyIn) (*mcpsdk.CallToolResult, dependencyOut, error) {
			if err := CheckToolFromCtx(ctx, "remove_dependency"); err != nil {
				return nil, dependencyOut{}, err
			}
			c, err := deps.Caller.Resolve(ctx)
			if err != nil {
				return nil, dependencyOut{}, err
			}
			p, err := deps.Projects.Get(ctx, c, in.ProjectSlug)
			if err != nil {
				return nil, dependencyOut{}, err
			}
			if err := CheckProjectFromCtx(ctx, p.ID); err != nil {
				return nil, dependencyOut{}, err
			}
			if err := deps.Issues.RemoveDependency(ctx, c, in.ProjectSlug, in.BlockedNumber, in.BlockerNumber); err != nil {
				return nil, dependencyOut{}, err
			}
			return nil, dependencyOut{OK: true, BlockedNumber: in.BlockedNumber, BlockerNumber: in.BlockerNumber}, nil
		})
}

// ===== list_dependencies =====

type listDependenciesIn struct {
	ProjectSlug string `json:"project_slug" jsonschema:"required field"`
	Number      int    `json:"number" jsonschema:"issue number (>=1)"`
}
type listDependenciesOut struct {
	BlockedBy []issueSummary `json:"blocked_by"`
	Blocks    []issueSummary `json:"blocks"`
}

func registerListDependencies(srv *mcpsdk.Server, deps ServerDeps) {
	mcpsdk.AddTool(srv,
		&mcpsdk.Tool{Name: "list_dependencies", Description: "List an issue's dependencies: {blocked_by:[...], blocks:[...]}."},
		func(ctx context.Context, _ *mcpsdk.CallToolRequest, in listDependenciesIn) (*mcpsdk.CallToolResult, listDependenciesOut, error) {
			if err := CheckToolFromCtx(ctx, "list_dependencies"); err != nil {
				return nil, listDependenciesOut{}, err
			}
			c, err := deps.Caller.Resolve(ctx)
			if err != nil {
				return nil, listDependenciesOut{}, err
			}
			p, err := deps.Projects.Get(ctx, c, in.ProjectSlug)
			if err != nil {
				return nil, listDependenciesOut{}, err
			}
			if err := CheckProjectFromCtx(ctx, p.ID); err != nil {
				return nil, listDependenciesOut{}, err
			}
			blockedBy, blocks, err := deps.Issues.ListDependencies(ctx, c, in.ProjectSlug, in.Number)
			if err != nil {
				return nil, listDependenciesOut{}, err
			}
			out := listDependenciesOut{
				BlockedBy: make([]issueSummary, 0, len(blockedBy)),
				Blocks:    make([]issueSummary, 0, len(blocks)),
			}
			for _, i := range blockedBy {
				out.BlockedBy = append(out.BlockedBy, issueSummary{Number: i.Number, Title: i.Title, Status: string(i.Status)})
			}
			for _, i := range blocks {
				out.Blocks = append(out.Blocks, issueSummary{Number: i.Number, Title: i.Title, Status: string(i.Status)})
			}
			return nil, out, nil
		})
}

// ===== submit_decomposition =====
//
// 一次原子调用提交整张任务分解图（子任务 + 依赖边）。先做 in-batch DAG 校验，全部
// 通过后再建子任务、建边。ref 为 batch 内本地引用键，落库后映射为 issue number。

type decompSubtaskIn struct {
	Ref         string `json:"ref" jsonschema:"local reference key used by edges"`
	Title       string `json:"title" jsonschema:"title (non-empty)"`
	Description string `json:"description,omitempty"`
	Priority    int    `json:"priority,omitempty" jsonschema:"0..3 (higher = sooner)"`
}
type decompEdgeIn struct {
	BlockedRef string `json:"blocked_ref" jsonschema:"ref of the blocked subtask"`
	BlockerRef string `json:"blocker_ref" jsonschema:"ref of the blocking subtask"`
}
type submitDecompositionIn struct {
	ProjectSlug  string            `json:"project_slug" jsonschema:"required field"`
	ParentNumber *int              `json:"parent_number,omitempty" jsonschema:"optional anchor issue number; subtasks become its children"`
	Subtasks     []decompSubtaskIn `json:"subtasks"`
	Edges        []decompEdgeIn    `json:"edges,omitempty"`
}
type submitDecompositionOut struct {
	OK          bool           `json:"ok"`
	RefToNumber map[string]int `json:"ref_to_number"`
}

func registerSubmitDecomposition(srv *mcpsdk.Server, deps ServerDeps) {
	mcpsdk.AddTool(srv,
		&mcpsdk.Tool{Name: "submit_decomposition", Description: "Submit a whole task decomposition (subtasks + blocks/blocked-by edges) atomically. Validates acyclicity before any write; maps refs to created issue numbers."},
		func(ctx context.Context, _ *mcpsdk.CallToolRequest, in submitDecompositionIn) (*mcpsdk.CallToolResult, submitDecompositionOut, error) {
			if err := CheckToolFromCtx(ctx, "submit_decomposition"); err != nil {
				return nil, submitDecompositionOut{}, err
			}
			c, err := deps.Caller.Resolve(ctx)
			if err != nil {
				return nil, submitDecompositionOut{}, err
			}
			p, err := deps.Projects.Get(ctx, c, in.ProjectSlug)
			if err != nil {
				return nil, submitDecompositionOut{}, err
			}
			if err := CheckProjectFromCtx(ctx, p.ID); err != nil {
				return nil, submitDecompositionOut{}, err
			}
			res, err := applyDecomposition(ctx, deps.Issues, c, in)
			if err != nil {
				return nil, submitDecompositionOut{}, err
			}
			return nil, submitDecompositionOut{OK: true, RefToNumber: res}, nil
		})
}

// applyDecomposition 在 mcp 包内实现分解落库（不引入 orchestrator 以避免 import cycle）。
// 先 ref 去重 + 边端点校验 + DAG 校验，再建子任务、建边。
func applyDecomposition(ctx context.Context, issues project.IssueService, c project.Caller, in submitDecompositionIn) (map[string]int, error) {
	if len(in.Subtasks) == 0 {
		return nil, errs.New(errs.CodeInvalidInput, "decomposition 至少需要一个子任务")
	}
	refSet := make(map[string]struct{}, len(in.Subtasks))
	for _, st := range in.Subtasks {
		if st.Ref == "" {
			return nil, errs.New(errs.CodeInvalidInput, "子任务 ref 不能为空")
		}
		if st.Title == "" {
			return nil, errs.New(errs.CodeInvalidInput, "子任务 title 不能为空")
		}
		if _, dup := refSet[st.Ref]; dup {
			return nil, errs.New(errs.CodeInvalidInput, "子任务 ref 重复: "+st.Ref)
		}
		if st.Priority < 0 || st.Priority > 3 {
			return nil, errs.New(errs.CodeInvalidInput, "子任务 priority 必须在 0..3")
		}
		refSet[st.Ref] = struct{}{}
	}
	for _, e := range in.Edges {
		if _, ok := refSet[e.BlockedRef]; !ok {
			return nil, errs.New(errs.CodeInvalidInput, "边引用了未知 blocked ref: "+e.BlockedRef)
		}
		if _, ok := refSet[e.BlockerRef]; !ok {
			return nil, errs.New(errs.CodeInvalidInput, "边引用了未知 blocker ref: "+e.BlockerRef)
		}
		if e.BlockedRef == e.BlockerRef {
			return nil, errs.New(errs.CodeInvalidInput, "边不能自指: "+e.BlockedRef)
		}
	}
	if err := decompositionIsDAG(in.Subtasks, in.Edges); err != nil {
		return nil, err
	}

	refToNum := make(map[string]int, len(in.Subtasks))
	for _, st := range in.Subtasks {
		prio := st.Priority
		ci := project.CreateIssueInput{Title: st.Title, Description: st.Description, Priority: &prio}
		var iss *project.Issue
		var err error
		if in.ParentNumber != nil {
			iss, err = issues.CreateSubtask(ctx, c, in.ProjectSlug, *in.ParentNumber, ci)
		} else {
			iss, err = issues.Create(ctx, c, in.ProjectSlug, ci)
		}
		if err != nil {
			return nil, err
		}
		refToNum[st.Ref] = iss.Number
	}
	for _, e := range in.Edges {
		if _, err := issues.AddDependency(ctx, c, in.ProjectSlug, project.AddDependencyInput{
			BlockedNumber: refToNum[e.BlockedRef], BlockerNumber: refToNum[e.BlockerRef],
		}); err != nil {
			return nil, err
		}
	}
	return refToNum, nil
}

// decompositionIsDAG 对 (子任务, 边) 做 Kahn 拓扑排序，存在环返回 CodeInvalidInput。
func decompositionIsDAG(subtasks []decompSubtaskIn, edges []decompEdgeIn) error {
	indeg := make(map[string]int, len(subtasks))
	for _, st := range subtasks {
		indeg[st.Ref] = 0
	}
	adj := make(map[string][]string, len(subtasks))
	for _, e := range edges {
		adj[e.BlockerRef] = append(adj[e.BlockerRef], e.BlockedRef)
		indeg[e.BlockedRef]++
	}
	queue := make([]string, 0, len(subtasks))
	for ref, d := range indeg {
		if d == 0 {
			queue = append(queue, ref)
		}
	}
	visited := 0
	for len(queue) > 0 {
		cur := queue[0]
		queue = queue[1:]
		visited++
		for _, next := range adj[cur] {
			indeg[next]--
			if indeg[next] == 0 {
				queue = append(queue, next)
			}
		}
	}
	if visited != len(subtasks) {
		return errs.New(errs.CodeInvalidInput, "decomposition 边集合存在环")
	}
	return nil
}

// ===== update_issue =====

type updateIssueIn struct {
	ProjectSlug string `json:"project_slug" jsonschema:"required field"`
	Number      int    `json:"number" jsonschema:"issue number (>=1)"`
	Title       string `json:"title,omitempty"`
	Description string `json:"description,omitempty"`
}
type issueUpdateOut struct {
	OK    bool        `json:"ok"`
	Issue issueDetail `json:"issue"`
}

func registerUpdateIssue(srv *mcpsdk.Server, deps ServerDeps) {
	mcpsdk.AddTool(srv,
		&mcpsdk.Tool{Name: "update_issue", Description: "Update an existing issue."},
		func(ctx context.Context, _ *mcpsdk.CallToolRequest, in updateIssueIn) (*mcpsdk.CallToolResult, issueUpdateOut, error) {
			if err := CheckToolFromCtx(ctx, "update_issue"); err != nil {
				return nil, issueUpdateOut{}, err
			}
			c, err := deps.Caller.Resolve(ctx)
			if err != nil {
				return nil, issueUpdateOut{}, err
			}
			p, err := deps.Projects.Get(ctx, c, in.ProjectSlug)
			if err != nil {
				return nil, issueUpdateOut{}, err
			}
			if err := CheckProjectFromCtx(ctx, p.ID); err != nil {
				return nil, issueUpdateOut{}, err
			}
			input := project.UpdateIssueInput{}
			if in.Title != "" {
				input.Title = &in.Title
			}
			if in.Description != "" {
				input.Description = &in.Description
			}
			i, err := deps.Issues.Update(ctx, c, in.ProjectSlug, in.Number, input)
			if err != nil {
				return nil, issueUpdateOut{}, err
			}
			return nil, issueUpdateOut{OK: true, Issue: issueDetail{
				Number: i.Number, Title: i.Title, Description: i.Description,
				Status: string(i.Status), CreatedAt: i.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
			}}, nil
		})
}

// ===== close/reopen_issue =====

type closeIssueIn struct {
	ProjectSlug string `json:"project_slug" jsonschema:"required field"`
	Number      int    `json:"number" jsonschema:"issue number (>=1)"`
}
type okOut struct {
	OK bool `json:"ok"`
}

func registerCloseIssue(srv *mcpsdk.Server, deps ServerDeps) {
	mcpsdk.AddTool(srv,
		&mcpsdk.Tool{Name: "close_issue", Description: "Close an issue (idempotent)."},
		func(ctx context.Context, _ *mcpsdk.CallToolRequest, in closeIssueIn) (*mcpsdk.CallToolResult, okOut, error) {
			if err := CheckToolFromCtx(ctx, "close_issue"); err != nil {
				return nil, okOut{}, err
			}
			c, err := deps.Caller.Resolve(ctx)
			if err != nil {
				return nil, okOut{}, err
			}
			p, err := deps.Projects.Get(ctx, c, in.ProjectSlug)
			if err != nil {
				return nil, okOut{}, err
			}
			if err := CheckProjectFromCtx(ctx, p.ID); err != nil {
				return nil, okOut{}, err
			}
			if err := deps.Issues.Close(ctx, c, in.ProjectSlug, in.Number); err != nil {
				return nil, okOut{}, err
			}
			return nil, okOut{OK: true}, nil
		})
}

func registerReopenIssue(srv *mcpsdk.Server, deps ServerDeps) {
	mcpsdk.AddTool(srv,
		&mcpsdk.Tool{Name: "reopen_issue", Description: "Reopen a closed issue (idempotent)."},
		func(ctx context.Context, _ *mcpsdk.CallToolRequest, in closeIssueIn) (*mcpsdk.CallToolResult, okOut, error) {
			if err := CheckToolFromCtx(ctx, "reopen_issue"); err != nil {
				return nil, okOut{}, err
			}
			c, err := deps.Caller.Resolve(ctx)
			if err != nil {
				return nil, okOut{}, err
			}
			p, err := deps.Projects.Get(ctx, c, in.ProjectSlug)
			if err != nil {
				return nil, okOut{}, err
			}
			if err := CheckProjectFromCtx(ctx, p.ID); err != nil {
				return nil, okOut{}, err
			}
			if err := deps.Issues.Reopen(ctx, c, in.ProjectSlug, in.Number); err != nil {
				return nil, okOut{}, err
			}
			return nil, okOut{OK: true}, nil
		})
}

// ===== assign_issue =====

type assignIssueIn struct {
	ProjectSlug string  `json:"project_slug" jsonschema:"required field"`
	Number      int     `json:"number" jsonschema:"issue number (>=1)"`
	AssigneeID  *string `json:"assignee_id,omitempty"`
}

func registerAssignIssue(srv *mcpsdk.Server, deps ServerDeps) {
	mcpsdk.AddTool(srv,
		&mcpsdk.Tool{Name: "assign_issue", Description: "Assign or unassign an issue."},
		func(ctx context.Context, _ *mcpsdk.CallToolRequest, in assignIssueIn) (*mcpsdk.CallToolResult, okOut, error) {
			if err := CheckToolFromCtx(ctx, "assign_issue"); err != nil {
				return nil, okOut{}, err
			}
			c, err := deps.Caller.Resolve(ctx)
			if err != nil {
				return nil, okOut{}, err
			}
			p, err := deps.Projects.Get(ctx, c, in.ProjectSlug)
			if err != nil {
				return nil, okOut{}, err
			}
			if err := CheckProjectFromCtx(ctx, p.ID); err != nil {
				return nil, okOut{}, err
			}
			var assignee *uuid.UUID
			if in.AssigneeID != nil && *in.AssigneeID != "" {
				aid, err := uuid.Parse(*in.AssigneeID)
				if err != nil {
					return nil, okOut{}, errs.Wrap(err, errs.CodeInvalidInput, "assignee_id parse")
				}
				assignee = &aid
			}
			if _, err := deps.Issues.Assign(ctx, c, in.ProjectSlug, in.Number, assignee); err != nil {
				return nil, okOut{}, err
			}
			return nil, okOut{OK: true}, nil
		})
}

// ===== create_requirement =====

type createRequirementIn struct {
	ProjectSlug string `json:"project_slug" jsonschema:"required field"`
	Title       string `json:"title" jsonschema:"title (non-empty)"`
	Description string `json:"description,omitempty"`
}
type requirementCreateOut struct {
	OK          bool              `json:"ok"`
	Requirement requirementDetail `json:"requirement"`
}

func registerCreateRequirement(srv *mcpsdk.Server, deps ServerDeps) {
	mcpsdk.AddTool(srv,
		&mcpsdk.Tool{Name: "create_requirement", Description: "Create a new requirement."},
		func(ctx context.Context, _ *mcpsdk.CallToolRequest, in createRequirementIn) (*mcpsdk.CallToolResult, requirementCreateOut, error) {
			if err := CheckToolFromCtx(ctx, "create_requirement"); err != nil {
				return nil, requirementCreateOut{}, err
			}
			c, err := deps.Caller.Resolve(ctx)
			if err != nil {
				return nil, requirementCreateOut{}, err
			}
			p, err := deps.Projects.Get(ctx, c, in.ProjectSlug)
			if err != nil {
				return nil, requirementCreateOut{}, err
			}
			if err := CheckProjectFromCtx(ctx, p.ID); err != nil {
				return nil, requirementCreateOut{}, err
			}
			r, err := deps.Reqs.Create(ctx, c, in.ProjectSlug, project.CreateRequirementInput{
				Title: in.Title, Description: in.Description,
			})
			if err != nil {
				return nil, requirementCreateOut{}, err
			}
			return nil, requirementCreateOut{OK: true, Requirement: requirementDetail{
				Number: r.Number, Title: r.Title, Description: r.Description,
				Phase: string(r.Phase), Status: string(r.Status),
				CreatedAt: r.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
			}}, nil
		})
}

// ===== update_requirement =====

type updateRequirementIn struct {
	ProjectSlug string `json:"project_slug" jsonschema:"required field"`
	Number      int    `json:"number" jsonschema:"issue number (>=1)"`
	Title       string `json:"title,omitempty"`
	Description string `json:"description,omitempty"`
}

func registerUpdateRequirement(srv *mcpsdk.Server, deps ServerDeps) {
	mcpsdk.AddTool(srv,
		&mcpsdk.Tool{Name: "update_requirement", Description: "Update an existing requirement."},
		func(ctx context.Context, _ *mcpsdk.CallToolRequest, in updateRequirementIn) (*mcpsdk.CallToolResult, requirementCreateOut, error) {
			if err := CheckToolFromCtx(ctx, "update_requirement"); err != nil {
				return nil, requirementCreateOut{}, err
			}
			c, err := deps.Caller.Resolve(ctx)
			if err != nil {
				return nil, requirementCreateOut{}, err
			}
			p, err := deps.Projects.Get(ctx, c, in.ProjectSlug)
			if err != nil {
				return nil, requirementCreateOut{}, err
			}
			if err := CheckProjectFromCtx(ctx, p.ID); err != nil {
				return nil, requirementCreateOut{}, err
			}
			input := project.UpdateRequirementInput{}
			if in.Title != "" {
				input.Title = &in.Title
			}
			if in.Description != "" {
				input.Description = &in.Description
			}
			r, err := deps.Reqs.Update(ctx, c, in.ProjectSlug, in.Number, input)
			if err != nil {
				return nil, requirementCreateOut{}, err
			}
			return nil, requirementCreateOut{OK: true, Requirement: requirementDetail{
				Number: r.Number, Title: r.Title, Description: r.Description,
				Phase: string(r.Phase), Status: string(r.Status),
				CreatedAt: r.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
			}}, nil
		})
}

// ===== close/reopen_requirement =====

type closeRequirementIn struct {
	ProjectSlug string `json:"project_slug" jsonschema:"required field"`
	Number      int    `json:"number" jsonschema:"issue number (>=1)"`
}

func registerCloseRequirement(srv *mcpsdk.Server, deps ServerDeps) {
	mcpsdk.AddTool(srv,
		&mcpsdk.Tool{Name: "close_requirement", Description: "Close a requirement (idempotent)."},
		func(ctx context.Context, _ *mcpsdk.CallToolRequest, in closeRequirementIn) (*mcpsdk.CallToolResult, okOut, error) {
			if err := CheckToolFromCtx(ctx, "close_requirement"); err != nil {
				return nil, okOut{}, err
			}
			c, err := deps.Caller.Resolve(ctx)
			if err != nil {
				return nil, okOut{}, err
			}
			p, err := deps.Projects.Get(ctx, c, in.ProjectSlug)
			if err != nil {
				return nil, okOut{}, err
			}
			if err := CheckProjectFromCtx(ctx, p.ID); err != nil {
				return nil, okOut{}, err
			}
			if err := deps.Reqs.Close(ctx, c, in.ProjectSlug, in.Number); err != nil {
				return nil, okOut{}, err
			}
			return nil, okOut{OK: true}, nil
		})
}

func registerReopenRequirement(srv *mcpsdk.Server, deps ServerDeps) {
	mcpsdk.AddTool(srv,
		&mcpsdk.Tool{Name: "reopen_requirement", Description: "Reopen a closed requirement (idempotent)."},
		func(ctx context.Context, _ *mcpsdk.CallToolRequest, in closeRequirementIn) (*mcpsdk.CallToolResult, okOut, error) {
			if err := CheckToolFromCtx(ctx, "reopen_requirement"); err != nil {
				return nil, okOut{}, err
			}
			c, err := deps.Caller.Resolve(ctx)
			if err != nil {
				return nil, okOut{}, err
			}
			p, err := deps.Projects.Get(ctx, c, in.ProjectSlug)
			if err != nil {
				return nil, okOut{}, err
			}
			if err := CheckProjectFromCtx(ctx, p.ID); err != nil {
				return nil, okOut{}, err
			}
			if err := deps.Reqs.Reopen(ctx, c, in.ProjectSlug, in.Number); err != nil {
				return nil, okOut{}, err
			}
			return nil, okOut{OK: true}, nil
		})
}

// ===== set_requirement_phase =====

type setRequirementPhaseIn struct {
	ProjectSlug string `json:"project_slug" jsonschema:"required field"`
	Number      int    `json:"number" jsonschema:"issue number (>=1)"`
	Phase       string `json:"phase" jsonschema:"required field"`
}

func registerSetRequirementPhase(srv *mcpsdk.Server, deps ServerDeps) {
	mcpsdk.AddTool(srv,
		&mcpsdk.Tool{Name: "set_requirement_phase", Description: "Change a requirement's phase. Entry gates are enforced: e.g. moving to 'implementing' requires an approved (verdict=pass) auditing artifact; moving to 'done' requires the workspace PR to be merged. A blocked transition returns phase_gate_failed."},
		func(ctx context.Context, _ *mcpsdk.CallToolRequest, in setRequirementPhaseIn) (*mcpsdk.CallToolResult, requirementCreateOut, error) {
			if err := CheckToolFromCtx(ctx, "set_requirement_phase"); err != nil {
				return nil, requirementCreateOut{}, err
			}
			c, err := deps.Caller.Resolve(ctx)
			if err != nil {
				return nil, requirementCreateOut{}, err
			}
			p, err := deps.Projects.Get(ctx, c, in.ProjectSlug)
			if err != nil {
				return nil, requirementCreateOut{}, err
			}
			if err := CheckProjectFromCtx(ctx, p.ID); err != nil {
				return nil, requirementCreateOut{}, err
			}
			r, err := deps.Reqs.ChangePhase(ctx, c, in.ProjectSlug, in.Number, project.Phase(in.Phase))
			if err != nil {
				return nil, requirementCreateOut{}, err
			}
			return nil, requirementCreateOut{OK: true, Requirement: requirementDetail{
				Number: r.Number, Title: r.Title, Description: r.Description,
				Phase: string(r.Phase), Status: string(r.Status),
				CreatedAt: r.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
			}}, nil
		})
}

// ===== add_requirement_artifact =====

type addRequirementArtifactIn struct {
	ProjectSlug string `json:"project_slug" jsonschema:"required field"`
	Number      int    `json:"number" jsonschema:"requirement number (>=1)"`
	Phase       string `json:"phase" jsonschema:"artifact phase: planning|prototyping|auditing|implementing|reviewing"`
	Content     string `json:"content" jsonschema:"artifact content (non-empty)"`
	Note        string `json:"note,omitempty"`
}
type artifactDetail struct {
	Phase     string `json:"phase"`
	Version   int    `json:"version"`
	Content   string `json:"content"`
	Note      string `json:"note,omitempty"`
	CreatedAt string `json:"created_at"`
	Verdict   string `json:"verdict"`
	Approved  bool   `json:"approved"`
}

func artifactDetailFrom(a *project.Artifact) artifactDetail {
	return artifactDetail{
		Phase: string(a.Phase), Version: a.Version, Content: a.Content, Note: a.Note,
		CreatedAt: a.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
		Verdict:   string(a.Verdict),
		Approved:  a.Verdict == project.VerdictPass,
	}
}

type artifactCreateOut struct {
	OK       bool           `json:"ok"`
	Artifact artifactDetail `json:"artifact"`
}

func registerAddRequirementArtifact(srv *mcpsdk.Server, deps ServerDeps) {
	mcpsdk.AddTool(srv,
		&mcpsdk.Tool{Name: "add_requirement_artifact", Description: "Add a new phase artifact version (planning/prototyping/auditing/implementing/reviewing) to a requirement."},
		func(ctx context.Context, _ *mcpsdk.CallToolRequest, in addRequirementArtifactIn) (*mcpsdk.CallToolResult, artifactCreateOut, error) {
			if err := CheckToolFromCtx(ctx, "add_requirement_artifact"); err != nil {
				return nil, artifactCreateOut{}, err
			}
			c, err := deps.Caller.Resolve(ctx)
			if err != nil {
				return nil, artifactCreateOut{}, err
			}
			p, err := deps.Projects.Get(ctx, c, in.ProjectSlug)
			if err != nil {
				return nil, artifactCreateOut{}, err
			}
			if err := CheckProjectFromCtx(ctx, p.ID); err != nil {
				return nil, artifactCreateOut{}, err
			}
			art, err := deps.Artifacts.Create(ctx, c, in.ProjectSlug, in.Number, project.CreateArtifactInput{
				Phase: project.Phase(in.Phase), Content: in.Content, Note: in.Note,
			})
			if err != nil {
				return nil, artifactCreateOut{}, err
			}
			return nil, artifactCreateOut{OK: true, Artifact: artifactDetailFrom(art)}, nil
		})
}

// ===== approve_requirement_artifact =====

type approveRequirementArtifactIn struct {
	ProjectSlug string `json:"project_slug" jsonschema:"required field"`
	Number      int    `json:"number" jsonschema:"requirement number (>=1)"`
	Phase       string `json:"phase" jsonschema:"artifact phase: planning|prototyping|auditing|implementing|reviewing"`
	Version     int    `json:"version" jsonschema:"artifact version (>=1)"`
	Verdict     string `json:"verdict" jsonschema:"review verdict: pass|fail"`
}

func registerApproveRequirementArtifact(srv *mcpsdk.Server, deps ServerDeps) {
	mcpsdk.AddTool(srv,
		&mcpsdk.Tool{Name: "approve_requirement_artifact", Description: "Set the review verdict (pass/fail) on a requirement artifact version. A 'pass' verdict marks the phase's approved/current artifact, which downstream phase gates require (e.g. an approved auditing artifact unblocks the move to 'implementing')."},
		func(ctx context.Context, _ *mcpsdk.CallToolRequest, in approveRequirementArtifactIn) (*mcpsdk.CallToolResult, artifactCreateOut, error) {
			if err := CheckToolFromCtx(ctx, "approve_requirement_artifact"); err != nil {
				return nil, artifactCreateOut{}, err
			}
			c, err := deps.Caller.Resolve(ctx)
			if err != nil {
				return nil, artifactCreateOut{}, err
			}
			p, err := deps.Projects.Get(ctx, c, in.ProjectSlug)
			if err != nil {
				return nil, artifactCreateOut{}, err
			}
			if err := CheckProjectFromCtx(ctx, p.ID); err != nil {
				return nil, artifactCreateOut{}, err
			}
			art, err := deps.Artifacts.Approve(ctx, c, in.ProjectSlug, in.Number, project.Phase(in.Phase), in.Version, project.Verdict(in.Verdict))
			if err != nil {
				return nil, artifactCreateOut{}, err
			}
			return nil, artifactCreateOut{OK: true, Artifact: artifactDetailFrom(art)}, nil
		})
}

// ===== list_requirement_artifacts =====

type listRequirementArtifactsIn struct {
	ProjectSlug string `json:"project_slug" jsonschema:"required field"`
	Number      int    `json:"number" jsonschema:"requirement number (>=1)"`
	Phase       string `json:"phase,omitempty" jsonschema:"filter by phase: planning|prototyping|auditing|implementing|reviewing; empty = all"`
}
type listRequirementArtifactsOut struct {
	Artifacts []artifactDetail `json:"artifacts"`
}

func registerListRequirementArtifacts(srv *mcpsdk.Server, deps ServerDeps) {
	mcpsdk.AddTool(srv,
		&mcpsdk.Tool{Name: "list_requirement_artifacts", Description: "List phase artifact versions of a requirement."},
		func(ctx context.Context, _ *mcpsdk.CallToolRequest, in listRequirementArtifactsIn) (*mcpsdk.CallToolResult, listRequirementArtifactsOut, error) {
			if err := CheckToolFromCtx(ctx, "list_requirement_artifacts"); err != nil {
				return nil, listRequirementArtifactsOut{}, err
			}
			c, err := deps.Caller.Resolve(ctx)
			if err != nil {
				return nil, listRequirementArtifactsOut{}, err
			}
			p, err := deps.Projects.Get(ctx, c, in.ProjectSlug)
			if err != nil {
				return nil, listRequirementArtifactsOut{}, err
			}
			if err := CheckProjectFromCtx(ctx, p.ID); err != nil {
				return nil, listRequirementArtifactsOut{}, err
			}
			arts, err := deps.Artifacts.List(ctx, c, in.ProjectSlug, in.Number, project.Phase(in.Phase))
			if err != nil {
				return nil, listRequirementArtifactsOut{}, err
			}
			out := listRequirementArtifactsOut{Artifacts: make([]artifactDetail, 0, len(arts))}
			for _, art := range arts {
				out.Artifacts = append(out.Artifacts, artifactDetailFrom(art))
			}
			return nil, out, nil
		})
}

// ===== create_project =====

type createProjectIn struct {
	Slug        string `json:"slug" jsonschema:"project slug (non-empty)"`
	Name        string `json:"name" jsonschema:"display name (non-empty)"`
	Description string `json:"description,omitempty"`
	Visibility  string `json:"visibility,omitempty" jsonschema:"private|internal (default private)"`
}
type projectDetail struct {
	Slug        string `json:"slug"`
	Name        string `json:"name"`
	Description string `json:"description,omitempty"`
	Visibility  string `json:"visibility"`
	Archived    bool   `json:"archived"`
	CreatedAt   string `json:"created_at"`
}
type projectCreateOut struct {
	OK      bool          `json:"ok"`
	Project projectDetail `json:"project"`
}

func projectDetailFrom(p *project.Project) projectDetail {
	return projectDetail{
		Slug: p.Slug, Name: p.Name, Description: p.Description,
		Visibility: string(p.Visibility), Archived: p.ArchivedAt != nil,
		CreatedAt: p.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
	}
}

// checkProjectScopeUnrestricted 用于 create_project：token 若被限定到具体
// project 列表，则不允许创建落在列表外的新项目（新项目 ID 创建前未知，
// 只能整体拒绝）。
func checkProjectScopeUnrestricted(ctx context.Context) error {
	sess, ok := FromContext(ctx)
	if !ok {
		return errs.New(errs.CodeInternal, "mcp: AuthSession missing")
	}
	if sess.Scope.ProjectIDs != nil {
		return errs.New(errs.CodeMcpTokenScopeViolation,
			"token is restricted to specific projects; creating new projects is not allowed")
	}
	return nil
}

func registerCreateProject(srv *mcpsdk.Server, deps ServerDeps) {
	mcpsdk.AddTool(srv,
		&mcpsdk.Tool{Name: "create_project", Description: "Create a new project."},
		func(ctx context.Context, _ *mcpsdk.CallToolRequest, in createProjectIn) (*mcpsdk.CallToolResult, projectCreateOut, error) {
			if err := CheckToolFromCtx(ctx, "create_project"); err != nil {
				return nil, projectCreateOut{}, err
			}
			if err := checkProjectScopeUnrestricted(ctx); err != nil {
				return nil, projectCreateOut{}, err
			}
			c, err := deps.Caller.Resolve(ctx)
			if err != nil {
				return nil, projectCreateOut{}, err
			}
			p, err := deps.Projects.Create(ctx, c, project.CreateProjectInput{
				Slug: in.Slug, Name: in.Name, Description: in.Description,
				Visibility: project.Visibility(in.Visibility),
			})
			if err != nil {
				return nil, projectCreateOut{}, err
			}
			return nil, projectCreateOut{OK: true, Project: projectDetailFrom(p)}, nil
		})
}

// ===== update_project =====

type updateProjectIn struct {
	ProjectSlug string `json:"project_slug" jsonschema:"required field"`
	Name        string `json:"name,omitempty"`
	Description string `json:"description,omitempty"`
	Visibility  string `json:"visibility,omitempty" jsonschema:"private|internal"`
}

func registerUpdateProject(srv *mcpsdk.Server, deps ServerDeps) {
	mcpsdk.AddTool(srv,
		&mcpsdk.Tool{Name: "update_project", Description: "Update a project (name/description/visibility)."},
		func(ctx context.Context, _ *mcpsdk.CallToolRequest, in updateProjectIn) (*mcpsdk.CallToolResult, projectCreateOut, error) {
			if err := CheckToolFromCtx(ctx, "update_project"); err != nil {
				return nil, projectCreateOut{}, err
			}
			c, err := deps.Caller.Resolve(ctx)
			if err != nil {
				return nil, projectCreateOut{}, err
			}
			p, err := deps.Projects.Get(ctx, c, in.ProjectSlug)
			if err != nil {
				return nil, projectCreateOut{}, err
			}
			if err := CheckProjectFromCtx(ctx, p.ID); err != nil {
				return nil, projectCreateOut{}, err
			}
			input := project.UpdateProjectInput{}
			if in.Name != "" {
				input.Name = &in.Name
			}
			if in.Description != "" {
				input.Description = &in.Description
			}
			if in.Visibility != "" {
				v := project.Visibility(in.Visibility)
				input.Visibility = &v
			}
			p, err = deps.Projects.Update(ctx, c, in.ProjectSlug, input)
			if err != nil {
				return nil, projectCreateOut{}, err
			}
			return nil, projectCreateOut{OK: true, Project: projectDetailFrom(p)}, nil
		})
}

// ===== archive/unarchive_project =====

type archiveProjectIn struct {
	ProjectSlug string `json:"project_slug" jsonschema:"required field"`
}

func registerArchiveProject(srv *mcpsdk.Server, deps ServerDeps) {
	mcpsdk.AddTool(srv,
		&mcpsdk.Tool{Name: "archive_project", Description: "Archive a project (idempotent)."},
		func(ctx context.Context, _ *mcpsdk.CallToolRequest, in archiveProjectIn) (*mcpsdk.CallToolResult, okOut, error) {
			if err := CheckToolFromCtx(ctx, "archive_project"); err != nil {
				return nil, okOut{}, err
			}
			c, err := deps.Caller.Resolve(ctx)
			if err != nil {
				return nil, okOut{}, err
			}
			p, err := deps.Projects.Get(ctx, c, in.ProjectSlug)
			if err != nil {
				return nil, okOut{}, err
			}
			if err := CheckProjectFromCtx(ctx, p.ID); err != nil {
				return nil, okOut{}, err
			}
			if err := deps.Projects.Archive(ctx, c, in.ProjectSlug); err != nil {
				return nil, okOut{}, err
			}
			return nil, okOut{OK: true}, nil
		})
}

func registerUnarchiveProject(srv *mcpsdk.Server, deps ServerDeps) {
	mcpsdk.AddTool(srv,
		&mcpsdk.Tool{Name: "unarchive_project", Description: "Unarchive a project (idempotent)."},
		func(ctx context.Context, _ *mcpsdk.CallToolRequest, in archiveProjectIn) (*mcpsdk.CallToolResult, okOut, error) {
			if err := CheckToolFromCtx(ctx, "unarchive_project"); err != nil {
				return nil, okOut{}, err
			}
			c, err := deps.Caller.Resolve(ctx)
			if err != nil {
				return nil, okOut{}, err
			}
			p, err := deps.Projects.Get(ctx, c, in.ProjectSlug)
			if err != nil {
				return nil, okOut{}, err
			}
			if err := CheckProjectFromCtx(ctx, p.ID); err != nil {
				return nil, okOut{}, err
			}
			if err := deps.Projects.Unarchive(ctx, c, in.ProjectSlug); err != nil {
				return nil, okOut{}, err
			}
			return nil, okOut{OK: true}, nil
		})
}

// 保证 fmt 引用（编译期检查）
var _ = fmt.Sprintf
