package services

import (
	"context"
	"database/sql"
	"errors"
	"fmt"
	"sync"

	"github.com/jmoiron/sqlx"
)

// ErrUnknownCourt is returned when a court ID is not found in paliad.courts.
var ErrUnknownCourt = errors.New("unknown court")

// Court is the deadline-computation slice of a court row from paliad.courts.
// The rich Gerichtsverzeichnis (addresses, languages, contacts) lives in the
// static catalog at internal/handlers/courts.go and is sibling to this entity,
// not master/replica — the static slice is what users browse, paliad.courts
// is what the deadline math joins against.
type Court struct {
	ID         string  `db:"id"          json:"id"`
	Code       string  `db:"code"        json:"code"`
	NameDE     string  `db:"name_de"     json:"nameDE"`
	NameEN     string  `db:"name_en"     json:"nameEN"`
	Country    string  `db:"country"     json:"country"`           // ISO-3166 alpha-2
	Regime     *string `db:"regime"      json:"regime,omitempty"` // 'UPC' | 'EPO' | NULL
	CourtType  string  `db:"court_type"  json:"courtType"`
	ParentID   *string `db:"parent_id"   json:"parentId,omitempty"`
	SortOrder  int     `db:"sort_order"  json:"sortOrder"`
	IsActive   bool    `db:"is_active"   json:"isActive"`
}

// RegimeOrEmpty returns the court's regime as a plain string ("UPC" / "EPO"
// / "") so callers don't have to dereference Court.Regime.
func (c Court) RegimeOrEmpty() string {
	if c.Regime == nil {
		return ""
	}
	return *c.Regime
}

// CourtService loads + caches paliad.courts. Cache is built on first lookup
// and shared across subsequent calls; the catalog is small (~50 rows) and
// changes only via migration, so a single-shot load is fine.
type CourtService struct {
	db    *sqlx.DB
	once  sync.Once
	mu    sync.RWMutex
	byID  map[string]Court
	all   []Court
	err   error
}

// NewCourtService wires the service to the DB.
func NewCourtService(db *sqlx.DB) *CourtService {
	return &CourtService{db: db}
}

func (s *CourtService) load() {
	if s.db == nil {
		s.err = fmt.Errorf("courts service: no DB configured")
		return
	}
	var rows []Court
	err := s.db.SelectContext(context.Background(), &rows, `
		SELECT id, code, name_de, name_en, country, regime, court_type, parent_id, sort_order, is_active
		  FROM paliad.courts
		 WHERE is_active = true
		 ORDER BY sort_order, id`)
	if err != nil {
		s.err = fmt.Errorf("load courts: %w", err)
		return
	}
	s.all = rows
	s.byID = make(map[string]Court, len(rows))
	for _, c := range rows {
		s.byID[c.ID] = c
	}
}

// ensureLoaded loads the catalog once. Subsequent calls are no-ops.
func (s *CourtService) ensureLoaded() error {
	s.once.Do(s.load)
	return s.err
}

// Lookup returns the court row for an ID. ErrUnknownCourt when not found.
func (s *CourtService) Lookup(id string) (Court, error) {
	if id == "" {
		return Court{}, ErrUnknownCourt
	}
	if err := s.ensureLoaded(); err != nil {
		return Court{}, err
	}
	s.mu.RLock()
	defer s.mu.RUnlock()
	c, ok := s.byID[id]
	if !ok {
		return Court{}, ErrUnknownCourt
	}
	return c, nil
}

// CountryRegime resolves a court ID to its (country, regime) tuple for
// holiday-calendar lookup. Empty courtID falls back to (defaultCountry,
// defaultRegime) so legacy callers without a court_id still get sensible
// behaviour. ErrUnknownCourt when courtID is non-empty and not in the table.
func (s *CourtService) CountryRegime(courtID, defaultCountry, defaultRegime string) (country, regime string, err error) {
	if courtID == "" {
		return defaultCountry, defaultRegime, nil
	}
	c, err := s.Lookup(courtID)
	if err != nil {
		return "", "", err
	}
	return c.Country, c.RegimeOrEmpty(), nil
}

// All returns the full active court list (read-only — caller must not
// mutate). Order: sort_order asc, id asc.
func (s *CourtService) All() ([]Court, error) {
	if err := s.ensureLoaded(); err != nil {
		return nil, err
	}
	s.mu.RLock()
	defer s.mu.RUnlock()
	out := make([]Court, len(s.all))
	copy(out, s.all)
	return out, nil
}

// ByCourtType returns all active courts of a given type (e.g. "UPC-LD").
// Useful for the Fristenrechner court-picker UI.
func (s *CourtService) ByCourtType(courtType string) ([]Court, error) {
	if err := s.ensureLoaded(); err != nil {
		return nil, err
	}
	s.mu.RLock()
	defer s.mu.RUnlock()
	out := make([]Court, 0, 8)
	for _, c := range s.all {
		if c.CourtType == courtType {
			out = append(out, c)
		}
	}
	return out, nil
}

// guard against nil-deref when courtID is supplied by an API caller and the
// service is wired. Not used internally; exposed so handlers can return
// 404-equivalents on bad input without leaking sql errors.
var _ = sql.ErrNoRows
