package mcp

import (
	"context"

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

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

// registerChangeRequestTools registers the change-request (PR) tools.
//
// Gate boundary (spec decision): create_pull_request / merge_pull_request /
// get_pr_status / list_pull_requests are exposed, but review approval is NOT an
// MCP tool — it is HTTP/web-only so an agent cannot self-approve its own PR and
// bypass the merge gate. merge_pull_request calls ChangeReqs.Merge, which
// hard-blocks unless review_verdict=='approved' (surfaced as a FailedPrecondition
// MCP error). Operators may further exclude create/merge from agent system-token
// scopes via the token whitelist.
func registerChangeRequestTools(srv *mcpsdk.Server, deps ServerDeps) {
	registerCreatePullRequest(srv, deps)
	registerGetPRStatus(srv, deps)
	registerMergePullRequest(srv, deps)
	registerListPullRequests(srv, deps)
}

// ===== shared DTO =====

type changeRequestDetail struct {
	ID            string `json:"id"`
	ProjectID     string `json:"project_id"`
	WorkspaceID   string `json:"workspace_id"`
	Number        int    `json:"number"`
	Title         string `json:"title"`
	SourceBranch  string `json:"source_branch"`
	TargetBranch  string `json:"target_branch"`
	State         string `json:"state"`
	ReviewVerdict string `json:"review_verdict"`
	CIState       string `json:"ci_state"`
	ExternalID    int64  `json:"external_id,omitempty"`
	ExternalURL   string `json:"external_url,omitempty"`
	MergeSHA      string `json:"merge_commit_sha,omitempty"`
}

func changeRequestDetailFrom(cr *changerequest.ChangeRequest) changeRequestDetail {
	d := changeRequestDetail{
		ID: cr.ID.String(), ProjectID: cr.ProjectID.String(), WorkspaceID: cr.WorkspaceID.String(),
		Number: cr.Number, Title: cr.Title,
		SourceBranch: cr.SourceBranch, TargetBranch: cr.TargetBranch,
		State: string(cr.State), ReviewVerdict: string(cr.ReviewVerdict), CIState: string(cr.CIState),
		ExternalURL: cr.ExternalURL, MergeSHA: cr.MergeCommitSHA,
	}
	if cr.ExternalID != nil {
		d.ExternalID = *cr.ExternalID
	}
	return d
}

// resolvePRByNumber scopes the workspace (CheckToolFromCtx-style project gate)
// and finds the change request whose per-project number matches within that
// workspace. Returns NotFound when no such CR exists in scope.
func resolvePRByNumber(ctx context.Context, deps ServerDeps, workspaceID string, number int) (*changerequest.ChangeRequest, error) {
	c, err := deps.Caller.Resolve(ctx)
	if err != nil {
		return nil, err
	}
	w, _, err := scopedWorkspace(ctx, deps, c, workspaceID)
	if err != nil {
		return nil, err
	}
	list, err := deps.ChangeReqs.ListByWorkspace(ctx, changerequest.Caller{UserID: c.UserID, IsAdmin: c.IsAdmin}, w.ID)
	if err != nil {
		return nil, err
	}
	for _, cr := range list {
		if cr.Number == number {
			return cr, nil
		}
	}
	return nil, errs.New(errs.CodeNotFound, "change request not found in workspace")
}

// ===== create_pull_request =====

type createPRIn struct {
	WorkspaceID   string `json:"workspace_id" jsonschema:"workspace UUID"`
	SourceBranch  string `json:"source_branch" jsonschema:"branch to merge from (non-empty)"`
	TargetBranch  string `json:"target_branch,omitempty" jsonschema:"branch to merge into (defaults to workspace default branch)"`
	Title         string `json:"title" jsonschema:"PR title (non-empty)"`
	Description   string `json:"description,omitempty"`
	RequirementID string `json:"requirement_id,omitempty" jsonschema:"optional linked requirement UUID"`
	IssueID       string `json:"issue_id,omitempty" jsonschema:"optional linked issue UUID"`
}
type changeRequestOut struct {
	OK            bool                `json:"ok"`
	ChangeRequest changeRequestDetail `json:"change_request"`
}

func registerCreatePullRequest(srv *mcpsdk.Server, deps ServerDeps) {
	mcpsdk.AddTool(srv,
		&mcpsdk.Tool{Name: "create_pull_request", Description: "Open a pull request on the git host from a workspace branch and persist it as a change request. The PR must be approved by a human (web only) before it can be merged."},
		func(ctx context.Context, _ *mcpsdk.CallToolRequest, in createPRIn) (*mcpsdk.CallToolResult, changeRequestOut, error) {
			if err := CheckToolFromCtx(ctx, "create_pull_request"); err != nil {
				return nil, changeRequestOut{}, err
			}
			c, err := deps.Caller.Resolve(ctx)
			if err != nil {
				return nil, changeRequestOut{}, err
			}
			w, _, err := scopedWorkspace(ctx, deps, c, in.WorkspaceID)
			if err != nil {
				return nil, changeRequestOut{}, err
			}
			input := changerequest.CreateInput{
				SourceBranch: in.SourceBranch, TargetBranch: in.TargetBranch,
				Title: in.Title, Description: in.Description,
			}
			if in.RequirementID != "" {
				id, perr := uuid.Parse(in.RequirementID)
				if perr != nil {
					return nil, changeRequestOut{}, errs.Wrap(perr, errs.CodeInvalidInput, "requirement_id parse")
				}
				input.RequirementID = &id
			}
			if in.IssueID != "" {
				id, perr := uuid.Parse(in.IssueID)
				if perr != nil {
					return nil, changeRequestOut{}, errs.Wrap(perr, errs.CodeInvalidInput, "issue_id parse")
				}
				input.IssueID = &id
			}
			cr, err := deps.ChangeReqs.Create(ctx, changerequest.Caller{UserID: c.UserID, IsAdmin: c.IsAdmin}, w.ID, input)
			if err != nil {
				return nil, changeRequestOut{}, err
			}
			return nil, changeRequestOut{OK: true, ChangeRequest: changeRequestDetailFrom(cr)}, nil
		})
}

// ===== get_pr_status =====

type prNumberIn struct {
	WorkspaceID string `json:"workspace_id" jsonschema:"workspace UUID"`
	Number      int    `json:"number" jsonschema:"per-project change-request number"`
}

func registerGetPRStatus(srv *mcpsdk.Server, deps ServerDeps) {
	mcpsdk.AddTool(srv,
		&mcpsdk.Tool{Name: "get_pr_status", Description: "Get a change request by number, refreshing its CI/merge state from the git host."},
		func(ctx context.Context, _ *mcpsdk.CallToolRequest, in prNumberIn) (*mcpsdk.CallToolResult, changeRequestOut, error) {
			if err := CheckToolFromCtx(ctx, "get_pr_status"); err != nil {
				return nil, changeRequestOut{}, err
			}
			cr, err := resolvePRByNumber(ctx, deps, in.WorkspaceID, in.Number)
			if err != nil {
				return nil, changeRequestOut{}, err
			}
			c, err := deps.Caller.Resolve(ctx)
			if err != nil {
				return nil, changeRequestOut{}, err
			}
			cr, err = deps.ChangeReqs.SyncStatus(ctx, changerequest.Caller{UserID: c.UserID, IsAdmin: c.IsAdmin}, cr.ID)
			if err != nil {
				return nil, changeRequestOut{}, err
			}
			return nil, changeRequestOut{OK: true, ChangeRequest: changeRequestDetailFrom(cr)}, nil
		})
}

// ===== merge_pull_request (HARD-GATED) =====

type mergePRIn struct {
	WorkspaceID string `json:"workspace_id" jsonschema:"workspace UUID"`
	Number      int    `json:"number" jsonschema:"per-project change-request number"`
	Method      string `json:"method,omitempty" jsonschema:"merge method: merge|squash|rebase"`
}

func registerMergePullRequest(srv *mcpsdk.Server, deps ServerDeps) {
	mcpsdk.AddTool(srv,
		&mcpsdk.Tool{Name: "merge_pull_request", Description: "Merge a change request on the git host. HARD-BLOCKED unless review_verdict=='approved' (approval is human/web-only); returns a failed_precondition error otherwise."},
		func(ctx context.Context, _ *mcpsdk.CallToolRequest, in mergePRIn) (*mcpsdk.CallToolResult, changeRequestOut, error) {
			if err := CheckToolFromCtx(ctx, "merge_pull_request"); err != nil {
				return nil, changeRequestOut{}, err
			}
			cr, err := resolvePRByNumber(ctx, deps, in.WorkspaceID, in.Number)
			if err != nil {
				return nil, changeRequestOut{}, err
			}
			c, err := deps.Caller.Resolve(ctx)
			if err != nil {
				return nil, changeRequestOut{}, err
			}
			cr, err = deps.ChangeReqs.Merge(ctx, changerequest.Caller{UserID: c.UserID, IsAdmin: c.IsAdmin}, cr.ID, in.Method)
			if err != nil {
				return nil, changeRequestOut{}, err
			}
			return nil, changeRequestOut{OK: true, ChangeRequest: changeRequestDetailFrom(cr)}, nil
		})
}

// ===== list_pull_requests =====

type listPRsOut struct {
	ChangeRequests []changeRequestDetail `json:"change_requests"`
}

func registerListPullRequests(srv *mcpsdk.Server, deps ServerDeps) {
	mcpsdk.AddTool(srv,
		&mcpsdk.Tool{Name: "list_pull_requests", Description: "List change requests (PRs) of a workspace."},
		func(ctx context.Context, _ *mcpsdk.CallToolRequest, in getWorkspaceIn) (*mcpsdk.CallToolResult, listPRsOut, error) {
			if err := CheckToolFromCtx(ctx, "list_pull_requests"); err != nil {
				return nil, listPRsOut{}, err
			}
			c, err := deps.Caller.Resolve(ctx)
			if err != nil {
				return nil, listPRsOut{}, err
			}
			w, _, err := scopedWorkspace(ctx, deps, c, in.WorkspaceID)
			if err != nil {
				return nil, listPRsOut{}, err
			}
			out, err := deps.ChangeReqs.ListByWorkspace(ctx, changerequest.Caller{UserID: c.UserID, IsAdmin: c.IsAdmin}, w.ID)
			if err != nil {
				return nil, listPRsOut{}, err
			}
			res := listPRsOut{ChangeRequests: make([]changeRequestDetail, 0, len(out))}
			for _, cr := range out {
				res.ChangeRequests = append(res.ChangeRequests, changeRequestDetailFrom(cr))
			}
			return nil, res, nil
		})
}
