from __future__ import annotations

import json
from pathlib import Path

from src.errors import UserError
from src.gbp.photos import select_photos
from src.gbp.text import generate_post_text


def generate_payload(
    *, out_dir: Path, deal_id: int, run_id: str | None, max_photos: int
) -> dict[str, object]:
    run_dir, run_id_used = _resolve_audit_run_dir(out_dir=out_dir, run_id=run_id)
    records = _read_audit_records(run_dir / "audit.jsonl")

    title, venue, place = _deal_context(records=records, deal_id=deal_id)
    selected = select_photos(records=records, deal_id=deal_id, max_photos=max_photos)
    text = generate_post_text(title=title, venue=venue, place=place)

    return {
        "deal_id": int(deal_id),
        "deal_title": title,
        "venue_name": venue,
        "place": place,
        "text": text,
        "photos": [
            {
                "photo_sha256": p.photo_sha256,
                "gbp_jpg_path": p.gbp_jpg_path,
                "confidence": p.confidence,
            }
            for p in selected
        ],
        "audit_run_id": run_id_used,
    }


def write_payload(
    *, out_dir: Path, deal_id: int, payload: dict[str, object], dry_run: bool
) -> Path | None:
    if dry_run:
        return None
    dest = out_dir / "gbp" / "posts" / f"deal_{int(deal_id)}.json"
    dest.parent.mkdir(parents=True, exist_ok=True)
    text = json.dumps(payload, indent=2, sort_keys=True, ensure_ascii=True) + "\n"
    tmp = dest.with_suffix(dest.suffix + ".tmp")
    try:
        tmp.write_text(text, encoding="utf-8", newline="\n")
        tmp.replace(dest)
    except OSError as exc:
        raise UserError(f"Failed to write GBP payload: {dest} ({exc})") from exc
    return dest


def _resolve_audit_run_dir(*, out_dir: Path, run_id: str | None) -> tuple[Path, str]:
    root = out_dir / "audit"
    if run_id:
        rid = str(run_id).strip()
        if not rid:
            raise UserError("Invalid run id.")
        run_dir = root / rid
        if not run_dir.exists():
            raise UserError(f"Run not found: {run_dir}")
        return (run_dir, rid)

    if not root.exists():
        raise UserError(f"Audit root not found: {root}. Run 'run' first.")
    dirs = sorted([p for p in root.iterdir() if p.is_dir()])
    if not dirs:
        raise UserError(f"No runs found under: {root}. Run 'run' first.")
    return (dirs[-1], dirs[-1].name)


def _read_audit_records(path: Path) -> list[dict[str, object]]:
    if not path.exists():
        raise UserError(f"Audit file not found: {path}")
    out: list[dict[str, object]] = []
    for i, line in enumerate(path.read_text(encoding="utf-8").splitlines(), start=1):
        if not line.strip():
            continue
        try:
            obj = json.loads(line)
        except json.JSONDecodeError as exc:
            raise UserError(f"Invalid JSONL at {path}:{i}: {exc.msg}") from exc
        if not isinstance(obj, dict):
            raise UserError(f"Invalid JSONL record at {path}:{i}: expected object")
        out.append(obj)
    return out


def _deal_context(*, records: list[dict[str, object]], deal_id: int) -> tuple[str, str, str]:
    filtered = [r for r in records if _as_int(r.get("deal_id")) == int(deal_id)]
    if not filtered:
        raise UserError(
            f"No audit rows found for deal_id={deal_id}. Run audit with matching enabled."
        )
    best = max(filtered, key=_confidence_key)
    title = _require_str(best.get("deal_title"), "deal_title")
    venue = _require_str(best.get("venue_name"), "venue_name")
    place = _require_str(best.get("place"), "place")
    return (title, venue, place)


def _confidence_key(record: dict[str, object]) -> tuple[float, str]:
    conf = record.get("confidence")
    value = float(conf) if isinstance(conf, (int, float)) else 0.0
    digest = str(record.get("photo_sha256") or "")
    return (value, digest)


def _as_int(value: object) -> int | None:
    if isinstance(value, int):
        return value
    if isinstance(value, str) and value.isdigit():
        return int(value)
    return None


def _require_str(value: object, field: str) -> str:
    if value is None:
        raise UserError(f"Missing {field} in audit. Re-run 'run' with matching enabled.")
    text = str(value).strip()
    if not text:
        raise UserError(f"Missing {field} in audit. Re-run 'run' with matching enabled.")
    return text
