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

package cmd

import (
	"context"
	"errors"
	"fmt"
	"log"
	"log/slog"
	"net"
	"net/http"
	"strconv"

	"github.com/modelcontextprotocol/go-sdk/mcp"
	"github.com/spf13/cobra"

	"git.secluded.site/lunatask-mcp-server/internal/client"
	"git.secluded.site/lunatask-mcp-server/internal/config"
	"git.secluded.site/lunatask-mcp-server/tools/areas"
	"git.secluded.site/lunatask-mcp-server/tools/habits"
	"git.secluded.site/lunatask-mcp-server/tools/shared"
	"git.secluded.site/lunatask-mcp-server/tools/tasks"
	"git.secluded.site/lunatask-mcp-server/tools/timestamp"
)

// Serve errors.
var (
	errNoConfig         = errors.New("config file not found; run 'lunatask-mcp-server config'")
	errNoAreas          = errors.New("config must have at least one area; run 'lunatask-mcp-server config'")
	errInvalidArea      = errors.New("area must have both name and id")
	errInvalidGoal      = errors.New("goal must have both name and id")
	errUnknownTransport = errors.New("unknown transport (valid: stdio, sse, http)")
)

// Transport modes for the MCP server.
const (
	TransportStdio = "stdio"
	TransportSSE   = "sse"
	TransportHTTP  = "http"
)

//nolint:exhaustruct // cobra only requires a subset of fields
var serveCmd = &cobra.Command{
	Use:   "serve",
	Short: "Start the MCP server",
	Long: `Start the MCP server using the configured transport mode.

Transport modes:
  stdio   Communicate over stdin/stdout (default, for CLI tools like Crush)
  sse     Server-Sent Events over HTTP (for Home Assistant)
  http    Streamable HTTP transport

The server uses configuration from:
  1. Config file (~/.config/lunatask-mcp-server/config.toml)
  2. Access token from LUNATASK_ACCESS_TOKEN env var or system keyring

Run 'lunatask-mcp-server config' to set up configuration interactively.`,
	RunE: runServe,
}

var (
	configPath string
	transport  string
)

func init() {
	serveCmd.Flags().StringVarP(
		&configPath, "config", "c", "",
		"path to config file (default: ~/.config/lunatask-mcp-server/config.toml)",
	)
	serveCmd.Flags().StringVarP(
		&transport, "transport", "t", "",
		"transport mode: stdio, sse, http (overrides config)",
	)
}

func runServe(_ *cobra.Command, _ []string) error {
	cfg, err := loadServerConfig()
	if err != nil {
		return err
	}

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

	slog.Debug("access token loaded", "source", source.String())

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

	mcpServer := newMCPServer(cfg, accessToken)

	effectiveTransport := cfg.Server.Transport
	if transport != "" {
		effectiveTransport = transport
	}

	switch effectiveTransport {
	case TransportStdio:
		return runStdio(mcpServer)
	case TransportSSE:
		return runSSE(mcpServer, cfg)
	case TransportHTTP:
		return runHTTP(mcpServer, cfg)
	default:
		return errUnknownTransport
	}
}

func loadServerConfig() (*config.Config, error) {
	var cfg *config.Config

	var err error

	if configPath != "" {
		cfg, err = config.LoadFrom(configPath)
	} else {
		cfg, err = config.Load()
	}

	if err != nil {
		if errors.Is(err, config.ErrNotFound) {
			return nil, errNoConfig
		}

		return nil, err
	}

	return cfg, nil
}

func validateServerConfig(cfg *config.Config) error {
	if len(cfg.Areas) == 0 {
		return errNoAreas
	}

	for _, area := range cfg.Areas {
		if area.Name == "" || area.ID == "" {
			return errInvalidArea
		}

		for _, goal := range area.Goals {
			if goal.Name == "" || goal.ID == "" {
				return errInvalidGoal
			}
		}
	}

	if _, err := shared.LoadLocation(cfg.Timezone); err != nil {
		return err
	}

	return nil
}

func newMCPServer(cfg *config.Config, accessToken string) *mcp.Server {
	mcpServer := mcp.NewServer(
		&mcp.Implementation{
			Name:    "Lunatask MCP Server",
			Version: version,
		},
		nil,
	)

	mcpServer.AddReceivingMiddleware(loggingMiddleware)

	areaProviders := toAreaProviders(cfg.Areas)
	habitProviders := toHabitProviders(cfg.Habits)

	registerResources(mcpServer, areaProviders, habitProviders)
	registerTimestampTool(mcpServer, cfg)
	registerTaskTools(mcpServer, cfg, accessToken, areaProviders)
	registerHabitTool(mcpServer, cfg, accessToken, habitProviders)

	return mcpServer
}

func loggingMiddleware(next mcp.MethodHandler) mcp.MethodHandler {
	return func(ctx context.Context, method string, req mcp.Request) (mcp.Result, error) {
		slog.Debug("request", "method", method)

		result, err := next(ctx, method, req)
		if err != nil {
			slog.Error("request failed", "method", method, "error", err)
		} else {
			slog.Debug("request succeeded", "method", method)
		}

		return result, err
	}
}

func runStdio(mcpServer *mcp.Server) error {
	if err := mcpServer.Run(context.Background(), &mcp.StdioTransport{}); err != nil {
		return fmt.Errorf("stdio server error: %w", err)
	}

	return nil
}

func runSSE(mcpServer *mcp.Server, cfg *config.Config) error {
	hostPort := net.JoinHostPort(cfg.Server.Host, strconv.Itoa(cfg.Server.Port))
	handler := mcp.NewSSEHandler(func(_ *http.Request) *mcp.Server {
		return mcpServer
	}, nil)

	log.Printf("SSE server listening on %s", hostPort)

	if err := http.ListenAndServe(hostPort, handler); err != nil { //nolint:gosec // config-provided host:port
		return fmt.Errorf("SSE server error: %w", err)
	}

	return nil
}

func runHTTP(mcpServer *mcp.Server, cfg *config.Config) error {
	hostPort := net.JoinHostPort(cfg.Server.Host, strconv.Itoa(cfg.Server.Port))
	handler := mcp.NewStreamableHTTPHandler(func(_ *http.Request) *mcp.Server {
		return mcpServer
	}, nil)

	log.Printf("HTTP server listening on %s", hostPort)

	if err := http.ListenAndServe(hostPort, handler); err != nil { //nolint:gosec // config-provided host:port
		return fmt.Errorf("HTTP server error: %w", err)
	}

	return nil
}

// configArea wraps config.Area to implement shared.AreaProvider.
type configArea struct {
	area config.Area
}

func (a configArea) GetName() string { return a.area.Name }
func (a configArea) GetID() string   { return a.area.ID }
func (a configArea) GetKey() string  { return a.area.Key }
func (a configArea) GetGoals() []shared.GoalProvider {
	providers := make([]shared.GoalProvider, len(a.area.Goals))
	for i, g := range a.area.Goals {
		providers[i] = configGoal{goal: g}
	}

	return providers
}

// configGoal wraps config.Goal to implement shared.GoalProvider.
type configGoal struct {
	goal config.Goal
}

func (g configGoal) GetName() string { return g.goal.Name }
func (g configGoal) GetID() string   { return g.goal.ID }
func (g configGoal) GetKey() string  { return g.goal.Key }

// configHabit wraps config.Habit to implement shared.HabitProvider.
type configHabit struct {
	habit config.Habit
}

func (h configHabit) GetName() string { return h.habit.Name }
func (h configHabit) GetID() string   { return h.habit.ID }
func (h configHabit) GetKey() string  { return h.habit.Key }

func toAreaProviders(configAreas []config.Area) []shared.AreaProvider {
	providers := make([]shared.AreaProvider, len(configAreas))
	for idx, area := range configAreas {
		providers[idx] = configArea{area: area}
	}

	return providers
}

func toHabitProviders(configHabits []config.Habit) []shared.HabitProvider {
	providers := make([]shared.HabitProvider, len(configHabits))
	for idx, habit := range configHabits {
		providers[idx] = configHabit{habit: habit}
	}

	return providers
}

func registerResources(
	mcpServer *mcp.Server,
	areaProviders []shared.AreaProvider,
	habitProviders []shared.HabitProvider,
) {
	areasHandler := areas.NewHandler(areaProviders)
	mcpServer.AddResource(&mcp.Resource{
		Name:        "areas",
		URI:         areas.ResourceURI,
		Description: areas.ResourceDescription,
		MIMEType:    "application/json",
	}, areasHandler.HandleRead)

	habitsResourceHandler := habits.NewResourceHandler(habitProviders)
	mcpServer.AddResource(&mcp.Resource{
		Name:        "habits",
		URI:         habits.ResourceURI,
		Description: habits.ResourceDescription,
		MIMEType:    "application/json",
	}, habitsResourceHandler.HandleRead)
}

func registerTimestampTool(mcpServer *mcp.Server, cfg *config.Config) {
	if !cfg.Tools.GetTimestamp {
		return
	}

	handler := timestamp.NewHandler(cfg.Timezone)

	mcp.AddTool(mcpServer, &mcp.Tool{
		Name:        "get_timestamp",
		Description: timestamp.ToolDescription,
	}, handler.Handle)
}

func registerTaskTools(
	mcpServer *mcp.Server,
	cfg *config.Config,
	accessToken string,
	areaProviders []shared.AreaProvider,
) {
	if !cfg.Tools.CreateTask && !cfg.Tools.UpdateTask && !cfg.Tools.DeleteTask {
		return
	}

	handler := tasks.NewHandler(accessToken, cfg.Timezone, areaProviders)

	if cfg.Tools.CreateTask {
		mcp.AddTool(mcpServer, &mcp.Tool{
			Name:        "create_task",
			Description: tasks.CreateToolDescription,
		}, handler.HandleCreate)
	}

	if cfg.Tools.UpdateTask {
		mcp.AddTool(mcpServer, &mcp.Tool{
			Name:        "update_task",
			Description: tasks.UpdateToolDescription,
		}, handler.HandleUpdate)
	}

	if cfg.Tools.DeleteTask {
		mcp.AddTool(mcpServer, &mcp.Tool{
			Name:        "delete_task",
			Description: tasks.DeleteToolDescription,
		}, handler.HandleDelete)
	}
}

func registerHabitTool(
	mcpServer *mcp.Server,
	cfg *config.Config,
	accessToken string,
	habitProviders []shared.HabitProvider,
) {
	if !cfg.Tools.TrackHabitActivity {
		return
	}

	handler := habits.NewHandler(accessToken, habitProviders)

	mcp.AddTool(mcpServer, &mcp.Tool{
		Name:        "track_habit_activity",
		Description: habits.TrackToolDescription,
	}, handler.HandleTrack)
}
