// 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"

	"git.secluded.site/go-lunatask"
	"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"`
	TokenHash string      `toml:"token_hash,omitempty"`
	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"`
	AddTimelineNote bool `toml:"add_timeline_note"`
	TrackHabit      bool `toml:"track_habit"`

	Create bool `toml:"create"` // task, note, person, journal
	Update bool `toml:"update"` // task, note, person
	Delete bool `toml:"delete"` // task, note, person
	Query  bool `toml:"query"`  // all entities; default-disabled fallback for resources
}

// 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.
// Note: Query is default-disabled because it's a fallback for agents
// that don't support MCP resources.
func (t *ToolsConfig) ApplyDefaults() {
	// If all are false (zero value), enable everything except Query
	if !t.GetTimestamp && !t.AddTimelineNote && !t.TrackHabit &&
		!t.Create && !t.Update && !t.Delete && !t.Query {
		t.GetTimestamp = true
		t.AddTimelineNote = true
		t.TrackHabit = true
		t.Create = true
		t.Update = true
		t.Delete = true
		// Query: default-disabled (fallback for agents without resource support)
	}
}

// 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"`
	Workflow lunatask.Workflow `json:"workflow" toml:"workflow"`
	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
}
