"""Catalog data model, validation, and JSONL IO."""

from __future__ import annotations

from dataclasses import dataclass, asdict, fields
from typing import Optional

import json
from pathlib import Path

MODES = frozenset({"audio", "video", "av"})
LICENSES = frozenset({"public_domain", "cc0", "cc_by", "cc_by_nc"})
REVIEW_STATUSES = frozenset({"proposed", "approved"})
COORD_FIELDS = ("left", "right", "dark", "light")
COORD_MIN = 0
COORD_MAX = 4
ATTRIBUTION_LICENSES = frozenset({"cc_by", "cc_by_nc"})


class CatalogError(ValueError):
    """Raised when a catalog record is structurally invalid."""


@dataclass
class Record:
    id: str
    title: str
    source_url: str
    source_archive: str
    license: str
    mode: str
    left: int
    right: int
    dark: int
    light: int
    duration_s: int
    file_path: str
    review_status: str = "proposed"
    attribution: str = ""
    resolution: str = ""
    dominant_color: str = ""
    rationale: str = ""
    reviewed_at: Optional[str] = None
    notes: str = ""


def validate(record: Record) -> None:
    """Raise CatalogError if the record is structurally invalid."""
    if not record.id:
        raise CatalogError("record id must be non-empty")
    if record.mode not in MODES:
        raise CatalogError(
            f"invalid mode {record.mode!r}; expected one of {sorted(MODES)}"
        )
    if record.license not in LICENSES:
        raise CatalogError(
            f"invalid license {record.license!r}; expected one of {sorted(LICENSES)}"
        )
    if record.review_status not in REVIEW_STATUSES:
        raise CatalogError(
            f"invalid review_status {record.review_status!r}; "
            f"expected one of {sorted(REVIEW_STATUSES)}"
        )
    for axis in COORD_FIELDS:
        value = getattr(record, axis)
        if isinstance(value, bool) or not isinstance(value, int):
            raise CatalogError(f"coordinate {axis} must be an int, got {value!r}")
        if not (COORD_MIN <= value <= COORD_MAX):
            raise CatalogError(
                f"coordinate {axis}={value} out of range {COORD_MIN}..{COORD_MAX}"
            )
    if record.duration_s < 0:
        raise CatalogError("duration_s must be non-negative")
    if record.license in ATTRIBUTION_LICENSES and not record.attribution:
        raise CatalogError(f"license {record.license} requires attribution")


def record_to_dict(record: Record) -> dict:
    """Convert a Record to a plain dict suitable for JSON serialization."""
    return asdict(record)


def record_from_dict(data: dict) -> Record:
    """Build a Record from a dict, rejecting unknown or missing fields."""
    known = {f.name for f in fields(Record)}
    unknown = set(data) - known
    if unknown:
        raise CatalogError(f"unknown fields: {sorted(unknown)}")
    try:
        return Record(**data)
    except TypeError as exc:
        raise CatalogError(str(exc)) from exc


def load_catalog(path) -> list[Record]:
    """Load and validate every record from a JSONL file. Blank lines skipped."""
    path = Path(path)
    records: list[Record] = []
    with path.open("r", encoding="utf-8") as fh:
        for lineno, raw in enumerate(fh, start=1):
            line = raw.strip()
            if not line:
                continue
            try:
                data = json.loads(line)
            except json.JSONDecodeError as exc:
                raise CatalogError(f"line {lineno}: invalid JSON: {exc}") from exc
            record = record_from_dict(data)
            validate(record)
            records.append(record)
    return records


def save_catalog(records, path) -> None:
    """Validate and write all records to a JSONL file (overwrites)."""
    path = Path(path)
    with path.open("w", encoding="utf-8") as fh:
        for record in records:
            validate(record)
            fh.write(json.dumps(record_to_dict(record), ensure_ascii=False) + "\n")


def append_record(record, path) -> None:
    """Validate and append a single record to a JSONL file."""
    validate(record)
    path = Path(path)
    with path.open("a", encoding="utf-8") as fh:
        fh.write(json.dumps(record_to_dict(record), ensure_ascii=False) + "\n")


def index_by_id(records) -> dict:
    """Map id -> Record, raising CatalogError on a duplicate id."""
    idx: dict = {}
    for r in records:
        if r.id in idx:
            raise CatalogError(f"duplicate record id: {r.id!r}")
        idx[r.id] = r
    return idx


def validate_catalog(records) -> None:
    """Validate every record AND cross-record invariants (currently: unique ids)."""
    for r in records:
        validate(r)
    index_by_id(records)  # raises on duplicate id
