package mcp

import (
	"context"

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

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

// registerChatTools 注册 chat 相关 MCP 工具：会话 CRUD、发消息、消息查询、
// prompt 模板 CRUD、模型列表。
//
// 范围边界（spec 决策）：LLM endpoint/model 管理、用量统计、附件上传、
// 流式接收（SSE）与 cancel/retry（强依赖流式上下文）不经 MCP 暴露。
// send_message 的 assistant 回复异步生成，调用方用 list_recent_messages 轮询。
func registerChatTools(srv *mcpsdk.Server, deps ServerDeps) {
	registerListRecentMessages(srv, deps)
	registerListConversations(srv, deps)
	registerGetConversation(srv, deps)
	registerCreateConversation(srv, deps)
	registerUpdateConversationTitle(srv, deps)
	registerDeleteConversation(srv, deps)
	registerSendMessage(srv, deps)
	registerListPromptTemplates(srv, deps)
	registerCreatePromptTemplate(srv, deps)
	registerUpdatePromptTemplate(srv, deps)
	registerDeletePromptTemplate(srv, deps)
	registerListModels(srv, deps)
}

// ===== 共用 DTO / helper =====

type conversationDetail struct {
	ID            string `json:"id"`
	Title         string `json:"title,omitempty"`
	ModelID       string `json:"model_id"`
	ProjectID     string `json:"project_id,omitempty"`
	WorkspaceID   string `json:"workspace_id,omitempty"`
	IssueID       string `json:"issue_id,omitempty"`
	RequirementID string `json:"requirement_id,omitempty"`
	CreatedAt     string `json:"created_at"`
	UpdatedAt     string `json:"updated_at"`
}

func conversationDetailFrom(cv *chat.Conversation) conversationDetail {
	d := conversationDetail{
		ID: cv.ID.String(), ModelID: cv.ModelID.String(),
		CreatedAt: cv.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
		UpdatedAt: cv.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"),
	}
	if cv.Title != nil {
		d.Title = *cv.Title
	}
	if cv.ProjectID != nil {
		d.ProjectID = cv.ProjectID.String()
	}
	if cv.WorkspaceID != nil {
		d.WorkspaceID = cv.WorkspaceID.String()
	}
	if cv.IssueID != nil {
		d.IssueID = cv.IssueID.String()
	}
	if cv.RequirementID != nil {
		d.RequirementID = cv.RequirementID.String()
	}
	return d
}

// mountRefs 是会话挂载维度的 UUID 集合（nil = 未挂载）。
type mountRefs struct {
	ProjectID     *uuid.UUID
	WorkspaceID   *uuid.UUID
	IssueID       *uuid.UUID
	RequirementID *uuid.UUID
}

// resolveMountRefs 把 MCP 友好的挂载入参（project_slug + 编号）解析为 UUID，
// 并完成 project scope 校验。requirement/issue 编号必须随 project_slug 一起给。
func resolveMountRefs(ctx context.Context, deps ServerDeps, c project.Caller,
	projectSlug, workspaceID string, requirementNumber, issueNumber int) (mountRefs, error) {
	var refs mountRefs
	if projectSlug == "" && (requirementNumber > 0 || issueNumber > 0) {
		return refs, errs.New(errs.CodeInvalidInput,
			"project_slug is required when requirement_number/issue_number is set")
	}
	if projectSlug != "" {
		p, err := deps.Projects.Get(ctx, c, projectSlug)
		if err != nil {
			return refs, err
		}
		if err := CheckProjectFromCtx(ctx, p.ID); err != nil {
			return refs, err
		}
		pid := p.ID
		refs.ProjectID = &pid
		if requirementNumber > 0 {
			r, err := deps.Reqs.Get(ctx, c, projectSlug, requirementNumber)
			if err != nil {
				return refs, err
			}
			rid := r.ID
			refs.RequirementID = &rid
		}
		if issueNumber > 0 {
			i, err := deps.Issues.Get(ctx, c, projectSlug, issueNumber)
			if err != nil {
				return refs, err
			}
			iid := i.ID
			refs.IssueID = &iid
		}
	}
	if workspaceID != "" {
		w, _, err := scopedWorkspace(ctx, deps, c, workspaceID)
		if err != nil {
			return refs, err
		}
		wid := w.ID
		refs.WorkspaceID = &wid
	}
	return refs, nil
}

// scopedConversation 解析 conversation_id、取会话（service 层含 ownership 校验）
// 并做项目 scope 校验：ProjectID 为 nil 的个人会话不受 project scope 约束。
// limit 控制随会话返回的最近消息条数，仅 get_conversation 需要，其余入口传 1。
func scopedConversation(ctx context.Context, deps ServerDeps, userID uuid.UUID,
	conversationID string, limit int) (*chat.Conversation, []chat.Message, error) {
	convID, err := uuid.Parse(conversationID)
	if err != nil {
		return nil, nil, errs.Wrap(err, errs.CodeInvalidInput, "conversation_id parse")
	}
	cv, msgs, err := deps.Conversations.Get(ctx, userID, convID, limit)
	if err != nil {
		return nil, nil, err
	}
	if cv.ProjectID != nil {
		if err := CheckProjectFromCtx(ctx, *cv.ProjectID); err != nil {
			return nil, nil, err
		}
	}
	return cv, msgs, nil
}

// ===== list_recent_messages =====

type listRecentMessagesIn struct {
	ConversationID string `json:"conversation_id" jsonschema:"UUID of the conversation"`
	BeforeID       *int64 `json:"before_id,omitempty" jsonschema:"cursor: return messages with ID < this value"`
	Limit          int    `json:"limit,omitempty" jsonschema:"max messages to return (default 50, max 200)"`
}
type messageSummary struct {
	ID        int64  `json:"id"`
	Role      string `json:"role"`
	Content   string `json:"content"`
	Status    string `json:"status"`
	CreatedAt string `json:"created_at"`
}
type listRecentMessagesOut struct {
	Messages []messageSummary `json:"messages"`
}

func messageSummaryFrom(m chat.Message) messageSummary {
	return messageSummary{
		ID:        m.ID,
		Role:      string(m.Role),
		Content:   m.Content,
		Status:    string(m.Status),
		CreatedAt: m.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
	}
}

func registerListRecentMessages(srv *mcpsdk.Server, deps ServerDeps) {
	mcpsdk.AddTool(srv,
		&mcpsdk.Tool{Name: "list_recent_messages", Description: "List recent chat messages in a conversation."},
		func(ctx context.Context, _ *mcpsdk.CallToolRequest, in listRecentMessagesIn) (*mcpsdk.CallToolResult, listRecentMessagesOut, error) {
			if err := CheckToolFromCtx(ctx, "list_recent_messages"); err != nil {
				return nil, listRecentMessagesOut{}, err
			}
			c, err := deps.Caller.Resolve(ctx)
			if err != nil {
				return nil, listRecentMessagesOut{}, err
			}

			cv, _, err := scopedConversation(ctx, deps, c.UserID, in.ConversationID, 1)
			if err != nil {
				return nil, listRecentMessagesOut{}, err
			}

			limit := in.Limit
			if limit <= 0 {
				limit = 50
			}
			if limit > 200 {
				limit = 200
			}

			msgs, err := deps.Messages.ListMessages(ctx, c.UserID, cv.ID, in.BeforeID, limit)
			if err != nil {
				return nil, listRecentMessagesOut{}, err
			}

			out := listRecentMessagesOut{Messages: make([]messageSummary, 0, len(msgs))}
			for _, m := range msgs {
				out.Messages = append(out.Messages, messageSummaryFrom(m))
			}
			return nil, out, nil
		})
}

// ===== list_conversations =====

type listConversationsIn struct {
	ProjectSlug       string `json:"project_slug,omitempty" jsonschema:"filter by project"`
	WorkspaceID       string `json:"workspace_id,omitempty" jsonschema:"filter by workspace UUID"`
	RequirementNumber int    `json:"requirement_number,omitempty" jsonschema:"filter by requirement (needs project_slug)"`
	IssueNumber       int    `json:"issue_number,omitempty" jsonschema:"filter by issue (needs project_slug)"`
	TitleQuery        string `json:"title_query,omitempty" jsonschema:"fuzzy title filter"`
	BeforeID          string `json:"before_id,omitempty" jsonschema:"cursor: conversation UUID"`
	Limit             int    `json:"limit,omitempty" jsonschema:"max conversations to return (default 50, max 200)"`
}
type listConversationsOut struct {
	Conversations []conversationDetail `json:"conversations"`
}

func registerListConversations(srv *mcpsdk.Server, deps ServerDeps) {
	mcpsdk.AddTool(srv,
		&mcpsdk.Tool{Name: "list_conversations", Description: "List the caller's chat conversations."},
		func(ctx context.Context, _ *mcpsdk.CallToolRequest, in listConversationsIn) (*mcpsdk.CallToolResult, listConversationsOut, error) {
			if err := CheckToolFromCtx(ctx, "list_conversations"); err != nil {
				return nil, listConversationsOut{}, err
			}
			c, err := deps.Caller.Resolve(ctx)
			if err != nil {
				return nil, listConversationsOut{}, err
			}
			refs, err := resolveMountRefs(ctx, deps, c, in.ProjectSlug, in.WorkspaceID, in.RequirementNumber, in.IssueNumber)
			if err != nil {
				return nil, listConversationsOut{}, err
			}
			filter := chat.ConversationFilter{
				ProjectID: refs.ProjectID, WorkspaceID: refs.WorkspaceID,
				IssueID: refs.IssueID, RequirementID: refs.RequirementID,
				TitleQuery: in.TitleQuery,
			}
			limit := in.Limit
			if limit <= 0 {
				limit = 50
			}
			if limit > 200 {
				limit = 200
			}
			filter.Limit = limit
			if in.BeforeID != "" {
				bid, err := uuid.Parse(in.BeforeID)
				if err != nil {
					return nil, listConversationsOut{}, errs.Wrap(err, errs.CodeInvalidInput, "before_id parse")
				}
				filter.BeforeID = &bid
			}
			cvs, err := deps.Conversations.List(ctx, c.UserID, filter)
			if err != nil {
				return nil, listConversationsOut{}, err
			}
			out := listConversationsOut{Conversations: make([]conversationDetail, 0, len(cvs))}
			for i := range cvs {
				// 与 list_acp_sessions 一致：scope 外项目挂载的会话静默跳过；
				// ProjectID 为 nil 的个人会话不受 project scope 约束。
				if cvs[i].ProjectID != nil {
					if err := CheckProjectFromCtx(ctx, *cvs[i].ProjectID); err != nil {
						continue
					}
				}
				out.Conversations = append(out.Conversations, conversationDetailFrom(&cvs[i]))
			}
			return nil, out, nil
		})
}

// ===== get_conversation =====

type getConversationIn struct {
	ConversationID string `json:"conversation_id" jsonschema:"UUID of the conversation"`
	Limit          int    `json:"limit,omitempty" jsonschema:"max recent messages to include (default 50, max 200)"`
}
type getConversationOut struct {
	Conversation conversationDetail `json:"conversation"`
	Messages     []messageSummary   `json:"messages"`
}

func registerGetConversation(srv *mcpsdk.Server, deps ServerDeps) {
	mcpsdk.AddTool(srv,
		&mcpsdk.Tool{Name: "get_conversation", Description: "Get a conversation with its recent messages."},
		func(ctx context.Context, _ *mcpsdk.CallToolRequest, in getConversationIn) (*mcpsdk.CallToolResult, getConversationOut, error) {
			if err := CheckToolFromCtx(ctx, "get_conversation"); err != nil {
				return nil, getConversationOut{}, err
			}
			c, err := deps.Caller.Resolve(ctx)
			if err != nil {
				return nil, getConversationOut{}, err
			}
			limit := in.Limit
			if limit <= 0 {
				limit = 50
			}
			if limit > 200 {
				limit = 200
			}
			cv, msgs, err := scopedConversation(ctx, deps, c.UserID, in.ConversationID, limit)
			if err != nil {
				return nil, getConversationOut{}, err
			}
			out := getConversationOut{
				Conversation: conversationDetailFrom(cv),
				Messages:     make([]messageSummary, 0, len(msgs)),
			}
			for _, m := range msgs {
				out.Messages = append(out.Messages, messageSummaryFrom(m))
			}
			return nil, out, nil
		})
}

// ===== create_conversation =====

type createConversationIn struct {
	ModelID           string `json:"model_id" jsonschema:"LLM model UUID (see list_models)"`
	TemplateID        string `json:"template_id,omitempty" jsonschema:"prompt template UUID"`
	SystemPrompt      string `json:"system_prompt,omitempty" jsonschema:"custom system prompt (overrides template)"`
	ProjectSlug       string `json:"project_slug,omitempty" jsonschema:"mount to a project"`
	WorkspaceID       string `json:"workspace_id,omitempty" jsonschema:"mount to a workspace UUID"`
	RequirementNumber int    `json:"requirement_number,omitempty" jsonschema:"mount to a requirement (needs project_slug)"`
	IssueNumber       int    `json:"issue_number,omitempty" jsonschema:"mount to an issue (needs project_slug)"`
}
type conversationCreateOut struct {
	OK           bool               `json:"ok"`
	Conversation conversationDetail `json:"conversation"`
}

func registerCreateConversation(srv *mcpsdk.Server, deps ServerDeps) {
	mcpsdk.AddTool(srv,
		&mcpsdk.Tool{Name: "create_conversation", Description: "Create a new chat conversation."},
		func(ctx context.Context, _ *mcpsdk.CallToolRequest, in createConversationIn) (*mcpsdk.CallToolResult, conversationCreateOut, error) {
			if err := CheckToolFromCtx(ctx, "create_conversation"); err != nil {
				return nil, conversationCreateOut{}, err
			}
			c, err := deps.Caller.Resolve(ctx)
			if err != nil {
				return nil, conversationCreateOut{}, err
			}
			modelID, err := uuid.Parse(in.ModelID)
			if err != nil {
				return nil, conversationCreateOut{}, errs.Wrap(err, errs.CodeInvalidInput, "model_id parse")
			}
			refs, err := resolveMountRefs(ctx, deps, c, in.ProjectSlug, in.WorkspaceID, in.RequirementNumber, in.IssueNumber)
			if err != nil {
				return nil, conversationCreateOut{}, err
			}
			input := chat.CreateConversationInput{
				ModelID:   modelID,
				ProjectID: refs.ProjectID, WorkspaceID: refs.WorkspaceID,
				IssueID: refs.IssueID, RequirementID: refs.RequirementID,
			}
			if in.TemplateID != "" {
				tid, err := uuid.Parse(in.TemplateID)
				if err != nil {
					return nil, conversationCreateOut{}, errs.Wrap(err, errs.CodeInvalidInput, "template_id parse")
				}
				input.TemplateID = &tid
			}
			if in.SystemPrompt != "" {
				input.SystemPrompt = &in.SystemPrompt
			}
			cv, err := deps.Conversations.Create(ctx, c.UserID, input)
			if err != nil {
				return nil, conversationCreateOut{}, err
			}
			return nil, conversationCreateOut{OK: true, Conversation: conversationDetailFrom(cv)}, nil
		})
}

// ===== update_conversation_title =====

type updateConversationTitleIn struct {
	ConversationID string `json:"conversation_id" jsonschema:"UUID of the conversation"`
	Title          string `json:"title" jsonschema:"new title (non-empty)"`
}

func registerUpdateConversationTitle(srv *mcpsdk.Server, deps ServerDeps) {
	mcpsdk.AddTool(srv,
		&mcpsdk.Tool{Name: "update_conversation_title", Description: "Rename a conversation."},
		func(ctx context.Context, _ *mcpsdk.CallToolRequest, in updateConversationTitleIn) (*mcpsdk.CallToolResult, okOut, error) {
			if err := CheckToolFromCtx(ctx, "update_conversation_title"); err != nil {
				return nil, okOut{}, err
			}
			c, err := deps.Caller.Resolve(ctx)
			if err != nil {
				return nil, okOut{}, err
			}
			cv, _, err := scopedConversation(ctx, deps, c.UserID, in.ConversationID, 1)
			if err != nil {
				return nil, okOut{}, err
			}
			if err := deps.Conversations.UpdateTitle(ctx, c.UserID, cv.ID, in.Title); err != nil {
				return nil, okOut{}, err
			}
			return nil, okOut{OK: true}, nil
		})
}

// ===== delete_conversation =====

type deleteConversationIn struct {
	ConversationID string `json:"conversation_id" jsonschema:"UUID of the conversation"`
}

func registerDeleteConversation(srv *mcpsdk.Server, deps ServerDeps) {
	mcpsdk.AddTool(srv,
		&mcpsdk.Tool{Name: "delete_conversation", Description: "Soft-delete a conversation."},
		func(ctx context.Context, _ *mcpsdk.CallToolRequest, in deleteConversationIn) (*mcpsdk.CallToolResult, okOut, error) {
			if err := CheckToolFromCtx(ctx, "delete_conversation"); err != nil {
				return nil, okOut{}, err
			}
			c, err := deps.Caller.Resolve(ctx)
			if err != nil {
				return nil, okOut{}, err
			}
			cv, _, err := scopedConversation(ctx, deps, c.UserID, in.ConversationID, 1)
			if err != nil {
				return nil, okOut{}, err
			}
			if err := deps.Conversations.SoftDelete(ctx, c.UserID, cv.ID); err != nil {
				return nil, okOut{}, err
			}
			return nil, okOut{OK: true}, nil
		})
}

// ===== send_message =====

type sendMessageIn struct {
	ConversationID string `json:"conversation_id" jsonschema:"UUID of the conversation"`
	Content        string `json:"content" jsonschema:"message content (non-empty)"`
}
type sendMessageOut struct {
	OK                 bool  `json:"ok"`
	UserMessageID      int64 `json:"user_message_id"`
	AssistantMessageID int64 `json:"assistant_message_id"`
}

func registerSendMessage(srv *mcpsdk.Server, deps ServerDeps) {
	mcpsdk.AddTool(srv,
		&mcpsdk.Tool{Name: "send_message", Description: "Send a user message to a conversation. The assistant reply is generated asynchronously; poll list_recent_messages for the result (attachments are not supported via MCP)."},
		func(ctx context.Context, _ *mcpsdk.CallToolRequest, in sendMessageIn) (*mcpsdk.CallToolResult, sendMessageOut, error) {
			if err := CheckToolFromCtx(ctx, "send_message"); err != nil {
				return nil, sendMessageOut{}, err
			}
			c, err := deps.Caller.Resolve(ctx)
			if err != nil {
				return nil, sendMessageOut{}, err
			}
			cv, _, err := scopedConversation(ctx, deps, c.UserID, in.ConversationID, 1)
			if err != nil {
				return nil, sendMessageOut{}, err
			}
			userMsgID, assistantMsgID, err := deps.Messages.Send(ctx, c.UserID, cv.ID, in.Content, nil)
			if err != nil {
				return nil, sendMessageOut{}, err
			}
			return nil, sendMessageOut{OK: true, UserMessageID: userMsgID, AssistantMessageID: assistantMsgID}, nil
		})
}

// ===== list_prompt_templates =====

type templateDetail struct {
	ID      string `json:"id"`
	Name    string `json:"name"`
	Scope   string `json:"scope"`
	Content string `json:"content"`
}
type listPromptTemplatesOut struct {
	Templates []templateDetail `json:"templates"`
}

func registerListPromptTemplates(srv *mcpsdk.Server, deps ServerDeps) {
	mcpsdk.AddTool(srv,
		&mcpsdk.Tool{Name: "list_prompt_templates", Description: "List prompt templates visible to the caller (system + own)."},
		func(ctx context.Context, _ *mcpsdk.CallToolRequest, _ struct{}) (*mcpsdk.CallToolResult, listPromptTemplatesOut, error) {
			if err := CheckToolFromCtx(ctx, "list_prompt_templates"); err != nil {
				return nil, listPromptTemplatesOut{}, err
			}
			c, err := deps.Caller.Resolve(ctx)
			if err != nil {
				return nil, listPromptTemplatesOut{}, err
			}
			ts, err := deps.Templates.List(ctx, c.UserID)
			if err != nil {
				return nil, listPromptTemplatesOut{}, err
			}
			out := listPromptTemplatesOut{Templates: make([]templateDetail, 0, len(ts))}
			for _, t := range ts {
				out.Templates = append(out.Templates, templateDetail{
					ID: t.ID.String(), Name: t.Name, Scope: string(t.Scope), Content: t.Content,
				})
			}
			return nil, out, nil
		})
}

// ===== create_prompt_template =====

type createPromptTemplateIn struct {
	Name    string `json:"name" jsonschema:"template name (non-empty)"`
	Content string `json:"content" jsonschema:"template content (non-empty)"`
	Scope   string `json:"scope,omitempty" jsonschema:"system|user (default user; system requires admin)"`
}
type templateCreateOut struct {
	OK       bool           `json:"ok"`
	Template templateDetail `json:"template"`
}

func registerCreatePromptTemplate(srv *mcpsdk.Server, deps ServerDeps) {
	mcpsdk.AddTool(srv,
		&mcpsdk.Tool{Name: "create_prompt_template", Description: "Create a prompt template. scope=system requires admin."},
		func(ctx context.Context, _ *mcpsdk.CallToolRequest, in createPromptTemplateIn) (*mcpsdk.CallToolResult, templateCreateOut, error) {
			if err := CheckToolFromCtx(ctx, "create_prompt_template"); err != nil {
				return nil, templateCreateOut{}, err
			}
			c, err := deps.Caller.Resolve(ctx)
			if err != nil {
				return nil, templateCreateOut{}, err
			}
			scope := chat.TemplateScope(in.Scope)
			if scope == "" {
				scope = chat.TemplateScopeUser
			}
			// 与 HTTP handler 对齐：system 模板仅 admin 可建（service 层不做此校验）。
			if scope == chat.TemplateScopeSystem && !c.IsAdmin {
				return nil, templateCreateOut{}, errs.New(errs.CodeForbidden, "admin only: system-scoped template")
			}
			t, err := deps.Templates.Create(ctx, c.UserID, in.Name, in.Content, scope)
			if err != nil {
				return nil, templateCreateOut{}, err
			}
			return nil, templateCreateOut{OK: true, Template: templateDetail{
				ID: t.ID.String(), Name: t.Name, Scope: string(t.Scope), Content: t.Content,
			}}, nil
		})
}

// ===== update_prompt_template =====

type updatePromptTemplateIn struct {
	TemplateID string `json:"template_id" jsonschema:"template UUID"`
	Name       string `json:"name" jsonschema:"new name (non-empty)"`
	Content    string `json:"content" jsonschema:"new content (non-empty)"`
}

func registerUpdatePromptTemplate(srv *mcpsdk.Server, deps ServerDeps) {
	mcpsdk.AddTool(srv,
		&mcpsdk.Tool{Name: "update_prompt_template", Description: "Update a prompt template (owner or system template)."},
		func(ctx context.Context, _ *mcpsdk.CallToolRequest, in updatePromptTemplateIn) (*mcpsdk.CallToolResult, okOut, error) {
			if err := CheckToolFromCtx(ctx, "update_prompt_template"); err != nil {
				return nil, okOut{}, err
			}
			c, err := deps.Caller.Resolve(ctx)
			if err != nil {
				return nil, okOut{}, err
			}
			tid, err := uuid.Parse(in.TemplateID)
			if err != nil {
				return nil, okOut{}, errs.Wrap(err, errs.CodeInvalidInput, "template_id parse")
			}
			if err := deps.Templates.Update(ctx, c.UserID, c.IsAdmin, tid, in.Name, in.Content); err != nil {
				return nil, okOut{}, err
			}
			return nil, okOut{OK: true}, nil
		})
}

// ===== delete_prompt_template =====

type deletePromptTemplateIn struct {
	TemplateID string `json:"template_id" jsonschema:"template UUID"`
}

func registerDeletePromptTemplate(srv *mcpsdk.Server, deps ServerDeps) {
	mcpsdk.AddTool(srv,
		&mcpsdk.Tool{Name: "delete_prompt_template", Description: "Delete a prompt template (owner or system template)."},
		func(ctx context.Context, _ *mcpsdk.CallToolRequest, in deletePromptTemplateIn) (*mcpsdk.CallToolResult, okOut, error) {
			if err := CheckToolFromCtx(ctx, "delete_prompt_template"); err != nil {
				return nil, okOut{}, err
			}
			c, err := deps.Caller.Resolve(ctx)
			if err != nil {
				return nil, okOut{}, err
			}
			tid, err := uuid.Parse(in.TemplateID)
			if err != nil {
				return nil, okOut{}, errs.Wrap(err, errs.CodeInvalidInput, "template_id parse")
			}
			if err := deps.Templates.Delete(ctx, c.UserID, c.IsAdmin, tid); err != nil {
				return nil, okOut{}, err
			}
			return nil, okOut{OK: true}, nil
		})
}

// ===== list_models =====

type listModelsIn struct {
	IncludeDisabled bool `json:"include_disabled,omitempty" jsonschema:"include disabled models (default false)"`
}
type modelSummary struct {
	ID              string `json:"id"`
	ModelID         string `json:"model_id"`
	DisplayName     string `json:"display_name"`
	ContextWindow   int    `json:"context_window"`
	MaxOutputTokens int    `json:"max_output_tokens"`
	Enabled         bool   `json:"enabled"`
}
type listModelsOut struct {
	Models []modelSummary `json:"models"`
}

func registerListModels(srv *mcpsdk.Server, deps ServerDeps) {
	mcpsdk.AddTool(srv,
		&mcpsdk.Tool{Name: "list_models", Description: "List available LLM models (for create_conversation)."},
		func(ctx context.Context, _ *mcpsdk.CallToolRequest, in listModelsIn) (*mcpsdk.CallToolResult, listModelsOut, error) {
			if err := CheckToolFromCtx(ctx, "list_models"); err != nil {
				return nil, listModelsOut{}, err
			}
			if _, err := deps.Caller.Resolve(ctx); err != nil {
				return nil, listModelsOut{}, err
			}
			ms, err := deps.Endpoints.ListModels(ctx, !in.IncludeDisabled)
			if err != nil {
				return nil, listModelsOut{}, err
			}
			out := listModelsOut{Models: make([]modelSummary, 0, len(ms))}
			for _, m := range ms {
				out.Models = append(out.Models, modelSummary{
					ID: m.ID.String(), ModelID: m.ModelID, DisplayName: m.DisplayName,
					ContextWindow: m.ContextWindow, MaxOutputTokens: m.MaxOutputTokens,
					Enabled: m.Enabled,
				})
			}
			return nil, out, nil
		})
}
