from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from sqlalchemy import text

from .auth import require_token
from .db import get_db

# Store Navigator — the WowPlatter "Search & Inventory Management" drill-down, RecordGod-side.
# Store → Rack → Crate → Item, over the migrated virtual_* geometry. See RECORDGOD_NAVIGATOR_PLAN.md.
# Dims are METRES here (the 3D-store convention the data was migrated into), not WowPlatter's old cm.
router = APIRouter(prefix="/nav", tags=["navigator"])


@router.get("/spaces")
async def spaces(ident=Depends(require_token), db=Depends(get_db)):
    rows = await db.execute(text("""
        SELECT id, name, room_width::float AS room_width, room_depth::float AS room_depth,
               (is_default = 'y') AS is_default
        FROM virtual_space WHERE visible = 'y'
        ORDER BY (is_default = 'y') DESC, id
    """))
    return {"spaces": [dict(r) for r in rows.mappings()]}


@router.get("/store-layout")
async def store_layout(space_id: int | None = None, ident=Depends(require_token), db=Depends(get_db)):
    """All racks (position + facing + type dims + counts) for one space — the top-down store view."""
    if space_id is None:
        sp = (await db.execute(text(
            "SELECT id FROM virtual_space WHERE visible='y' ORDER BY (is_default='y') DESC, id LIMIT 1"
        ))).first()
        space_id = sp[0] if sp else None
    space = (await db.execute(text("""
        SELECT id, name, room_width::float AS room_width, room_depth::float AS room_depth
        FROM virtual_space WHERE id = :s
    """), {"s": space_id})).mappings().first()
    racks = [dict(r) for r in (await db.execute(text("""
        SELECT r.id, r.name, r.pos_x::float AS x, r.pos_z::float AS z,
               r.rotation_y::float AS rot, r.direction,
               rt.type AS rack_type, rt.name AS type_name,
               rt.width::float AS w, rt.depth::float AS d, coalesce(rt.levels,1)::int AS levels,
               (SELECT count(*) FROM virtual_crate c WHERE c.rack_id = r.id AND c.visible = 'y') AS crates,
               (SELECT count(*) FROM inventory i
                  WHERE i.crate_id IN (SELECT id FROM virtual_crate c WHERE c.rack_id = r.id)
                    AND i.in_stock) AS items
        FROM virtual_rack r LEFT JOIN virtual_rack_type rt ON rt.id = r.rack_type_id
        WHERE r.visible = 'y' AND r.space_id = :s
          AND coalesce(rt.rack_purpose, 'stock') <> 'prop'   -- functional racks only; props = 3D scenery
    """), {"s": space_id})).mappings()]
    return {"ok": True, "space": dict(space) if space else None, "racks": racks}


@router.get("/rack/{rack_id}")
async def rack_detail(rack_id: int, ident=Depends(require_token), db=Depends(get_db)):
    """One rack: its type dims/levels + every crate (level, slot, facing, label, item count)."""
    rack = (await db.execute(text("""
        SELECT r.id, r.name, r.direction, r.pos_x::float AS x, r.pos_z::float AS z,
               r.rotation_y::float AS rot, r.space_id,
               rt.type AS rack_type, rt.name AS type_name,
               rt.width::float AS w, rt.depth::float AS d,
               coalesce(rt.levels,1)::int AS levels, coalesce(rt.slots_per_level,0)::int AS slots_per_level
        FROM virtual_rack r LEFT JOIN virtual_rack_type rt ON rt.id = r.rack_type_id
        WHERE r.id = :id
    """), {"id": rack_id})).mappings().first()
    if not rack:
        raise HTTPException(404, "rack not found")
    crates = [dict(r) for r in (await db.execute(text("""
        SELECT c.id, c.name, c.label_text, c.rack_level_index::int AS level, c.slot_number::int AS slot,
               c.direction, c.pos_x::float AS x, c.pos_z::float AS z, c.crate_purpose,
               ct.name AS crate_type, ct.material_color AS color,
               (SELECT count(*) FROM inventory i WHERE i.crate_id = c.id AND i.in_stock) AS items
        FROM virtual_crate c LEFT JOIN virtual_crate_type ct ON ct.id = c.crate_type_id
        WHERE c.rack_id = :id AND c.visible = 'y'
        ORDER BY c.rack_level_index NULLS FIRST, c.slot_number NULLS LAST
    """), {"id": rack_id})).mappings()]
    levels = [dict(r) for r in (await db.execute(text("""
        SELECT l.level_index::int AS level, coalesce(l.slots,0)::int AS slots
        FROM virtual_rack_type_level l
        JOIN virtual_rack r ON r.rack_type_id = l.rack_type_id
        WHERE r.id = :id ORDER BY l.level_index
    """), {"id": rack_id})).mappings()]
    return {"ok": True, "rack": dict(rack), "crates": crates, "levels": levels}


class RackEditIn(BaseModel):
    name: str | None = None
    pos_x: float | None = None       # move the rack on the store map (metres)
    pos_z: float | None = None
    direction: str | None = None     # forward | back | left | right
    rotation_y: float | None = None  # fine rotation (degrees)


@router.post("/rack/{rack_id}")
async def rack_edit(rack_id: int, body: RackEditIn, ident=Depends(require_token), db=Depends(get_db)):
    fields = {k: v for k, v in body.model_dump().items() if v is not None}
    if not fields:
        return {"ok": True, "unchanged": True}
    sets = ", ".join(f"{k} = :{k}" for k in fields) + ", updated_at = now()"
    r = await db.execute(text(f"UPDATE virtual_rack SET {sets} WHERE id = :i"), {**fields, "i": rack_id})
    await db.commit()
    if not r.rowcount:
        raise HTTPException(404, "rack not found")
    return {"ok": True}


@router.get("/crate/{crate_id}")
async def crate_contents(crate_id: int, ident=Depends(require_token), db=Depends(get_db)):
    """A crate's records, in slot order — the Crate Contents list."""
    crate = (await db.execute(text("""
        SELECT c.id, c.name, c.label_text, c.rack_id, c.rack_level_index::int AS level,
               c.slot_number::int AS slot, c.direction, c.crate_purpose,
               c.updated_at AS last_scanned, c.updated_by, r.name AS rack_name, rt.type AS rack_type,
               (SELECT count(*) FROM inventory i WHERE i.crate_id = c.id AND i.in_stock) AS items_count
        FROM virtual_crate c LEFT JOIN virtual_rack r ON r.id = c.rack_id
        LEFT JOIN virtual_rack_type rt ON rt.id = r.rack_type_id
        WHERE c.id = :id
    """), {"id": crate_id})).mappings().first()
    if not crate:
        raise HTTPException(404, "crate not found")
    items = [dict(r) for r in (await db.execute(text("""
        SELECT i.sku, i.release_id, i.slot_number::int AS slot, coalesce(i.title, dc.title) AS title,
               dc.artist, i.price::float AS price, i.condition, i.in_stock,
               (SELECT string_agg(DISTINCT g.genre_name, ', ') FROM disc_release_genre g WHERE g.release_id = i.release_id) AS genre,
               (SELECT string_agg(DISTINCT s.style_name, ', ') FROM disc_release_style s WHERE s.release_id = i.release_id) AS style
        FROM inventory i LEFT JOIN disc_cache dc ON dc.release_id = i.release_id
        WHERE i.crate_id = :id
        ORDER BY i.slot_number NULLS LAST, i.sku
    """), {"id": crate_id})).mappings()]
    return {"ok": True, "crate": dict(crate), "items": items}


@router.get("/release/{release_id}")
async def release_info(release_id: int, ident=Depends(require_token), db=Depends(get_db)):
    """Full release metadata (from the disc_release mirror) + every inventory copy + where each lives."""
    dr = (await db.execute(text("""
        SELECT dr.id, dr.title, dr.artists_sort AS artist, dr.country, dr.year, dr.thumb, dr.notes, dr.master_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,
          (SELECT value FROM disc_release_identifier WHERE release_id=dr.id AND type ILIKE 'barcode' LIMIT 1) AS barcode
        FROM disc_release dr WHERE dr.id = :r
    """), {"r": release_id})).mappings().first()
    items = [dict(r) for r in (await db.execute(text("""
        SELECT i.sku, i.price::float AS price, i.condition, i.sleeve_cond, i.in_stock,
               i.crate_id, i.slot_number::int AS slot, c.label_text AS crate, r.name AS rack,
               c.rack_level_index::int AS level
        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 ORDER BY i.in_stock DESC, i.sku
    """), {"r": release_id})).mappings()]
    return {"ok": True, "release": dict(dr) if dr else None, "items": items}


# ── Scanner — assign scanned items to a crate's slots (Replace / Append / Prepend / Insert) ────
class CrateEditIn(BaseModel):
    name: str | None = None
    label_text: str | None = None
    crate_purpose: str | None = None
    direction: str | None = None     # forward | back | left | right — the crate's facing (rotate)


@router.post("/crate/{crate_id}")
async def crate_edit(crate_id: int, body: CrateEditIn, ident=Depends(require_token), db=Depends(get_db)):
    fields = {k: v for k, v in body.model_dump().items() if v is not None}
    if not fields:
        return {"ok": True, "unchanged": True}
    sets = ", ".join(f"{k} = :{k}" for k in fields) + ", updated_at = now()"
    r = await db.execute(text(f"UPDATE virtual_crate SET {sets} WHERE id = :i"), {**fields, "i": crate_id})
    await db.commit()
    if not r.rowcount:
        raise HTTPException(404, "crate not found")
    return {"ok": True}


class ScanAssignIn(BaseModel):
    crate_id: int
    lines: list[str]
    mode: str = "replace"           # replace | append | prepend | insert
    insert_at: int | None = None
    dry_run: bool = False


async def _resolve_sku(db, ln):
    """A scanned line → the inventory SKU it refers to (exact sku, else release_id, else barcode)."""
    ln = ln.strip()
    if not ln:
        return None
    r = (await db.execute(text("SELECT sku FROM inventory WHERE sku = :v LIMIT 1"), {"v": ln})).first()
    if r:
        return r[0]
    if ln.isdigit():
        r = (await db.execute(text("SELECT sku FROM inventory WHERE release_id = :v AND in_stock "
                                   "ORDER BY (crate_id IS NULL) DESC, sku LIMIT 1"), {"v": int(ln)})).first()
        if r:
            return r[0]
    r = (await db.execute(text("""SELECT i.sku FROM disc_release_identifier di JOIN inventory i ON i.release_id = di.release_id
           WHERE di.value = :v AND i.in_stock ORDER BY (i.crate_id IS NULL) DESC, i.sku LIMIT 1"""), {"v": ln})).first()
    return r[0] if r else None


class InsertOneIn(BaseModel):
    item: str          # release id / sku / barcode → resolved to one inventory sku
    crate_id: int
    slot: int          # insert AT this slot; existing items at >= slot shift down by one


@router.post("/insert")
async def insert_one(body: InsertOneIn, ident=Depends(require_token), db=Depends(get_db)):
    """Drop a single record into a crate at a chosen slot, shifting the rest down (the Quick Insert)."""
    sku = await _resolve_sku(db, body.item.strip())
    if not sku:
        raise HTTPException(404, "no record found for that ID / SKU / barcode")
    slot = max(1, body.slot)
    # detach the record first so it isn't caught by the shift (handles re-filing within the same crate)
    await db.execute(text("UPDATE inventory SET crate_id=NULL, slot_number=NULL WHERE sku=:sku"), {"sku": sku})
    shifted = (await db.execute(text(
        "UPDATE inventory SET slot_number = slot_number + 1, updated_at = now() "
        "WHERE crate_id = :c AND slot_number >= :s"), {"c": body.crate_id, "s": slot})).rowcount or 0
    await db.execute(text(
        "UPDATE inventory SET crate_id = :c, slot_number = :s, updated_at = now() WHERE sku = :sku"),
        {"c": body.crate_id, "s": slot, "sku": sku})
    await db.execute(text("UPDATE virtual_crate SET updated_at = now() WHERE id = :c"), {"c": body.crate_id})
    await db.commit()
    return {"ok": True, "sku": sku, "slot": slot, "shifted": shifted}


@router.post("/scan")
async def scan_assign(body: ScanAssignIn, ident=Depends(require_token), db=Depends(get_db)):
    resolved, not_found = [], []
    for ln in body.lines:
        if not ln.strip():
            continue
        sku = await _resolve_sku(db, ln)
        if sku:
            resolved.append(sku)
        else:
            not_found.append(ln.strip())
    res = list(dict.fromkeys(resolved))             # unique, keep scan order
    existing = [r[0] for r in (await db.execute(text(
        "SELECT sku FROM inventory WHERE crate_id = :c AND in_stock ORDER BY slot_number NULLS LAST, sku"
    ), {"c": body.crate_id})).all()]
    keep = [s for s in existing if s not in res]    # existing items not in the scan
    if body.mode == "replace":
        order = res
    elif body.mode == "append":
        order = keep + res
    elif body.mode == "prepend":
        order = res + keep
    elif body.mode == "insert":
        at = max(0, (body.insert_at or 1) - 1)
        order = keep[:at] + res + keep[at:]
    else:
        raise HTTPException(400, "bad mode")

    if body.dry_run:
        meta = {}
        if order:
            rows = await db.execute(text("""SELECT i.sku, coalesce(i.title, dc.title) AS title, dc.artist
                FROM inventory i LEFT JOIN disc_cache dc ON dc.release_id = i.release_id WHERE i.sku = ANY(:s)"""),
                {"s": order})
            meta = {r["sku"]: r for r in rows.mappings()}
        plan = [{"slot": n, "sku": s, "title": (meta.get(s) or {}).get("title"),
                 "artist": (meta.get(s) or {}).get("artist")} for n, s in enumerate(order, 1)]
        return {"ok": True, "preview": True, "plan": plan, "not_found": not_found, "count": len(order)}

    if body.mode == "replace":                       # Replace All: items dropped from the crate get unfiled
        removed = [s for s in existing if s not in order]
        if removed:
            await db.execute(text("UPDATE inventory SET crate_id=NULL, slot_number=NULL, updated_at=now() "
                                  "WHERE sku = ANY(:s)"), {"s": removed})
    for n, sku in enumerate(order, 1):
        await db.execute(text("UPDATE inventory SET crate_id=:c, slot_number=:n, updated_at=now() WHERE sku=:sku"),
                         {"c": body.crate_id, "n": n, "sku": sku})
    await db.execute(text("UPDATE virtual_crate SET updated_at=now() WHERE id=:c"), {"c": body.crate_id})
    await db.commit()
    return {"ok": True, "assigned": len(order), "not_found": not_found}


# ── Reorganize — A-Z re-file planner (server scans + applies; client plans the moves) ──────────
class ScanIn(BaseModel):
    crate_ids: list[int]


class Move(BaseModel):
    sku: str
    new_crate_id: int
    new_slot: int


class Leftover(BaseModel):
    sku: str
    archive_location: str | None = None


class ApplyIn(BaseModel):
    matched: list[Move] = []
    leftovers: list[Leftover] = []


@router.post("/reorg/scan")
async def reorg_scan(body: ScanIn, ident=Depends(require_token), db=Depends(get_db)):
    """Every in-stock item across the source crates — the pool the client sorts + redistributes."""
    ids = [c for c in body.crate_ids if c and c > 0]
    if not ids:
        raise HTTPException(400, "no crate ids")
    rows = [dict(r) for r in (await db.execute(text("""
        SELECT i.sku, i.release_id, i.crate_id, i.slot_number::int AS slot, i.price::float AS price,
               i.condition, coalesce(i.title, dc.title) AS title, dc.artist, dc.year, dc.thumb,
               c.label_text AS crate_label
        FROM inventory i
        LEFT JOIN disc_cache dc ON dc.release_id = i.release_id
        LEFT JOIN virtual_crate c ON c.id = i.crate_id
        WHERE i.crate_id = ANY(:ids) AND i.in_stock
        ORDER BY i.crate_id, i.slot_number NULLS LAST
    """), {"ids": ids})).mappings()]
    return {"ok": True, "items": rows, "count": len(rows), "crate_ids": ids}


@router.post("/reorg/apply")
async def reorg_apply(body: ApplyIn, ident=Depends(require_token), db=Depends(get_db)):
    """Commit the plan in ONE transaction: matched → new crate+slot, leftovers → unfiled/archived."""
    if not body.matched and not body.leftovers:
        raise HTTPException(400, "nothing to apply")
    moved = 0
    for m in body.matched:
        if not m.sku or m.new_crate_id < 1 or m.new_slot < 1:
            continue
        r = await db.execute(text("""
            UPDATE inventory SET crate_id=:c, slot_number=:s, updated_at=now()
            WHERE sku=:sku
        """), {"c": m.new_crate_id, "s": m.new_slot, "sku": m.sku})
        moved += r.rowcount or 0
    archived = 0
    for lo in body.leftovers:
        if not lo.sku:
            continue
        r = await db.execute(text("""
            UPDATE inventory SET crate_id=NULL, slot_number=NULL, location=:loc, updated_at=now()
            WHERE sku=:sku
        """), {"loc": lo.archive_location, "sku": lo.sku})
        archived += r.rowcount or 0
    if moved == 0 and body.matched:
        # nothing matched a real SKU — roll back rather than half-apply
        await db.rollback()
        raise HTTPException(409, f"0 of {len(body.matched)} items matched an inventory SKU — nothing written")
    await db.commit()
    return {"ok": True, "matched_updated": moved, "leftovers_archived": archived}


@router.post("/reorg/compress/{crate_id}")
async def reorg_compress(crate_id: int, ident=Depends(require_token), db=Depends(get_db)):
    """Close gaps in a crate's slot numbering — renumber 1..N by current slot order."""
    await db.execute(text("""
        WITH ranked AS (
            SELECT sku, row_number() OVER (ORDER BY slot_number NULLS LAST, sku) AS rn
            FROM inventory WHERE crate_id = :c AND in_stock)
        UPDATE inventory i SET slot_number = ranked.rn, updated_at = now()
        FROM ranked WHERE i.sku = ranked.sku
    """), {"c": crate_id})
    n = (await db.execute(text(
        "SELECT count(*) FROM inventory WHERE crate_id=:c AND in_stock"), {"c": crate_id})).scalar()
    await db.commit()
    return {"ok": True, "slots": n}
