"""Inventory API endpoints — products, items, barcode scanning, tags, stats."""

from __future__ import annotations

import asyncio
import logging
import uuid
from pathlib import Path
from typing import List, Optional

import aiofiles
from fastapi import APIRouter, Depends, File, Form, HTTPException, UploadFile, status
from pydantic import BaseModel

from app.cloud_session import CloudUser, _auth_label, get_session

log = logging.getLogger(__name__)
from app.db.session import get_store
from app.services.expiration_predictor import ExpirationPredictor

_predictor = ExpirationPredictor()
from app.db.store import Store
from app.models.schemas.inventory import (
    BarcodeScanResponse,
    BulkAddByNameRequest,
    BulkAddByNameResponse,
    BulkAddItemResult,
    DiscardRequest,
    InventoryItemCreate,
    InventoryItemResponse,
    InventoryItemUpdate,
    InventoryStats,
    PartialConsumeRequest,
    ProductCreate,
    ProductResponse,
    ProductUpdate,
    TagCreate,
    TagResponse,
)
from app.models.schemas.label_capture import LabelConfirmRequest

router = APIRouter()


# ── Helpers ───────────────────────────────────────────────────────────────────

def _user_constraints(store) -> list[str]:
    """Load active dietary constraints from user settings (comma-separated string)."""
    raw = store.get_setting("dietary_constraints") or ""
    return [c.strip() for c in raw.split(",") if c.strip()]


def _enrich_item(item: dict, user_constraints: list[str] | None = None) -> dict:
    """Attach computed fields: opened_expiry_date, secondary_state/uses/warning/discard_signs."""
    from datetime import date, timedelta
    opened = item.get("opened_date")
    if opened:
        days = _predictor.days_after_opening(item.get("category"))
        if days is not None:
            try:
                opened_expiry = date.fromisoformat(opened) + timedelta(days=days)
                item = {**item, "opened_expiry_date": str(opened_expiry)}
            except ValueError:
                pass
    if "opened_expiry_date" not in item:
        item = {**item, "opened_expiry_date": None}

    # Secondary use window — check sell-by date (not opened expiry).
    # Apply dietary constraint filter (e.g. wine suppressed for halal/alcohol-free).
    sec = _predictor.secondary_state(item.get("category"), item.get("expiration_date"))
    sec = _predictor.filter_secondary_by_constraints(sec, user_constraints or [])
    item = {
        **item,
        "secondary_state": sec["label"] if sec else None,
        "secondary_uses": sec["uses"] if sec else None,
        "secondary_warning": sec["warning"] if sec else None,
        "secondary_discard_signs": sec["discard_signs"] if sec else None,
    }
    return item


# ── Products ──────────────────────────────────────────────────────────────────

@router.post("/products", response_model=ProductResponse, status_code=status.HTTP_201_CREATED)
async def create_product(body: ProductCreate, store: Store = Depends(get_store)):
    product, _ = await asyncio.to_thread(
        store.get_or_create_product,
        body.name,
        body.barcode,
        brand=body.brand,
        category=body.category,
        description=body.description,
        image_url=body.image_url,
        nutrition_data=body.nutrition_data,
        source=body.source,
        source_data=body.source_data,
    )
    return ProductResponse.model_validate(product)


@router.get("/products", response_model=List[ProductResponse])
async def list_products(store: Store = Depends(get_store)):
    products = await asyncio.to_thread(store.list_products)
    return [ProductResponse.model_validate(p) for p in products]


@router.get("/products/{product_id}", response_model=ProductResponse)
async def get_product(product_id: int, store: Store = Depends(get_store)):
    product = await asyncio.to_thread(store.get_product, product_id)
    if not product:
        raise HTTPException(status_code=404, detail="Product not found")
    return ProductResponse.model_validate(product)


@router.get("/products/barcode/{barcode}", response_model=ProductResponse)
async def get_product_by_barcode(barcode: str, store: Store = Depends(get_store)):
    product = await asyncio.to_thread(
        store._fetch_one, "SELECT * FROM products WHERE barcode = ?", (barcode,)
    )
    if not product:
        raise HTTPException(status_code=404, detail="Product not found")
    return ProductResponse.model_validate(product)


@router.patch("/products/{product_id}", response_model=ProductResponse)
async def update_product(
    product_id: int, body: ProductUpdate, store: Store = Depends(get_store)
):
    updates = body.model_dump(exclude_none=True)
    if not updates:
        product = await asyncio.to_thread(store.get_product, product_id)
    else:
        import json
        sets = ", ".join(f"{k} = ?" for k in updates)
        values = []
        for k, v in updates.items():
            values.append(json.dumps(v) if isinstance(v, dict) else v)
        values.append(product_id)
        await asyncio.to_thread(
            store.conn.execute,
            f"UPDATE products SET {sets}, updated_at = datetime('now') WHERE id = ?",
            values,
        )
        store.conn.commit()
        product = await asyncio.to_thread(store.get_product, product_id)
    if not product:
        raise HTTPException(status_code=404, detail="Product not found")
    return ProductResponse.model_validate(product)


@router.delete("/products/{product_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_product(product_id: int, store: Store = Depends(get_store)):
    existing = await asyncio.to_thread(store.get_product, product_id)
    if not existing:
        raise HTTPException(status_code=404, detail="Product not found")
    await asyncio.to_thread(
        store.conn.execute, "DELETE FROM products WHERE id = ?", (product_id,)
    )
    store.conn.commit()


# ── Inventory items ───────────────────────────────────────────────────────────

@router.post("/items", response_model=InventoryItemResponse, status_code=status.HTTP_201_CREATED)
async def create_inventory_item(
    body: InventoryItemCreate,
    store: Store = Depends(get_store),
    session: CloudUser = Depends(get_session),
):
    log.info("add_item auth=%s tier=%s product_id=%s", _auth_label(session.user_id), session.tier, body.product_id)
    item = await asyncio.to_thread(
        store.add_inventory_item,
        body.product_id,
        body.location,
        quantity=body.quantity,
        unit=body.unit,
        sublocation=body.sublocation,
        purchase_date=str(body.purchase_date) if body.purchase_date else None,
        expiration_date=str(body.expiration_date) if body.expiration_date else None,
        notes=body.notes,
        source=body.source,
    )
    # RETURNING * omits joined columns (product_name, barcode, category).
    # Re-fetch with the products JOIN so the response is fully populated (#99).
    full_item = await asyncio.to_thread(store.get_inventory_item, item["id"])
    return InventoryItemResponse.model_validate(full_item)


@router.post("/items/bulk-add-by-name", response_model=BulkAddByNameResponse)
async def bulk_add_items_by_name(body: BulkAddByNameRequest, store: Store = Depends(get_store)):
    """Create pantry items from a list of ingredient names (no barcode required).

    Uses get_or_create_product so re-adding an existing product is idempotent.
    """
    results: list[BulkAddItemResult] = []
    for entry in body.items:
        try:
            product, _ = await asyncio.to_thread(
                store.get_or_create_product, entry.name, None, source="manual"
            )
            item = await asyncio.to_thread(
                store.add_inventory_item,
                product["id"],
                entry.location,
                quantity=entry.quantity,
                unit=entry.unit,
                source="manual",
            )
            results.append(BulkAddItemResult(name=entry.name, ok=True, item_id=item["id"]))
        except Exception as exc:
            results.append(BulkAddItemResult(name=entry.name, ok=False, error=str(exc)))

    added = sum(1 for r in results if r.ok)
    return BulkAddByNameResponse(added=added, failed=len(results) - added, results=results)


@router.get("/items", response_model=List[InventoryItemResponse])
async def list_inventory_items(
    location: Optional[str] = None,
    item_status: str = "available",
    store: Store = Depends(get_store),
):
    items = await asyncio.to_thread(store.list_inventory, location, item_status)
    constraints = await asyncio.to_thread(_user_constraints, store)
    return [InventoryItemResponse.model_validate(_enrich_item(i, constraints)) for i in items]


@router.get("/items/expiring", response_model=List[InventoryItemResponse])
async def get_expiring_items(days: int = 7, store: Store = Depends(get_store)):
    items = await asyncio.to_thread(store.expiring_soon, days)
    constraints = await asyncio.to_thread(_user_constraints, store)
    return [InventoryItemResponse.model_validate(_enrich_item(i, constraints)) for i in items]


@router.get("/items/{item_id}", response_model=InventoryItemResponse)
async def get_inventory_item(item_id: int, store: Store = Depends(get_store)):
    item = await asyncio.to_thread(store.get_inventory_item, item_id)
    if not item:
        raise HTTPException(status_code=404, detail="Inventory item not found")
    constraints = await asyncio.to_thread(_user_constraints, store)
    return InventoryItemResponse.model_validate(_enrich_item(item, constraints))


@router.patch("/items/{item_id}", response_model=InventoryItemResponse)
async def update_inventory_item(
    item_id: int, body: InventoryItemUpdate, store: Store = Depends(get_store)
):
    updates = body.model_dump(exclude_none=True)
    if "purchase_date" in updates and updates["purchase_date"]:
        updates["purchase_date"] = str(updates["purchase_date"])
    if "expiration_date" in updates and updates["expiration_date"]:
        updates["expiration_date"] = str(updates["expiration_date"])
    if "opened_date" in updates and updates["opened_date"]:
        updates["opened_date"] = str(updates["opened_date"])
    item = await asyncio.to_thread(store.update_inventory_item, item_id, **updates)
    if not item:
        raise HTTPException(status_code=404, detail="Inventory item not found")
    constraints = await asyncio.to_thread(_user_constraints, store)
    return InventoryItemResponse.model_validate(_enrich_item(item, constraints))


@router.post("/items/{item_id}/open", response_model=InventoryItemResponse)
async def mark_item_opened(item_id: int, store: Store = Depends(get_store)):
    """Record that this item was opened today, triggering secondary shelf-life tracking."""
    from datetime import date
    item = await asyncio.to_thread(
        store.update_inventory_item,
        item_id,
        opened_date=str(date.today()),
    )
    if not item:
        raise HTTPException(status_code=404, detail="Inventory item not found")
    constraints = await asyncio.to_thread(_user_constraints, store)
    return InventoryItemResponse.model_validate(_enrich_item(item, constraints))


@router.post("/items/{item_id}/consume", response_model=InventoryItemResponse)
async def consume_item(
    item_id: int,
    body: Optional[PartialConsumeRequest] = None,
    store: Store = Depends(get_store),
):
    """Consume an inventory item fully or partially.

    When body.quantity is provided, decrements by that amount and only marks
    status=consumed when quantity reaches zero. Omit body to consume all.
    """
    from datetime import datetime, timezone
    now = datetime.now(timezone.utc).isoformat()
    if body is not None:
        item = await asyncio.to_thread(
            store.partial_consume_item, item_id, body.quantity, now
        )
    else:
        item = await asyncio.to_thread(
            store.update_inventory_item,
            item_id,
            status="consumed",
            consumed_at=now,
        )
    if not item:
        raise HTTPException(status_code=404, detail="Inventory item not found")
    constraints = await asyncio.to_thread(_user_constraints, store)
    return InventoryItemResponse.model_validate(_enrich_item(item, constraints))


@router.post("/items/{item_id}/discard", response_model=InventoryItemResponse)
async def discard_item(
    item_id: int,
    body: DiscardRequest = DiscardRequest(),
    store: Store = Depends(get_store),
):
    """Mark an item as discarded (not used, spoiled, etc).

    Optional reason field accepts free text or a preset label
    ('not used', 'spoiled', 'excess', 'other').
    """
    from datetime import datetime, timezone
    item = await asyncio.to_thread(
        store.update_inventory_item,
        item_id,
        status="discarded",
        consumed_at=datetime.now(timezone.utc).isoformat(),
        disposal_reason=body.reason,
    )
    if not item:
        raise HTTPException(status_code=404, detail="Inventory item not found")
    constraints = await asyncio.to_thread(_user_constraints, store)
    return InventoryItemResponse.model_validate(_enrich_item(item, constraints))


@router.delete("/items/{item_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_inventory_item(item_id: int, store: Store = Depends(get_store)):
    existing = await asyncio.to_thread(store.get_inventory_item, item_id)
    if not existing:
        raise HTTPException(status_code=404, detail="Inventory item not found")
    await asyncio.to_thread(
        store.conn.execute, "DELETE FROM inventory_items WHERE id = ?", (item_id,)
    )
    store.conn.commit()


# ── Barcode scanning ──────────────────────────────────────────────────────────

class BarcodeScanTextRequest(BaseModel):
    barcode: str
    location: str = "pantry"
    quantity: float = 1.0
    auto_add_to_inventory: bool = True


def _captured_to_product_info(row: dict) -> dict:
    """Convert a captured_products row to the product_info dict shape used by
    the barcode scan flow (mirrors what OpenFoodFactsService returns)."""
    macros: dict = {}
    for field in ("calories", "fat_g", "saturated_fat_g", "carbs_g", "sugar_g",
                  "fiber_g", "protein_g", "sodium_mg", "serving_size_g"):
        if row.get(field) is not None:
            macros[field] = row[field]
    return {
        "name": row.get("product_name") or row.get("barcode", "Unknown Product"),
        "brand": row.get("brand"),
        "category": None,
        "nutrition_data": macros,
        "ingredient_names": row.get("ingredient_names") or [],
        "allergens": row.get("allergens") or [],
        "source": "visual_capture",
    }


def _gap_message(tier: str, has_visual_capture: bool) -> str:
    if has_visual_capture:
        return "We couldn't find this product. Photograph the nutrition label to add it."
    return "Not found in any product database — add manually"


@router.post("/scan/text", response_model=BarcodeScanResponse)
async def scan_barcode_text(
    body: BarcodeScanTextRequest,
    store: Store = Depends(get_store),
    session: CloudUser = Depends(get_session),
):
    """Scan a barcode from a text string (e.g. from a hardware scanner or manual entry)."""
    log.info("scan auth=%s tier=%s barcode=%r", _auth_label(session.user_id), session.tier, body.barcode)
    from app.services.expiration_predictor import ExpirationPredictor
    from app.services.openfoodfacts import OpenFoodFactsService
    from app.tiers import can_use

    predictor = ExpirationPredictor()
    has_visual_capture = can_use("visual_label_capture", session.tier, session.has_byok)

    # 1. Check local captured-products cache before hitting FDC/OFF
    cached = await asyncio.to_thread(store.get_captured_product, body.barcode)
    if cached and cached.get("confirmed_by_user"):
        product_info: dict | None = _captured_to_product_info(cached)
        product_source = "visual_capture"
    else:
        off = OpenFoodFactsService()
        product_info = await off.lookup_product(body.barcode)
        product_source = "openfoodfacts"

    inventory_item = None

    if product_info and body.auto_add_to_inventory:
        product, _ = await asyncio.to_thread(
            store.get_or_create_product,
            product_info.get("name", body.barcode),
            body.barcode,
            brand=product_info.get("brand"),
            category=product_info.get("category"),
            nutrition_data=product_info.get("nutrition_data", {}),
            source=product_source,
            source_data=product_info,
        )
        exp = predictor.predict_expiration(
            product_info.get("category", ""),
            body.location,
            product_name=product_info.get("name", body.barcode),
            tier=session.tier,
            has_byok=session.has_byok,
        )
        # Use OFFs pack size when detected; caller-supplied quantity is a fallback
        resolved_qty = product_info.get("pack_quantity") or body.quantity
        resolved_unit = product_info.get("pack_unit") or "count"
        inventory_item = await asyncio.to_thread(
            store.add_inventory_item,
            product["id"], body.location,
            quantity=resolved_qty,
            unit=resolved_unit,
            expiration_date=str(exp) if exp else None,
            source="barcode_scan",
        )
        result_product = ProductResponse.model_validate(product)
    else:
        result_product = None

    product_found = product_info is not None
    needs_capture = not product_found and has_visual_capture
    return BarcodeScanResponse(
        success=True,
        barcodes_found=1,
        results=[{
            "barcode": body.barcode,
            "barcode_type": "text",
            "product": result_product,
            "inventory_item": InventoryItemResponse.model_validate(inventory_item) if inventory_item else None,
            "added_to_inventory": inventory_item is not None,
            "needs_manual_entry": not product_found and not needs_capture,
            "needs_visual_capture": needs_capture,
            "message": "Added to inventory" if inventory_item else _gap_message(session.tier, needs_capture),
        }],
        message="Barcode processed",
    )


@router.post("/scan", response_model=BarcodeScanResponse)
async def scan_barcode_image(
    file: UploadFile = File(...),
    auto_add_to_inventory: bool = Form(True),
    location: str = Form("pantry"),
    quantity: float = Form(1.0),
    store: Store = Depends(get_store),
    session: CloudUser = Depends(get_session),
):
    """Scan a barcode from an uploaded image. Requires Phase 2 scanner integration."""
    log.info("scan_image auth=%s tier=%s", _auth_label(session.user_id), session.tier)
    from app.tiers import can_use
    has_visual_capture = can_use("visual_label_capture", session.tier, session.has_byok)

    temp_dir = Path("/tmp/kiwi_barcode_scans")
    temp_dir.mkdir(parents=True, exist_ok=True)
    temp_file = temp_dir / f"{uuid.uuid4()}_{file.filename}"
    try:
        async with aiofiles.open(temp_file, "wb") as f:
            await f.write(await file.read())
        from app.services.barcode_scanner import BarcodeScanner
        from app.services.expiration_predictor import ExpirationPredictor
        from app.services.openfoodfacts import OpenFoodFactsService

        image_bytes = temp_file.read_bytes()
        barcodes = await asyncio.to_thread(BarcodeScanner().scan_from_bytes, image_bytes)
        if not barcodes:
            return BarcodeScanResponse(
                success=False, barcodes_found=0, results=[],
                message="No barcodes detected in image"
            )

        off = OpenFoodFactsService()
        predictor = ExpirationPredictor()
        results = []
        for bc in barcodes:
            code = bc["data"]

            # Check local visual-capture cache before hitting FDC/OFF
            cached = await asyncio.to_thread(store.get_captured_product, code)
            if cached and cached.get("confirmed_by_user"):
                product_info: dict | None = _captured_to_product_info(cached)
                product_source = "visual_capture"
            else:
                product_info = await off.lookup_product(code)
                product_source = "openfoodfacts"

            db_product = None
            inventory_item = None
            if product_info:
                db_product, _ = await asyncio.to_thread(
                    store.get_or_create_product,
                    product_info.get("name", code),
                    code,
                    brand=product_info.get("brand"),
                    category=product_info.get("category"),
                    nutrition_data=product_info.get("nutrition_data", {}),
                    source=product_source,
                    source_data=product_info,
                )
                if auto_add_to_inventory:
                    exp = predictor.predict_expiration(
                        product_info.get("category", ""),
                        location,
                        product_name=product_info.get("name", code),
                        tier=session.tier,
                        has_byok=session.has_byok,
                    )
                    resolved_qty = product_info.get("pack_quantity") or quantity
                    resolved_unit = product_info.get("pack_unit") or "count"
                    inventory_item = await asyncio.to_thread(
                        store.add_inventory_item,
                        db_product["id"], location,
                        quantity=resolved_qty,
                        unit=resolved_unit,
                        expiration_date=str(exp) if exp else None,
                        source="barcode_scan",
                    )
            product_found = db_product is not None
            needs_capture = not product_found and has_visual_capture
            results.append({
                "barcode": code,
                "barcode_type": bc.get("type", "unknown"),
                "product": ProductResponse.model_validate(db_product) if db_product else None,
                "inventory_item": InventoryItemResponse.model_validate(inventory_item) if inventory_item else None,
                "added_to_inventory": inventory_item is not None,
                "needs_manual_entry": not product_found and not needs_capture,
                "needs_visual_capture": needs_capture,
                "message": "Added to inventory" if inventory_item else _gap_message(session.tier, needs_capture),
            })
        return BarcodeScanResponse(
            success=True, barcodes_found=len(barcodes), results=results,
            message=f"Processed {len(barcodes)} barcode(s)"
        )
    finally:
        if temp_file.exists():
            temp_file.unlink()


# ── Visual label capture (kiwi#79) ────────────────────────────────────────────

@router.post("/scan/label-capture")
async def capture_nutrition_label(
    file: UploadFile = File(...),
    barcode: str = Form(...),
    store: Store = Depends(get_store),
    session: CloudUser = Depends(get_session),
):
    """Photograph a nutrition label for an unenriched product (paid tier).

    Sends the image to the vision model and returns structured nutrition data
    for user review.  Fields extracted with confidence < 0.7 should be
    highlighted in amber in the UI.
    """
    from app.models.schemas.label_capture import LabelCaptureResponse
    from app.services.label_capture import extract_label
    from app.services.label_capture import needs_review as _needs_review
    from app.tiers import can_use

    if not can_use("visual_label_capture", session.tier, session.has_byok):
        raise HTTPException(status_code=403, detail="Visual label capture requires a Paid tier or higher.")
    log.info("label_capture tier=%s barcode=%r", session.tier, barcode)

    image_bytes = await file.read()
    extraction = await asyncio.to_thread(extract_label, image_bytes)

    return LabelCaptureResponse(
        barcode=barcode,
        product_name=extraction.get("product_name"),
        brand=extraction.get("brand"),
        serving_size_g=extraction.get("serving_size_g"),
        calories=extraction.get("calories"),
        fat_g=extraction.get("fat_g"),
        saturated_fat_g=extraction.get("saturated_fat_g"),
        carbs_g=extraction.get("carbs_g"),
        sugar_g=extraction.get("sugar_g"),
        fiber_g=extraction.get("fiber_g"),
        protein_g=extraction.get("protein_g"),
        sodium_mg=extraction.get("sodium_mg"),
        ingredient_names=extraction.get("ingredient_names") or [],
        allergens=extraction.get("allergens") or [],
        confidence=extraction.get("confidence", 0.0),
        needs_review=_needs_review(extraction),
    )


@router.post("/scan/label-confirm")
async def confirm_nutrition_label(
    body: LabelConfirmRequest,
    store: Store = Depends(get_store),
    session: CloudUser = Depends(get_session),
):
    """Confirm and save a user-reviewed label extraction.

    Saves the product to the local cache so future scans of the same barcode
    resolve instantly without another capture.  Optionally adds the item to
    the user's inventory.
    """
    from app.models.schemas.label_capture import LabelConfirmResponse
    from app.services.expiration_predictor import ExpirationPredictor
    from app.tiers import can_use

    if not can_use("visual_label_capture", session.tier, session.has_byok):
        raise HTTPException(status_code=403, detail="Visual label capture requires a Paid tier or higher.")
    log.info("label_confirm tier=%s barcode=%r", session.tier, body.barcode)

    # Persist to local visual-capture cache
    await asyncio.to_thread(
        store.save_captured_product,
        body.barcode,
        product_name=body.product_name,
        brand=body.brand,
        serving_size_g=body.serving_size_g,
        calories=body.calories,
        fat_g=body.fat_g,
        saturated_fat_g=body.saturated_fat_g,
        carbs_g=body.carbs_g,
        sugar_g=body.sugar_g,
        fiber_g=body.fiber_g,
        protein_g=body.protein_g,
        sodium_mg=body.sodium_mg,
        ingredient_names=body.ingredient_names,
        allergens=body.allergens,
        confidence=body.confidence,
        confirmed_by_user=True,
    )

    product_id: int | None = None
    inventory_item_id: int | None = None

    if body.auto_add:
        predictor = ExpirationPredictor()
        nutrition = {}
        for field in ("calories", "fat_g", "saturated_fat_g", "carbs_g", "sugar_g",
                      "fiber_g", "protein_g", "sodium_mg", "serving_size_g"):
            val = getattr(body, field, None)
            if val is not None:
                nutrition[field] = val

        product, _ = await asyncio.to_thread(
            store.get_or_create_product,
            body.product_name or body.barcode,
            body.barcode,
            brand=body.brand,
            category=None,
            nutrition_data=nutrition,
            source="visual_capture",
            source_data={},
        )
        product_id = product["id"]

        exp = predictor.predict_expiration(
            "",
            body.location,
            product_name=body.product_name or body.barcode,
            tier=session.tier,
            has_byok=session.has_byok,
        )
        inv_item = await asyncio.to_thread(
            store.add_inventory_item,
            product_id, body.location,
            quantity=body.quantity,
            unit="count",
            expiration_date=str(exp) if exp else None,
            source="visual_capture",
        )
        inventory_item_id = inv_item["id"]

    return LabelConfirmResponse(
        ok=True,
        barcode=body.barcode,
        product_id=product_id,
        inventory_item_id=inventory_item_id,
        message="Product saved" + (" and added to inventory" if body.auto_add else ""),
    )


# ── Tags ──────────────────────────────────────────────────────────────────────

@router.post("/tags", response_model=TagResponse, status_code=status.HTTP_201_CREATED)
async def create_tag(body: TagCreate, store: Store = Depends(get_store)):
    cur = await asyncio.to_thread(
        store.conn.execute,
        "INSERT INTO tags (name, slug, description, color, category) VALUES (?,?,?,?,?) RETURNING *",
        (body.name, body.slug, body.description, body.color, body.category),
    )
    store.conn.commit()
    import sqlite3
    store.conn.row_factory = sqlite3.Row
    return TagResponse.model_validate(store._row_to_dict(cur.fetchone()))


@router.get("/tags", response_model=List[TagResponse])
async def list_tags(
    category: Optional[str] = None, store: Store = Depends(get_store)
):
    if category:
        tags = await asyncio.to_thread(
            store._fetch_all, "SELECT * FROM tags WHERE category = ? ORDER BY name", (category,)
        )
    else:
        tags = await asyncio.to_thread(
            store._fetch_all, "SELECT * FROM tags ORDER BY name"
        )
    return [TagResponse.model_validate(t) for t in tags]


# ── Stats ─────────────────────────────────────────────────────────────────────

@router.post("/recalculate-expiry")
async def recalculate_expiry(
    session: CloudUser = Depends(get_session),
    store: Store = Depends(get_store),
) -> dict:
    """Re-run the expiration predictor over all available inventory items.

    Uses each item's stored purchase_date and current location. Safe to call
    multiple times — idempotent per session.
    """
    def _run(s: Store) -> tuple[int, int]:
        return s.recalculate_expiry(tier=session.tier, has_byok=session.has_byok)

    updated, skipped = await asyncio.to_thread(_run, store)
    return {"updated": updated, "skipped": skipped}


@router.get("/stats", response_model=InventoryStats)
async def get_inventory_stats(store: Store = Depends(get_store)):
    def _stats():
        rows = store._fetch_all(
            """SELECT status, location, COUNT(*) as cnt
               FROM inventory_items GROUP BY status, location"""
        )
        total = sum(r["cnt"] for r in rows)
        available = sum(r["cnt"] for r in rows if r["status"] == "available")
        expired = sum(r["cnt"] for r in rows if r["status"] == "expired")
        expiring = len(store.expiring_soon(7))
        locations = {}
        for r in rows:
            if r["status"] == "available":
                locations[r["location"]] = locations.get(r["location"], 0) + r["cnt"]
        return {
            "total_items": total,
            "available_items": available,
            "expiring_soon": expiring,
            "expired_items": expired,
            "locations": locations,
        }
    return InventoryStats.model_validate(await asyncio.to_thread(_stats))
