package mcp

import (
	"github.com/invopop/jsonschema"
	orderedmap "github.com/wk8/go-ordered-map/v2"

	"github.com/mayswind/ezbookkeeping/pkg/core"
	"github.com/mayswind/ezbookkeeping/pkg/errs"
	"github.com/mayswind/ezbookkeeping/pkg/models"
	"github.com/mayswind/ezbookkeeping/pkg/settings"
)

// MCPContainer contains the all mcp handlers
type MCPContainer struct {
	mcpTextContentTools      *orderedmap.OrderedMap[string, MCPToolHandler[MCPTextContent]]
	mcpImageContentTools     *orderedmap.OrderedMap[string, MCPToolHandler[MCPImageContent]]
	mcpAudioContentTools     *orderedmap.OrderedMap[string, MCPToolHandler[MCPAudioContent]]
	mcpResourceLinkTools     *orderedmap.OrderedMap[string, MCPToolHandler[MCPResourceLink]]
	mcpEmbeddedResourceTools *orderedmap.OrderedMap[string, MCPToolHandler[MCPEmbeddedResource]]
	mcpTools                 []*MCPTool
}

// Initialize a mcp handler container singleton instance
var (
	Container = &MCPContainer{}
)

// GetMCPTools returns the registered MCP tools
func (c *MCPContainer) GetMCPTools() []*MCPTool {
	if len(c.mcpTools) == 0 {
		return nil
	}

	return c.mcpTools
}

// HandleTool returns the result of the MCP tool handler based on the tool name
func (c *MCPContainer) HandleTool(ctx *core.WebContext, callToolReq *MCPCallToolRequest, user *models.User, currentConfig *settings.Config, services MCPAvailableServices) (any, error) {
	if handler, exists := c.mcpTextContentTools.Get(callToolReq.Name); exists {
		return handleTool(ctx, handler, currentConfig, services, callToolReq, user)
	}

	if handler, exists := c.mcpImageContentTools.Get(callToolReq.Name); exists {
		return handleTool(ctx, handler, currentConfig, services, callToolReq, user)
	}

	if handler, exists := c.mcpAudioContentTools.Get(callToolReq.Name); exists {
		return handleTool(ctx, handler, currentConfig, services, callToolReq, user)
	}

	if handler, exists := c.mcpResourceLinkTools.Get(callToolReq.Name); exists {
		return handleTool(ctx, handler, currentConfig, services, callToolReq, user)
	}

	if handler, exists := c.mcpEmbeddedResourceTools.Get(callToolReq.Name); exists {
		return handleTool(ctx, handler, currentConfig, services, callToolReq, user)
	}

	return nil, errs.ErrApiNotFound
}

// InitializeMCPHandlers initializes the all mcp handlers according to the config
func InitializeMCPHandlers(config *settings.Config) error {
	container := &MCPContainer{
		mcpTextContentTools:      orderedmap.New[string, MCPToolHandler[MCPTextContent]](),
		mcpImageContentTools:     orderedmap.New[string, MCPToolHandler[MCPImageContent]](),
		mcpAudioContentTools:     orderedmap.New[string, MCPToolHandler[MCPAudioContent]](),
		mcpResourceLinkTools:     orderedmap.New[string, MCPToolHandler[MCPResourceLink]](),
		mcpEmbeddedResourceTools: orderedmap.New[string, MCPToolHandler[MCPEmbeddedResource]](),
		mcpTools:                 make([]*MCPTool, 0),
	}

	registerMCPTextContentToolHandler(container, MCPAddTransactionToolHandler)
	registerMCPTextContentToolHandler(container, MCPQueryTransactionsToolHandler)
	registerMCPTextContentToolHandler(container, MCPQueryAllAccountsToolHandler)
	registerMCPTextContentToolHandler(container, MCPQueryAllTransactionCategoriesToolHandler)
	registerMCPTextContentToolHandler(container, MCPQueryAllTransactionTagsToolHandler)
	registerMCPTextContentToolHandler(container, MCPQueryLatestExchangeRatesToolHandler)

	Container = container
	return nil
}

func registerMCPTextContentToolHandler(c *MCPContainer, handler MCPToolHandler[MCPTextContent]) {
	registerMCPToolHandler(c, c.mcpTextContentTools, handler)
}

func registerMCPImageContentToolHandler(c *MCPContainer, handler MCPToolHandler[MCPImageContent]) {
	registerMCPToolHandler(c, c.mcpImageContentTools, handler)
}

func registerMCPAudioContentToolHandler(c *MCPContainer, handler MCPToolHandler[MCPAudioContent]) {
	registerMCPToolHandler(c, c.mcpAudioContentTools, handler)
}

func registerMCPResourceLinkToolHandler(c *MCPContainer, handler MCPToolHandler[MCPResourceLink]) {
	registerMCPToolHandler(c, c.mcpResourceLinkTools, handler)
}

func registerMCPEmbeddedResourceToolHandler(c *MCPContainer, handler MCPToolHandler[MCPEmbeddedResource]) {
	registerMCPToolHandler(c, c.mcpEmbeddedResourceTools, handler)
}

func registerMCPToolHandler[T MCPTextContent | MCPImageContent | MCPAudioContent | MCPResourceLink | MCPEmbeddedResource](c *MCPContainer, mcpToolHandlerMap *orderedmap.OrderedMap[string, MCPToolHandler[T]], handler MCPToolHandler[T]) {
	if _, exists := mcpToolHandlerMap.Get(handler.Name()); exists {
		return
	}

	mcpToolHandlerMap.Set(handler.Name(), handler)
	c.mcpTools = append(c.mcpTools, createNewMCPToolInfo(handler.Name(), handler))
}

func handleTool[T MCPTextContent | MCPImageContent | MCPAudioContent | MCPResourceLink | MCPEmbeddedResource](ctx *core.WebContext, handler MCPToolHandler[T], currentConfig *settings.Config, services MCPAvailableServices, callToolReq *MCPCallToolRequest, user *models.User) (any, error) {
	structuredResponse, result, err := handler.Handle(ctx, callToolReq, user, currentConfig, services)

	if err != nil {
		return nil, errs.Or(err, errs.ErrOperationFailed)
	}

	callToolResp := MCPCallToolResponse[T]{
		Content: result,
		IsError: false,
	}

	if ctx.GetHeader(MCPProtocolVersionHeaderName) >= string(ToolResultStructuredContentMinVersion) {
		callToolResp.StructuredContent = structuredResponse
	}

	return callToolResp, nil
}

func createNewMCPToolInfo[T MCPTextContent | MCPImageContent | MCPAudioContent | MCPResourceLink | MCPEmbeddedResource](name string, handler MCPToolHandler[T]) *MCPTool {
	mcpTool := &MCPTool{
		Name:        name,
		Description: handler.Description(),
	}

	schemeGenerator := jsonschema.Reflector{
		Anonymous:      true,
		DoNotReference: true,
		ExpandedStruct: true,
	}

	if handler.InputType() != nil {
		schema := schemeGenerator.ReflectFromType(handler.InputType())
		schema.Version = ""
		mcpTool.InputSchema = schema
	} else {
		mcpTool.InputSchema = &jsonschema.Schema{
			Type: "object",
		}
	}

	if handler.OutputType() != nil {
		schema := schemeGenerator.ReflectFromType(handler.OutputType())
		schema.Version = ""
		mcpTool.OutputSchema = schema
	} else {
		mcpTool.OutputSchema = &jsonschema.Schema{
			Type: "object",
		}
	}

	return mcpTool
}
