package services

// Widget catalog for the configurable dashboard (t-paliad-219).
//
// Design: docs/design-dashboard-configurable-2026-05-20.md §4 (catalog) and
// §18 Note B (settings schema).
//
// The catalog is the source of truth for which widgets a user can pick.
// Adding a new widget = add a WidgetKey const + append a WidgetDef in
// WidgetCatalog. Frontend has its own mirror in
// frontend/src/client/widgets/registry.ts; the two must stay in sync.
//
// Versioning rule (design §10): unknown keys in a user's saved layout are
// dropped silently at read time; write paths validate against the catalog.

import (
	"encoding/json"
	"fmt"
	"slices"
)

// WidgetKey is the catalog identifier for a single widget.
type WidgetKey string

const (
	WidgetDeadlineSummary       WidgetKey = "deadline-summary"
	WidgetMatterSummary         WidgetKey = "matter-summary"
	WidgetUpcomingDeadlines     WidgetKey = "upcoming-deadlines"
	WidgetUpcomingAppointments  WidgetKey = "upcoming-appointments"
	WidgetInlineAgenda          WidgetKey = "inline-agenda"
	WidgetRecentActivity        WidgetKey = "recent-activity"
	WidgetInboxApprovals        WidgetKey = "inbox-approvals"
	WidgetPinnedProjects        WidgetKey = "pinned-projects"
)

// KnownWidgetKeys is the canonical order used when seeding the factory
// default layout. New entries land at the bottom by default.
var KnownWidgetKeys = []WidgetKey{
	WidgetDeadlineSummary,
	WidgetMatterSummary,
	WidgetUpcomingDeadlines,
	WidgetUpcomingAppointments,
	WidgetInlineAgenda,
	WidgetRecentActivity,
	WidgetInboxApprovals,
	// WidgetPinnedProjects ships in Slice C (catalog expansion) — not in
	// the Slice A1 baseline. Keep the const above for forward-compat;
	// omit from KnownWidgetKeys until the widget module lands.
}

// WidgetSettingsSchema declares which knobs a widget exposes. nil = no
// per-widget settings (the gear icon is hidden in edit mode).
type WidgetSettingsSchema struct {
	// CountOptions lists permitted "count" values. Empty = no count knob.
	CountOptions []int
	// HorizonOptions lists permitted "horizon_days" values. Empty = no
	// horizon knob.
	HorizonOptions []int
	// CountAllowsAll is true when "all" is a legal value for count
	// (rendered as the literal -1 in the JSON). pinned-projects uses this.
	CountAllowsAll bool
}

// Validate enforces the schema against a raw settings blob. nil schema
// rejects any non-empty settings; empty settings always pass.
func (sch *WidgetSettingsSchema) Validate(raw json.RawMessage) error {
	if len(raw) == 0 || string(raw) == "null" {
		return nil
	}
	if sch == nil {
		return fmt.Errorf("%w: widget has no settings; got %s", ErrInvalidInput, string(raw))
	}

	var parsed struct {
		Count       *int `json:"count,omitempty"`
		HorizonDays *int `json:"horizon_days,omitempty"`
	}
	if err := json.Unmarshal(raw, &parsed); err != nil {
		return fmt.Errorf("%w: widget settings decode: %v", ErrInvalidInput, err)
	}

	if parsed.Count != nil {
		if len(sch.CountOptions) == 0 {
			return fmt.Errorf("%w: widget has no count knob", ErrInvalidInput)
		}
		if !(sch.CountAllowsAll && *parsed.Count == -1) && !slices.Contains(sch.CountOptions, *parsed.Count) {
			return fmt.Errorf("%w: count %d not in %v", ErrInvalidInput, *parsed.Count, sch.CountOptions)
		}
	}
	if parsed.HorizonDays != nil {
		if len(sch.HorizonOptions) == 0 {
			return fmt.Errorf("%w: widget has no horizon knob", ErrInvalidInput)
		}
		if !slices.Contains(sch.HorizonOptions, *parsed.HorizonDays) {
			return fmt.Errorf("%w: horizon_days %d not in %v", ErrInvalidInput, *parsed.HorizonDays, sch.HorizonOptions)
		}
	}
	return nil
}

// WidgetDef is one entry in the catalog. Title/description fields are the
// translation-key seeds; frontend resolves them via the i18n registry.
type WidgetDef struct {
	Key            WidgetKey            `json:"key"`
	TitleDE        string               `json:"title_de"`
	TitleEN        string               `json:"title_en"`
	DescriptionDE  string               `json:"description_de"`
	DescriptionEN  string               `json:"description_en"`
	DefaultVisible bool                 `json:"default_visible"`
	DefaultCount   *int                 `json:"default_count,omitempty"`
	DefaultHorizon *int                 `json:"default_horizon_days,omitempty"`
	Settings       *WidgetSettingsSchema `json:"settings,omitempty"`
}

// WidgetCatalog returns the v1 catalog. Returned by value (small struct
// slice) so callers can freely append i18n overrides for the wire format.
func WidgetCatalog() []WidgetDef {
	listCounts := []int{1, 3, 5, 10, 20}
	listHorizon := []int{7, 14, 30, 60}
	inboxCounts := []int{1, 3, 5, 10}
	agendaHorizon := []int{14, 30, 60}

	tenDefault := 10
	threeDefault := 3
	thirtyDefault := 30

	return []WidgetDef{
		{
			Key:            WidgetDeadlineSummary,
			TitleDE:        "Fristen auf einen Blick",
			TitleEN:        "Deadlines at a glance",
			DescriptionDE:  "Ampel-Karten für überfällige, heutige und kommende Fristen.",
			DescriptionEN:  "Traffic-light cards for overdue, today, and upcoming deadlines.",
			DefaultVisible: true,
		},
		{
			Key:            WidgetMatterSummary,
			TitleDE:        "Meine Akten",
			TitleEN:        "My Matters",
			DescriptionDE:  "Aktiv-, archiviert- und Gesamtzahl deiner sichtbaren Akten.",
			DescriptionEN:  "Active, archived and total counts of your visible matters.",
			DefaultVisible: true,
		},
		{
			Key:            WidgetUpcomingDeadlines,
			TitleDE:        "Kommende Fristen",
			TitleEN:        "Upcoming deadlines",
			DescriptionDE:  "Liste der nächsten Fristen — Anzahl und Zeitraum konfigurierbar.",
			DescriptionEN:  "List of upcoming deadlines — count and horizon configurable.",
			DefaultVisible: true,
			DefaultCount:   &tenDefault,
			DefaultHorizon: &thirtyDefault,
			Settings: &WidgetSettingsSchema{
				CountOptions:   listCounts,
				HorizonOptions: listHorizon,
			},
		},
		{
			Key:            WidgetUpcomingAppointments,
			TitleDE:        "Kommende Termine",
			TitleEN:        "Upcoming appointments",
			DescriptionDE:  "Liste der nächsten Termine — Anzahl und Zeitraum konfigurierbar.",
			DescriptionEN:  "List of upcoming appointments — count and horizon configurable.",
			DefaultVisible: true,
			DefaultCount:   &tenDefault,
			DefaultHorizon: &thirtyDefault,
			Settings: &WidgetSettingsSchema{
				CountOptions:   listCounts,
				HorizonOptions: listHorizon,
			},
		},
		{
			Key:            WidgetInlineAgenda,
			TitleDE:        "Agenda",
			TitleEN:        "Agenda",
			DescriptionDE:  "30-Tage-Agenda mit Fristen und Terminen kombiniert.",
			DescriptionEN:  "30-day agenda combining deadlines and appointments.",
			DefaultVisible: true,
			DefaultHorizon: &thirtyDefault,
			Settings: &WidgetSettingsSchema{
				HorizonOptions: agendaHorizon,
			},
		},
		{
			Key:            WidgetRecentActivity,
			TitleDE:        "Letzte Aktivität",
			TitleEN:        "Recent activity",
			DescriptionDE:  "Verlauf der letzten Ereignisse in deinen sichtbaren Akten.",
			DescriptionEN:  "Recent events across your visible matters.",
			DefaultVisible: true,
			DefaultCount:   &tenDefault,
			Settings: &WidgetSettingsSchema{
				CountOptions: listCounts,
			},
		},
		{
			Key:            WidgetInboxApprovals,
			TitleDE:        "Offene Freigaben",
			TitleEN:        "Open approvals",
			DescriptionDE:  "Deine offenen Freigaben mit Anzahl und einer kurzen Liste.",
			DescriptionEN:  "Your open approval requests with count and a short list.",
			DefaultVisible: true,
			DefaultCount:   &threeDefault,
			Settings: &WidgetSettingsSchema{
				CountOptions: inboxCounts,
			},
		},
	}
}

// LookupWidgetDef returns the catalog entry for a key, or false if unknown.
func LookupWidgetDef(key WidgetKey) (WidgetDef, bool) {
	for _, def := range WidgetCatalog() {
		if def.Key == key {
			return def, true
		}
	}
	return WidgetDef{}, false
}
