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

package mcp

import (
	"context"
	"fmt"
	"net"
	"net/http"
	"strconv"

	"git.secluded.site/lune/internal/config"
	"git.secluded.site/lune/internal/mcp/auth"
	"git.secluded.site/lune/internal/mcp/resources/areas"
	"git.secluded.site/lune/internal/mcp/resources/habits"
	noters "git.secluded.site/lune/internal/mcp/resources/note"
	"git.secluded.site/lune/internal/mcp/resources/notebooks"
	"git.secluded.site/lune/internal/mcp/resources/notes"
	"git.secluded.site/lune/internal/mcp/resources/people"
	personrs "git.secluded.site/lune/internal/mcp/resources/person"
	taskrs "git.secluded.site/lune/internal/mcp/resources/task"
	"git.secluded.site/lune/internal/mcp/resources/tasks"
	"git.secluded.site/lune/internal/mcp/shared"
	"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"
	"github.com/modelcontextprotocol/go-sdk/mcp"
	"github.com/spf13/cobra"
)

var version = "dev"

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

	areaProviders := toAreaProviders(cfg.Areas)
	habitProviders := shared.ToHabitProviders(cfg.Habits)
	notebookProviders := shared.ToNotebookProviders(cfg.Notebooks)

	registerResources(mcpServer, accessToken, areaProviders, habitProviders, notebookProviders)
	registerResourceTemplates(mcpServer, accessToken, areaProviders, notebookProviders)
	registerTools(mcpServer, cfg, accessToken, areaProviders, habitProviders, notebookProviders)

	return mcpServer
}

func toAreaProviders(cfgAreas []config.Area) []shared.AreaProvider {
	providers := make([]shared.AreaProvider, 0, len(cfgAreas))

	for _, area := range cfgAreas {
		providers = append(providers, shared.AreaProvider{
			ID:       area.ID,
			Name:     area.Name,
			Key:      area.Key,
			Workflow: area.Workflow,
			Goals:    shared.ToGoalProviders(area.Goals),
		})
	}

	return providers
}

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

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

	notebooksHandler := notebooks.NewHandler(notebookProviders)
	mcpServer.AddResource(&mcp.Resource{
		Name:        "notebooks",
		URI:         notebooks.ResourceURI,
		Description: notebooks.ResourceDescription,
		MIMEType:    "application/json",
	}, notebooksHandler.HandleRead)

	registerTaskListResources(mcpServer, accessToken, areaProviders)
	registerNoteListResources(mcpServer, accessToken, notebookProviders)
	registerPeopleListResources(mcpServer, accessToken)
}

func registerResourceTemplates(
	mcpServer *mcp.Server,
	accessToken string,
	areaProviders []shared.AreaProvider,
	notebookProviders []shared.NotebookProvider,
) {
	taskHandler := taskrs.NewHandler(accessToken)
	mcpServer.AddResourceTemplate(&mcp.ResourceTemplate{
		Name:        "task",
		URITemplate: taskrs.ResourceTemplate,
		Description: taskrs.ResourceDescription,
		MIMEType:    "application/json",
	}, taskHandler.HandleRead)

	noteHandler := noters.NewHandler(accessToken)
	mcpServer.AddResourceTemplate(&mcp.ResourceTemplate{
		Name:        "note",
		URITemplate: noters.ResourceTemplate,
		Description: noters.ResourceDescription,
		MIMEType:    "application/json",
	}, noteHandler.HandleRead)

	personHandler := personrs.NewHandler(accessToken)
	mcpServer.AddResourceTemplate(&mcp.ResourceTemplate{
		Name:        "person",
		URITemplate: personrs.ResourceTemplate,
		Description: personrs.ResourceDescription,
		MIMEType:    "application/json",
	}, personHandler.HandleRead)

	registerAreaTaskTemplates(mcpServer, accessToken, areaProviders)
	registerNotebookNoteTemplates(mcpServer, accessToken, notebookProviders)
}

func registerTaskListResources(
	mcpServer *mcp.Server,
	accessToken string,
	areaProviders []shared.AreaProvider,
) {
	handler := tasks.NewHandler(accessToken, areaProviders)

	mcpServer.AddResource(&mcp.Resource{
		Name:        "tasks-all",
		URI:         tasks.ResourceURIAll,
		Description: tasks.AllDescription,
		MIMEType:    "application/json",
	}, handler.HandleReadAll)

	mcpServer.AddResource(&mcp.Resource{
		Name:        "tasks-today",
		URI:         tasks.ResourceURIToday,
		Description: tasks.TodayDescription,
		MIMEType:    "application/json",
	}, handler.HandleReadToday)

	mcpServer.AddResource(&mcp.Resource{
		Name:        "tasks-overdue",
		URI:         tasks.ResourceURIOverdue,
		Description: tasks.OverdueDescription,
		MIMEType:    "application/json",
	}, handler.HandleReadOverdue)

	mcpServer.AddResource(&mcp.Resource{
		Name:        "tasks-next-7-days",
		URI:         tasks.ResourceURINext7Days,
		Description: tasks.Next7DaysDescription,
		MIMEType:    "application/json",
	}, handler.HandleReadNext7Days)

	mcpServer.AddResource(&mcp.Resource{
		Name:        "tasks-high-priority",
		URI:         tasks.ResourceURIHighPriority,
		Description: tasks.HighPriorityDescription,
		MIMEType:    "application/json",
	}, handler.HandleReadHighPriority)

	mcpServer.AddResource(&mcp.Resource{
		Name:        "tasks-now",
		URI:         tasks.ResourceURINow,
		Description: tasks.NowDescription,
		MIMEType:    "application/json",
	}, handler.HandleReadNow)

	mcpServer.AddResource(&mcp.Resource{
		Name:        "tasks-recent-completions",
		URI:         tasks.ResourceURIRecentCompletions,
		Description: tasks.RecentCompletionsDescription,
		MIMEType:    "application/json",
	}, handler.HandleReadRecentCompletions)
}

func registerAreaTaskTemplates(
	mcpServer *mcp.Server,
	accessToken string,
	areaProviders []shared.AreaProvider,
) {
	handler := tasks.NewHandler(accessToken, areaProviders)

	mcpServer.AddResourceTemplate(&mcp.ResourceTemplate{
		Name:        "area-tasks",
		URITemplate: tasks.AreaTasksTemplate,
		Description: tasks.AreaTasksDescription,
		MIMEType:    "application/json",
	}, handler.HandleReadAreaTasks)

	mcpServer.AddResourceTemplate(&mcp.ResourceTemplate{
		Name:        "area-tasks-today",
		URITemplate: tasks.AreaTodayTemplate,
		Description: tasks.AreaFilteredDescription,
		MIMEType:    "application/json",
	}, handler.HandleReadAreaTasks)

	mcpServer.AddResourceTemplate(&mcp.ResourceTemplate{
		Name:        "area-tasks-overdue",
		URITemplate: tasks.AreaOverdueTemplate,
		Description: tasks.AreaFilteredDescription,
		MIMEType:    "application/json",
	}, handler.HandleReadAreaTasks)

	mcpServer.AddResourceTemplate(&mcp.ResourceTemplate{
		Name:        "area-tasks-next-7-days",
		URITemplate: tasks.AreaNext7DaysTemplate,
		Description: tasks.AreaFilteredDescription,
		MIMEType:    "application/json",
	}, handler.HandleReadAreaTasks)

	mcpServer.AddResourceTemplate(&mcp.ResourceTemplate{
		Name:        "area-tasks-high-priority",
		URITemplate: tasks.AreaHighPriorityTemplate,
		Description: tasks.AreaFilteredDescription,
		MIMEType:    "application/json",
	}, handler.HandleReadAreaTasks)

	mcpServer.AddResourceTemplate(&mcp.ResourceTemplate{
		Name:        "area-tasks-now",
		URITemplate: tasks.AreaNowTemplate,
		Description: tasks.AreaFilteredDescription,
		MIMEType:    "application/json",
	}, handler.HandleReadAreaTasks)

	mcpServer.AddResourceTemplate(&mcp.ResourceTemplate{
		Name:        "area-tasks-recent-completions",
		URITemplate: tasks.AreaRecentCompletionsTempl,
		Description: tasks.AreaFilteredDescription,
		MIMEType:    "application/json",
	}, handler.HandleReadAreaTasks)
}

func registerNoteListResources(
	mcpServer *mcp.Server,
	accessToken string,
	notebookProviders []shared.NotebookProvider,
) {
	handler := notes.NewHandler(accessToken, notebookProviders)

	mcpServer.AddResource(&mcp.Resource{
		Name:        "notes-all",
		URI:         notes.ResourceURIAll,
		Description: notes.AllDescription,
		MIMEType:    "application/json",
	}, handler.HandleReadAll)

	mcpServer.AddResource(&mcp.Resource{
		Name:        "notes-pinned",
		URI:         notes.ResourceURIPinned,
		Description: notes.PinnedDescription,
		MIMEType:    "application/json",
	}, handler.HandleReadPinned)

	mcpServer.AddResource(&mcp.Resource{
		Name:        "notes-recent",
		URI:         notes.ResourceURIRecent,
		Description: notes.RecentDescription,
		MIMEType:    "application/json",
	}, handler.HandleReadRecent)
}

func registerPeopleListResources(
	mcpServer *mcp.Server,
	accessToken string,
) {
	handler := people.NewHandler(accessToken)

	mcpServer.AddResource(&mcp.Resource{
		Name:        "people-all",
		URI:         people.ResourceURIAll,
		Description: people.AllDescription,
		MIMEType:    "application/json",
	}, handler.HandleReadAll)

	mcpServer.AddResourceTemplate(&mcp.ResourceTemplate{
		Name:        "people-by-relationship",
		URITemplate: people.RelationshipTemplate,
		Description: people.RelationshipDescription,
		MIMEType:    "application/json",
	}, handler.HandleReadByRelationship)
}

func registerNotebookNoteTemplates(
	mcpServer *mcp.Server,
	accessToken string,
	notebookProviders []shared.NotebookProvider,
) {
	handler := notes.NewHandler(accessToken, notebookProviders)

	mcpServer.AddResourceTemplate(&mcp.ResourceTemplate{
		Name:        "notebook-notes",
		URITemplate: notes.NotebookNotesTemplate,
		Description: notes.NotebookNotesDescription,
		MIMEType:    "application/json",
	}, handler.HandleReadNotebookNotes)

	mcpServer.AddResourceTemplate(&mcp.ResourceTemplate{
		Name:        "notebook-notes-pinned",
		URITemplate: notes.NotebookNotesPinnedTemplate,
		Description: notes.NotebookFilteredDescription,
		MIMEType:    "application/json",
	}, handler.HandleReadNotebookNotes)

	mcpServer.AddResourceTemplate(&mcp.ResourceTemplate{
		Name:        "notebook-notes-recent",
		URITemplate: notes.NotebookNotesRecentTemplate,
		Description: notes.NotebookFilteredDescription,
		MIMEType:    "application/json",
	}, handler.HandleReadNotebookNotes)
}

func registerTools(
	mcpServer *mcp.Server,
	cfg *config.Config,
	accessToken string,
	areaProviders []shared.AreaProvider,
	habitProviders []shared.HabitProvider,
	notebookProviders []shared.NotebookProvider,
) {
	tools := &cfg.MCP.Tools

	if tools.GetTimestamp {
		tsHandler := timestamp.NewHandler(cfg.MCP.Timezone)
		mcp.AddTool(mcpServer, &mcp.Tool{
			Name:        timestamp.ToolName,
			Description: timestamp.ToolDescription,
			Annotations: timestamp.ToolAnnotations(),
		}, tsHandler.Handle)
	}

	if tools.AddTimelineNote {
		timelineHandler := timeline.NewHandler(accessToken)
		mcp.AddTool(mcpServer, &mcp.Tool{
			Name:        timeline.ToolName,
			Description: timeline.ToolDescription,
			Annotations: timeline.ToolAnnotations(),
		}, timelineHandler.Handle)
	}

	if tools.TrackHabit {
		habitHandler := habit.NewHandler(accessToken, habitProviders)
		mcp.AddTool(mcpServer, &mcp.Tool{
			Name:        habit.TrackToolName,
			Description: habit.TrackToolDescription,
			Annotations: habit.TrackToolAnnotations(),
		}, habitHandler.HandleTrack)
	}

	registerCRUDTools(mcpServer, cfg, tools, accessToken, areaProviders, habitProviders, notebookProviders)
}

func registerCRUDTools(
	mcpServer *mcp.Server,
	cfg *config.Config,
	tools *config.ToolsConfig,
	accessToken string,
	areaProviders []shared.AreaProvider,
	habitProviders []shared.HabitProvider,
	notebookProviders []shared.NotebookProvider,
) {
	if !tools.Create && !tools.Update && !tools.Delete && !tools.Query {
		return
	}

	crudHandler := crud.NewHandler(accessToken, cfg, areaProviders, habitProviders, notebookProviders)

	if tools.Create {
		mcp.AddTool(mcpServer, &mcp.Tool{
			Name:        crud.CreateToolName,
			Description: crud.CreateToolDescription,
			InputSchema: crud.CreateInputSchema(),
			Annotations: crud.CreateToolAnnotations(),
		}, crudHandler.HandleCreate)
	}

	if tools.Update {
		mcp.AddTool(mcpServer, &mcp.Tool{
			Name:        crud.UpdateToolName,
			Description: crud.UpdateToolDescription,
			InputSchema: crud.UpdateInputSchema(),
			Annotations: crud.UpdateToolAnnotations(),
		}, crudHandler.HandleUpdate)
	}

	if tools.Delete {
		mcp.AddTool(mcpServer, &mcp.Tool{
			Name:        crud.DeleteToolName,
			Description: crud.DeleteToolDescription,
			InputSchema: crud.DeleteInputSchema(),
			Annotations: crud.DeleteToolAnnotations(),
		}, crudHandler.HandleDelete)
	}

	if tools.Query {
		mcp.AddTool(mcpServer, &mcp.Tool{
			Name:        crud.QueryToolName,
			Description: crud.QueryToolDescription,
			InputSchema: crud.QueryInputSchema(),
			Annotations: crud.QueryToolAnnotations(),
		}, crudHandler.HandleQuery)
	}
}

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(cmd *cobra.Command, mcpServer *mcp.Server, cfg *config.Config) error {
	hostPort := net.JoinHostPort(resolveHost(cfg), strconv.Itoa(resolvePort(cfg)))
	handler := mcp.NewSSEHandler(func(_ *http.Request) *mcp.Server {
		return mcpServer
	}, nil)

	var httpHandler http.Handler = handler
	if cfg.MCP.TokenHash != "" {
		httpHandler = auth.Middleware(cfg.MCP.TokenHash)(handler)
	}

	fmt.Fprintf(cmd.OutOrStdout(), "SSE server listening on %s\n", hostPort)

	//nolint:gosec // MCP SDK controls server lifecycle; timeouts not applicable
	if err := http.ListenAndServe(hostPort, httpHandler); err != nil {
		return fmt.Errorf("SSE server error: %w", err)
	}

	return nil
}

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

	var httpHandler http.Handler = handler
	if cfg.MCP.TokenHash != "" {
		httpHandler = auth.Middleware(cfg.MCP.TokenHash)(handler)
	}

	fmt.Fprintf(cmd.OutOrStdout(), "HTTP server listening on %s\n", hostPort)

	//nolint:gosec // MCP SDK controls server lifecycle; timeouts not applicable
	if err := http.ListenAndServe(hostPort, httpHandler); err != nil {
		return fmt.Errorf("HTTP server error: %w", err)
	}

	return nil
}
