"""Manifest (JSONL) read/write/update with file locking."""

from __future__ import annotations

import json
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from pathlib import Path

from filelock import FileLock


@dataclass
class ManifestEntry:
    xc_id: str = ""
    gen: str = ""
    sp: str = ""
    ssp: str = ""
    en: str = ""
    family: str = ""
    group: str = ""
    country: str = ""
    locality: str = ""
    lat: float | None = None
    lon: float | None = None
    type: list[str] = field(default_factory=list)
    also: list[str] = field(default_factory=list)
    quality: str = ""
    length_seconds: float = 0.0
    date: str = ""
    recordist: str = ""
    license: str = ""
    original_format: str = ""
    original_filename: str = ""
    download_url: str = ""
    original_path: str = ""
    resampled_path: str = ""
    metadata_path: str = ""
    sha256_original: str = ""
    sha256_resampled: str = ""
    file_size_bytes: int = 0
    status: str = "pending"
    downloaded_at: str = ""
    converted_from: str = ""
    sample_rate: int = 0
    channels: int = 0
    duration_seconds: float = 0.0


class Manifest:
    """Manages the catalog.jsonl file with atomic append and file locking."""

    def __init__(self, data_dir: Path):
        self._path = data_dir / "catalog.jsonl"
        self._lock_path = data_dir / "catalog.lock"
        self._data_dir = data_dir

    @property
    def path(self) -> Path:
        return self._path

    def _lock(self) -> FileLock:
        return FileLock(str(self._lock_path))

    def read_all(self) -> dict[str, ManifestEntry]:
        """Read all manifest entries, keyed by xc_id."""
        entries: dict[str, ManifestEntry] = {}
        if not self._path.exists():
            return entries
        with self._lock():
            with open(self._path) as f:
                for line in f:
                    line = line.strip()
                    if not line:
                        continue
                    data = json.loads(line)
                    entry = ManifestEntry(**data)
                    entries[entry.xc_id] = entry
        return entries

    def append(self, entry: ManifestEntry) -> None:
        """Append a single entry to the manifest."""
        self._path.parent.mkdir(parents=True, exist_ok=True)
        with self._lock():
            with open(self._path, "a") as f:
                f.write(json.dumps(asdict(entry)) + "\n")

    def append_many(self, entries: list[ManifestEntry]) -> None:
        """Append multiple entries atomically."""
        if not entries:
            return
        self._path.parent.mkdir(parents=True, exist_ok=True)
        with self._lock():
            with open(self._path, "a") as f:
                for entry in entries:
                    f.write(json.dumps(asdict(entry)) + "\n")

    def update_entry(self, xc_id: str, **updates: object) -> None:
        """Update fields of an existing entry by rewriting the manifest."""
        entries = self.read_all()
        if xc_id not in entries:
            return
        entry = entries[xc_id]
        for k, v in updates.items():
            if hasattr(entry, k):
                setattr(entry, k, v)
        self._rewrite(entries)

    def _rewrite(self, entries: dict[str, ManifestEntry]) -> None:
        """Rewrite the entire manifest (used for updates)."""
        self._path.parent.mkdir(parents=True, exist_ok=True)
        tmp = self._path.with_suffix(".tmp")
        with self._lock():
            with open(tmp, "w") as f:
                for entry in entries.values():
                    f.write(json.dumps(asdict(entry)) + "\n")
            tmp.rename(self._path)

    def update_download_status(
        self,
        xc_id: str,
        sha256: str,
        file_size: int,
        status: str = "verified",
    ) -> None:
        """Update download status for a recording."""
        self.update_entry(
            xc_id,
            sha256_original=sha256,
            file_size_bytes=file_size,
            status=status,
            downloaded_at=datetime.now(timezone.utc).isoformat(),
        )
