import asyncio
import json
import re

import httpx
from fastapi import APIRouter, Depends, Query, HTTPException, Header
from pydantic import BaseModel
from sqlalchemy import text

from . import vault

_YT = re.compile(r"(?:v=|youtu\.be/|embed/|/v/)([\w-]{11})")


def _ytid(uri):
    if not uri:
        return None
    m = _YT.search(uri)
    if m:
        return m.group(1)
    return uri if re.fullmatch(r"[\w-]{11}", uri) else None

from .db import get_db
from .layout_routes import DEFAULTS

# PUBLIC storefront API — the customer-facing shop reads its theme/layout + catalog here.
# No auth (theme + in-stock catalog aren't secret); driven by what the builder saves.
router = APIRouter(prefix="/shop", tags=["shop"])


@router.get("/config")
async def shop_config(db=Depends(get_db)):
    row = (await db.execute(text("SELECT config FROM store_config WHERE store_id=1"))).first()
    cfg = row[0] if row else None
    if isinstance(cfg, str):
        cfg = json.loads(cfg)
    return cfg or DEFAULTS


@router.get("/catalog")
async def shop_catalog(q: str = Query(""), page: int = Query(1, ge=1), db=Depends(get_db)):
    per = 24
    where = ["i.in_stock", "i.store_id = 1"]
    params = {"per": per, "off": (page - 1) * per}
    if q:
        where.append("(coalesce(i.title, dc.title) ILIKE :q OR dc.artist ILIKE :q)")
        params["q"] = f"%{q}%"
    w = " AND ".join(where)
    items = [dict(r) for r in (await db.execute(text(f"""
        SELECT i.sku, coalesce(i.title, dc.title) AS title, dc.artist, dc.thumb,
               i.price::float AS price, i.condition, i.kind
        FROM inventory i LEFT JOIN disc_cache dc ON dc.release_id = i.release_id
        WHERE {w}
        ORDER BY i.updated_at DESC NULLS LAST
        LIMIT :per OFFSET :off"""), params)).mappings()]
    cparams = {k: v for k, v in params.items() if k not in ("per", "off")}
    total = (await db.execute(text(
        f"SELECT count(*) FROM inventory i LEFT JOIN disc_cache dc ON dc.release_id=i.release_id WHERE {w}"
    ), cparams)).scalar()
    return {"items": items, "page": page, "per": per, "total": total}


@router.get("/artist/{artist_id}")
async def shop_artist(artist_id: int, db=Depends(get_db)):
    r = (await db.execute(text("SELECT id, name, realname FROM disc_artist WHERE id=:i"),
                          {"i": artist_id})).mappings().first()
    return {"artist": dict(r) if r else {"id": artist_id, "name": f"Artist {artist_id}"}}


@router.get("/label/{label_id}")
async def shop_label(label_id: int, db=Depends(get_db)):
    r = (await db.execute(text("SELECT id, name, profile FROM disc_label WHERE id=:i"),
                          {"i": label_id})).mappings().first()
    return {"label": dict(r) if r else {"id": label_id, "name": f"Label {label_id}"}}


@router.get("/browse")
async def shop_browse(q: str = Query(""), genre: str = Query(""), style: str = Query(""),
                      fmt: str = Query(""), artist: int | None = None, label: int | None = None,
                      year_min: int | None = None, year_max: int | None = None,
                      price_min: float | None = None, price_max: float | None = None,
                      sort: str = Query("new"), page: int = Query(1, ge=1), db=Depends(get_db)):
    """Faceted, release-grouped catalog — the public Browse page. One row per release with its cheapest copy."""
    per = 24
    where = ["i.in_stock", "i.store_id = 1", "i.release_id IS NOT NULL"]
    params = {"per": per, "off": (page - 1) * per}
    if q:
        where.append("(coalesce(i.title, dr.title) ILIKE :q OR dr.artists_sort ILIKE :q)"); params["q"] = f"%{q}%"
    if genre:
        where.append("EXISTS (SELECT 1 FROM disc_release_genre g WHERE g.release_id=i.release_id AND g.genre_name=:genre)"); params["genre"] = genre
    if style:
        where.append("EXISTS (SELECT 1 FROM disc_release_style s WHERE s.release_id=i.release_id AND s.style_name=:style)"); params["style"] = style
    if fmt:
        where.append("EXISTS (SELECT 1 FROM disc_release_format f WHERE f.release_id=i.release_id AND f.name ILIKE :fmt)"); params["fmt"] = f"%{fmt}%"
    if artist:
        where.append("EXISTS (SELECT 1 FROM disc_release_artist ra WHERE ra.release_id=i.release_id AND ra.artist_id=:artist)"); params["artist"] = artist
    if label:
        where.append("EXISTS (SELECT 1 FROM disc_release_label rl WHERE rl.release_id=i.release_id AND rl.label_id=:label)"); params["label"] = label
    if year_min:
        where.append("dr.year >= :ymin"); params["ymin"] = year_min
    if year_max:
        where.append("dr.year <= :ymax"); params["ymax"] = year_max
    if price_min:
        where.append("i.price >= :pmin"); params["pmin"] = price_min
    if price_max:
        where.append("i.price <= :pmax"); params["pmax"] = price_max
    w = " AND ".join(where)
    order = {"new": "max(i.updated_at) DESC NULLS LAST", "price_asc": "min(i.price) ASC",
             "price_desc": "min(i.price) DESC", "year": "dr.year DESC NULLS LAST",
             "artist": "dr.artists_sort", "title": "min(coalesce(i.title, dr.title))"}.get(sort, "max(i.updated_at) DESC NULLS LAST")
    items = [dict(r) for r in (await db.execute(text(f"""
        SELECT i.release_id, coalesce(min(i.title), dr.title) AS title, dr.artists_sort AS artist,
               dr.year, dr.country, min(i.price)::float AS price, count(*) AS copies,
               (SELECT thumb FROM disc_cache dc WHERE dc.release_id=i.release_id) AS thumb,
               (SELECT artist_id FROM disc_release_artist ra WHERE ra.release_id=i.release_id ORDER BY ra.position LIMIT 1) AS artist_id,
               (SELECT string_agg(DISTINCT genre_name, ', ') FROM disc_release_genre g WHERE g.release_id=i.release_id) AS genre
        FROM inventory i JOIN disc_release dr ON dr.id = i.release_id
        WHERE {w}
        GROUP BY i.release_id, dr.title, dr.artists_sort, dr.year, dr.country
        ORDER BY {order} LIMIT :per OFFSET :off"""), params)).mappings()]
    cp = {k: v for k, v in params.items() if k not in ("per", "off")}
    total = (await db.execute(text(
        f"SELECT count(DISTINCT i.release_id) FROM inventory i JOIN disc_release dr ON dr.id=i.release_id WHERE {w}"
    ), cp)).scalar()
    return {"items": items, "page": page, "per": per, "total": total}


@router.get("/facets")
async def shop_facets(db=Depends(get_db)):
    async def top(tbl, col):
        return [dict(r) for r in (await db.execute(text(f"""
            SELECT x.{col} AS name, count(DISTINCT i.release_id) AS n
            FROM {tbl} x JOIN inventory i ON i.release_id = x.release_id
            WHERE i.in_stock AND i.store_id=1 AND x.{col} IS NOT NULL AND x.{col} <> ''
            GROUP BY x.{col} ORDER BY n DESC LIMIT 24"""))).mappings()]
    return {"genres": await top("disc_release_genre", "genre_name"),
            "styles": await top("disc_release_style", "style_name"),
            "formats": await top("disc_release_format", "name")}


@router.get("/release/{release_id}")
async def shop_release(release_id: int, db=Depends(get_db)):
    """Public release detail — metadata + tracklist + in-stock copies (with the Woo buy link)."""
    dr = (await db.execute(text("""
        SELECT dr.id, dr.title, dr.artists_sort AS artist, dr.country, dr.year, dr.notes, dr.master_id,
          (SELECT thumb FROM disc_cache dc WHERE dc.release_id=dr.id) AS thumb,
          (SELECT artist_id FROM disc_release_artist ra WHERE ra.release_id=dr.id ORDER BY ra.position LIMIT 1) AS artist_id,
          (SELECT string_agg(label_name || coalesce(' ('||catno||')',''), ', ') FROM disc_release_label WHERE release_id=dr.id) AS label,
          (SELECT string_agg(name || coalesce(' '||descriptions,''), ', ') FROM disc_release_format WHERE release_id=dr.id) AS format,
          (SELECT string_agg(DISTINCT genre_name, ', ') FROM disc_release_genre WHERE release_id=dr.id) AS genre,
          (SELECT string_agg(DISTINCT style_name, ', ') FROM disc_release_style WHERE release_id=dr.id) AS style
        FROM disc_release dr WHERE dr.id = :r"""), {"r": release_id})).mappings().first()
    tracks = [dict(r) for r in (await db.execute(text("""
        SELECT position, title, duration FROM disc_release_track WHERE release_id=:r ORDER BY sequence NULLS LAST, position
    """), {"r": release_id})).mappings()]
    copies = [dict(r) for r in (await db.execute(text("""
        SELECT i.sku, i.price::float AS price, i.condition, i.sleeve_cond, i.product_url,
               c.label_text AS crate, r.name AS rack
        FROM inventory i
        LEFT JOIN virtual_crate c ON c.id = i.crate_id
        LEFT JOIN virtual_rack r ON r.id = c.rack_id
        WHERE i.release_id=:r AND i.in_stock AND i.store_id=1 ORDER BY i.price
    """), {"r": release_id})).mappings()]
    return {"release": dict(dr) if dr else None, "tracks": tracks, "copies": copies}


def _apple_id(raw):
    """apple_id is country-scoped {"<cc>":"<album_id>"} (some rows double-JSON-encoded). Prefer AU
    (this is an AU store), then US, then whatever country is present → a real music.apple.com link."""
    if not raw:
        return None
    try:
        v = json.loads(raw)
        if isinstance(v, str):
            v = json.loads(v)
    except (ValueError, TypeError):
        return None
    if not isinstance(v, dict) or not v:
        return None
    cc = "au" if "au" in v else ("us" if "us" in v else next(iter(v)))
    aid = v.get(cc)
    return {"country": cc, "album_id": aid,
            "url": f"https://music.apple.com/{cc}/album/{aid}" if aid else None}


@router.get("/audio/{release_id}")
async def shop_audio(release_id: int, db=Depends(get_db)):
    """Playable / embeddable previews — Apple 30s preview + country-scoped Apple link, Beatport/Bandcamp, YouTube."""
    a = (await db.execute(text("""
        SELECT apple_id, apple_url, apple_preview_url, beatport_id, beatport_url, beatport_embed,
               beatport_genre, spotify_id, bandcamp_url, bandcamp_embed, soundcloud_url
        FROM disc_release_audio WHERE release_id = :r"""), {"r": release_id})).mappings().first()
    youtube = await _live_youtube(db, release_id)
    if not a:
        return {"apple": None, "apple_preview": None, "youtube": youtube}
    pv = a["apple_preview_url"]
    previews = []
    if isinstance(pv, str) and pv.lstrip().startswith("["):
        try:
            previews = [u for u in json.loads(pv) if u]
        except (ValueError, TypeError):
            previews = []
    elif pv:
        previews = [pv]
    return {"apple": _apple_id(a["apple_id"]), "apple_url": a["apple_url"],
            "apple_preview": previews[0] if previews else None, "apple_previews": previews,
            "beatport_url": a["beatport_url"], "beatport_embed": a["beatport_embed"],
            "beatport_id": a["beatport_id"], "bandcamp_url": a["bandcamp_url"],
            "bandcamp_embed": a["bandcamp_embed"], "soundcloud_url": a["soundcloud_url"],
            "spotify_id": a["spotify_id"], "youtube": youtube}


async def _live_youtube(db, release_id):
    """Pick a NON-dead YouTube clip — validates unchecked ones via YouTube oembed (200=live) and caches `dead`."""
    rows = [dict(r) for r in (await db.execute(text(
        "SELECT DISTINCT uri, dead FROM disc_release_video WHERE release_id=:r AND uri<>'' ORDER BY uri LIMIT 8"
    ), {"r": release_id})).mappings()]
    unchecked = [v for v in rows if v["dead"] is None][:4]
    if unchecked:
        async with httpx.AsyncClient(timeout=8) as c:
            async def chk(v):
                yid = _ytid(v["uri"])
                if not yid:
                    return v["uri"], True
                try:
                    rr = await c.get("https://www.youtube.com/oembed",
                                     params={"url": "https://youtu.be/" + yid, "format": "json"})
                    return v["uri"], rr.status_code != 200
                except Exception:
                    return v["uri"], None   # network blip — leave unchecked for next time
            checked = await asyncio.gather(*[chk(v) for v in unchecked])
        wrote = False
        for uri, dead in checked:
            if dead is not None:
                await db.execute(text("UPDATE disc_release_video SET dead=:d, checked_at=now() "
                                      "WHERE release_id=:r AND uri=:u"), {"d": dead, "r": release_id, "u": uri})
                for v in rows:
                    if v["uri"] == uri:
                        v["dead"] = dead
                wrote = True
        if wrote:
            await db.commit()
    for v in rows:
        if v["dead"] is False:
            yid = _ytid(v["uri"])
            if yid:
                return yid
    return None


@router.get("/audio-tracks/{release_id}")
async def shop_audio_tracks(release_id: int, db=Depends(get_db)):
    """Per-track 30s previews — lazily resolved from each track's Apple song id via the iTunes lookup API, then cached."""
    rows = [dict(r) for r in (await db.execute(text("""
        SELECT sequence, position, title, apple_id, apple_preview
        FROM disc_release_track WHERE release_id=:r AND apple_id ~ '^[0-9]+$'
        ORDER BY sequence NULLS LAST, position"""), {"r": release_id})).mappings()]
    need = [t["apple_id"] for t in rows if not t["apple_preview"]]
    if need:
        found = {}
        try:
            async with httpx.AsyncClient(timeout=15) as c:
                rr = await c.get("https://itunes.apple.com/lookup",
                                 params={"id": ",".join(need[:190]), "country": "au", "entity": "song"})
            found = {str(x.get("trackId")): x.get("previewUrl")
                     for x in rr.json().get("results", []) if x.get("previewUrl")}
        except Exception:
            found = {}
        for aid, prev in found.items():
            await db.execute(text("UPDATE disc_release_track SET apple_preview=:p WHERE release_id=:r AND apple_id=:a"),
                             {"p": prev, "r": release_id, "a": aid})
        if found:
            await db.commit()
        for t in rows:
            if not t["apple_preview"]:
                t["apple_preview"] = found.get(t["apple_id"])
    return {"tracks": [{"position": t["position"], "title": t["title"], "preview": t["apple_preview"]}
                       for t in rows if t["apple_preview"]]}


class WantIn(BaseModel):
    email: str
    artist: str = ""
    title: str = ""
    name: str = ""
    phone: str = ""
    format: str = ""
    max_price: float | None = None
    delivery_preference: str = "either"
    postcode: str = ""
    notes: str = ""
    release_id: int | None = None


@router.post("/wantlist")
async def shop_wantlist(body: WantIn, db=Depends(get_db)):
    """Public 'request a record we don't stock' intake — no auth, keyed by email (guest or known customer)."""
    if "@" not in body.email or not (body.artist.strip() or body.title.strip()):
        raise HTTPException(422, "email and an artist or title are required")
    pref = body.delivery_preference if body.delivery_preference in ("pickup", "post", "either") else "either"
    rid = (await db.execute(text("""
        INSERT INTO wantlist (release_id, artist, title, name, phone, email, format, max_price,
                              delivery_preference, postcode, notes)
        VALUES (:release_id, :artist, :title, :name, :phone, :email, :format, :max_price, :pref, :postcode, :notes)
        RETURNING id"""), {**body.model_dump(), "pref": pref})).scalar()
    await db.commit()
    return {"ok": True, "id": rid}


# ─── WordPress / Woo bridge ──────────────────────────────────────────────────────────────────
# The thin RecordGod WP plugin renders the storefront from /shop/*, creates Woo products on the fly
# at add-to-cart (via /shop/item), quotes postage (/shop/shipping/quote), and posts back completed
# orders (/shop/woo-order, bridge-key gated) so RecordGod stays the single source of truth.
@router.get("/item/{sku}")
async def shop_item(sku: str, db=Depends(get_db)):
    """One inventory item by SKU — what the bridge needs to mint a Woo product on the fly."""
    r = (await db.execute(text("""
        SELECT i.sku, coalesce(i.title, dc.title) AS title, dc.artist, dc.thumb,
               i.price::float AS price, i.in_stock, i.condition, i.release_id
        FROM inventory i LEFT JOIN disc_cache dc ON dc.release_id = i.release_id
        WHERE i.sku = :s AND i.store_id = 1"""), {"s": sku})).mappings().first()
    if not r:
        raise HTTPException(404, "item not found")
    return dict(r)


@router.get("/shipping/quote")
async def shop_shipping_quote(units: int | None = None, weight_g: int | None = None, db=Depends(get_db)):
    """Public AusPost quote for the WC shipping method (rates aren't secret)."""
    if weight_g is None:
        weight_g = max(1, units or 1) * 280
    parcels = max(1, -(-weight_g // 5000))
    per = -(-weight_g // parcels)
    rows = await db.execute(text(
        "SELECT service_key, price::float AS price FROM post_flat_rate "
        "WHERE :w BETWEEN weight_min_g AND weight_max_g"), {"w": per})
    seen = {r["service_key"]: r["price"] for r in rows.mappings()}
    opts = [{"service": k, "label": k.replace("_", " ").title(), "price": round(v * parcels, 2)}
            for k, v in sorted(seen.items(), key=lambda x: x[1])]
    return {"weight_g": weight_g, "parcels": parcels, "options": opts}


class WooOrderIn(BaseModel):
    order_number: str
    email: str | None = None
    name: str | None = None
    total: float = 0
    items: list[dict] = []          # [{sku, name, qty, price}]


@router.post("/woo-order")
async def woo_order(body: WooOrderIn, x_bridge_key: str = Header(None), db=Depends(get_db)):
    """Record a completed Woo order back into RecordGod (mark sold + log the online sale). Bridge-key gated."""
    key = await vault.get_secret(db, "bridge_key")
    if not key or x_bridge_key != key:
        raise HTTPException(401, "bad or missing bridge key")
    sn = "WC-" + str(body.order_number)
    if (await db.execute(text("SELECT 1 FROM sales WHERE sale_number = :n"), {"n": sn})).first():
        return {"ok": True, "duplicate": True}
    cust = None
    if body.email:
        c = (await db.execute(text("SELECT id FROM customer WHERE lower(email)=lower(:e) ORDER BY id LIMIT 1"),
                              {"e": body.email})).first()
        cust = c[0] if c else None
    sale_id = (await db.execute(text("""
        INSERT INTO sales (sale_number, customer_id, subtotal, total, status, payment_method, payment_status,
                           amount_paid, sale_date, created_at)
        VALUES (:sn, :cust, :tot, :tot, 'completed', 'woo', 'paid', :tot, now(), now()) RETURNING id"""),
        {"sn": sn, "cust": cust, "tot": body.total})).scalar()
    for it in body.items:
        await db.execute(text("""
            INSERT INTO sale_items (sale_id, sku, item_name, qty, unit_price, line_total)
            VALUES (:s, :sku, :n, :q, :p, :lt)"""),
            {"s": sale_id, "sku": it.get("sku"), "n": it.get("name") or it.get("sku"),
             "q": it.get("qty", 1), "p": it.get("price", 0),
             "lt": float(it.get("price", 0)) * int(it.get("qty", 1))})
    skus = [it["sku"] for it in body.items if it.get("sku")]
    if skus:
        await db.execute(text("UPDATE inventory SET in_stock=false, status='sold', sold_date=now() "
                              "WHERE sku = ANY(:s) AND store_id = 1"), {"s": skus})
    await db.commit()
    return {"ok": True, "sale_id": sale_id, "sale_number": sn}
