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"
	WidgetQuickActions          WidgetKey = "quick-actions"
)

// KnownWidgetKeys is the canonical order used when seeding the factory
// default layout. New entries land at the bottom by default.
//
// Slice C activated WidgetPinnedProjects (reusing the pin-machinery
// PinService that pre-dates t-paliad-219) and added WidgetQuickActions
// (pure UI; no backend data path) per m's brief on catalog expansion.
var KnownWidgetKeys = []WidgetKey{
	WidgetDeadlineSummary,
	WidgetMatterSummary,
	WidgetUpcomingDeadlines,
	WidgetUpcomingAppointments,
	WidgetInlineAgenda,
	WidgetRecentActivity,
	WidgetInboxApprovals,
	WidgetPinnedProjects,
	WidgetQuickActions,
}

// ViewOption is one entry in a widget's "view" knob — a presentation
// variant the widget supports (e.g. list vs calendar for upcoming-
// deadlines). The ID is what's persisted in user settings; the frontend
// picks the renderer based on it.
type ViewOption struct {
	ID      string `json:"id"`
	LabelDE string `json:"label_de"`
	LabelEN string `json:"label_en"`
}

// 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
	// CountMax is an upper bound for the "count" knob when the gear pane
	// exposes a free-form numeric input alongside the dropdown. Zero =
	// dropdown-only (legacy). When non-zero, the validator accepts any
	// integer in [1, CountMax] in addition to entries in CountOptions.
	CountMax int
	// HorizonMax is the analogue for "horizon_days". Zero = dropdown-only.
	HorizonMax int
	// Views lists the supported presentation variants for the widget. Empty
	// = the widget has a single hardcoded renderer (no view picker).
	Views []ViewOption
}

// rawWidgetSettings is the typed projection of the JSON we accept. New
// knobs land here; the validator + frontend gear pane stay in lock-step.
type rawWidgetSettings struct {
	Count       *int    `json:"count,omitempty"`
	HorizonDays *int    `json:"horizon_days,omitempty"`
	View        *string `json:"view,omitempty"`
}

// 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 rawWidgetSettings
	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 && sch.CountMax == 0 {
			return fmt.Errorf("%w: widget has no count knob", ErrInvalidInput)
		}
		ok := false
		if sch.CountAllowsAll && *parsed.Count == -1 {
			ok = true
		}
		if !ok && slices.Contains(sch.CountOptions, *parsed.Count) {
			ok = true
		}
		if !ok && sch.CountMax > 0 && *parsed.Count >= 1 && *parsed.Count <= sch.CountMax {
			ok = true
		}
		if !ok {
			return fmt.Errorf("%w: count %d not in %v (max %d)", ErrInvalidInput, *parsed.Count, sch.CountOptions, sch.CountMax)
		}
	}
	if parsed.HorizonDays != nil {
		if len(sch.HorizonOptions) == 0 && sch.HorizonMax == 0 {
			return fmt.Errorf("%w: widget has no horizon knob", ErrInvalidInput)
		}
		ok := false
		if slices.Contains(sch.HorizonOptions, *parsed.HorizonDays) {
			ok = true
		}
		if !ok && sch.HorizonMax > 0 && *parsed.HorizonDays >= 1 && *parsed.HorizonDays <= sch.HorizonMax {
			ok = true
		}
		if !ok {
			return fmt.Errorf("%w: horizon_days %d not in %v (max %d)", ErrInvalidInput, *parsed.HorizonDays, sch.HorizonOptions, sch.HorizonMax)
		}
	}
	if parsed.View != nil {
		if len(sch.Views) == 0 {
			return fmt.Errorf("%w: widget has no view knob", ErrInvalidInput)
		}
		ok := false
		for _, v := range sch.Views {
			if v.ID == *parsed.View {
				ok = true
				break
			}
		}
		if !ok {
			return fmt.Errorf("%w: view %q not in catalog", ErrInvalidInput, *parsed.View)
		}
	}
	return nil
}

// WidgetDef is one entry in the catalog. Title/description fields are the
// translation-key seeds; frontend resolves them via the i18n registry.
//
// Default size (W/H) drives both the factory layout and the resize
// clamp on the gear pane. W is grid columns 1..DashboardGridColumns; H is row
// span 1..N. Zero defaults are treated as W=DashboardGridColumns, H=1.
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"`
	DefaultView    string                `json:"default_view,omitempty"`
	DefaultW       int                   `json:"default_w,omitempty"`
	DefaultH       int                   `json:"default_h,omitempty"`
	MinW           int                   `json:"min_w,omitempty"`
	MaxW           int                   `json:"max_w,omitempty"`
	MinH           int                   `json:"min_h,omitempty"`
	MaxH           int                   `json:"max_h,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.
//
// Sizes use a 12-column grid (see DashboardGridColumns). Each widget declares its
// preferred default size (W/H) plus min/max clamps that the resize handle
// honours. Catalog defaults are tuned to fill the 12-col grid sensibly
// on first load — see FactoryDefaultLayout for the assembled flow.
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

	listOrCalendar := []ViewOption{
		{ID: "list", LabelDE: "Liste", LabelEN: "List"},
		{ID: "calendar", LabelDE: "Kalender", LabelEN: "Calendar"},
	}
	activityViews := []ViewOption{
		{ID: "full", LabelDE: "Ausführlich", LabelEN: "Full"},
		{ID: "compact", LabelDE: "Kompakt", LabelEN: "Compact"},
	}
	agendaViews := []ViewOption{
		{ID: "timeline", LabelDE: "Zeitachse", LabelEN: "Timeline"},
		{ID: "list", LabelDE: "Liste", LabelEN: "List"},
	}

	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,
			DefaultW:       12,
			DefaultH:       1,
			MinW:           6,
			MaxW:           12,
			MinH:           1,
			MaxH:           2,
		},
		{
			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,
			DefaultW:       6,
			DefaultH:       1,
			MinW:           4,
			MaxW:           12,
			MinH:           1,
			MaxH:           1,
		},
		{
			Key:            WidgetUpcomingDeadlines,
			TitleDE:        "Kommende Fristen",
			TitleEN:        "Upcoming deadlines",
			DescriptionDE:  "Liste der nächsten Fristen — Anzahl, Zeitraum und Darstellung konfigurierbar.",
			DescriptionEN:  "List of upcoming deadlines — count, horizon and view configurable.",
			DefaultVisible: true,
			DefaultCount:   &tenDefault,
			DefaultHorizon: &thirtyDefault,
			DefaultView:    "list",
			DefaultW:       6,
			DefaultH:       2,
			MinW:           4,
			MaxW:           12,
			MinH:           1,
			MaxH:           4,
			Settings: &WidgetSettingsSchema{
				CountOptions:   listCounts,
				CountMax:       50,
				HorizonOptions: listHorizon,
				HorizonMax:     365,
				Views:          listOrCalendar,
			},
		},
		{
			Key:            WidgetUpcomingAppointments,
			TitleDE:        "Kommende Termine",
			TitleEN:        "Upcoming appointments",
			DescriptionDE:  "Liste der nächsten Termine — Anzahl, Zeitraum und Darstellung konfigurierbar.",
			DescriptionEN:  "List of upcoming appointments — count, horizon and view configurable.",
			DefaultVisible: true,
			DefaultCount:   &tenDefault,
			DefaultHorizon: &thirtyDefault,
			DefaultView:    "list",
			DefaultW:       6,
			DefaultH:       2,
			MinW:           4,
			MaxW:           12,
			MinH:           1,
			MaxH:           4,
			Settings: &WidgetSettingsSchema{
				CountOptions:   listCounts,
				CountMax:       50,
				HorizonOptions: listHorizon,
				HorizonMax:     365,
				Views:          listOrCalendar,
			},
		},
		{
			Key:            WidgetInlineAgenda,
			TitleDE:        "Agenda",
			TitleEN:        "Agenda",
			DescriptionDE:  "Agenda mit Fristen und Terminen kombiniert — Zeitraum und Darstellung konfigurierbar.",
			DescriptionEN:  "Agenda combining deadlines and appointments — horizon and view configurable.",
			DefaultVisible: true,
			DefaultHorizon: &thirtyDefault,
			DefaultView:    "timeline",
			DefaultW:       12,
			DefaultH:       2,
			MinW:           6,
			MaxW:           12,
			MinH:           1,
			MaxH:           4,
			Settings: &WidgetSettingsSchema{
				HorizonOptions: agendaHorizon,
				HorizonMax:     365,
				Views:          agendaViews,
			},
		},
		{
			Key:            WidgetRecentActivity,
			TitleDE:        "Letzte Aktivität",
			TitleEN:        "Recent activity",
			DescriptionDE:  "Verlauf der letzten Ereignisse — Anzahl und Darstellung konfigurierbar.",
			DescriptionEN:  "Recent events across your visible matters — count and view configurable.",
			DefaultVisible: true,
			DefaultCount:   &tenDefault,
			DefaultView:    "full",
			DefaultW:       12,
			DefaultH:       2,
			MinW:           4,
			MaxW:           12,
			MinH:           1,
			MaxH:           4,
			Settings: &WidgetSettingsSchema{
				CountOptions: listCounts,
				CountMax:     50,
				Views:        activityViews,
			},
		},
		{
			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,
			DefaultW:       6,
			DefaultH:       1,
			MinW:           4,
			MaxW:           12,
			MinH:           1,
			MaxH:           2,
			Settings: &WidgetSettingsSchema{
				CountOptions: inboxCounts,
				CountMax:     50,
			},
		},
		// Slice C: pinned-projects rides on the pre-existing PinService
		// (paliad.user_pinned_projects, mig 062/063 — pre-dates this
		// task). DefaultVisible=false so existing users don't get a new
		// widget injected unannounced; they opt in via the picker.
		{
			Key:            WidgetPinnedProjects,
			TitleDE:        "Angepinnte Akten",
			TitleEN:        "Pinned matters",
			DescriptionDE:  "Schneller Zugriff auf deine angepinnten Akten.",
			DescriptionEN:  "Quick access to your pinned matters.",
			DefaultVisible: false,
			DefaultCount:   &tenDefault,
			DefaultW:       6,
			DefaultH:       2,
			MinW:           4,
			MaxW:           12,
			MinH:           1,
			MaxH:           4,
			Settings: &WidgetSettingsSchema{
				CountOptions:   listCounts,
				CountAllowsAll: true,
				CountMax:       50,
			},
		},
		// Slice C: quick-actions is pure UI — no backend payload, no
		// settings. Renders 3 affordances ("+ Akte", "+ Frist",
		// "+ Termin") that link to the existing create surfaces.
		{
			Key:            WidgetQuickActions,
			TitleDE:        "Schnellzugriff",
			TitleEN:        "Quick actions",
			DescriptionDE:  "Direkte Buttons für neue Akten, Fristen und Termine.",
			DescriptionEN:  "Direct buttons for new matters, deadlines, and appointments.",
			DefaultVisible: false,
			DefaultW:       12,
			DefaultH:       1,
			MinW:           6,
			MaxW:           12,
			MinH:           1,
			MaxH:           1,
		},
	}
}

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