package mcp

import (
	"context"
	"time"

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

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

// ===== ACP 桥接抽象 =====
//
// acp 包 import 了 mcp（supervisor 签发 system token），mcp 不能反向 import acp。
// 这里定义 acp-free 的桥接接口与 DTO，由 internal/acp/mcp_bridge.go 实现适配
// （acp → mcp 的既有依赖边，不新增循环）。
//
// 范围边界（spec 决策）：agent kind 的增删改（含加密 env）与权限 approve/deny
// 不经 MCP 暴露——前者是秘密载体，后者的设计初衷是人工把关。

// ACPAgentKind 是 agent kind 的 MCP 投影（不含 binary/args/env 等部署细节）。
type ACPAgentKind struct {
	ID            uuid.UUID
	Name          string
	DisplayName   string
	Description   string
	Enabled       bool
	ToolAllowlist []string
}

// ACPSession 是 ACP 会话的 MCP 投影。
type ACPSession struct {
	ID             uuid.UUID
	WorkspaceID    uuid.UUID
	ProjectID      uuid.UUID
	AgentKindID    uuid.UUID
	UserID         uuid.UUID
	IssueID        *uuid.UUID
	RequirementID  *uuid.UUID
	Branch         string
	IsMainWorktree bool
	Status         string
	LastError      *string
	LastStopReason *string
	StartedAt      time.Time
	EndedAt        *time.Time
	// 成本核算（只读）：运行时累计 token/cost + 有效预算上限。
	PromptTokens     int64
	CompletionTokens int64
	ThinkingTokens   int64
	TotalCostUSD     float64
	BudgetMaxCostUSD *float64
	BudgetMaxTokens  *int64
}

// ACPTurn 是 ACP 回合记录的 MCP 投影（只读）。
type ACPTurn struct {
	TurnIndex   int
	Status      string
	StopReason  *string
	UpdateCount int
	StartedAt   time.Time
	CompletedAt *time.Time
}

// ACPPermission 是待审权限请求的 MCP 投影（只读，不含原始 toolCall/options JSON）。
type ACPPermission struct {
	ID             uuid.UUID
	SessionID      uuid.UUID
	AgentRequestID string
	ToolName       string
	Status         string
	CreatedAt      time.Time
}

// ACPCreateSessionInput 与 acp.CreateSessionInput 字段一一对应。
type ACPCreateSessionInput struct {
	WorkspaceID       uuid.UUID
	AgentKindID       uuid.UUID
	Branch            *string
	IssueNumber       *int
	RequirementNumber *int
	InitialPrompt     *string
}

// ACPSessionFilter 与 acp.SessionListFilter 字段一一对应。
type ACPSessionFilter struct {
	All           bool
	RequirementID *uuid.UUID
}

// ACPRunDashboardInput 是 run-history 汇总入参（service 在 caller scope 内归一化）。
type ACPRunDashboardInput struct {
	ProjectID     *uuid.UUID
	RequirementID *uuid.UUID
	From          time.Time
	To            time.Time
}

// ACPRunDashboard 是 run-history 汇总（计数 + 成功率 + 成本 + 时长 + 按天序列）。
type ACPRunDashboard struct {
	From               time.Time
	To                 time.Time
	Total              int64
	Succeeded          int64
	Crashed            int64
	Active             int64
	SuccessRate        float64
	TotalCostUSD       float64
	TotalTokens        int64
	AvgDurationSeconds float64
	ByDay              []ACPRunDayRow
}

// ACPRunDayRow 是 run-history 按天的一行。
type ACPRunDayRow struct {
	Day          time.Time
	Total        int64
	Succeeded    int64
	Crashed      int64
	TotalCostUSD float64
}

// ACPBridge 是 mcp 调用 acp 服务的桥接接口。
type ACPBridge interface {
	ListAgentKinds(ctx context.Context, userID uuid.UUID, isAdmin bool) ([]ACPAgentKind, error)
	GetAgentKind(ctx context.Context, userID uuid.UUID, isAdmin bool, id uuid.UUID) (*ACPAgentKind, error)
	CreateSession(ctx context.Context, userID uuid.UUID, isAdmin bool, in ACPCreateSessionInput) (*ACPSession, error)
	GetSession(ctx context.Context, userID uuid.UUID, isAdmin bool, id uuid.UUID) (*ACPSession, error)
	ListSessions(ctx context.Context, userID uuid.UUID, isAdmin bool, f ACPSessionFilter) ([]ACPSession, error)
	TerminateSession(ctx context.Context, userID uuid.UUID, isAdmin bool, id uuid.UUID) error
	ListPendingPermissions(ctx context.Context, userID uuid.UUID, isAdmin bool, sessionID uuid.UUID) ([]ACPPermission, error)
	// ListPendingApprovals 返回调用者跨所有会话的待审权限请求（HITL inbox，read-only）。
	ListPendingApprovals(ctx context.Context, userID uuid.UUID, isAdmin bool) ([]ACPPermission, error)
	ListSessionTurns(ctx context.Context, userID uuid.UUID, isAdmin bool, sessionID uuid.UUID) ([]ACPTurn, error)
	// GetRunDashboard 返回 run-history 汇总（owner scope；admin 全量），read-only。
	GetRunDashboard(ctx context.Context, userID uuid.UUID, isAdmin bool, in ACPRunDashboardInput) (*ACPRunDashboard, error)
}

// registerACPTools 注册 ACP 相关 MCP 工具。
func registerACPTools(srv *mcpsdk.Server, deps ServerDeps) {
	registerListAgentKinds(srv, deps)
	registerGetAgentKind(srv, deps)
	registerCreateACPSession(srv, deps)
	registerGetACPSession(srv, deps)
	registerListACPSessions(srv, deps)
	registerTerminateACPSession(srv, deps)
	registerListPendingPermissions(srv, deps)
	registerListPendingApprovals(srv, deps)
	registerGetACPSessionTurns(srv, deps)
	registerGetRunDashboard(srv, deps)
}

// ===== DTO =====

type agentKindDetail struct {
	ID            string   `json:"id"`
	Name          string   `json:"name"`
	DisplayName   string   `json:"display_name"`
	Description   string   `json:"description,omitempty"`
	Enabled       bool     `json:"enabled"`
	ToolAllowlist []string `json:"tool_allowlist,omitempty"`
}

func agentKindDetailFrom(k ACPAgentKind) agentKindDetail {
	return agentKindDetail{
		ID: k.ID.String(), Name: k.Name, DisplayName: k.DisplayName,
		Description: k.Description, Enabled: k.Enabled, ToolAllowlist: k.ToolAllowlist,
	}
}

type acpSessionDetail struct {
	ID             string `json:"id"`
	WorkspaceID    string `json:"workspace_id"`
	ProjectID      string `json:"project_id"`
	AgentKindID    string `json:"agent_kind_id"`
	UserID         string `json:"user_id"`
	IssueID        string `json:"issue_id,omitempty"`
	RequirementID  string `json:"requirement_id,omitempty"`
	Branch         string `json:"branch,omitempty"`
	IsMainWorktree bool   `json:"is_main_worktree"`
	Status         string `json:"status"`
	LastError      string `json:"last_error,omitempty"`
	LastStopReason string `json:"last_stop_reason,omitempty"`
	StartedAt      string `json:"started_at"`
	EndedAt        string `json:"ended_at,omitempty"`
	// 成本核算（只读）。
	PromptTokens     int64    `json:"prompt_tokens"`
	CompletionTokens int64    `json:"completion_tokens"`
	ThinkingTokens   int64    `json:"thinking_tokens"`
	TotalCostUSD     float64  `json:"total_cost_usd"`
	BudgetMaxCostUSD *float64 `json:"budget_max_cost_usd,omitempty"`
	BudgetMaxTokens  *int64   `json:"budget_max_tokens,omitempty"`
}

func acpSessionDetailFrom(s *ACPSession) acpSessionDetail {
	d := acpSessionDetail{
		ID: s.ID.String(), WorkspaceID: s.WorkspaceID.String(),
		ProjectID: s.ProjectID.String(), AgentKindID: s.AgentKindID.String(),
		UserID: s.UserID.String(), Branch: s.Branch,
		IsMainWorktree: s.IsMainWorktree, Status: s.Status,
		StartedAt:        s.StartedAt.Format("2006-01-02T15:04:05Z07:00"),
		PromptTokens:     s.PromptTokens,
		CompletionTokens: s.CompletionTokens,
		ThinkingTokens:   s.ThinkingTokens,
		TotalCostUSD:     s.TotalCostUSD,
		BudgetMaxCostUSD: s.BudgetMaxCostUSD,
		BudgetMaxTokens:  s.BudgetMaxTokens,
	}
	if s.IssueID != nil {
		d.IssueID = s.IssueID.String()
	}
	if s.RequirementID != nil {
		d.RequirementID = s.RequirementID.String()
	}
	if s.LastError != nil {
		d.LastError = *s.LastError
	}
	if s.LastStopReason != nil {
		d.LastStopReason = *s.LastStopReason
	}
	if s.EndedAt != nil {
		d.EndedAt = s.EndedAt.Format("2006-01-02T15:04:05Z07:00")
	}
	return d
}

// checkNotSystemToken 拒绝 system token（agent 会话签发）调用：用于
// create_acp_session，防止 agent 在会话内套娃创建嵌套会话。
func checkNotSystemToken(ctx context.Context) error {
	sess, ok := FromContext(ctx)
	if !ok {
		return errs.New(errs.CodeInternal, "mcp: AuthSession missing")
	}
	if sess.Issuer == IssuerSystem {
		return errs.New(errs.CodeForbidden,
			"system tokens (agent sessions) cannot create nested ACP sessions")
	}
	return nil
}

// scopedACPSession 取 session 并做项目 scope 校验。
func scopedACPSession(ctx context.Context, deps ServerDeps, userID uuid.UUID, isAdmin bool, sessionID string) (*ACPSession, error) {
	sid, err := uuid.Parse(sessionID)
	if err != nil {
		return nil, errs.Wrap(err, errs.CodeInvalidInput, "session_id parse")
	}
	s, err := deps.ACP.GetSession(ctx, userID, isAdmin, sid)
	if err != nil {
		return nil, err
	}
	if err := CheckProjectFromCtx(ctx, s.ProjectID); err != nil {
		return nil, err
	}
	return s, nil
}

// ===== list_agent_kinds / get_agent_kind =====

type listAgentKindsOut struct {
	AgentKinds []agentKindDetail `json:"agent_kinds"`
}

func registerListAgentKinds(srv *mcpsdk.Server, deps ServerDeps) {
	mcpsdk.AddTool(srv,
		&mcpsdk.Tool{Name: "list_agent_kinds", Description: "List ACP agent kinds available to the caller."},
		func(ctx context.Context, _ *mcpsdk.CallToolRequest, _ struct{}) (*mcpsdk.CallToolResult, listAgentKindsOut, error) {
			if err := CheckToolFromCtx(ctx, "list_agent_kinds"); err != nil {
				return nil, listAgentKindsOut{}, err
			}
			c, err := deps.Caller.Resolve(ctx)
			if err != nil {
				return nil, listAgentKindsOut{}, err
			}
			ks, err := deps.ACP.ListAgentKinds(ctx, c.UserID, c.IsAdmin)
			if err != nil {
				return nil, listAgentKindsOut{}, err
			}
			out := listAgentKindsOut{AgentKinds: make([]agentKindDetail, 0, len(ks))}
			for _, k := range ks {
				out.AgentKinds = append(out.AgentKinds, agentKindDetailFrom(k))
			}
			return nil, out, nil
		})
}

type getAgentKindIn struct {
	AgentKindID string `json:"agent_kind_id" jsonschema:"agent kind UUID"`
}

func registerGetAgentKind(srv *mcpsdk.Server, deps ServerDeps) {
	mcpsdk.AddTool(srv,
		&mcpsdk.Tool{Name: "get_agent_kind", Description: "Get a single ACP agent kind by ID."},
		func(ctx context.Context, _ *mcpsdk.CallToolRequest, in getAgentKindIn) (*mcpsdk.CallToolResult, agentKindDetail, error) {
			if err := CheckToolFromCtx(ctx, "get_agent_kind"); err != nil {
				return nil, agentKindDetail{}, err
			}
			c, err := deps.Caller.Resolve(ctx)
			if err != nil {
				return nil, agentKindDetail{}, err
			}
			kid, err := uuid.Parse(in.AgentKindID)
			if err != nil {
				return nil, agentKindDetail{}, errs.Wrap(err, errs.CodeInvalidInput, "agent_kind_id parse")
			}
			k, err := deps.ACP.GetAgentKind(ctx, c.UserID, c.IsAdmin, kid)
			if err != nil {
				return nil, agentKindDetail{}, err
			}
			return nil, agentKindDetailFrom(*k), nil
		})
}

// ===== create_acp_session =====

type createACPSessionIn struct {
	WorkspaceID       string `json:"workspace_id" jsonschema:"workspace UUID"`
	AgentKindID       string `json:"agent_kind_id" jsonschema:"agent kind UUID"`
	Branch            string `json:"branch,omitempty" jsonschema:"work on this branch (mutually exclusive with issue/requirement number)"`
	IssueNumber       int    `json:"issue_number,omitempty" jsonschema:"bind to an issue"`
	RequirementNumber int    `json:"requirement_number,omitempty" jsonschema:"bind to a requirement"`
	InitialPrompt     string `json:"initial_prompt,omitempty" jsonschema:"prompt sent to the agent after startup"`
}
type acpSessionCreateOut struct {
	OK      bool             `json:"ok"`
	Session acpSessionDetail `json:"session"`
}

func registerCreateACPSession(srv *mcpsdk.Server, deps ServerDeps) {
	mcpsdk.AddTool(srv,
		&mcpsdk.Tool{Name: "create_acp_session", Description: "Start an ACP agent session on a workspace."},
		func(ctx context.Context, _ *mcpsdk.CallToolRequest, in createACPSessionIn) (*mcpsdk.CallToolResult, acpSessionCreateOut, error) {
			if err := CheckToolFromCtx(ctx, "create_acp_session"); err != nil {
				return nil, acpSessionCreateOut{}, err
			}
			if err := checkNotSystemToken(ctx); err != nil {
				return nil, acpSessionCreateOut{}, err
			}
			c, err := deps.Caller.Resolve(ctx)
			if err != nil {
				return nil, acpSessionCreateOut{}, err
			}
			w, _, err := scopedWorkspace(ctx, deps, c, in.WorkspaceID)
			if err != nil {
				return nil, acpSessionCreateOut{}, err
			}
			kid, err := uuid.Parse(in.AgentKindID)
			if err != nil {
				return nil, acpSessionCreateOut{}, errs.Wrap(err, errs.CodeInvalidInput, "agent_kind_id parse")
			}
			input := ACPCreateSessionInput{WorkspaceID: w.ID, AgentKindID: kid}
			if in.Branch != "" {
				input.Branch = &in.Branch
			}
			if in.IssueNumber > 0 {
				n := in.IssueNumber
				input.IssueNumber = &n
			}
			if in.RequirementNumber > 0 {
				n := in.RequirementNumber
				input.RequirementNumber = &n
			}
			if in.InitialPrompt != "" {
				input.InitialPrompt = &in.InitialPrompt
			}
			s, err := deps.ACP.CreateSession(ctx, c.UserID, c.IsAdmin, input)
			if err != nil {
				return nil, acpSessionCreateOut{}, err
			}
			return nil, acpSessionCreateOut{OK: true, Session: acpSessionDetailFrom(s)}, nil
		})
}

// ===== get/list/terminate_acp_session =====

type getACPSessionIn struct {
	SessionID string `json:"session_id" jsonschema:"ACP session UUID"`
}

func registerGetACPSession(srv *mcpsdk.Server, deps ServerDeps) {
	mcpsdk.AddTool(srv,
		&mcpsdk.Tool{Name: "get_acp_session", Description: "Get a single ACP session by ID."},
		func(ctx context.Context, _ *mcpsdk.CallToolRequest, in getACPSessionIn) (*mcpsdk.CallToolResult, acpSessionDetail, error) {
			if err := CheckToolFromCtx(ctx, "get_acp_session"); err != nil {
				return nil, acpSessionDetail{}, err
			}
			c, err := deps.Caller.Resolve(ctx)
			if err != nil {
				return nil, acpSessionDetail{}, err
			}
			s, err := scopedACPSession(ctx, deps, c.UserID, c.IsAdmin, in.SessionID)
			if err != nil {
				return nil, acpSessionDetail{}, err
			}
			return nil, acpSessionDetailFrom(s), nil
		})
}

type listACPSessionsIn struct {
	All           bool   `json:"all,omitempty" jsonschema:"admin only: list sessions of all users"`
	RequirementID string `json:"requirement_id,omitempty" jsonschema:"filter by requirement UUID"`
}
type listACPSessionsOut struct {
	Sessions []acpSessionDetail `json:"sessions"`
}

func registerListACPSessions(srv *mcpsdk.Server, deps ServerDeps) {
	mcpsdk.AddTool(srv,
		&mcpsdk.Tool{Name: "list_acp_sessions", Description: "List ACP sessions of the caller (all=true for admins)."},
		func(ctx context.Context, _ *mcpsdk.CallToolRequest, in listACPSessionsIn) (*mcpsdk.CallToolResult, listACPSessionsOut, error) {
			if err := CheckToolFromCtx(ctx, "list_acp_sessions"); err != nil {
				return nil, listACPSessionsOut{}, err
			}
			c, err := deps.Caller.Resolve(ctx)
			if err != nil {
				return nil, listACPSessionsOut{}, err
			}
			f := ACPSessionFilter{All: in.All}
			if in.RequirementID != "" {
				rid, err := uuid.Parse(in.RequirementID)
				if err != nil {
					return nil, listACPSessionsOut{}, errs.Wrap(err, errs.CodeInvalidInput, "requirement_id parse")
				}
				f.RequirementID = &rid
			}
			ss, err := deps.ACP.ListSessions(ctx, c.UserID, c.IsAdmin, f)
			if err != nil {
				return nil, listACPSessionsOut{}, err
			}
			out := listACPSessionsOut{Sessions: make([]acpSessionDetail, 0, len(ss))}
			for i := range ss {
				// 与 list_projects 一致：scope 外的条目静默跳过。
				if err := CheckProjectFromCtx(ctx, ss[i].ProjectID); err != nil {
					continue
				}
				out.Sessions = append(out.Sessions, acpSessionDetailFrom(&ss[i]))
			}
			return nil, out, nil
		})
}

func registerTerminateACPSession(srv *mcpsdk.Server, deps ServerDeps) {
	mcpsdk.AddTool(srv,
		&mcpsdk.Tool{Name: "terminate_acp_session", Description: "Terminate a running ACP session."},
		func(ctx context.Context, _ *mcpsdk.CallToolRequest, in getACPSessionIn) (*mcpsdk.CallToolResult, okOut, error) {
			if err := CheckToolFromCtx(ctx, "terminate_acp_session"); err != nil {
				return nil, okOut{}, err
			}
			c, err := deps.Caller.Resolve(ctx)
			if err != nil {
				return nil, okOut{}, err
			}
			s, err := scopedACPSession(ctx, deps, c.UserID, c.IsAdmin, in.SessionID)
			if err != nil {
				return nil, okOut{}, err
			}
			if err := deps.ACP.TerminateSession(ctx, c.UserID, c.IsAdmin, s.ID); err != nil {
				return nil, okOut{}, err
			}
			return nil, okOut{OK: true}, nil
		})
}

// ===== list_pending_permissions =====

type permissionDetail struct {
	ID             string `json:"id"`
	SessionID      string `json:"session_id"`
	AgentRequestID string `json:"agent_request_id"`
	ToolName       string `json:"tool_name"`
	Status         string `json:"status"`
	CreatedAt      string `json:"created_at"`
}
type listPendingPermissionsOut struct {
	Permissions []permissionDetail `json:"permissions"`
}

func registerListPendingPermissions(srv *mcpsdk.Server, deps ServerDeps) {
	mcpsdk.AddTool(srv,
		&mcpsdk.Tool{Name: "list_pending_permissions", Description: "List pending permission requests of an ACP session (read-only; approval stays with humans in the web console)."},
		func(ctx context.Context, _ *mcpsdk.CallToolRequest, in getACPSessionIn) (*mcpsdk.CallToolResult, listPendingPermissionsOut, error) {
			if err := CheckToolFromCtx(ctx, "list_pending_permissions"); err != nil {
				return nil, listPendingPermissionsOut{}, err
			}
			c, err := deps.Caller.Resolve(ctx)
			if err != nil {
				return nil, listPendingPermissionsOut{}, err
			}
			s, err := scopedACPSession(ctx, deps, c.UserID, c.IsAdmin, in.SessionID)
			if err != nil {
				return nil, listPendingPermissionsOut{}, err
			}
			ps, err := deps.ACP.ListPendingPermissions(ctx, c.UserID, c.IsAdmin, s.ID)
			if err != nil {
				return nil, listPendingPermissionsOut{}, err
			}
			out := listPendingPermissionsOut{Permissions: make([]permissionDetail, 0, len(ps))}
			for _, p := range ps {
				out.Permissions = append(out.Permissions, permissionDetail{
					ID: p.ID.String(), SessionID: p.SessionID.String(),
					AgentRequestID: p.AgentRequestID, ToolName: p.ToolName,
					Status:    p.Status,
					CreatedAt: p.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
				})
			}
			return nil, out, nil
		})
}

// ===== list_pending_approvals (cross-session HITL inbox) =====

func registerListPendingApprovals(srv *mcpsdk.Server, deps ServerDeps) {
	mcpsdk.AddTool(srv,
		&mcpsdk.Tool{Name: "list_pending_approvals", Description: "List the caller's pending tool-permission requests across all their ACP sessions (read-only HITL inbox; approval stays with humans in the web console)."},
		func(ctx context.Context, _ *mcpsdk.CallToolRequest, _ struct{}) (*mcpsdk.CallToolResult, listPendingPermissionsOut, error) {
			if err := CheckToolFromCtx(ctx, "list_pending_approvals"); err != nil {
				return nil, listPendingPermissionsOut{}, err
			}
			c, err := deps.Caller.Resolve(ctx)
			if err != nil {
				return nil, listPendingPermissionsOut{}, err
			}
			ps, err := deps.ACP.ListPendingApprovals(ctx, c.UserID, c.IsAdmin)
			if err != nil {
				return nil, listPendingPermissionsOut{}, err
			}
			out := listPendingPermissionsOut{Permissions: make([]permissionDetail, 0, len(ps))}
			for _, p := range ps {
				out.Permissions = append(out.Permissions, permissionDetail{
					ID: p.ID.String(), SessionID: p.SessionID.String(),
					AgentRequestID: p.AgentRequestID, ToolName: p.ToolName,
					Status:    p.Status,
					CreatedAt: p.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
				})
			}
			return nil, out, nil
		})
}

// ===== get_acp_session_turns =====

type acpTurnDetail struct {
	TurnIndex   int    `json:"turn_index"`
	Status      string `json:"status"`
	StopReason  string `json:"stop_reason,omitempty"`
	UpdateCount int    `json:"update_count"`
	StartedAt   string `json:"started_at"`
	CompletedAt string `json:"completed_at,omitempty"`
}
type getACPSessionTurnsOut struct {
	Turns []acpTurnDetail `json:"turns"`
}

func registerGetACPSessionTurns(srv *mcpsdk.Server, deps ServerDeps) {
	mcpsdk.AddTool(srv,
		&mcpsdk.Tool{Name: "get_acp_session_turns", Description: "List the agent turns of an ACP session (read-only; turn_index, status, stop_reason, update_count, timestamps)."},
		func(ctx context.Context, _ *mcpsdk.CallToolRequest, in getACPSessionIn) (*mcpsdk.CallToolResult, getACPSessionTurnsOut, error) {
			if err := CheckToolFromCtx(ctx, "get_acp_session_turns"); err != nil {
				return nil, getACPSessionTurnsOut{}, err
			}
			c, err := deps.Caller.Resolve(ctx)
			if err != nil {
				return nil, getACPSessionTurnsOut{}, err
			}
			s, err := scopedACPSession(ctx, deps, c.UserID, c.IsAdmin, in.SessionID)
			if err != nil {
				return nil, getACPSessionTurnsOut{}, err
			}
			ts, err := deps.ACP.ListSessionTurns(ctx, c.UserID, c.IsAdmin, s.ID)
			if err != nil {
				return nil, getACPSessionTurnsOut{}, err
			}
			out := getACPSessionTurnsOut{Turns: make([]acpTurnDetail, 0, len(ts))}
			for _, t := range ts {
				d := acpTurnDetail{
					TurnIndex:   t.TurnIndex,
					Status:      t.Status,
					UpdateCount: t.UpdateCount,
					StartedAt:   t.StartedAt.Format("2006-01-02T15:04:05Z07:00"),
				}
				if t.StopReason != nil {
					d.StopReason = *t.StopReason
				}
				if t.CompletedAt != nil {
					d.CompletedAt = t.CompletedAt.Format("2006-01-02T15:04:05Z07:00")
				}
				out.Turns = append(out.Turns, d)
			}
			return nil, out, nil
		})
}

// ===== get_run_dashboard (run-history rollup) =====

type getRunDashboardIn struct {
	// 可选过滤；留空表示不按该维度过滤。from/to 为 RFC3339；留空走默认 30 天窗口。
	ProjectID     string `json:"project_id,omitempty"`
	RequirementID string `json:"requirement_id,omitempty"`
	From          string `json:"from,omitempty"`
	To            string `json:"to,omitempty"`
}

type runDayDetail struct {
	Day          string  `json:"day"`
	Total        int64   `json:"total"`
	Succeeded    int64   `json:"succeeded"`
	Crashed      int64   `json:"crashed"`
	TotalCostUSD float64 `json:"total_cost_usd"`
}

type getRunDashboardOut struct {
	From               string         `json:"from"`
	To                 string         `json:"to"`
	Total              int64          `json:"total"`
	Succeeded          int64          `json:"succeeded"`
	Crashed            int64          `json:"crashed"`
	Active             int64          `json:"active"`
	SuccessRate        float64        `json:"success_rate"`
	TotalCostUSD       float64        `json:"total_cost_usd"`
	TotalTokens        int64          `json:"total_tokens"`
	AvgDurationSeconds float64        `json:"avg_duration_seconds"`
	ByDay              []runDayDetail `json:"by_day"`
}

func registerGetRunDashboard(srv *mcpsdk.Server, deps ServerDeps) {
	mcpsdk.AddTool(srv,
		&mcpsdk.Tool{Name: "get_run_dashboard", Description: "Get a run-history rollup of ACP sessions (read-only): counts, success rate, total cost/tokens, avg duration, and a per-day series. Scoped to the caller's own sessions (admins see all). Optional filters: project_id, requirement_id, from/to (RFC3339)."},
		func(ctx context.Context, _ *mcpsdk.CallToolRequest, in getRunDashboardIn) (*mcpsdk.CallToolResult, getRunDashboardOut, error) {
			if err := CheckToolFromCtx(ctx, "get_run_dashboard"); err != nil {
				return nil, getRunDashboardOut{}, err
			}
			c, err := deps.Caller.Resolve(ctx)
			if err != nil {
				return nil, getRunDashboardOut{}, err
			}
			var di ACPRunDashboardInput
			if in.ProjectID != "" {
				id, perr := uuid.Parse(in.ProjectID)
				if perr != nil {
					return nil, getRunDashboardOut{}, errs.Wrap(perr, errs.CodeInvalidInput, "project_id parse")
				}
				di.ProjectID = &id
			}
			if in.RequirementID != "" {
				id, perr := uuid.Parse(in.RequirementID)
				if perr != nil {
					return nil, getRunDashboardOut{}, errs.Wrap(perr, errs.CodeInvalidInput, "requirement_id parse")
				}
				di.RequirementID = &id
			}
			if in.From != "" {
				t, terr := time.Parse(time.RFC3339, in.From)
				if terr != nil {
					return nil, getRunDashboardOut{}, errs.Wrap(terr, errs.CodeInvalidInput, "from parse")
				}
				di.From = t
			}
			if in.To != "" {
				t, terr := time.Parse(time.RFC3339, in.To)
				if terr != nil {
					return nil, getRunDashboardOut{}, errs.Wrap(terr, errs.CodeInvalidInput, "to parse")
				}
				di.To = t
			}
			d, err := deps.ACP.GetRunDashboard(ctx, c.UserID, c.IsAdmin, di)
			if err != nil {
				return nil, getRunDashboardOut{}, err
			}
			out := getRunDashboardOut{
				From:               d.From.Format("2006-01-02T15:04:05Z07:00"),
				To:                 d.To.Format("2006-01-02T15:04:05Z07:00"),
				Total:              d.Total,
				Succeeded:          d.Succeeded,
				Crashed:            d.Crashed,
				Active:             d.Active,
				SuccessRate:        d.SuccessRate,
				TotalCostUSD:       d.TotalCostUSD,
				TotalTokens:        d.TotalTokens,
				AvgDurationSeconds: d.AvgDurationSeconds,
				ByDay:              make([]runDayDetail, 0, len(d.ByDay)),
			}
			for _, r := range d.ByDay {
				out.ByDay = append(out.ByDay, runDayDetail{
					Day:          r.Day.Format("2006-01-02T15:04:05Z07:00"),
					Total:        r.Total,
					Succeeded:    r.Succeeded,
					Crashed:      r.Crashed,
					TotalCostUSD: r.TotalCostUSD,
				})
			}
			return nil, out, nil
		})
}
