from __future__ import annotations

import json
import shutil
import tempfile
import zipfile
from dataclasses import dataclass
from pathlib import Path
from typing import Any


@dataclass(frozen=True)
class FixtureImage:
    path: Path
    content_type: str


@dataclass(frozen=True)
class FixtureRecord:
    record_id: str
    record_type: str  # "ro" | "coagulation"
    log_date: str | None
    upload_id: str
    root: Path
    images: dict[str, FixtureImage]
    review: dict[str, Any] | None
    response_payload: dict[str, Any] | None

    @property
    def review_status(self) -> str | None:
        return self.review.get("status") if self.review else None


@dataclass(frozen=True)
class FixtureManifest:
    version: str
    records: list[FixtureRecord]


def load_fixtures(path: str | Path) -> tuple[FixtureManifest, Path | None]:
    """Load a fixture bundle from a directory or a `.zip` (as exported by
    fluid-backend's `export_wtp_ocr_fixtures` management command).

    Returns `(manifest, temp_dir)`. `temp_dir` is the extraction directory to clean up
    with `cleanup()` afterwards when `path` was a zip, else `None`.
    """
    path = Path(path).expanduser()
    temp_dir: Path | None = None

    if path.is_file() and path.suffix.lower() == ".zip":
        temp_dir = Path(tempfile.mkdtemp(prefix="wtp_ocr_fixtures_"))
        with zipfile.ZipFile(path) as zf:
            zf.extractall(temp_dir)
        root = temp_dir
    elif path.is_dir():
        root = path
    else:
        raise FileNotFoundError(f"Fixture bundle not found: {path}")

    manifest_path = root / "manifest.json"
    if not manifest_path.is_file():
        # a zip's contents may sit under a single nested folder rather than at the root
        candidates = list(root.glob("*/manifest.json"))
        if len(candidates) == 1:
            manifest_path = candidates[0]
            root = manifest_path.parent
        else:
            raise FileNotFoundError(f"No manifest.json found under {root}")

    manifest_data = json.loads(manifest_path.read_text())
    records = [_load_record(root / entry["path"]) for entry in manifest_data["records"]]

    return FixtureManifest(version=manifest_data.get("version", "unknown"), records=records), temp_dir


def _load_record(record_root: Path) -> FixtureRecord:
    meta = json.loads((record_root / "meta.json").read_text())

    images = {
        side: FixtureImage(
            path=record_root / info["file"],
            content_type=info.get("content_type") or "image/jpeg",
        )
        for side, info in meta.get("images", {}).items()
    }

    review = None
    review_path = record_root / "expected" / "review.json"
    if review_path.is_file():
        review = json.loads(review_path.read_text())

    response_payload = None
    response_path = record_root / "expected" / "response_payload.json"
    if response_path.is_file():
        response_payload = json.loads(response_path.read_text())

    return FixtureRecord(
        record_id=record_root.name,
        record_type=meta["record_type"],
        log_date=meta.get("log_date"),
        upload_id=meta["upload_id"],
        root=record_root,
        images=images,
        review=review,
        response_payload=response_payload,
    )


def filter_records(
    records: list[FixtureRecord],
    *,
    record_types: list[str] | None = None,
    statuses: set[str | None] | None = None,
    record_ids: set[str] | None = None,
) -> list[FixtureRecord]:
    result = []
    for record in records:
        if record_types and record.record_type not in record_types:
            continue
        if statuses is not None and record.review_status not in statuses:
            continue
        if record_ids is not None and record.record_id not in record_ids:
            continue
        result.append(record)
    return result


def cleanup(temp_dir: Path | None) -> None:
    if temp_dir is not None:
        shutil.rmtree(temp_dir, ignore_errors=True)
