// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
//
// SPDX-License-Identifier: AGPL-3.0-or-later

// Package mcp provides the MCP server command for lune.
package mcp

import (
	"errors"
	"fmt"

	"git.secluded.site/lune/internal/client"
	"git.secluded.site/lune/internal/config"
	"git.secluded.site/lune/internal/mcp/tools/crud"
	"git.secluded.site/lune/internal/mcp/tools/habit"
	"git.secluded.site/lune/internal/mcp/tools/timeline"
	"git.secluded.site/lune/internal/mcp/tools/timestamp"
	mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp"
	"github.com/spf13/cobra"
)

// Transport constants.
const (
	TransportStdio = "stdio"
	TransportSSE   = "sse"
	TransportHTTP  = "http"
)

var (
	errUnknownTransport  = errors.New("unknown transport; use stdio, sse, or http")
	errNoToken           = errors.New("no access token; run 'lune init' first")
	errMutuallyExclusive = errors.New("--enabled-tools and --disabled-tools are mutually exclusive")
	errUnknownTool       = errors.New("unknown tool name")
)

var (
	transport     string
	host          string
	port          int
	enabledTools  []string
	disabledTools []string
)

// Cmd is the mcp command for starting the MCP server.
var Cmd = &cobra.Command{
	Use:   "mcp",
	Short: "Start the MCP server",
	Long: `Start a Model Context Protocol server for LLM tool integration.

The MCP server exposes Lunatask resources and tools that can be used by
LLM assistants (like Claude) to interact with your Lunatask data.

Transports:
  stdio  - Standard input/output (default, for local integrations)
  sse    - Server-sent events over HTTP
  http   - Streamable HTTP

Examples:
  lune mcp                    # Start with stdio (default)
  lune mcp -t sse             # Start SSE server on configured host:port
  lune mcp -t sse --port 9000 # Override port`,
	RunE: runMCP,
}

func init() {
	Cmd.Flags().StringVarP(&transport, "transport", "t", "",
		"Transport type: stdio, sse, http (default: stdio or config)")
	Cmd.Flags().StringVar(&host, "host", "", "Server host (for sse/http)")
	Cmd.Flags().IntVar(&port, "port", 0, "Server port (for sse/http)")
	Cmd.Flags().StringSliceVar(&enabledTools, "enabled-tools", nil,
		"Enable only these tools (comma-separated); overrides config")
	Cmd.Flags().StringSliceVar(&disabledTools, "disabled-tools", nil,
		"Disable these tools (comma-separated); overrides config")
}

func runMCP(cmd *cobra.Command, _ []string) error {
	if len(enabledTools) > 0 && len(disabledTools) > 0 {
		return errMutuallyExclusive
	}

	cfg, err := loadConfig()
	if err != nil {
		return err
	}

	if err := resolveTools(cfg); err != nil {
		return err
	}

	token, err := client.GetToken()
	if err != nil {
		return err
	}

	if token == "" {
		return errNoToken
	}

	mcpServer := newMCPServer(cfg, token)

	return runTransport(cmd, mcpServer, cfg)
}

func runTransport(cmd *cobra.Command, mcpServer *mcpsdk.Server, cfg *config.Config) error {
	switch resolveTransport(cfg) {
	case TransportStdio:
		return runStdio(mcpServer)
	case TransportSSE:
		return runSSE(cmd, mcpServer, cfg)
	case TransportHTTP:
		return runHTTP(cmd, mcpServer, cfg)
	default:
		return errUnknownTransport
	}
}

func loadConfig() (*config.Config, error) {
	cfg, err := config.Load()
	if err != nil {
		if errors.Is(err, config.ErrNotFound) {
			cfg = &config.Config{}
		} else {
			return nil, fmt.Errorf("loading config: %w", err)
		}
	}

	cfg.MCP.MCPDefaults()

	return cfg, nil
}

func resolveTransport(_ *config.Config) string {
	if transport != "" {
		return transport
	}

	return TransportStdio
}

func resolveHost(cfg *config.Config) string {
	if host != "" {
		return host
	}

	return cfg.MCP.Host
}

func resolvePort(cfg *config.Config) int {
	if port != 0 {
		return port
	}

	return cfg.MCP.Port
}

// validToolNames maps MCP tool names to their ToolsConfig field setters.
var validToolNames = map[string]func(*config.ToolsConfig, bool){
	timestamp.ToolName:  func(t *config.ToolsConfig, v bool) { t.GetTimestamp = v },
	timeline.ToolName:   func(t *config.ToolsConfig, v bool) { t.AddTimelineNote = v },
	habit.TrackToolName: func(t *config.ToolsConfig, v bool) { t.TrackHabit = v },
	crud.CreateToolName: func(t *config.ToolsConfig, v bool) { t.Create = v },
	crud.UpdateToolName: func(t *config.ToolsConfig, v bool) { t.Update = v },
	crud.DeleteToolName: func(t *config.ToolsConfig, v bool) { t.Delete = v },
	crud.QueryToolName:  func(t *config.ToolsConfig, v bool) { t.Query = v },
}

// resolveTools modifies cfg.MCP.Tools based on CLI flags.
// If --enabled-tools is set, only those tools are enabled.
// If --disabled-tools is set, all tools except those are enabled.
// If neither is set, config values are used unchanged.
func resolveTools(cfg *config.Config) error {
	if len(enabledTools) > 0 {
		// Validate all tool names first
		for _, name := range enabledTools {
			if _, ok := validToolNames[name]; !ok {
				return fmt.Errorf("%w: %s", errUnknownTool, name)
			}
		}

		// Start with everything disabled
		cfg.MCP.Tools = config.ToolsConfig{}

		// Enable only specified tools
		for _, name := range enabledTools {
			validToolNames[name](&cfg.MCP.Tools, true)
		}

		return nil
	}

	if len(disabledTools) > 0 {
		// Validate all tool names first
		for _, name := range disabledTools {
			if _, ok := validToolNames[name]; !ok {
				return fmt.Errorf("%w: %s", errUnknownTool, name)
			}
		}

		// Start with everything enabled
		cfg.MCP.Tools = config.ToolsConfig{}
		cfg.MCP.Tools.ApplyDefaults()

		// Disable specified tools
		for _, name := range disabledTools {
			validToolNames[name](&cfg.MCP.Tools, false)
		}

		return nil
	}

	// Neither flag set, use config as-is
	return nil
}
