package services

// RenderSpec is the structured render description that drives how a view
// is presented. Same shape lives in paliad.user_views.render_spec (jsonb)
// and is composed by the frontend's view editor.
//
// Design: docs/design-data-display-model-2026-05-06.md §4 (Q4-Q5).
//
// Q4 lock-in (m, 2026-05-07): three shapes — list, cards, calendar.
// "Activity" is content selection (sources + filters), not visualisation —
// achieved via shape="list", list.density="compact", list.columns=
// ["time","actor","title","project"]. Same component, different config.

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

// RenderShape identifies which presentation component a view uses.
type RenderShape string

const (
	ShapeList     RenderShape = "list"
	ShapeCards    RenderShape = "cards"
	ShapeCalendar RenderShape = "calendar"
	// ShapeTimeline (t-paliad-177 Slice 4, faraday-Q7): cross-project
	// horizontal chart rendered by frontend/src/client/views/shape-
	// timeline-cv.ts on top of the same SVG renderer that powers
	// /projects/{id}/chart. Lane axis = project_id. Adapter is lossy:
	// ProjectionService projected rows are NOT surfaced (ViewService
	// doesn't run the calculator). UI tooltip on first open documents
	// the limitation.
	ShapeTimeline RenderShape = "timeline"
)

// AllShapes lists every supported shape. Used by the validator and by
// the in-page shape switcher.
var AllShapes = []RenderShape{ShapeList, ShapeCards, ShapeCalendar, ShapeTimeline}

// RenderSpec is the top-level render description.
//
// Per-shape config blocks are kept on save even when a different shape
// is selected, so flipping back to a previously-used shape preserves
// its tweaks (Q5 design decision).
type RenderSpec struct {
	Shape    RenderShape     `json:"shape"`
	List     *ListConfig     `json:"list,omitempty"`
	Cards    *CardsConfig    `json:"cards,omitempty"`
	Calendar *CalendarConfig `json:"calendar,omitempty"`
	Timeline *TimelineConfig `json:"timeline,omitempty"`
}

// TimelineConfig is the per-shape config for shape=timeline. Mirrors the
// URL-state knobs of the standalone /projects/{id}/chart page: a saved
// CV-timeline view bakes the user's chosen palette / density / range
// preset into render_spec so reopening the view restores the same
// visual. None are required — empty defaults match the standalone
// chart's defaults (default palette, standard density, 1y range).
type TimelineConfig struct {
	Palette     string `json:"palette,omitempty"`
	Density     string `json:"density,omitempty"`
	RangePreset string `json:"range_preset,omitempty"`
	RangeFrom   string `json:"range_from,omitempty"`
	RangeTo     string `json:"range_to,omitempty"`
}

// ListConfig is the per-shape config for shape=list. Powers both the
// /events table look (density=comfortable) and the activity-feed look
// (density=compact + actor/time columns).
//
// RowAction tells shape-list which row interaction to wire when the
// universal <FilterBar> renders the table. "navigate" (the default and
// the contract for the existing /agenda/dashboard surfaces) routes a
// row click to a per-kind detail page. "complete_toggle" is the
// /events deadline-row pattern (checkbox + reopen button). "approve"
// is the /inbox approver row (approve/reject buttons + revoke). "none"
// is read-only (audit views, retrospective lists).
//
// shape-list.ts honours this when emitting the table's `entity-table`
// classes — `entity-table--readonly` plus `none` skips the navigate
// handler entirely.
type ListConfig struct {
	Columns   []string      `json:"columns,omitempty"`
	Sort      SortOrder     `json:"sort,omitempty"`
	Density   ListDensity   `json:"density,omitempty"`
	RowAction ListRowAction `json:"row_action,omitempty"`
}

// CardsConfig is the per-shape config for shape=cards.
type CardsConfig struct {
	GroupBy        CardsGroupBy `json:"group_by,omitempty"`
	Sort           SortOrder    `json:"sort,omitempty"`
	ShowEmptyDays  bool         `json:"show_empty_days,omitempty"`
}

// CalendarConfig is the per-shape config for shape=calendar.
type CalendarConfig struct {
	DefaultView   CalendarView `json:"default_view,omitempty"`
	ShowWeekends  bool         `json:"show_weekends,omitempty"`
}

type SortOrder string

const (
	SortDateAsc  SortOrder = "date_asc"
	SortDateDesc SortOrder = "date_desc"
)

type ListDensity string

const (
	DensityComfortable ListDensity = "comfortable"
	DensityCompact     ListDensity = "compact"
)

// ListRowAction identifies which row interaction the list-shape renderer
// should wire. Defaults to RowActionNavigate when empty so existing
// SystemView definitions and saved user views continue to render rows
// that route to the per-kind detail page.
type ListRowAction string

const (
	RowActionNavigate       ListRowAction = "navigate"
	RowActionCompleteToggle ListRowAction = "complete_toggle"
	RowActionApprove        ListRowAction = "approve"
	RowActionNone           ListRowAction = "none"
)

// KnownRowActions is the registry the validator checks against. Adding a
// new action = add a const above AND append here AND extend
// shape-list.ts's switch.
var KnownRowActions = []ListRowAction{
	RowActionNavigate,
	RowActionCompleteToggle,
	RowActionApprove,
	RowActionNone,
}

type CardsGroupBy string

const (
	CardsGroupByDay  CardsGroupBy = "day"
	CardsGroupByWeek CardsGroupBy = "week"
	CardsGroupByNone CardsGroupBy = "none"
)

type CalendarView string

const (
	CalendarMonth CalendarView = "month"
	CalendarWeek  CalendarView = "week"
)

// KnownListColumns is the curated set of column keys the list-shape
// renderer understands. Validator rejects anything else so a typo in
// the editor surfaces immediately.
var KnownListColumns = []string{
	"date", "time", "title", "project", "actor", "status",
	"rule", "event_type", "location", "appointment_type",
	"approval_status", "decided_by", "kind",
}

// Validate runs the full RenderSpec contract.
func (s *RenderSpec) Validate() error {
	if s == nil {
		return fmt.Errorf("%w: render_spec is required", ErrInvalidInput)
	}
	switch s.Shape {
	case ShapeList, ShapeCards, ShapeCalendar, ShapeTimeline:
		// fine
	default:
		return fmt.Errorf("%w: unknown render_spec.shape %q", ErrInvalidInput, s.Shape)
	}

	if s.List != nil {
		if err := s.List.validate(); err != nil {
			return err
		}
	}
	if s.Cards != nil {
		if err := s.Cards.validate(); err != nil {
			return err
		}
	}
	if s.Calendar != nil {
		if err := s.Calendar.validate(); err != nil {
			return err
		}
	}
	if s.Timeline != nil {
		if err := s.Timeline.validate(); err != nil {
			return err
		}
	}
	return nil
}

// KnownTimelinePalettes / Densities / Ranges mirror the frontend enums
// in shape-timeline-chart.ts. Anything outside this set is rejected so
// a stray value from an old build / hostile editor can't sneak into
// stored render_spec rows.
var (
	knownTimelinePalettes = []string{
		"default", "kind-coded", "track-coded", "high-contrast", "print",
	}
	knownTimelineDensities = []string{
		"compact", "standard", "spacious",
	}
	knownTimelineRanges = []string{
		"1y", "2y", "all", "custom",
	}
)

func (c *TimelineConfig) validate() error {
	if c.Palette != "" && !slices.Contains(knownTimelinePalettes, c.Palette) {
		return fmt.Errorf("%w: unknown timeline.palette %q", ErrInvalidInput, c.Palette)
	}
	if c.Density != "" && !slices.Contains(knownTimelineDensities, c.Density) {
		return fmt.Errorf("%w: unknown timeline.density %q", ErrInvalidInput, c.Density)
	}
	if c.RangePreset != "" && !slices.Contains(knownTimelineRanges, c.RangePreset) {
		return fmt.Errorf("%w: unknown timeline.range_preset %q", ErrInvalidInput, c.RangePreset)
	}
	// RangeFrom / RangeTo are free-form ISO dates — the frontend regex-
	// checks them; here we only verify they're plain ASCII length-bounded
	// so a giant string can't bloat the jsonb column.
	if len(c.RangeFrom) > 32 {
		return fmt.Errorf("%w: timeline.range_from too long", ErrInvalidInput)
	}
	if len(c.RangeTo) > 32 {
		return fmt.Errorf("%w: timeline.range_to too long", ErrInvalidInput)
	}
	return nil
}

func (c *ListConfig) validate() error {
	for _, col := range c.Columns {
		if !slices.Contains(KnownListColumns, col) {
			return fmt.Errorf("%w: unknown list.columns value %q", ErrInvalidInput, col)
		}
	}
	switch c.Sort {
	case "", SortDateAsc, SortDateDesc:
	default:
		return fmt.Errorf("%w: unknown list.sort %q", ErrInvalidInput, c.Sort)
	}
	switch c.Density {
	case "", DensityComfortable, DensityCompact:
	default:
		return fmt.Errorf("%w: unknown list.density %q", ErrInvalidInput, c.Density)
	}
	if c.RowAction != "" && !slices.Contains(KnownRowActions, c.RowAction) {
		return fmt.Errorf("%w: unknown list.row_action %q", ErrInvalidInput, c.RowAction)
	}
	return nil
}

func (c *CardsConfig) validate() error {
	switch c.GroupBy {
	case "", CardsGroupByDay, CardsGroupByWeek, CardsGroupByNone:
	default:
		return fmt.Errorf("%w: unknown cards.group_by %q", ErrInvalidInput, c.GroupBy)
	}
	switch c.Sort {
	case "", SortDateAsc, SortDateDesc:
	default:
		return fmt.Errorf("%w: unknown cards.sort %q", ErrInvalidInput, c.Sort)
	}
	return nil
}

func (c *CalendarConfig) validate() error {
	switch c.DefaultView {
	case "", CalendarMonth, CalendarWeek:
	default:
		return fmt.Errorf("%w: unknown calendar.default_view %q", ErrInvalidInput, c.DefaultView)
	}
	return nil
}

// DefaultRenderSpec returns a minimal valid spec — used as the seed when
// the editor opens with a blank canvas.
func DefaultRenderSpec() RenderSpec {
	return RenderSpec{
		Shape: ShapeList,
		List: &ListConfig{
			Sort:    SortDateAsc,
			Density: DensityComfortable,
		},
	}
}

// MarshalRenderSpec validates + encodes for storage.
func MarshalRenderSpec(s RenderSpec) ([]byte, error) {
	if err := s.Validate(); err != nil {
		return nil, err
	}
	return json.Marshal(s)
}

// UnmarshalRenderSpec parses + validates.
func UnmarshalRenderSpec(b []byte) (RenderSpec, error) {
	var s RenderSpec
	if err := json.Unmarshal(b, &s); err != nil {
		return RenderSpec{}, fmt.Errorf("%w: render_spec malformed: %v", ErrInvalidInput, err)
	}
	if err := s.Validate(); err != nil {
		return RenderSpec{}, err
	}
	return s, nil
}
