"""D22 pending fetch store · 60s push_token · inbox / fetch-response。"""

from __future__ import annotations

import hashlib
import hmac
import json
import secrets
import uuid
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Any

import yaml

ALLOWED_FETCH_RESOURCES = frozenset({"virtual_personas"})


def _now_iso() -> str:
    return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")


def _parse_iso(raw: str) -> datetime | None:
    try:
        text = str(raw).replace("Z", "+00:00")
        return datetime.fromisoformat(text)
    except ValueError:
        return None


def fetch_cache_dir(home: Path, username: str, resource: str) -> Path:
    return Path(home) / "control" / "fetch_cache" / username / resource


def canonical_token_payload(*, fetch_id: str, username: str, resource: str, exp: int) -> str:
    body = {
        "exp": exp,
        "fetch_id": fetch_id,
        "resource": resource,
        "username": username,
    }
    return json.dumps(body, sort_keys=True, separators=(",", ":"))


def issue_push_token(
    bearer_secret: str,
    *,
    fetch_id: str,
    username: str,
    resource: str,
    exp: int,
) -> str:
    payload = canonical_token_payload(
        fetch_id=fetch_id,
        username=username,
        resource=resource,
        exp=exp,
    )
    return hmac.new(
        bearer_secret.encode("utf-8"),
        payload.encode("utf-8"),
        hashlib.sha256,
    ).hexdigest()


def verify_push_token(
    bearer_secret: str,
    *,
    fetch_id: str,
    username: str,
    resource: str,
    exp: int,
    token: str,
) -> bool:
    expected = issue_push_token(
        bearer_secret,
        fetch_id=fetch_id,
        username=username,
        resource=resource,
        exp=exp,
    )
    return hmac.compare_digest(expected, str(token).strip())


def fetch_push_token_ttl_sec(config: dict[str, Any]) -> int:
    tenant_auth = config.get("tenant_auth") or {}
    if isinstance(tenant_auth, dict):
        raw = tenant_auth.get("fetch_push_token_ttl_sec", 60)
        try:
            return max(1, int(raw))
        except (TypeError, ValueError):
            pass
    return 60


class PendingFetchStore:
    def __init__(self, home: Path) -> None:
        self._home = Path(home)
        self._dir = self._home / "control" / "pending_fetches"
        self._dir.mkdir(parents=True, exist_ok=True)

    @property
    def directory(self) -> Path:
        return self._dir

    def _path(self, fetch_id: str) -> Path:
        return self._dir / f"{fetch_id}.yaml"

    def _load(self, fetch_id: str) -> dict[str, Any] | None:
        path = self._path(fetch_id)
        if not path.is_file():
            return None
        with path.open(encoding="utf-8") as f:
            data = yaml.safe_load(f) or {}
        return data if isinstance(data, dict) else None

    def _save(self, fetch_id: str, row: dict[str, Any]) -> None:
        with self._path(fetch_id).open("w", encoding="utf-8") as f:
            yaml.safe_dump(row, f, allow_unicode=True, default_flow_style=False, sort_keys=False)

    def _is_expired(self, row: dict[str, Any], *, now: datetime | None = None) -> bool:
        now = now or datetime.now(timezone.utc)
        exp_raw = row.get("expires_at")
        if not exp_raw:
            return True
        exp_dt = _parse_iso(str(exp_raw))
        return exp_dt is None or exp_dt <= now

    def _expire_if_needed(self, row: dict[str, Any], fetch_id: str) -> dict[str, Any]:
        status = str(row.get("status", "pending")).lower()
        if status != "pending":
            return row
        if self._is_expired(row):
            row = dict(row)
            row["status"] = "timeout"
            row["closed_at"] = _now_iso()
            self._save(fetch_id, row)
        return row

    def create_pending(
        self,
        *,
        username: str,
        resource: str,
        bearer_secret: str,
        ttl_sec: int,
    ) -> dict[str, Any]:
        resource = str(resource).strip()
        if resource not in ALLOWED_FETCH_RESOURCES:
            raise ValueError(f"unsupported resource: {resource}")

        now = datetime.now(timezone.utc)
        fetch_id = uuid.uuid4().hex
        exp_dt = now + timedelta(seconds=ttl_sec)
        exp_unix = int(exp_dt.timestamp())
        push_token = issue_push_token(
            bearer_secret,
            fetch_id=fetch_id,
            username=username,
            resource=resource,
            exp=exp_unix,
        )
        row = {
            "fetch_id": fetch_id,
            "username": username,
            "resource": resource,
            "status": "pending",
            "created_at": _now_iso(),
            "expires_at": exp_dt.replace(microsecond=0).isoformat().replace("+00:00", "Z"),
            "exp_unix": exp_unix,
            "push_token": push_token,
        }
        self._save(fetch_id, row)
        return {
            "fetch_id": fetch_id,
            "username": username,
            "resource": resource,
            "status": "pending",
            "expires_at": row["expires_at"],
            "push_token": push_token,
        }

    def get(self, fetch_id: str) -> dict[str, Any] | None:
        row = self._load(fetch_id)
        if row is None:
            return None
        return self._expire_if_needed(row, fetch_id)

    def list_for_user(self, username: str) -> list[dict[str, Any]]:
        name = username.strip().lower()
        items: list[dict[str, Any]] = []
        for path in sorted(self._dir.glob("*.yaml")):
            fetch_id = path.stem
            row = self._load(fetch_id)
            if not isinstance(row, dict):
                continue
            row = self._expire_if_needed(row, fetch_id)
            if str(row.get("username", "")).lower() != name:
                continue
            if str(row.get("status", "")).lower() != "pending":
                continue
            if self._is_expired(row):
                continue
            items.append(
                {
                    "fetch_id": fetch_id,
                    "resource": row.get("resource"),
                    "push_token": row.get("push_token"),
                    "expires_at": row.get("expires_at"),
                }
            )
        return items

    def complete_fetch(
        self,
        *,
        fetch_id: str,
        push_token: str,
        bearer_secret: str,
        payload: Any,
    ) -> dict[str, Any]:
        row = self.get(fetch_id)
        if row is None:
            raise KeyError("fetch_not_found")
        status = str(row.get("status", "")).lower()
        if status == "timeout":
            raise ValueError("fetch_expired")
        if status != "pending":
            raise ValueError("fetch_already_closed")

        username = str(row.get("username", ""))
        resource = str(row.get("resource", ""))
        exp_unix = int(row.get("exp_unix") or 0)
        if not verify_push_token(
            bearer_secret,
            fetch_id=fetch_id,
            username=username,
            resource=resource,
            exp=exp_unix,
            token=push_token,
        ):
            raise ValueError("push_token_invalid")

        cache_dir = fetch_cache_dir(self._home, username, resource)
        cache_dir.mkdir(parents=True, exist_ok=True)
        latest = cache_dir / "latest.json"
        envelope = {
            "username": username,
            "resource": resource,
            "fetch_id": fetch_id,
            "fetched_at": _now_iso(),
            "payload": payload,
        }
        latest.write_text(json.dumps(envelope, ensure_ascii=False, indent=2), encoding="utf-8")

        closed = dict(row)
        closed["status"] = "ok"
        closed["closed_at"] = _now_iso()
        self._save(fetch_id, closed)

        return {
            "fetch_id": fetch_id,
            "username": username,
            "resource": resource,
            "status": "ok",
            "cache_path": str(latest),
        }

    def read_cache_preview(self, username: str, resource: str) -> dict[str, Any] | None:
        latest = fetch_cache_dir(self._home, username, resource) / "latest.json"
        if not latest.is_file():
            return None
        try:
            return json.loads(latest.read_text(encoding="utf-8"))
        except json.JSONDecodeError:
            return None
