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

// Package notes provides MCP resources for filtered note lists.
package notes

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"sort"
	"strings"
	"time"

	"git.secluded.site/go-lunatask"
	"git.secluded.site/lune/internal/mcp/shared"
	"github.com/modelcontextprotocol/go-sdk/mcp"
)

// ErrUnknownNotebook indicates the notebook reference could not be resolved.
var ErrUnknownNotebook = errors.New("unknown notebook")

// Resource URIs for note list filters.
const (
	ResourceURIAll    = "lunatask://notes"
	ResourceURIPinned = "lunatask://notes/pinned"
	ResourceURIRecent = "lunatask://notes/recent"
)

// Notebook-scoped resource templates.
const (
	NotebookNotesTemplate       = "lunatask://notes/{notebook}"
	NotebookNotesPinnedTemplate = "lunatask://notes/{notebook}/pinned"
	NotebookNotesRecentTemplate = "lunatask://notes/{notebook}/recent"
)

// Resource descriptions.
const (
	AllDescription = `All notes. EXPENSIVE - prefer filtered resources.`

	PinnedDescription = `Pinned notes only.`

	RecentDescription = `Recently created or updated notes (last 7 days).`

	NotebookNotesDescription = `Notes in a specific notebook. EXPENSIVE - prefer filtered resources.
{notebook} accepts config key or UUID.`

	NotebookFilteredDescription = `Filtered notes in notebook. {notebook} accepts config key or UUID.`
)

// RecentWindow is the time window for recent notes.
const RecentWindow = 7 * 24 * time.Hour

// Handler handles note list resource requests.
type Handler struct {
	client    *lunatask.Client
	notebooks []shared.NotebookProvider
}

// NewHandler creates a new notes resource handler.
func NewHandler(accessToken string, notebooks []shared.NotebookProvider) *Handler {
	return &Handler{
		client:    lunatask.NewClient(accessToken, lunatask.UserAgent("lune-mcp/1.0")),
		notebooks: notebooks,
	}
}

// noteSummary represents a note in list output.
type noteSummary struct {
	DeepLink   string  `json:"deep_link"`
	NotebookID *string `json:"notebook_id,omitempty"`
	DateOn     *string `json:"date_on,omitempty"`
	Pinned     bool    `json:"pinned"`
	CreatedAt  string  `json:"created_at"`
	UpdatedAt  string  `json:"updated_at"`
}

// FilterType identifies which filter to apply.
type FilterType int

// Filter type constants.
const (
	TypeAll FilterType = iota
	TypePinned
	TypeRecent
)

// HandleReadAll handles the all notes resource.
func (h *Handler) HandleReadAll(
	ctx context.Context,
	req *mcp.ReadResourceRequest,
) (*mcp.ReadResourceResult, error) {
	return h.handleFiltered(ctx, req, TypeAll, "")
}

// HandleReadPinned handles the pinned notes resource.
func (h *Handler) HandleReadPinned(
	ctx context.Context,
	req *mcp.ReadResourceRequest,
) (*mcp.ReadResourceResult, error) {
	return h.handleFiltered(ctx, req, TypePinned, "")
}

// HandleReadRecent handles the recent notes resource.
func (h *Handler) HandleReadRecent(
	ctx context.Context,
	req *mcp.ReadResourceRequest,
) (*mcp.ReadResourceResult, error) {
	return h.handleFiltered(ctx, req, TypeRecent, "")
}

// HandleReadNotebookNotes handles notebook-scoped note resources.
func (h *Handler) HandleReadNotebookNotes(
	ctx context.Context,
	req *mcp.ReadResourceRequest,
) (*mcp.ReadResourceResult, error) {
	notebookRef, filterType := parseNotebookURI(req.Params.URI)
	if notebookRef == "" {
		return nil, fmt.Errorf("invalid URI %q: %w", req.Params.URI, mcp.ResourceNotFoundError(req.Params.URI))
	}

	notebookID, err := h.resolveNotebookRef(notebookRef)
	if err != nil {
		return nil, fmt.Errorf("invalid notebook %q: %w", notebookRef, mcp.ResourceNotFoundError(req.Params.URI))
	}

	return h.handleFiltered(ctx, req, filterType, notebookID)
}

// resolveNotebookRef resolves a notebook reference to a UUID.
// Accepts config key or UUID.
func (h *Handler) resolveNotebookRef(input string) (string, error) {
	if err := lunatask.ValidateUUID(input); err == nil {
		return input, nil
	}

	for _, nb := range h.notebooks {
		if nb.Key == input {
			return nb.ID, nil
		}
	}

	return "", fmt.Errorf("%w: %s", ErrUnknownNotebook, input)
}

func (h *Handler) handleFiltered(
	ctx context.Context,
	req *mcp.ReadResourceRequest,
	filterType FilterType,
	notebookID string,
) (*mcp.ReadResourceResult, error) {
	notes, err := h.client.ListNotes(ctx, nil)
	if err != nil {
		return nil, fmt.Errorf("fetching notes: %w", err)
	}

	if notebookID != "" {
		notes = filterByNotebook(notes, notebookID)
	}

	notes = applyFilter(notes, filterType)

	summaries := buildSummaries(notes)

	data, err := json.MarshalIndent(summaries, "", "  ")
	if err != nil {
		return nil, fmt.Errorf("marshaling notes: %w", err)
	}

	return &mcp.ReadResourceResult{
		Contents: []*mcp.ResourceContents{{
			URI:      req.Params.URI,
			MIMEType: "application/json",
			Text:     string(data),
		}},
	}, nil
}

func applyFilter(notes []lunatask.Note, filterType FilterType) []lunatask.Note {
	switch filterType {
	case TypeAll:
		return notes
	case TypePinned:
		return filterPinned(notes)
	case TypeRecent:
		return filterRecent(notes)
	default:
		return notes
	}
}

func filterPinned(notes []lunatask.Note) []lunatask.Note {
	result := make([]lunatask.Note, 0)

	for _, note := range notes {
		if note.Pinned {
			result = append(result, note)
		}
	}

	return result
}

func filterRecent(notes []lunatask.Note) []lunatask.Note {
	cutoff := time.Now().Add(-RecentWindow)
	result := make([]lunatask.Note, 0)

	for _, note := range notes {
		if note.CreatedAt.After(cutoff) || note.UpdatedAt.After(cutoff) {
			result = append(result, note)
		}
	}

	sort.Slice(result, func(i, j int) bool {
		return result[i].UpdatedAt.After(result[j].UpdatedAt)
	})

	return result
}

func filterByNotebook(notes []lunatask.Note, notebookID string) []lunatask.Note {
	result := make([]lunatask.Note, 0)

	for _, note := range notes {
		if note.NotebookID != nil && *note.NotebookID == notebookID {
			result = append(result, note)
		}
	}

	return result
}

func buildSummaries(notes []lunatask.Note) []noteSummary {
	summaries := make([]noteSummary, 0, len(notes))

	for _, note := range notes {
		summary := noteSummary{
			NotebookID: note.NotebookID,
			Pinned:     note.Pinned,
			CreatedAt:  note.CreatedAt.Format(time.RFC3339),
			UpdatedAt:  note.UpdatedAt.Format(time.RFC3339),
		}

		summary.DeepLink, _ = lunatask.BuildDeepLink(lunatask.ResourceNote, note.ID)

		if note.DateOn != nil {
			s := note.DateOn.Format("2006-01-02")
			summary.DateOn = &s
		}

		summaries = append(summaries, summary)
	}

	return summaries
}

// parseNotebookURI extracts notebook and filter type from notebook-scoped URIs.
// Examples:
//   - lunatask://notes/work -> work, TypeAll
//   - lunatask://notes/work/pinned -> work, TypePinned
//   - lunatask://notes/work/recent -> work, TypeRecent
func parseNotebookURI(uri string) (string, FilterType) {
	const prefix = "lunatask://notes/"

	filterNameToType := map[string]FilterType{
		"pinned": TypePinned,
		"recent": TypeRecent,
	}

	if !strings.HasPrefix(uri, prefix) {
		return "", TypeAll
	}

	const maxParts = 2

	rest := strings.TrimPrefix(uri, prefix)
	parts := strings.SplitN(rest, "/", maxParts)

	if len(parts) == 0 || parts[0] == "" {
		return "", TypeAll
	}

	// Check if first part is a global filter (not a notebook ref)
	if parts[0] == "pinned" || parts[0] == "recent" {
		return "", TypeAll
	}

	notebookRef := parts[0]

	if len(parts) == 1 {
		return notebookRef, TypeAll
	}

	filterType, ok := filterNameToType[parts[1]]
	if !ok {
		return "", TypeAll
	}

	return notebookRef, filterType
}
