package services

// LayoutSpec — JSON shape for paliad.user_card_layouts.layout_json.
//
// Design: docs/design-projects-page-2026-05-07.md §5b.3.
//
// Validation surface (server-side):
//   - Every fact key must be in KnownFactKeys.
//   - Each key appears at most once (no duplicates).
//   - "title-row" must be the first visible fact (always-on, structural).
//   - count is bounded [1, 5] when set (only meaningful for next-events /
//     recent-verlauf).
//   - density ∈ {compact, roomy}.
//   - gridColumns ∈ {auto, 2, 3, 4}.
//
// JSON shape mirrors the TypeScript type in
// frontend/src/client/projects-cards-types.ts.

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

// FactKey enumerates the cards facts the user can show / hide / reorder.
type FactKey string

const (
	FactTitleRow       FactKey = "title-row"
	FactTypeChip       FactKey = "type-chip"
	FactStatusChip     FactKey = "status-chip"
	FactClientMatter   FactKey = "client-matter"
	FactParentPath     FactKey = "parent-path"
	FactDeadlineCounts FactKey = "deadline-counts"
	FactNextEvents     FactKey = "next-events"
	FactRecentVerlauf  FactKey = "recent-verlauf"
	FactTeamChips      FactKey = "team-chips"
	FactReference      FactKey = "reference"
	FactLastActivityAt FactKey = "last-activity-at"
)

// KnownFactKeys is the registry. Adding a new fact = add a const above
// AND append here. Frontend has its own mirror in projects-cards.ts.
var KnownFactKeys = []FactKey{
	FactTitleRow,
	FactTypeChip,
	FactStatusChip,
	FactClientMatter,
	FactParentPath,
	FactDeadlineCounts,
	FactNextEvents,
	FactRecentVerlauf,
	FactTeamChips,
	FactReference,
	FactLastActivityAt,
}

// CardDensity controls per-card padding + line-height (Kompakt / Geräumig).
// Distinct type from t-paliad-144's ListDensity (which only ranges over
// {comfortable, compact} and applies to the views.list render shape).
type CardDensity string

const (
	CardDensityCompact CardDensity = "compact"
	CardDensityRoomy   CardDensity = "roomy"
)

// GridColumns controls the responsive grid. "auto" lets the browser
// fit-as-many-as-possible at minmax(280px, 1fr); 2/3/4 force fixed columns.
type GridColumns string

const (
	GridAuto GridColumns = "auto"
	GridTwo  GridColumns = "2"
	GridThree GridColumns = "3"
	GridFour GridColumns = "4"
)

// LayoutFact is a single fact entry in the ordered facts[] array.
type LayoutFact struct {
	Key     FactKey `json:"key"`
	Visible bool    `json:"visible"`
	// Count is meaningful for next-events and recent-verlauf only. nil for
	// every other key. Bounded [1, 5] when set; default 3 (the seed value).
	Count *int `json:"count,omitempty"`
}

// LayoutSpec is the persisted card-layout shape.
type LayoutSpec struct {
	Facts          []LayoutFact `json:"facts"`
	Density        CardDensity  `json:"density"`
	GridColumns    GridColumns  `json:"grid_columns"`
	ShowAllLevels  bool         `json:"show_all_levels"`
}

// DefaultLayoutSpec returns the seed "Standard" layout per design §5b.4 —
// rich content set, all 9 facts visible, roomy density, auto grid.
func DefaultLayoutSpec() LayoutSpec {
	three := 3
	return LayoutSpec{
		Facts: []LayoutFact{
			{Key: FactTitleRow, Visible: true},
			{Key: FactTypeChip, Visible: true},
			{Key: FactStatusChip, Visible: true},
			{Key: FactClientMatter, Visible: true},
			{Key: FactParentPath, Visible: true},
			{Key: FactDeadlineCounts, Visible: true},
			{Key: FactNextEvents, Visible: true, Count: &three},
			{Key: FactRecentVerlauf, Visible: true, Count: &three},
			{Key: FactTeamChips, Visible: true},
		},
		Density:       CardDensityRoomy,
		GridColumns:   GridAuto,
		ShowAllLevels: false,
	}
}

// Validate enforces the structural invariants. Returns ErrInvalidInput
// wrapped with a precise message on the first violation.
func (s LayoutSpec) Validate() error {
	if len(s.Facts) == 0 {
		return fmt.Errorf("%w: layout.facts is empty", ErrInvalidInput)
	}

	// First visible fact must be title-row.
	firstVisible := -1
	for i, f := range s.Facts {
		if f.Visible {
			firstVisible = i
			break
		}
	}
	if firstVisible == -1 {
		return fmt.Errorf("%w: layout has no visible facts", ErrInvalidInput)
	}
	if s.Facts[firstVisible].Key != FactTitleRow {
		return fmt.Errorf("%w: first visible fact must be %q (got %q)",
			ErrInvalidInput, FactTitleRow, s.Facts[firstVisible].Key)
	}

	seen := make(map[FactKey]bool, len(s.Facts))
	for i, f := range s.Facts {
		if !slices.Contains(KnownFactKeys, f.Key) {
			return fmt.Errorf("%w: layout.facts[%d].key %q is not a known fact",
				ErrInvalidInput, i, f.Key)
		}
		if seen[f.Key] {
			return fmt.Errorf("%w: layout.facts has duplicate key %q",
				ErrInvalidInput, f.Key)
		}
		seen[f.Key] = true
		if f.Count != nil {
			if f.Key != FactNextEvents && f.Key != FactRecentVerlauf {
				return fmt.Errorf("%w: layout.facts[%d] count is only valid for next-events / recent-verlauf",
					ErrInvalidInput, i)
			}
			if *f.Count < 1 || *f.Count > 5 {
				return fmt.Errorf("%w: layout.facts[%d].count %d out of range [1, 5]",
					ErrInvalidInput, i, *f.Count)
			}
		}
	}

	switch s.Density {
	case CardDensityCompact, CardDensityRoomy:
	default:
		return fmt.Errorf("%w: layout.density %q invalid", ErrInvalidInput, s.Density)
	}

	switch s.GridColumns {
	case GridAuto, GridTwo, GridThree, GridFour:
	default:
		return fmt.Errorf("%w: layout.grid_columns %q invalid", ErrInvalidInput, s.GridColumns)
	}

	return nil
}

// ParseLayoutSpec decodes JSON bytes and validates. Used both by the HTTP
// handler (request body) and by the service (read-back from the DB column).
func ParseLayoutSpec(b []byte) (LayoutSpec, error) {
	var s LayoutSpec
	if err := json.Unmarshal(b, &s); err != nil {
		return LayoutSpec{}, fmt.Errorf("%w: layout JSON decode: %v", ErrInvalidInput, err)
	}
	if err := s.Validate(); err != nil {
		return LayoutSpec{}, err
	}
	return s, nil
}
