from __future__ import annotations

import json
from dataclasses import asdict, dataclass
from pathlib import Path

from src.config.gbp_config import GbpConfig, load_gbp_config, normalize_location_key
from src.errors import UserError
from src.gbp.client import GbpClient


@dataclass(frozen=True)
class PublishResult:
    deal_id: int
    place: str
    location_id: str
    dry_run: bool
    published: bool
    response: dict[str, object] | None
    audit_run_id: str | None

    def to_json(self) -> str:
        return json.dumps(asdict(self), indent=2, sort_keys=True, ensure_ascii=True)


def publish_deal_post(
    *, out_dir: Path, deal_id: int, dry_run: bool, confirm: bool
) -> PublishResult:
    payload, payload_path = _load_payload(out_dir=out_dir, deal_id=deal_id)
    place = _require_str(payload.get("place"), "place")
    text = _require_str(payload.get("text"), "text")
    audit_run_id = _optional_str(payload.get("audit_run_id"))
    cfg = load_gbp_config(require_auth=not dry_run)
    location_id = _resolve_location_id(cfg.location_id_map, place)
    _require_confirm(dry_run=dry_run, confirm=confirm)
    if dry_run:
        return _result(
            deal_id=deal_id,
            place=place,
            location_id=location_id,
            dry_run=True,
            published=False,
            response=None,
            audit_run_id=audit_run_id,
        )
    client = _client_from_cfg(cfg)
    resp = client.create_local_post(location_id=location_id, summary=text)
    result = _result(
        deal_id=deal_id,
        place=place,
        location_id=location_id,
        dry_run=False,
        published=True,
        response={"status_code": resp.status_code, "payload": resp.payload},
        audit_run_id=audit_run_id,
    )
    _write_publish_artifact(
        out_dir=out_dir,
        audit_run_id=audit_run_id,
        deal_id=deal_id,
        payload_path=payload_path,
        result=result,
    )
    return result


def _require_confirm(*, dry_run: bool, confirm: bool) -> None:
    if not dry_run and not confirm:
        raise UserError("Refusing to publish without --confirm.")


def _result(
    *,
    deal_id: int,
    place: str,
    location_id: str,
    dry_run: bool,
    published: bool,
    response: dict[str, object] | None,
    audit_run_id: str | None,
) -> PublishResult:
    return PublishResult(
        deal_id=int(deal_id),
        place=place,
        location_id=location_id,
        dry_run=dry_run,
        published=published,
        response=response,
        audit_run_id=audit_run_id,
    )


def _load_payload(*, out_dir: Path, deal_id: int) -> tuple[dict[str, object], Path]:
    path = out_dir / "gbp" / "posts" / f"deal_{int(deal_id)}.json"
    if not path.exists():
        raise UserError(
            f"Missing GBP payload file: {path}. Run 'gbp generate' without --dry-run first."
        )
    try:
        data = json.loads(path.read_text(encoding="utf-8"))
    except json.JSONDecodeError as exc:
        raise UserError(f"Invalid JSON in payload file: {path} ({exc.msg})") from exc
    if not isinstance(data, dict):
        raise UserError(f"Invalid payload: expected JSON object: {path}")
    return (data, path)


def _resolve_location_id(location_id_map: dict[str, str], place: str) -> str:
    key = normalize_location_key(place)
    if key in location_id_map:
        return location_id_map[key]
    raise UserError(f"Unmapped GBP location for place {place!r}. Add it to GBP_LOCATION_ID_MAP.")


def _client_from_cfg(cfg: GbpConfig) -> GbpClient:
    if not getattr(cfg, "account_id", None) or not getattr(cfg, "access_token", None):
        raise UserError("Missing GBP auth (GBP_ACCOUNT_ID / GBP_ACCESS_TOKEN).")
    return GbpClient(
        base_url=str(cfg.base_url),
        account_id=str(cfg.account_id),
        access_token=str(cfg.access_token),
    )


def _write_publish_artifact(
    *,
    out_dir: Path,
    audit_run_id: str | None,
    deal_id: int,
    payload_path: Path,
    result: PublishResult,
) -> None:
    if not audit_run_id:
        return
    audit_dir = out_dir / "audit" / audit_run_id
    audit_dir.mkdir(parents=True, exist_ok=True)
    dest = audit_dir / f"gbp_publish_deal_{int(deal_id)}.json"
    payload = {"payload_file": str(payload_path)} | asdict(result)
    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 publish artifact: {dest} ({exc})") from exc


def _require_str(value: object, field: str) -> str:
    if value is None:
        raise UserError(f"Missing {field} in GBP payload.")
    text = str(value).strip()
    if not text:
        raise UserError(f"Missing {field} in GBP payload.")
    return text


def _optional_str(value: object) -> str | None:
    if value is None:
        return None
    text = str(value).strip()
    return text or None
