"""Shared pydantic models for the TidalCycles → tracks tooling (L'Armada).

One typed, JSON-serializable vocabulary across the fleet: provenance, the
locate-matrix cell, the master edit-decision list (EDL), and the sample TF-IDF
report. Every produced artifact is a model → validated on read, dumped on write.

Design rules encoded here:
  - **Metadata carries provenance** (value + source TYPE + locator + as_of), so any
    fact is traceable and corrections propagate. (feedback_metadata_provenance)
  - **The ear is the oracle**: a match isn't `verified` until source=user/ear.
  - **Never invent gig metadata**: gig is a site slug (a pointer), not a fact.

JSON: `m.model_dump_json(indent=1)` to write, `Model.model_validate_json(txt)` to
read. Enums serialize as their string value.
"""
from __future__ import annotations

from datetime import date
from enum import Enum
from typing import Optional

from pydantic import BaseModel, Field


# ── provenance ────────────────────────────────────────────────────────────────
class Source(str, Enum):
    user = "user"        # PLN said so (typed by hand)
    ear = "ear"          # PLN's ear on audio — the oracle for identity/mastering
    file = "file"        # read from a canonical file (site tracklist, .tidal…)
    web = "web"          # scraped/fetched online
    derived = "derived"  # computed (locate xcorr, tfidf, date-join…)


class Provenance(BaseModel):
    """Where a value came from, so it can be trusted and corrected."""
    source: Source
    locator: str = ""                 # path, URL, "xcorr z=29", "datejoin±3d"…
    as_of: date
    note: str = ""


# ── locate-matrix ─────────────────────────────────────────────────────────────
class MatchState(str, Enum):
    absent = "absent"                          # looked, track not in this take
    candidate = "candidate"                    # metadata says it should be here
    candidate_ambiguous = "candidate-ambiguous"  # single-track take, which track?
    suspect = "suspect"                        # audio hit but low-z / conflicting
    verified = "verified"                      # ear-gated truth


class LocateSignals(BaseModel):
    metadata: bool = False
    join: Optional[str] = None         # "date-exact" | "date±3d" | "label"
    z: Optional[float] = None          # xcorr confidence (z ≥ 20 floor, ear still gates)
    tempo_ok: Optional[bool] = None    # setcps BPM matches
    fingerprint: Optional[float] = None  # IDF-weighted sample/orbit overlap (L3)


class LocateCell(BaseModel):
    """One (track × recording) cell of the fleet ledger."""
    track: str                          # canonical id = .tidal path (merges aliases)
    name: Optional[str] = None          # display name in this gig ("Pitbul Punk")
    tidal: Optional[str] = None
    take: str                           # Ardour take or .aup id
    gig: Optional[str] = None           # site slug — a pointer, not an audio fact
    take_type: Optional[str] = None     # track | SET | sketch
    bpm: Optional[float] = None
    offset_s: Optional[float] = None
    dur_s: Optional[float] = None
    signals: LocateSignals = Field(default_factory=LocateSignals)
    state: MatchState = MatchState.candidate
    provenance: Optional[Provenance] = None


# ── master edit-decision list (EDL) — ear-feedback → typed fixes ──────────────
class EditOp(str, Enum):
    trim_in = "trim_in"            # cut N seconds off the head
    trim_out = "trim_out"          # cut the tail at `at`
    fade_in = "fade_in"
    fade_out = "fade_out"
    crossfade = "crossfade"        # `at`→`to` overlap into next track
    gain = "gain"                  # level nudge (value = dB), e.g. kick match
    eq_highshelf = "eq_highshelf"  # tame highs (value = dB @ ~freq in note)
    investigate = "investigate"    # flagged anomaly, no op yet


class EditKind(str, Enum):
    bad_cut = "bad_cut"            # boundary bleed (also = ground-truth label!)
    laborious = "laborious"        # drags / outro too long
    harsh = "harsh"                # high-end piercing (esp. mobile)
    level = "level"                # cross-track loudness mismatch
    anomaly = "anomaly"            # dropout, weird silence
    transition = "transition"      # abrupt / needs crossfade


class EditStatus(str, Enum):
    open = "open"
    applied = "applied"
    rejected = "rejected"


class MasterEdit(BaseModel):
    """A single mastering decision, sourced from the ear, replayable + labelable.

    Each `bad_cut` is simultaneously a fix instruction AND a ground-truth label
    for the boundary/edge detectors (#25/#31): tune them until they catch these.
    """
    take: str = "Take89"
    track_no: Optional[int] = None
    name: str
    tidal: Optional[str] = None
    op: EditOp
    at: str                         # timecode "m:ss" or seconds, within the track
    to: Optional[str] = None        # end timecode for crossfade/range ops
    value: Optional[float] = None   # dB for gain/eq, seconds for fades
    kind: EditKind
    reason: str
    device: str = ""                # "WH-1000XM5+phone" — translation context
    status: EditStatus = EditStatus.open
    provenance: Provenance


class MasterEDL(BaseModel):
    take: str
    gig: Optional[str] = None
    edits: list[MasterEdit] = Field(default_factory=list)


# ── sample TF-IDF ─────────────────────────────────────────────────────────────
class SampleHit(BaseModel):
    sample: str
    score: float          # tf-idf
    df: int               # document frequency across corpus


class TrackSignature(BaseModel):
    n_samples: int
    tf: dict[str, int]
    top_tfidf: list[SampleHit] = Field(default_factory=list)


class TfidfReport(BaseModel):
    corpus: str
    n_docs: int
    vocab_size: int
    df: dict[str, int]
    idf: dict[str, float]
    tracks: dict[str, TrackSignature]
