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

// Package config handles loading and saving lune configuration from TOML.
package config

import (
	"errors"
	"fmt"
	"os"
	"path/filepath"

	"github.com/BurntSushi/toml"
)

// ErrNotFound indicates the config file doesn't exist.
var ErrNotFound = errors.New("config file not found")

// Config represents the lune configuration file structure.
type Config struct {
	UI        UIConfig   `toml:"ui"`
	Defaults  Defaults   `toml:"defaults"`
	MCP       MCPConfig  `toml:"mcp"`
	Areas     []Area     `toml:"areas"`
	Notebooks []Notebook `toml:"notebooks"`
	Habits    []Habit    `toml:"habits"`
}

// MCPConfig holds MCP server settings.
type MCPConfig struct {
	Host     string      `toml:"host"`
	Port     int         `toml:"port"`
	Timezone string      `toml:"timezone"`
	Tools    ToolsConfig `toml:"tools"`
}

// ToolsConfig controls which MCP tools are enabled.
// All tools default to enabled when not explicitly set.
type ToolsConfig struct {
	GetTimestamp bool `toml:"get_timestamp"`

	CreateTask bool `toml:"create_task"`
	UpdateTask bool `toml:"update_task"`
	DeleteTask bool `toml:"delete_task"`
	ListTasks  bool `toml:"list_tasks"`
	ShowTask   bool `toml:"show_task"`

	CreateNote bool `toml:"create_note"`
	UpdateNote bool `toml:"update_note"`
	DeleteNote bool `toml:"delete_note"`
	ListNotes  bool `toml:"list_notes"`
	ShowNote   bool `toml:"show_note"`

	CreatePerson   bool `toml:"create_person"`
	UpdatePerson   bool `toml:"update_person"`
	DeletePerson   bool `toml:"delete_person"`
	ListPeople     bool `toml:"list_people"`
	ShowPerson     bool `toml:"show_person"`
	PersonTimeline bool `toml:"person_timeline"`

	TrackHabit bool `toml:"track_habit"`
	ListHabits bool `toml:"list_habits"`

	CreateJournal bool `toml:"create_journal"`
}

// MCPDefaults applies default values to MCP config.
func (c *MCPConfig) MCPDefaults() {
	if c.Host == "" {
		c.Host = "localhost"
	}

	if c.Port == 0 {
		c.Port = 8080
	}

	if c.Timezone == "" {
		c.Timezone = "UTC"
	}

	c.Tools.ApplyDefaults()
}

// ApplyDefaults enables all tools if none are explicitly configured.
//
//nolint:cyclop // Complexity from repetitive boolean checks; structurally simple.
func (t *ToolsConfig) ApplyDefaults() {
	// If all are false (zero value), enable everything
	if !t.GetTimestamp && !t.CreateTask && !t.UpdateTask && !t.DeleteTask &&
		!t.ListTasks && !t.ShowTask && !t.CreateNote && !t.UpdateNote &&
		!t.DeleteNote && !t.ListNotes && !t.ShowNote && !t.CreatePerson &&
		!t.UpdatePerson && !t.DeletePerson && !t.ListPeople && !t.ShowPerson &&
		!t.PersonTimeline && !t.TrackHabit && !t.ListHabits && !t.CreateJournal {
		t.GetTimestamp = true
		t.CreateTask = true
		t.UpdateTask = true
		t.DeleteTask = true
		t.ListTasks = true
		t.ShowTask = true
		t.CreateNote = true
		t.UpdateNote = true
		t.DeleteNote = true
		t.ListNotes = true
		t.ShowNote = true
		t.CreatePerson = true
		t.UpdatePerson = true
		t.DeletePerson = true
		t.ListPeople = true
		t.ShowPerson = true
		t.PersonTimeline = true
		t.TrackHabit = true
		t.ListHabits = true
		t.CreateJournal = true
	}
}

// UIConfig holds user interface preferences.
type UIConfig struct {
	Color string `toml:"color"` // "always", "never", "auto"
}

// Defaults holds default selections for commands.
type Defaults struct {
	Area     string `toml:"area"`
	Notebook string `toml:"notebook"`
}

// Area represents a Lunatask area of life with its goals.
//
//nolint:recvcheck // Value receivers for Keyed interface; pointer receiver for GoalByKey is intentional.
type Area struct {
	ID    string `json:"id"    toml:"id"`
	Name  string `json:"name"  toml:"name"`
	Key   string `json:"key"   toml:"key"`
	Goals []Goal `json:"goals" toml:"goals"`
}

// Goal represents a goal within an area.
type Goal struct {
	ID   string `json:"id"   toml:"id"`
	Name string `json:"name" toml:"name"`
	Key  string `json:"key"  toml:"key"`
}

// Notebook represents a Lunatask notebook for notes.
type Notebook struct {
	ID   string `json:"id"   toml:"id"`
	Name string `json:"name" toml:"name"`
	Key  string `json:"key"  toml:"key"`
}

// Habit represents a trackable habit.
type Habit struct {
	ID   string `json:"id"   toml:"id"`
	Name string `json:"name" toml:"name"`
	Key  string `json:"key"  toml:"key"`
}

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

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

// GetKey returns the area key.
func (a Area) GetKey() string { return a.Key }

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

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

// GetKey returns the goal key.
func (g Goal) GetKey() string { return g.Key }

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

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

// GetKey returns the habit key.
func (h Habit) GetKey() string { return h.Key }

// GetID returns the notebook ID.
func (n Notebook) GetID() string { return n.ID }

// GetName returns the notebook name.
func (n Notebook) GetName() string { return n.Name }

// GetKey returns the notebook key.
func (n Notebook) GetKey() string { return n.Key }

// Path returns the path to the config file.
func Path() (string, error) {
	configDir, err := os.UserConfigDir()
	if err != nil {
		return "", fmt.Errorf("getting config dir: %w", err)
	}

	return filepath.Join(configDir, "lune", "config.toml"), nil
}

// Load reads the config file. Returns ErrNotFound if the file doesn't exist.
func Load() (*Config, error) {
	path, err := Path()
	if err != nil {
		return nil, err
	}

	var cfg Config
	if _, err := toml.DecodeFile(path, &cfg); err != nil {
		if errors.Is(err, os.ErrNotExist) {
			return nil, ErrNotFound
		}

		return nil, fmt.Errorf("decoding config: %w", err)
	}

	return &cfg, nil
}

// Save writes the config to disk.
func (c *Config) Save() error {
	path, err := Path()
	if err != nil {
		return err
	}

	if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
		return fmt.Errorf("creating config dir: %w", err)
	}

	//nolint:gosec,mnd // path is from user config dir; 0o600 is intentional
	f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600)
	if err != nil {
		return fmt.Errorf("creating config file: %w", err)
	}

	if err := toml.NewEncoder(f).Encode(c); err != nil {
		_ = f.Close()

		return fmt.Errorf("encoding config: %w", err)
	}

	if err := f.Close(); err != nil {
		return fmt.Errorf("closing config file: %w", err)
	}

	return nil
}

// AreaByKey finds an area by its key.
func (c *Config) AreaByKey(key string) *Area {
	for i := range c.Areas {
		if c.Areas[i].Key == key {
			return &c.Areas[i]
		}
	}

	return nil
}

// NotebookByKey finds a notebook by its key.
func (c *Config) NotebookByKey(key string) *Notebook {
	for i := range c.Notebooks {
		if c.Notebooks[i].Key == key {
			return &c.Notebooks[i]
		}
	}

	return nil
}

// HabitByKey finds a habit by its key.
func (c *Config) HabitByKey(key string) *Habit {
	for i := range c.Habits {
		if c.Habits[i].Key == key {
			return &c.Habits[i]
		}
	}

	return nil
}

// GoalByKey finds a goal within this area by its key.
func (a *Area) GoalByKey(key string) *Goal {
	for i := range a.Goals {
		if a.Goals[i].Key == key {
			return &a.Goals[i]
		}
	}

	return nil
}

// GoalMatch pairs a goal with its parent area.
type GoalMatch struct {
	Goal *Goal
	Area *Area
}

// FindGoalsByKey returns all goals matching the given key across all areas.
func (c *Config) FindGoalsByKey(key string) []GoalMatch {
	var matches []GoalMatch

	for i := range c.Areas {
		area := &c.Areas[i]

		for j := range area.Goals {
			if area.Goals[j].Key == key {
				matches = append(matches, GoalMatch{
					Goal: &area.Goals[j],
					Area: area,
				})
			}
		}
	}

	return matches
}

// AreaByID finds an area by its ID.
func (c *Config) AreaByID(id string) *Area {
	for i := range c.Areas {
		if c.Areas[i].ID == id {
			return &c.Areas[i]
		}
	}

	return nil
}

// GoalByID finds a goal by its ID across all areas.
func (c *Config) GoalByID(id string) *GoalMatch {
	for i := range c.Areas {
		area := &c.Areas[i]

		for j := range area.Goals {
			if area.Goals[j].ID == id {
				return &GoalMatch{
					Goal: &area.Goals[j],
					Area: area,
				}
			}
		}
	}

	return nil
}

// NotebookByID finds a notebook by its ID.
func (c *Config) NotebookByID(id string) *Notebook {
	for i := range c.Notebooks {
		if c.Notebooks[i].ID == id {
			return &c.Notebooks[i]
		}
	}

	return nil
}
