package toolx

import (
	"strings"

	"cs-agent/internal/pkg/enums"
)

type ToolSpec struct {
	Code          string
	ServerCode    string
	Name          string
	Title         string
	Description   string
	SourceType    enums.ToolSourceType
	AutoInjected  bool
	DirectAccess  bool
	RuntimeStatic bool
	Aliases       []string
	Appendix      string
}

type ToolMetadata struct {
	ToolCode   string
	ServerCode string
	ToolName   string
	SourceType enums.ToolSourceType
}

var (
	BuiltinToolSearch = ToolSpec{
		Code:         "builtin/tool_search",
		ServerCode:   "builtin",
		Name:         "tool_search",
		Title:        "搜索并调用动态工具",
		Description:  "用于搜索当前允许使用的 MCP 工具，并在确认目标 toolCode 后动态调用该工具。适合处理长尾工具，不应替代固定内置流程工具。",
		SourceType:   enums.ToolSourceTypeBuiltin,
		AutoInjected: true,
		DirectAccess: true,
		Appendix: strings.TrimSpace(`
当你需要使用长尾 MCP 能力时，优先使用 tool_search 工具，并遵守以下规则：
1. 先调用 tool_search 搜索需要的动态工具，再继续使用已选中的真实工具。
2. 不要假设所有长尾工具一开始就可见；只有被 tool_search 选中的工具，后续模型调用才会暴露出来。
3. 如果当前已有固定内置工具可以完成任务，优先使用固定工具，不要滥用 tool_search。
`),
	}
	BuiltinSkill = ToolSpec{
		Code:         "builtin/skill",
		ServerCode:   "builtin",
		Name:         "skill",
		Title:        "加载专项技能说明",
		Description:  "用于按需加载当前 Agent 可用的专项技能说明文档，适合在需要专项处理规则时再注入上下文。",
		SourceType:   enums.ToolSourceTypeBuiltin,
		AutoInjected: true,
	}
	GraphTriageServiceRequest = ToolSpec{
		Code:          "graph/triage_service_request",
		ServerCode:    "graph",
		Name:          "triage_service_request",
		Title:         "升级分流判断",
		Description:   "Graph Tool。用于综合分析当前对话，判断应继续解答、整理工单草稿还是转人工，并在需要建单时一并整理工单草稿。",
		SourceType:    enums.ToolSourceTypeGraph,
		RuntimeStatic: true,
		Appendix: strings.TrimSpace(`
当你需要判断“继续解答 / 建单 / 转人工”这类复杂升级路径时，优先先调用 triage_service_request 这个 Graph Tool，并遵守以下规则：
1. 该工具会综合当前对话输出 recommendedAction，并在需要建单时附带 ticketDraft。
2. 如果 recommendedAction=continue_answering，则优先继续澄清或解答，不要直接升级。
3. 如果 recommendedAction=prepare_ticket，则优先使用 ticketDraft 或继续补充缺失字段，再调用 create_ticket_with_confirmation。
4. 如果 recommendedAction=handoff_to_human，则确认理由充分后再调用 handoff_to_human。
5. 当升级路径不明确时，优先使用该工具，而不是直接凭主 prompt 做复杂分流判断。
`),
	}
	GraphAnalyzeConversation = ToolSpec{
		Code:          "graph/analyze_conversation",
		ServerCode:    "graph",
		Name:          "analyze_conversation",
		Title:         "分析对话风险与摘要",
		Description:   "Graph Tool。用于整理当前对话摘要、识别风险信号，并给出继续解答、建单或转人工的建议。",
		SourceType:    enums.ToolSourceTypeGraph,
		RuntimeStatic: true,
		Appendix: strings.TrimSpace(`
当对话可能涉及投诉升级、退款赔偿、明显负面情绪、是否要建单、是否要转人工等复杂判断时，优先调用 analyze_conversation 这个 Graph Tool，并遵守以下规则：
1. 该工具用于输出结构化摘要、风险信号和下一步建议，不代表实际已经建单或转人工。
2. 如果工具建议为 handoff_to_human，应先确认是否满足转人工条件，再考虑调用 handoff_to_human。
3. 如果工具建议为 prepare_ticket，应优先调用 prepare_ticket_draft 或继续补充信息，而不是直接建单。
4. 如果工具建议为 continue_answering，优先继续澄清和解答，不要过早升级动作。
`),
	}
	GraphPrepareTicketDraft = ToolSpec{
		Code:          "graph/prepare_ticket_draft",
		ServerCode:    "graph",
		Name:          "prepare_ticket_draft",
		Title:         "整理工单草稿",
		Description:   "Graph Tool。用于根据当前会话和已收集信息整理工单草稿，输出建议标题、描述、缺失字段和追问建议。",
		SourceType:    enums.ToolSourceTypeGraph,
		RuntimeStatic: true,
		Appendix: strings.TrimSpace(`
当用户已经表达了建单、投诉、报障、售后处理等诉求，但工单标题、描述或问题整理还比较散乱时，优先调用 prepare_ticket_draft 这个 Graph Tool，并遵守以下规则：
1. 该工具用于整理工单草稿，会返回建议标题、建议描述、缺失字段和追问建议。
2. 如果工具返回 ready=false，优先根据 missingFields 和 followUpQuestions 继续追问，不要直接创建工单。
3. 如果工具返回 ready=true，再结合结果考虑调用 create_ticket_with_confirmation。
4. 该工具用于“整理草稿”，不代表已经创建工单。
`),
	}
	GraphCreateTicketConfirm = ToolSpec{
		Code:          "graph/create_ticket_with_confirmation",
		ServerCode:    "graph",
		Name:          "create_ticket_with_confirmation",
		Title:         "创建工单确认流程",
		Description:   "Graph Tool。用于封装建单参数整理、用户确认、真正建单和结果返回的确定性流程。",
		SourceType:    enums.ToolSourceTypeGraph,
		DirectAccess:  true,
		RuntimeStatic: true,
		Aliases:       []string{"builtin/create_ticket_with_confirmation"},
		Appendix: strings.TrimSpace(`
你可以在确认信息充分后调用 create_ticket_with_confirmation 这个 Graph Tool 来创建工单，但必须遵守以下规则：
1. 只有在用户明确表达希望提交工单、投诉、报障、售后处理等诉求时，才考虑调用该工具。
2. 调用前你必须已经整理出清晰的工单标题和问题描述；如果信息还比较散乱，优先先调用 prepare_ticket_draft 或继续追问，不要过早调用。
3. 一旦准备创建工单，必须调用 create_ticket_with_confirmation 工具，禁止直接口头宣称“已经创建工单”。
4. 该 Graph Tool 会先向用户发起确认。用户确认后才会真正创建工单；用户取消则结束本次建单流程。
5. 如果用户只是咨询、抱怨或泛泛表达不满，但没有明确要求建单，优先继续澄清，不要主动创建工单。
`),
	}
	GraphHandoffConversation = ToolSpec{
		Code:          "graph/handoff_to_human",
		ServerCode:    "graph",
		Name:          "handoff_to_human",
		Title:         "转人工确认流程",
		Description:   "Graph Tool。用于封装转人工原因整理、用户确认、真正转人工和结果返回的确定性流程。",
		SourceType:    enums.ToolSourceTypeGraph,
		DirectAccess:  true,
		RuntimeStatic: true,
		Appendix: strings.TrimSpace(`
你可以在确认需要人工介入后调用 handoff_to_human 这个 Graph Tool 来转人工，但必须遵守以下规则：
1. 只有在用户明确要求人工客服，或你已经判断该问题必须由人工继续处理时，才调用该工具。
2. 调用前先尽量整理清楚转人工原因；如果理由含糊，先追问或澄清，不要直接转人工。
3. 一旦决定转人工，必须调用 handoff_to_human 工具，禁止只在回复里口头说“我帮你转人工了”。
4. 该 Graph Tool 会先向用户发起确认。用户确认后才会真正转人工；用户取消则结束本次转人工流程。
5. 如果问题仍可由当前对话继续解决，优先继续解答，不要过早转人工。
6. 如果工具返回 terminal=true 且 shouldRetry=false，说明转人工流程已经结束，禁止重复调用该工具。
`),
	}
	RegisteredToolSpecs = []ToolSpec{
		BuiltinToolSearch,
		BuiltinSkill,
		GraphTriageServiceRequest,
		GraphAnalyzeConversation,
		GraphPrepareTicketDraft,
		GraphCreateTicketConfirm,
		GraphHandoffConversation,
	}
)

var (
	toolSpecByCode       = buildToolSpecByCode()
	toolSpecByName       = buildToolSpecByName()
	toolAliasToCanonical = buildToolAliasToCanonical()
)

func buildToolSpecByCode() map[string]ToolSpec {
	ret := make(map[string]ToolSpec, len(RegisteredToolSpecs))
	for _, spec := range RegisteredToolSpecs {
		if strings.TrimSpace(spec.Code) == "" {
			continue
		}
		ret[spec.Code] = spec
	}
	return ret
}

func buildToolAliasToCanonical() map[string]string {
	ret := make(map[string]string)
	for _, spec := range RegisteredToolSpecs {
		for _, alias := range spec.Aliases {
			alias = strings.TrimSpace(alias)
			if alias == "" {
				continue
			}
			ret[alias] = spec.Code
		}
	}
	return ret
}

func buildToolSpecByName() map[string]ToolSpec {
	ret := make(map[string]ToolSpec, len(RegisteredToolSpecs))
	for _, spec := range RegisteredToolSpecs {
		name := strings.TrimSpace(spec.Name)
		if name == "" {
			continue
		}
		ret[name] = spec
	}
	return ret
}

func ListRegisteredToolSpecs() []ToolSpec {
	return append([]ToolSpec(nil), RegisteredToolSpecs...)
}

func GetRegisteredToolSpec(toolCode string) (ToolSpec, bool) {
	toolCode = NormalizeToolCodeAlias(strings.TrimSpace(toolCode))
	spec, ok := toolSpecByCode[toolCode]
	return spec, ok
}

func GetRegisteredToolSpecByName(name string) (ToolSpec, bool) {
	name = strings.TrimSpace(name)
	spec, ok := toolSpecByName[name]
	return spec, ok
}

func GetRegisteredToolTitle(toolCode string) string {
	spec, ok := GetRegisteredToolSpec(toolCode)
	if !ok {
		return ""
	}
	return spec.Title
}

func GetRegisteredToolDescription(toolCode string) string {
	spec, ok := GetRegisteredToolSpec(toolCode)
	if !ok {
		return ""
	}
	return spec.Description
}

func GetRegisteredToolIdentity(toolCode string) (serverCode, toolName string, ok bool) {
	spec, ok := GetRegisteredToolSpec(toolCode)
	if !ok {
		return "", "", false
	}
	return spec.ServerCode, spec.Name, true
}

func ResolveToolSourceType(toolCode string) enums.ToolSourceType {
	if spec, ok := GetRegisteredToolSpec(toolCode); ok {
		return spec.SourceType
	}
	toolCode = strings.TrimSpace(toolCode)
	switch {
	case strings.HasPrefix(toolCode, "graph/"):
		return enums.ToolSourceTypeGraph
	case strings.HasPrefix(toolCode, "builtin/"):
		return enums.ToolSourceTypeBuiltin
	default:
		return enums.ToolSourceTypeMCP
	}
}

func IsAutoInjectedToolCode(toolCode string) bool {
	spec, ok := GetRegisteredToolSpec(toolCode)
	return ok && spec.AutoInjected
}

func ListAgentDirectToolSpecs() []ToolSpec {
	ret := make([]ToolSpec, 0, len(RegisteredToolSpecs))
	for _, spec := range RegisteredToolSpecs {
		if !spec.DirectAccess {
			continue
		}
		ret = append(ret, spec)
	}
	return ret
}

func ListRuntimeStaticToolSpecs() []ToolSpec {
	ret := make([]ToolSpec, 0, len(RegisteredToolSpecs))
	for _, spec := range RegisteredToolSpecs {
		if !spec.RuntimeStatic {
			continue
		}
		ret = append(ret, spec)
	}
	return ret
}

func IsAgentDirectToolCode(toolCode string) bool {
	toolCode = NormalizeToolCodeAlias(strings.TrimSpace(toolCode))
	spec, ok := GetRegisteredToolSpec(toolCode)
	return ok && spec.DirectAccess
}

func IsAgentDirectGraphToolCode(toolCode string) bool {
	toolCode = NormalizeToolCodeAlias(strings.TrimSpace(toolCode))
	spec, ok := GetRegisteredToolSpec(toolCode)
	return ok && spec.DirectAccess && spec.SourceType == enums.ToolSourceTypeGraph
}

func NormalizeToolCodeAlias(toolCode string) string {
	toolCode = strings.TrimSpace(toolCode)
	if canonical, ok := toolAliasToCanonical[toolCode]; ok {
		return canonical
	}
	return toolCode
}

func BuildToolAppendices(hasDynamicMCPTools bool, toolCodes map[string]string) []string {
	return BuildToolAppendicesForCodes(hasDynamicMCPTools, toolCodesFromMap(toolCodes))
}

func BuildToolAppendicesForCodes(hasDynamicMCPTools bool, toolCodes []string) []string {
	ret := make([]string, 0, len(toolCodes)+1)
	normalizedToolCodes := NormalizeToolCodes(toolCodes)
	if hasDynamicMCPTools && strings.TrimSpace(BuiltinToolSearch.Appendix) != "" {
		ret = append(ret, BuiltinToolSearch.Appendix)
	}
	for _, spec := range RegisteredToolSpecs {
		if strings.TrimSpace(spec.Appendix) == "" {
			continue
		}
		if spec.Code == BuiltinToolSearch.Code {
			continue
		}
		if containsNormalizedToolCode(normalizedToolCodes, spec.Code) {
			ret = append(ret, spec.Appendix)
		}
	}
	return ret
}

func BuildToolMetadata(toolCode string) (serverCode, toolName string, sourceType enums.ToolSourceType, ok bool) {
	spec, ok := GetRegisteredToolSpec(toolCode)
	if !ok {
		return "", "", ResolveToolSourceType(toolCode), false
	}
	return spec.ServerCode, spec.Name, spec.SourceType, true
}

func ResolveToolMetadata(toolCode string, fallbackName string) ToolMetadata {
	toolCode = NormalizeToolCodeAlias(strings.TrimSpace(toolCode))
	fallbackName = strings.TrimSpace(fallbackName)
	serverCode, toolName, sourceType, ok := BuildToolMetadata(toolCode)
	if !ok && toolName == "" {
		toolName = fallbackName
	}
	if toolName == "" {
		toolName = fallbackName
	}
	return ToolMetadata{
		ToolCode:   toolCode,
		ServerCode: strings.TrimSpace(serverCode),
		ToolName:   strings.TrimSpace(toolName),
		SourceType: sourceType,
	}
}

func IsAlwaysAllowedToolCode(toolCode string) bool {
	return NormalizeToolCodeAlias(strings.TrimSpace(toolCode)) == GraphHandoffConversation.Code
}

func IsImpliedAllowedToolCode(toolCode string, allowedToolCodes map[string]struct{}) bool {
	toolCode = NormalizeToolCodeAlias(strings.TrimSpace(toolCode))
	if toolCode == "" || len(allowedToolCodes) == 0 {
		return false
	}
	switch toolCode {
	case GraphTriageServiceRequest.Code, GraphAnalyzeConversation.Code:
		if _, ok := allowedToolCodes[GraphCreateTicketConfirm.Code]; ok {
			return true
		}
		if _, ok := allowedToolCodes[GraphHandoffConversation.Code]; ok {
			return true
		}
	case GraphPrepareTicketDraft.Code:
		_, ok := allowedToolCodes[GraphCreateTicketConfirm.Code]
		return ok
	}
	return false
}

func hasToolCode(toolCodes map[string]string, target string) bool {
	return containsNormalizedToolCode(toolCodesFromMap(toolCodes), target)
}

func containsNormalizedToolCode(toolCodes []string, target string) bool {
	target = strings.TrimSpace(target)
	if target == "" || len(toolCodes) == 0 {
		return false
	}
	for _, item := range NormalizeToolCodes(toolCodes) {
		if item == target {
			return true
		}
	}
	return false
}

func toolCodesFromMap(toolCodes map[string]string) []string {
	if len(toolCodes) == 0 {
		return nil
	}
	ret := make([]string, 0, len(toolCodes))
	for _, item := range toolCodes {
		ret = append(ret, item)
	}
	return ret
}

func NormalizeToolCodes(items []string) []string {
	if len(items) == 0 {
		return nil
	}
	ret := make([]string, 0, len(items))
	seen := make(map[string]struct{}, len(items))
	for _, item := range items {
		item = NormalizeToolCodeAlias(strings.TrimSpace(item))
		if item == "" {
			continue
		}
		if _, ok := seen[item]; ok {
			continue
		}
		seen[item] = struct{}{}
		ret = append(ret, item)
	}
	return ret
}

func IntersectToolCodes(left []string, right []string) []string {
	left = NormalizeToolCodes(left)
	right = NormalizeToolCodes(right)
	switch {
	case len(left) == 0:
		return right
	case len(right) == 0:
		return left
	}
	rightSet := make(map[string]struct{}, len(right))
	for _, item := range right {
		rightSet[item] = struct{}{}
	}
	ret := make([]string, 0, len(left))
	for _, item := range left {
		if _, ok := rightSet[item]; ok {
			ret = append(ret, item)
		}
	}
	return ret
}
