from fastapi import APIRouter, Depends, Query, HTTPException, UploadFile, File
from fastapi.responses import Response
from fastapi.responses import RedirectResponse
from typing import Optional
from pydantic import BaseModel

from auth.models import TokenPayload
from auth.dependencies import require_permission
from manufacturing.models import (
    BatchCreate, BatchResponse,
    DeviceInventoryItem, DeviceInventoryListResponse,
    DeviceStatusUpdate, DeviceAssign,
    ManufacturingStats,
)
from manufacturing import service
from manufacturing import audit
from shared.exceptions import NotFoundError
from shared.firebase import get_db as get_firestore


class LifecycleEntryPatch(BaseModel):
    index: int
    date: Optional[str] = None
    note: Optional[str] = None

class LifecycleEntryCreate(BaseModel):
    status_id: str
    date: Optional[str] = None
    note: Optional[str] = None

VALID_FLASH_ASSETS = {"bootloader.bin", "partitions.bin"}
VALID_HW_TYPES_MFG = {"vesper", "vesper_plus", "vesper_pro", "agnus", "agnus_mini", "chronos", "chronos_pro"}
# Bespoke UIDs are dynamic — we allow any non-empty slug that doesn't clash with
# a standard hw_type name. The flash-asset upload endpoint checks this below.

router = APIRouter(prefix="/api/manufacturing", tags=["manufacturing"])


@router.get("/stats", response_model=ManufacturingStats)
def get_stats(
    _user: TokenPayload = Depends(require_permission("manufacturing", "view")),
):
    return service.get_stats()


@router.get("/audit-log")
async def get_audit_log(
    limit: int = Query(20, ge=1, le=100),
    _user: TokenPayload = Depends(require_permission("manufacturing", "view")),
):
    entries = await audit.get_recent(limit=limit)
    return {"entries": entries}


@router.post("/batch", response_model=BatchResponse, status_code=201)
async def create_batch(
    body: BatchCreate,
    user: TokenPayload = Depends(require_permission("manufacturing", "add")),
):
    result = service.create_batch(body)
    await audit.log_action(
        admin_user=user.email,
        action="batch_created",
        detail={
            "batch_id": result.batch_id,
            "board_type": result.board_type,
            "board_version": result.board_version,
            "quantity": len(result.serial_numbers),
        },
    )
    return result


@router.get("/devices", response_model=DeviceInventoryListResponse)
def list_devices(
    status: Optional[str] = Query(None),
    hw_type: Optional[str] = Query(None),
    search: Optional[str] = Query(None),
    limit: int = Query(100, ge=1, le=500),
    offset: int = Query(0, ge=0),
    _user: TokenPayload = Depends(require_permission("manufacturing", "view")),
):
    items = service.list_devices(
        status=status,
        hw_type=hw_type,
        search=search,
        limit=limit,
        offset=offset,
    )
    return DeviceInventoryListResponse(devices=items, total=len(items))


@router.get("/devices/{sn}", response_model=DeviceInventoryItem)
def get_device(
    sn: str,
    _user: TokenPayload = Depends(require_permission("manufacturing", "view")),
):
    return service.get_device_by_sn(sn)


@router.get("/customers/search")
def search_customers(
    q: str = Query(""),
    _user: TokenPayload = Depends(require_permission("manufacturing", "view")),
):
    """Search CRM customers by name, email, phone, organization, or tags."""
    results = service.search_customers(q)
    return {"results": results}


@router.get("/customers/{customer_id}")
def get_customer(
    customer_id: str,
    _user: TokenPayload = Depends(require_permission("manufacturing", "view")),
):
    """Get a single CRM customer by ID."""
    db = get_firestore()
    doc = db.collection("crm_customers").document(customer_id).get()
    if not doc.exists:
        raise HTTPException(status_code=404, detail="Customer not found")
    data = doc.to_dict() or {}
    loc = data.get("location") or {}
    city = loc.get("city") if isinstance(loc, dict) else None
    return {
        "id": doc.id,
        "name": data.get("name") or "",
        "surname": data.get("surname") or "",
        "email": data.get("email") or "",
        "organization": data.get("organization") or "",
        "phone": data.get("phone") or "",
        "city": city or "",
    }


@router.patch("/devices/{sn}/status", response_model=DeviceInventoryItem)
async def update_status(
    sn: str,
    body: DeviceStatusUpdate,
    user: TokenPayload = Depends(require_permission("manufacturing", "edit")),
):
    # Guard: claimed requires at least one user in user_list
    # (allow if explicitly force_claimed=true, which the mfg UI sets after adding a user manually)
    if body.status.value == "claimed":
        db = get_firestore()
        docs = list(db.collection("devices").where("serial_number", "==", sn).limit(1).stream())
        if docs:
            data = docs[0].to_dict() or {}
            user_list = data.get("user_list", []) or []
            if not user_list and not getattr(body, "force_claimed", False):
                raise HTTPException(
                    status_code=400,
                    detail="Cannot set status to 'claimed': device has no users in user_list. "
                           "Assign a user first, then set to Claimed.",
                )

    # Guard: sold requires a customer assigned
    if body.status.value == "sold":
        db = get_firestore()
        docs = list(db.collection("devices").where("serial_number", "==", sn).limit(1).stream())
        if docs:
            data = docs[0].to_dict() or {}
            if not data.get("customer_id"):
                raise HTTPException(
                    status_code=400,
                    detail="Cannot set status to 'sold' without an assigned customer. "
                           "Use the 'Assign to Customer' action first.",
                )

    result = service.update_device_status(sn, body, set_by=user.email)
    await audit.log_action(
        admin_user=user.email,
        action="status_updated",
        serial_number=sn,
        detail={"status": body.status.value, "note": body.note},
    )
    return result


@router.patch("/devices/{sn}/lifecycle", response_model=DeviceInventoryItem)
async def patch_lifecycle_entry(
    sn: str,
    body: LifecycleEntryPatch,
    user: TokenPayload = Depends(require_permission("manufacturing", "edit")),
):
    """Edit the date and/or note of a lifecycle history entry by index."""
    db = get_firestore()
    docs = list(db.collection("devices").where("serial_number", "==", sn).limit(1).stream())
    if not docs:
        raise HTTPException(status_code=404, detail="Device not found")
    doc_ref = docs[0].reference
    data = docs[0].to_dict() or {}
    history = data.get("lifecycle_history") or []
    if body.index < 0 or body.index >= len(history):
        raise HTTPException(status_code=400, detail="Invalid lifecycle entry index")
    if body.date is not None:
        history[body.index]["date"] = body.date
    if body.note is not None:
        history[body.index]["note"] = body.note
    doc_ref.update({"lifecycle_history": history})
    from manufacturing.service import _doc_to_inventory_item
    return _doc_to_inventory_item(doc_ref.get())


@router.post("/devices/{sn}/lifecycle", response_model=DeviceInventoryItem, status_code=200)
async def create_lifecycle_entry(
    sn: str,
    body: LifecycleEntryCreate,
    user: TokenPayload = Depends(require_permission("manufacturing", "edit")),
):
    """Upsert a lifecycle history entry for the given status_id.

    If an entry for this status already exists it is overwritten in-place;
    otherwise a new entry is appended.  This prevents duplicate entries when
    a status is visited more than once (max one entry per status).
    """
    from datetime import datetime, timezone
    db = get_firestore()
    docs = list(db.collection("devices").where("serial_number", "==", sn).limit(1).stream())
    if not docs:
        raise HTTPException(status_code=404, detail="Device not found")
    doc_ref = docs[0].reference
    data = docs[0].to_dict() or {}
    history = list(data.get("lifecycle_history") or [])

    new_entry = {
        "status_id": body.status_id,
        "date": body.date or datetime.now(timezone.utc).isoformat(),
        "note": body.note,
        "set_by": user.email,
    }

    # Overwrite existing entry for this status if present, else append
    existing_idx = next(
        (i for i, e in enumerate(history) if e.get("status_id") == body.status_id),
        None,
    )
    if existing_idx is not None:
        history[existing_idx] = new_entry
    else:
        history.append(new_entry)

    doc_ref.update({"lifecycle_history": history})
    from manufacturing.service import _doc_to_inventory_item
    return _doc_to_inventory_item(doc_ref.get())


@router.delete("/devices/{sn}/lifecycle/{index}", response_model=DeviceInventoryItem)
async def delete_lifecycle_entry(
    sn: str,
    index: int,
    user: TokenPayload = Depends(require_permission("manufacturing", "edit")),
):
    """Delete a lifecycle history entry by index. Cannot delete the entry for the current status."""
    db = get_firestore()
    docs = list(db.collection("devices").where("serial_number", "==", sn).limit(1).stream())
    if not docs:
        raise HTTPException(status_code=404, detail="Device not found")
    doc_ref = docs[0].reference
    data = docs[0].to_dict() or {}
    history = data.get("lifecycle_history") or []
    if index < 0 or index >= len(history):
        raise HTTPException(status_code=400, detail="Invalid lifecycle entry index")
    current_status = data.get("mfg_status", "")
    if history[index].get("status_id") == current_status:
        raise HTTPException(status_code=400, detail="Cannot delete the entry for the current status. Change the status first.")
    history.pop(index)
    doc_ref.update({"lifecycle_history": history})
    from manufacturing.service import _doc_to_inventory_item
    return _doc_to_inventory_item(doc_ref.get())


@router.get("/devices/{sn}/nvs.bin")
async def download_nvs(
    sn: str,
    hw_type_override: Optional[str] = Query(None, description="Override hw_type written to NVS (for bespoke firmware)"),
    hw_revision_override: Optional[str] = Query(None, description="Override hw_revision written to NVS (for bespoke firmware)"),
    nvs_schema: Optional[str] = Query(None, description="NVS schema to use: 'legacy' or 'new' (default)"),
    user: TokenPayload = Depends(require_permission("manufacturing", "view")),
):
    binary = service.get_nvs_binary(sn, hw_type_override=hw_type_override, hw_revision_override=hw_revision_override, legacy=(nvs_schema == "legacy"))
    await audit.log_action(
        admin_user=user.email,
        action="device_flashed",
        serial_number=sn,
    )
    return Response(
        content=binary,
        media_type="application/octet-stream",
        headers={"Content-Disposition": f'attachment; filename="{sn}_nvs.bin"'},
    )


@router.post("/devices/{sn}/assign", response_model=DeviceInventoryItem)
async def assign_device(
    sn: str,
    body: DeviceAssign,
    user: TokenPayload = Depends(require_permission("manufacturing", "edit")),
):
    try:
        result = service.assign_device(sn, body)
    except NotFoundError as e:
        raise HTTPException(status_code=404, detail=str(e))
    await audit.log_action(
        admin_user=user.email,
        action="device_assigned",
        serial_number=sn,
        detail={"customer_id": body.customer_id},
    )
    return result


@router.delete("/devices/{sn}", status_code=204)
async def delete_device(
    sn: str,
    force: bool = Query(False, description="Required to delete sold/claimed devices"),
    user: TokenPayload = Depends(require_permission("manufacturing", "delete")),
):
    """Delete a device. Sold/claimed devices require force=true."""
    try:
        service.delete_device(sn, force=force)
    except NotFoundError:
        raise HTTPException(status_code=404, detail="Device not found")
    except PermissionError as e:
        raise HTTPException(status_code=403, detail=str(e))
    await audit.log_action(
        admin_user=user.email,
        action="device_deleted",
        serial_number=sn,
        detail={"force": force},
    )


@router.post("/devices/{sn}/email/manufactured", status_code=204)
async def send_manufactured_email(
    sn: str,
    user: TokenPayload = Depends(require_permission("manufacturing", "edit")),
):
    """Send the 'device manufactured' notification to the assigned customer's email."""
    db = get_firestore()
    docs = list(db.collection("devices").where("serial_number", "==", sn).limit(1).stream())
    if not docs:
        raise HTTPException(status_code=404, detail="Device not found")
    data = docs[0].to_dict() or {}
    customer_id = data.get("customer_id")
    if not customer_id:
        raise HTTPException(status_code=400, detail="No customer assigned to this device")
    customer_doc = db.collection("crm_customers").document(customer_id).get()
    if not customer_doc.exists:
        raise HTTPException(status_code=404, detail="Assigned customer not found")
    cdata = customer_doc.to_dict() or {}
    email = cdata.get("email")
    if not email:
        raise HTTPException(status_code=400, detail="Customer has no email address")
    name_parts = [cdata.get("name") or "", cdata.get("surname") or ""]
    customer_name = " ".join(p for p in name_parts if p).strip() or None
    hw_family = data.get("hw_family") or data.get("hw_type") or ""
    from utils.emails.device_mfged_mail import send_device_manufactured_email
    send_device_manufactured_email(
        customer_email=email,
        serial_number=sn,
        device_name=hw_family.replace("_", " ").title(),
        customer_name=customer_name,
    )
    await audit.log_action(
        admin_user=user.email,
        action="email_manufactured_sent",
        serial_number=sn,
        detail={"recipient": email},
    )


@router.post("/devices/{sn}/email/assigned", status_code=204)
async def send_assigned_email(
    sn: str,
    user: TokenPayload = Depends(require_permission("manufacturing", "edit")),
):
    """Send the 'device assigned / app instructions' email to the assigned user(s)."""
    db = get_firestore()
    docs = list(db.collection("devices").where("serial_number", "==", sn).limit(1).stream())
    if not docs:
        raise HTTPException(status_code=404, detail="Device not found")
    data = docs[0].to_dict() or {}
    user_list = data.get("user_list") or []
    if not user_list:
        raise HTTPException(status_code=400, detail="No users assigned to this device")
    hw_family = data.get("hw_family") or data.get("hw_type") or ""
    device_name = hw_family.replace("_", " ").title()
    from utils.emails.device_assigned_mail import send_device_assigned_email
    errors = []
    for uid in user_list:
        try:
            user_doc = db.collection("users").document(uid).get()
            if not user_doc.exists:
                continue
            udata = user_doc.to_dict() or {}
            email = udata.get("email")
            if not email:
                continue
            display_name = udata.get("display_name") or udata.get("name") or None
            send_device_assigned_email(
                user_email=email,
                serial_number=sn,
                device_name=device_name,
                user_name=display_name,
            )
        except Exception as exc:
            errors.append(str(exc))
    if errors:
        raise HTTPException(status_code=500, detail=f"Some emails failed: {'; '.join(errors)}")
    await audit.log_action(
        admin_user=user.email,
        action="email_assigned_sent",
        serial_number=sn,
        detail={"user_count": len(user_list)},
    )


@router.delete("/devices", status_code=200)
async def delete_unprovisioned(
    user: TokenPayload = Depends(require_permission("manufacturing", "delete")),
):
    """Delete all devices with status 'manufactured' (never provisioned)."""
    deleted = service.delete_unprovisioned_devices()
    await audit.log_action(
        admin_user=user.email,
        action="bulk_delete_unprovisioned",
        detail={"count": len(deleted), "serial_numbers": deleted},
    )
    return {"deleted": deleted, "count": len(deleted)}


@router.get("/devices/{sn}/firmware.bin")
def redirect_firmware(
    sn: str,
    _user: TokenPayload = Depends(require_permission("manufacturing", "view")),
):
    """Redirect to the latest stable firmware binary for this device's hw_type.
    Resolves to GET /api/firmware/{hw_type}/stable/{version}/firmware.bin.
    """
    url = service.get_firmware_url(sn)
    return RedirectResponse(url=url, status_code=302)


# ─────────────────────────────────────────────────────────────────────────────
# Flash assets — bootloader.bin and partitions.bin per hw_type
# These are the binaries that must be flashed at fixed addresses during full
# provisioning (0x1000 bootloader, 0x8000 partition table).
# They are NOT flashed during OTA updates — only during initial provisioning.
# Upload once per hw_type after each PlatformIO build that changes the layout.
# ─────────────────────────────────────────────────────────────────────────────

@router.get("/flash-assets")
def list_flash_assets(
    _user: TokenPayload = Depends(require_permission("manufacturing", "view")),
):
    """Return asset status for all known board types (and any discovered bespoke UIDs).

    Checks the filesystem directly — no database involved.
    Each entry contains: hw_type, bootloader (exists, size, uploaded_at), partitions (same), note.
    """
    return {"assets": service.list_flash_assets()}


@router.delete("/flash-assets/{hw_type}/{asset}", status_code=204)
async def delete_flash_asset(
    hw_type: str,
    asset: str,
    user: TokenPayload = Depends(require_permission("manufacturing", "delete")),
):
    """Delete a single flash asset file (bootloader.bin or partitions.bin)."""
    if asset not in VALID_FLASH_ASSETS:
        raise HTTPException(status_code=400, detail=f"Invalid asset. Must be one of: {', '.join(sorted(VALID_FLASH_ASSETS))}")
    try:
        service.delete_flash_asset(hw_type, asset)
    except NotFoundError as e:
        raise HTTPException(status_code=404, detail=str(e))
    await audit.log_action(
        admin_user=user.email,
        action="flash_asset_deleted",
        detail={"hw_type": hw_type, "asset": asset},
    )


class FlashAssetNoteBody(BaseModel):
    note: str


@router.put("/flash-assets/{hw_type}/note", status_code=204)
async def set_flash_asset_note(
    hw_type: str,
    body: FlashAssetNoteBody,
    _user: TokenPayload = Depends(require_permission("manufacturing", "edit")),
):
    """Save (or overwrite) the note for a hw_type's flash asset set.

    The note is stored as note.txt next to the binary files.
    Pass an empty string to clear the note.
    """
    service.set_flash_asset_note(hw_type, body.note)


@router.post("/flash-assets/{hw_type}/{asset}", status_code=204)
async def upload_flash_asset(
    hw_type: str,
    asset: str,
    file: UploadFile = File(...),
    _user: TokenPayload = Depends(require_permission("manufacturing", "add")),
):
    """Upload a bootloader.bin or partitions.bin for a given hw_type.

    These are build artifacts from PlatformIO (.pio/build/{env}/bootloader.bin
    and .pio/build/{env}/partitions.bin). Upload them once per hw_type after
    each PlatformIO build that changes the partition layout.
    """
    # hw_type can be a standard board type OR a bespoke UID (any non-empty slug)
    if not hw_type or len(hw_type) > 128:
        raise HTTPException(status_code=400, detail="Invalid hw_type/bespoke UID.")
    if asset not in VALID_FLASH_ASSETS:
        raise HTTPException(status_code=400, detail=f"Invalid asset. Must be one of: {', '.join(sorted(VALID_FLASH_ASSETS))}")
    data = await file.read()
    service.save_flash_asset(hw_type, asset, data)


@router.get("/devices/{sn}/bootloader.bin")
def download_bootloader(
    sn: str,
    hw_type_override: Optional[str] = Query(None, description="Override hw_type for flash asset lookup (for bespoke firmware)"),
    _user: TokenPayload = Depends(require_permission("manufacturing", "view")),
):
    """Return the bootloader.bin for this device's hw_type (flashed at 0x1000)."""
    item = service.get_device_by_sn(sn)
    hw_type = hw_type_override or item.hw_type
    try:
        data = service.get_flash_asset(hw_type, "bootloader.bin")
    except NotFoundError as e:
        raise HTTPException(status_code=404, detail=str(e))
    return Response(
        content=data,
        media_type="application/octet-stream",
        headers={"Content-Disposition": f'attachment; filename="bootloader_{hw_type}.bin"'},
    )


@router.get("/devices/{sn}/partitions.bin")
def download_partitions(
    sn: str,
    hw_type_override: Optional[str] = Query(None, description="Override hw_type for flash asset lookup (for bespoke firmware)"),
    _user: TokenPayload = Depends(require_permission("manufacturing", "view")),
):
    """Return the partitions.bin for this device's hw_type (flashed at 0x8000)."""
    item = service.get_device_by_sn(sn)
    hw_type = hw_type_override or item.hw_type
    try:
        data = service.get_flash_asset(hw_type, "partitions.bin")
    except NotFoundError as e:
        raise HTTPException(status_code=404, detail=str(e))
    return Response(
        content=data,
        media_type="application/octet-stream",
        headers={"Content-Disposition": f'attachment; filename="partitions_{hw_type}.bin"'},
    )
