"""DealGod client — RecordGod's single seam to the universal brain (BaseGod). Implements the frozen
DealGod API contract: identify · value · lore · supply · metal-prices (defined + owned in the DealGod
hub repo, mirrored here). Reference consumer: the other StoreGod skins copy this call pattern.

Key from the vault (`dealgod_api_key`), sent as X-Api-Key. Base URL is env-configurable so we can mock
against a local stub / `:8001` before DG1 allowlists the routes on api.dealgod.pro:
    DEALGOD_API_BASE=http://localhost:8001   # dev mock
Endpoints: identify · value · lore · supply. The guard ships default-open, so calls work pre-entitlement.
"""
import asyncio
import os
import httpx

BASE = os.getenv("DEALGOD_API_BASE", "https://api.dealgod.pro").rstrip("/")


async def _key(db):
    # per-skin SCOPED customer key (set on the container, e.g. ToolGod's tools-only key) wins;
    # else the shared vault key (master/owner). Lets each skin run as its own scoped customer.
    k = os.getenv("DEALGOD_API_KEY")
    if k:
        return k
    from . import vault   # lazy so this module imports cleanly for the standalone selfcheck
    return await vault.get_secret(db, "dealgod_api_key")


def _norm_candidate(c: dict) -> dict:
    """Frozen contract: candidates carry ref_type:'canon'|'release' + ref_id — records resolve to a Discogs
    release_id, NOT a canon_id (SCANGOD's structural point). Echo convenience keys so callers switch on either,
    and back-fill ref_type/ref_id from a legacy/direct shape."""
    rt = c.get("ref_type")
    if rt is None:
        if c.get("release_id") is not None:
            c["ref_type"], c["ref_id"], rt = "release", c["release_id"], "release"
        elif c.get("canon_id") is not None:
            c["ref_type"], c["ref_id"], rt = "canon", c["canon_id"], "canon"
    if rt == "release":
        c.setdefault("release_id", c.get("ref_id"))
    elif rt == "canon":
        c.setdefault("canon_id", c.get("ref_id"))
    return c


async def identify(db, clues: dict, *, images=None, labels=None, hints=None, scope=None, features=None) -> dict:
    """POST /api/identify. clues = {barcode?,isbn?,catno?,text?}; images = [b64,…≤4]; labels = [ocr…];
    hints = {brand,size,weight,notes}; scope = DOMAINS (e.g. ['music']). Returns {candidates:[{ref_type,ref_id,slug,
    category,display_name,confidence,id_keys,traps,…}]} — normalised so each candidate has canon_id OR release_id set."""
    key = await _key(db)
    if not key:
        return {"candidates": [], "error": "no_key"}
    payload = dict(clues or {})
    if images:
        payload["images"] = images
    if labels:
        payload["labels"] = labels
    if hints:
        payload["hints"] = hints
    body = {"clues": payload}
    if scope:
        body["scope"] = list(scope)
    try:
        async with httpx.AsyncClient(timeout=30, headers={"X-Api-Key": key}) as c:
            r = await c.post(f"{BASE}/api/identify", json=body)
    except Exception as e:
        return {"candidates": [], "error": str(e)}
    if r.status_code != 200:
        return {"candidates": [], "status": r.status_code}
    d = r.json()
    d["candidates"] = [_norm_candidate(x) for x in d.get("candidates", [])]
    return d


async def value(db, *, ref_type=None, ref_id=None, canon_id=None, release_id=None, condition=None, region="AU") -> dict | None:
    """POST /api/value — frozen primary shape is {ref_type,ref_id} ('canon'|'release'); canon_id/release_id accepted
    as convenience aliases. Returns {low,typ,high,currency,as_of,n?,melt_floor?,comps?,supply?,source[]} or None."""
    key = await _key(db)
    if not key:
        return None
    if ref_type is None:                       # derive the universal ref from whichever alias the caller passed
        if release_id is not None:
            ref_type, ref_id = "release", release_id
        elif canon_id is not None:
            ref_type, ref_id = "canon", canon_id
    if ref_type is None or ref_id is None:
        return None
    body = {"ref_type": ref_type, "ref_id": ref_id, "region": region}
    if condition:
        body["condition"] = condition
    try:
        async with httpx.AsyncClient(timeout=25, headers={"X-Api-Key": key}) as c:
            r = await c.post(f"{BASE}/api/value", json=body)
        return r.json() if r.status_code == 200 else None
    except Exception:
        return None


async def lore(db, ref) -> dict | None:
    """GET /api/lore/{ref} — {ref} = canon_id (int) OR slug. The harvested resale wiki
    (description, tier, variants, faults, id_keys, traps, related) or None."""
    key = await _key(db)
    if not key or ref is None:
        return None
    try:
        async with httpx.AsyncClient(timeout=20, headers={"X-Api-Key": key}) as c:
            r = await c.get(f"{BASE}/api/lore/{ref}")
        return r.json() if r.status_code == 200 else None
    except Exception:
        return None


async def metal_prices(db) -> dict | None:
    """GET /api/metal-prices — live AU spot (gold/silver/platinum oz) + per-gram. Powers the jewellery melt tool."""
    key = await _key(db)
    if not key:
        return None
    try:
        async with httpx.AsyncClient(timeout=15, headers={"X-Api-Key": key}) as c:
            r = await c.get(f"{BASE}/api/metal-prices")
        return r.json() if r.status_code == 200 else None
    except Exception:
        return None


async def supply(db, ids: list) -> dict:
    """POST /api/supply (LIVE). Batched ≤200; batches run CONCURRENTLY (bounded) so a big buylist
    scan isn't a serial network crawl. Returns {<id str>: {store_count, discogs_seller_count, …}};
    degrades to {} / drops a failed batch. ponytail: Semaphore(5) caps load — the brain has its own
    rate limits + matcher caps; raise it only if buylists get huge and the brain can take it."""
    key = await _key(db)
    if not key or not ids:
        return {}
    batches = [ids[i:i + 200] for i in range(0, len(ids), 200)]
    out, sem = {}, asyncio.Semaphore(5)
    async with httpx.AsyncClient(timeout=25, headers={"X-Api-Key": key}) as c:
        async def _one(batch):
            async with sem:
                try:
                    r = await c.post(f"{BASE}/api/supply", json={"ids": batch})
                    if r.status_code == 200:
                        out.update(r.json().get("results", {}))   # single-threaded asyncio → no race
                except Exception:
                    pass
        await asyncio.gather(*(_one(b) for b in batches))
    return out


def _selfcheck():
    rel = _norm_candidate({"ref_type": "release", "ref_id": 12345, "display_name": "X"})
    assert rel["release_id"] == 12345 and rel["ref_type"] == "release"
    can = _norm_candidate({"ref_type": "canon", "ref_id": 678, "slug": "roland-jp-8000"})
    assert can["canon_id"] == 678
    legacy = _norm_candidate({"release_id": 999})        # legacy/direct shape → derives ref_type/ref_id
    assert legacy["ref_type"] == "release" and legacy["ref_id"] == 999
    legacy_c = _norm_candidate({"canon_id": 42})
    assert legacy_c["ref_type"] == "canon" and legacy_c["ref_id"] == 42
    print("ok")


if __name__ == "__main__":
    _selfcheck()
