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

package main

import (
	"context"
	"fmt"
	"log"
	"net"
	"os"
	"strconv"

	"github.com/BurntSushi/toml"
	"github.com/mark3labs/mcp-go/mcp"
	"github.com/mark3labs/mcp-go/server"

	"git.sr.ht/~amolith/lunatask-mcp-server/tools"
)

// Goal represents a Lunatask goal with its name and ID.
type Goal struct {
	Name string `toml:"name"`
	ID   string `toml:"id"`
}

// GetName returns the goal's name.
func (g Goal) GetName() string { return g.Name }

// GetID returns the goal's ID.
func (g Goal) GetID() string { return g.ID }

// Area represents a Lunatask area with its name, ID, and its goals.
type Area struct {
	Name  string `toml:"name"`
	ID    string `toml:"id"`
	Goals []Goal `toml:"goal"`
}

// GetName returns the area's name.
func (a Area) GetName() string { return a.Name }

// GetID returns the area's ID.
func (a Area) GetID() string { return a.ID }

// GetGoals returns the area's goals as a slice of tools.GoalProvider.
func (a Area) GetGoals() []tools.GoalProvider {
	providers := make([]tools.GoalProvider, len(a.Goals))
	for i, g := range a.Goals {
		providers[i] = g // Goal implements GoalProvider
	}

	return providers
}

// Habit represents a Lunatask habit with its name and ID.
type Habit struct {
	Name string `toml:"name"`
	ID   string `toml:"id"`
}

// GetName returns the habit's name.
func (h Habit) GetName() string { return h.Name }

// GetID returns the habit's ID.
func (h Habit) GetID() string { return h.ID }

// ServerConfig holds the application's configuration loaded from TOML.
type ServerConfig struct {
	Host string `toml:"host"`
	Port int    `toml:"port"`
}

type Config struct {
	AccessToken string       `toml:"access_token"`
	Areas       []Area       `toml:"area"`
	Server      ServerConfig `toml:"server"`
	Timezone    string       `toml:"timezone"`
	Habit       []Habit      `toml:"habit"`
}

var version = ""

func main() {
	configPath := "./config.toml"

	for i, arg := range os.Args {
		switch arg {
		case "-v", "--version":
			if version == "" {
				version = "unknown, build with `just build` or copy/paste the build command from ./justfile"
			}

			fmt.Println("lunatask-mcp-server:", version)
			os.Exit(0)
		case "-c", "--config":
			if i+1 < len(os.Args) {
				configPath = os.Args[i+1]
			}
		}
	}

	if _, err := os.Stat(configPath); os.IsNotExist(err) {
		createDefaultConfigFile(configPath)
	}

	var config Config
	if _, err := toml.DecodeFile(configPath, &config); err != nil {
		log.Fatalf("Failed to load config file %s: %v", configPath, err)
	}

	if config.AccessToken == "" || len(config.Areas) == 0 {
		log.Fatalf("Config file must provide access_token and at least one area.")
	}

	for i, area := range config.Areas {
		if area.Name == "" || area.ID == "" {
			log.Fatalf("All areas (areas[%d]) must have both a name and id", i)
		}

		for j, goal := range area.Goals {
			if goal.Name == "" || goal.ID == "" {
				log.Fatalf("All goals (areas[%d].goals[%d]) must have both a name and id", i, j)
			}
		}
	}

	// Validate timezone config on startup
	if _, err := tools.LoadLocation(config.Timezone); err != nil {
		log.Fatalf("Timezone validation failed: %v", err)
	}

	mcpServer := NewMCPServer(&config)

	hostPort := net.JoinHostPort(config.Server.Host, strconv.Itoa(config.Server.Port))
	baseURL := "http://" + hostPort
	sseServer := server.NewSSEServer(mcpServer, server.WithBaseURL(baseURL))
	listenAddr := hostPort
	log.Printf("SSE server listening on %s (baseURL: %s)", listenAddr, baseURL)

	if err := sseServer.Start(listenAddr); err != nil {
		log.Fatalf("Server error: %v", err)
	}
}

// closeFile properly closes a file, handling any errors.
func closeFile(f *os.File) {
	err := f.Close()
	if err != nil {
		log.Printf("Error closing file: %v", err)
	}
}

func NewMCPServer(appConfig *Config) *server.MCPServer {
	hooks := &server.Hooks{}

	hooks.AddBeforeAny(func(ctx context.Context, id any, method mcp.MCPMethod, message any) {
		fmt.Printf("beforeAny: %s, %v, %v\n", method, id, message)
	})
	hooks.AddOnSuccess(func(ctx context.Context, id any, method mcp.MCPMethod, message any, result any) {
		fmt.Printf("onSuccess: %s, %v, %v, %v\n", method, id, message, result)
	})
	hooks.AddOnError(func(ctx context.Context, id any, method mcp.MCPMethod, message any, err error) {
		fmt.Printf("onError: %s, %v, %v, %v\n", method, id, message, err)
	})
	hooks.AddBeforeInitialize(func(ctx context.Context, id any, message *mcp.InitializeRequest) {
		fmt.Printf("beforeInitialize: %v, %v\n", id, message)
	})
	hooks.AddAfterInitialize(func(ctx context.Context, id any, message *mcp.InitializeRequest, result *mcp.InitializeResult) {
		fmt.Printf("afterInitialize: %v, %v, %v\n", id, message, result)
	})
	hooks.AddAfterCallTool(func(ctx context.Context, id any, message *mcp.CallToolRequest, result *mcp.CallToolResult) {
		fmt.Printf("afterCallTool: %v, %v, %v\n", id, message, result)
	})
	hooks.AddBeforeCallTool(func(ctx context.Context, id any, message *mcp.CallToolRequest) {
		fmt.Printf("beforeCallTool: %v, %v\n", id, message)
	})

	mcpServer := server.NewMCPServer(
		"Lunatask MCP Server",
		"0.1.0",
		server.WithHooks(hooks),
		server.WithToolCapabilities(true),
	)

	// Prepare AreaProviders
	areaProviders := make([]tools.AreaProvider, len(appConfig.Areas))
	for i, area := range appConfig.Areas {
		areaProviders[i] = area // Area implements AreaProvider
	}

	// Prepare HabitProviders
	habitProviders := make([]tools.HabitProvider, len(appConfig.Habit))
	for i, habit := range appConfig.Habit {
		habitProviders[i] = habit // Habit implements HabitProvider
	}

	handlerCfg := tools.HandlerConfig{
		AccessToken: appConfig.AccessToken,
		Timezone:    appConfig.Timezone,
		Areas:       areaProviders,
		Habits:      habitProviders,
	}
	toolHandlers := tools.NewHandlers(handlerCfg)

	mcpServer.AddTool(mcp.NewTool("get_timestamp",
		mcp.WithDescription("Parses natural language dates into formatted timestamps for task scheduling. Use this tool when creating or updating tasks that include dates or times (after using list_areas_and_goals first). Supports relative dates (e.g., 'tomorrow', '1 week', 'next friday'), absolute dates (e.g., 'january 15', '2024-03-10'), and times (e.g., 'at 2pm', 'sunday at 19:00'). Also accepts 'now' for current timestamp."),
		mcp.WithString("natural_language_date",
			mcp.Description("Natural language date expression. Examples: 'tomorrow', '1 week', 'sunday at 19:00', 'january 15 at 2pm', 'next friday', 'now'. The tool will parse this into a properly formatted timestamp for use with task scheduling."),
			mcp.Required(),
		),
	), toolHandlers.HandleGetTimestamp)

	mcpServer.AddTool(
		mcp.NewTool(
			"list_areas_and_goals",
			mcp.WithDescription("Lists all available areas and their associated goals with their IDs. Use this tool FIRST before creating or updating tasks to identify valid area_id and goal_id values. Areas represent broad categories of work, and goals are specific objectives within those areas. Each task must belong to an area and can optionally be associated with a goal within that area."),
		),
		toolHandlers.HandleListAreasAndGoals,
	)

	mcpServer.AddTool(mcp.NewTool("create_task",
		mcp.WithDescription("Creates a new task in Lunatask. WORKFLOW: First use list_areas_and_goals to identify valid area_id and goal_id values, then use get_timestamp if scheduling the task. Only include optional parameters if the user indicates or hints at them. Try to interpret speech-to-text input that may not be entirely accurate."),
		mcp.WithString("area_id",
			mcp.Description("Area ID in which to create the task. Must be a valid area_id from list_areas_and_goals tool."),
			mcp.Required(),
		),
		mcp.WithString("goal_id",
			mcp.Description("Optional goal ID to associate the task with. Must be a valid goal_id from list_areas_and_goals that belongs to the specified area. Only include if the task relates to a specific goal."),
		),
		mcp.WithString("name",
			mcp.Description("Plain text task name using sentence case."),
			mcp.Required(),
		),
		mcp.WithString("note",
			mcp.Description("Additional details or notes for the task, using Markdown formatting. Include any extra context, requirements, or information provided by the user that doesn't fit in the task name."),
		),
		mcp.WithNumber("estimate",
			mcp.Description("Estimated completion time in minutes (0-720, max 12 hours). Only include if user mentions a time estimate like '30 minutes' (pass 30) or '2 hours' (pass 120). Omit if no estimate is provided."),
			mcp.Min(0),
			mcp.Max(720),
		),
		mcp.WithString("priority",
			mcp.Description("Task priority level. Valid values: 'lowest', 'low', 'neutral', 'high', 'highest'. Only include if user explicitly mentions priority or urgency. Omit for normal tasks."),
			mcp.Enum("lowest", "low", "neutral", "high", "highest"),
		),
		mcp.WithString("motivation",
			mcp.Description("Level of importance for the task. Valid values: 'must' (critical/required), 'should' (important), 'want' (nice-to-have). Only include if the user's language suggests strong obligation ('I need to', 'I have to') vs preference ('I'd like to', 'I want to')."),
			mcp.Enum("must", "should", "want"),
		),
		mcp.WithString("eisenhower",
			mcp.Description("Eisenhower Matrix quadrant for task prioritization. Valid values: 'both urgent and important', 'urgent, but not important', 'important, but not urgent', 'neither urgent nor important', 'uncategorised'. Only include for areas which the user has indicated follow the Eisenhower workflow."),
			mcp.Enum("both urgent and important", "urgent, but not important", "important, but not urgent", "neither urgent nor important", "uncategorised"),
		),
		mcp.WithString("status",
			mcp.Description("Initial task status. Valid values: 'later' (someday/backlog), 'next' (upcoming/soon), 'started' (in progress), 'waiting' (blocked), 'completed' (finished). Infer from context: 'working on' = 'started', 'soon'/'upcoming' = 'next', 'blocked'/'waiting for' = 'waiting'. Omit for normal new tasks (defaults to appropriate status)."),
			mcp.Enum("later", "next", "started", "waiting", "completed"),
		),
		mcp.WithString("scheduled_on",
			mcp.Description("Scheduled date/time for the task. Must use the formatted timestamp returned by get_timestamp tool. Only include if user specifies when the task should be done."),
		),
	), toolHandlers.HandleCreateTask)

	mcpServer.AddTool(mcp.NewTool("update_task",
		mcp.WithDescription("Updates an existing task. Only provided fields will be updated. WORKFLOW: Use list_areas_and_goals first if changing area/goal, then get_timestamp if changing schedule. Only include parameters that are being changed. Empty strings will clear existing values for text fields."),
		mcp.WithString("task_id",
			mcp.Description("ID of the task to update."),
			mcp.Required(),
		),
		mcp.WithString("area_id",
			mcp.Description("New Area ID for the task. Must be a valid area_id from list_areas_and_goals tool. Only include if moving the task to a different area. If omitted, the task will remain in its current area."),
		),
		mcp.WithString("goal_id",
			mcp.Description("New Goal ID for the task. Must be a valid goal_id from list_areas_and_goals that belongs to the task's area (current or new). Only include if changing the goal association."),
		),
		mcp.WithString("name",
			mcp.Description("New plain text task name using sentence case. Sending an empty string WILL clear the name."),
			mcp.Required(),
		),
		mcp.WithString("note",
			mcp.Description("New note for the task, using Markdown formatting. Sending an empty string WILL clear the existing note. Only include if changing the task notes."),
		),
		mcp.WithNumber("estimate",
			mcp.Description("New estimated completion time in minutes (0-720, max 12 hours). Only include if user mentions changing the time estimate. Note: update_task has a lower maximum than create_task."),
			mcp.Min(0),
			mcp.Max(720),
		),
		mcp.WithString("priority",
			mcp.Description("New task priority level. Valid values: 'lowest', 'low', 'neutral', 'high', 'highest'. Only include if user wants to change the priority."),
			mcp.Enum("lowest", "low", "neutral", "high", "highest"),
		),
		mcp.WithString("motivation",
			mcp.Description("New level of importance for the task. Valid values: 'must' (critical/required), 'should' (important), 'want' (nice-to-have), or empty string to clear. Only include if changing the motivation level."),
			mcp.Enum("must", "should", "want", ""),
		),
		mcp.WithString("eisenhower",
			mcp.Description("New Eisenhower Matrix quadrant for task prioritization. Valid values: 'both urgent and important', 'urgent, but not important', 'important, but not urgent', 'neither urgent nor important', 'uncategorised' (clears the field). Only include for areas which the user has indicated follow the Eisenhower workflow."),
			mcp.Enum("both urgent and important", "urgent, but not important", "important, but not urgent", "neither urgent nor important", "uncategorised"),
		),
		mcp.WithString("status",
			mcp.Description("New task status. Valid values: 'later' (someday/backlog), 'next' (upcoming/soon), 'started' (in progress), 'waiting' (blocked), 'completed' (finished), or empty string to clear. Only include if changing the task status."),
			mcp.Enum("later", "next", "started", "waiting", "completed", ""),
		),
		mcp.WithString("scheduled_on",
			mcp.Description("New scheduled date/time for the task. Must use the formatted timestamp returned by get_timestamp tool. Sending an empty string might clear the scheduled date. Only include if changing the schedule."),
		),
	), toolHandlers.HandleUpdateTask)

	mcpServer.AddTool(mcp.NewTool("delete_task",
		mcp.WithDescription("Permanently deletes an existing task from Lunatask. This action cannot be undone."),
		mcp.WithString("task_id",
			mcp.Description("ID of the task to delete. This must be a valid task ID from an existing task in Lunatask."),
			mcp.Required(),
		),
	), toolHandlers.HandleDeleteTask)

	mcpServer.AddTool(
		mcp.NewTool(
			"list_habits_and_activities",
			mcp.WithDescription("Lists all configured habits and their IDs for habit tracking. Use this tool FIRST before track_habit_activity to identify valid habit_id values. Shows habit names, descriptions, and unique identifiers needed for tracking activities."),
		),
		toolHandlers.HandleListHabitsAndActivities,
	)

	mcpServer.AddTool(mcp.NewTool("track_habit_activity",
		mcp.WithDescription("Records completion of a habit activity in Lunatask. WORKFLOW: First use list_habits_and_activities to get valid habit_id, then use get_timestamp to format the performed_on date."),
		mcp.WithString("habit_id",
			mcp.Description("ID of the habit to track activity for. Must be a valid habit_id from list_habits_and_activities tool."),
			mcp.Required(),
		),
		mcp.WithString("performed_on",
			mcp.Description("Timestamp when the habit was performed. Must use the formatted timestamp returned by get_timestamp tool. Examples: if user says 'I did this yesterday', use get_timestamp with 'yesterday'."),
			mcp.Required(),
		),
	), toolHandlers.HandleTrackHabitActivity)

	return mcpServer
}

func createDefaultConfigFile(configPath string) {
	defaultConfig := Config{
		Server: ServerConfig{
			Host: "localhost",
			Port: 8080,
		},
		AccessToken: "",
		Timezone:    "UTC",
		Areas: []Area{{
			Name: "Example Area",
			ID:   "area-id-placeholder",
			Goals: []Goal{{
				Name: "Example Goal",
				ID:   "goal-id-placeholder",
			}},
		}},
		Habit: []Habit{{
			Name: "Example Habit",
			ID:   "habit-id-placeholder",
		}},
	}

	file, err := os.OpenFile(configPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o600)
	if err != nil {
		log.Fatalf("Failed to create default config at %s: %v", configPath, err)
	}
	defer closeFile(file)

	if err := toml.NewEncoder(file).Encode(defaultConfig); err != nil {
		log.Fatalf("Failed to encode default config to %s: %v", configPath, err)
	}

	fmt.Printf("A default config has been created at %s.\nPlease edit it to provide your Lunatask access token, correct area/goal IDs, and your timezone (IANA/Olson format, e.g. 'America/New_York'), then restart the server.\n", configPath)
	os.Exit(1)
}
