import os
import uuid
import json
from fastapi import APIRouter, Depends, HTTPException, status, UploadFile, File
from pydantic import BaseModel
from sqlalchemy.orm import Session
from typing import List

from database import get_db
from models.product import Product, Category, ProductOption, ProductQuickOption, ProductIngredient, ProductPreferenceSet, ProductPreferenceChoice, ProductModifierGroup
from models.prep_zone import PrepZone
from models.order import OrderItem
from models.user import User
from schemas.product import (
    ProductCreate, ProductUpdate, ProductOut, ProductReorderItem,
    CategoryCreate, CategoryUpdate, CategoryOut, CategoryReorderItem,
    SubcategoryReorderItem, ParentGeneralReorderItem,
    PreferenceSetCreate, ProductQuickOptionCreate,
    CategoryReparentRequest, ModifierGroupCreate, ModifierGroupOut,
)
from routers.deps import get_current_user, require_menu_manager
from services.sse_bus import broadcast_sync

router = APIRouter()


def _broadcast_products_changed():
    broadcast_sync("products_changed", {})

IMAGE_DIR = "/app/data/product_images"


def _replace_modifier_groups(db, product, groups):
    """Recreate modifier groups and return a list of created DB objects (index-aligned)."""
    for g in product.modifier_groups:
        db.delete(g)
    db.flush()
    created = []
    for i, g in enumerate(groups):
        new_g = ProductModifierGroup(
            product_id=product.id,
            modifier_type=g.modifier_type,
            name=g.name,
            sort_order=i,
        )
        db.add(new_g)
        db.flush()
        created.append(new_g)
    return created


def _replace_quick_options(db, product, quick_options):
    for qo in product.quick_options:
        db.delete(qo)
    db.flush()
    for i, qo in enumerate(quick_options):
        db.add(ProductQuickOption(
            product_id=product.id,
            name=qo.name,
            price=qo.price,
            allow_multiple=qo.allow_multiple,
            sort_order=qo.sort_order if qo.sort_order else i,
            is_favorite=qo.is_favorite,
            favorite_sort_order=qo.favorite_sort_order,
            is_compact=qo.is_compact,
        ))


def _replace_options(db, product, options, group_id_map=None):
    for opt in product.options:
        db.delete(opt)
    db.flush()
    for opt in options:
        sub_json = json.dumps([s.model_dump() for s in opt.sub_choices]) if opt.sub_choices else None
        resolved_group = group_id_map[opt.group_id] if (group_id_map and opt.group_id is not None and opt.group_id < len(group_id_map)) else None
        db.add(ProductOption(
            product_id=product.id,
            name=opt.name,
            extra_cost=opt.extra_cost,
            allow_multiple=opt.allow_multiple,
            multi_select=opt.multi_select,
            sub_choices=sub_json,
            is_favorite=opt.is_favorite,
            favorite_sort_order=opt.favorite_sort_order,
            is_compact=opt.is_compact,
            group_id=resolved_group,
        ))


def _replace_ingredients(db, product, ingredients, group_id_map=None):
    for ing in product.ingredients:
        db.delete(ing)
    db.flush()
    for ing in ingredients:
        resolved_group = group_id_map[ing.group_id] if (group_id_map and ing.group_id is not None and ing.group_id < len(group_id_map)) else None
        db.add(ProductIngredient(product_id=product.id, **ing.model_dump(exclude={'group_id'}), group_id=resolved_group))


def _replace_preference_sets(db, product, sets: List[PreferenceSetCreate], group_id_map=None):
    for ps in product.preference_sets:
        db.delete(ps)
    db.flush()
    for ps in sets:
        shared_json = json.dumps(ps.shared_subset.model_dump()) if ps.shared_subset else None
        resolved_group = group_id_map[ps.group_id] if (group_id_map and ps.group_id is not None and ps.group_id < len(group_id_map)) else None
        new_set = ProductPreferenceSet(
            product_id=product.id,
            name=ps.name,
            shared_subset=shared_json,
            is_favorite=ps.is_favorite,
            favorite_sort_order=ps.favorite_sort_order,
            group_id=resolved_group,
            allow_multi_select=ps.allow_multi_select,
            allow_choice_quantity=ps.allow_choice_quantity,
        )
        db.add(new_set)
        db.flush()
        created_choices = []
        for ch in ps.choices:
            sub_json = json.dumps([s.model_dump() for s in ch.sub_choices]) if ch.sub_choices else None
            choice = ProductPreferenceChoice(
                set_id=new_set.id,
                name=ch.name,
                extra_cost=ch.extra_cost,
                sub_choices=sub_json,
                disables_subset=ch.disables_subset,
                is_compact=ch.is_compact,
            )
            db.add(choice)
            db.flush()
            created_choices.append(choice)
        if ps.default_choice_index is not None and 0 <= ps.default_choice_index < len(created_choices):
            new_set.default_choice_id = created_choices[ps.default_choice_index].id


# ── Categories ────────────────────────────────────────────────────────────────

@router.get("/tags")
def list_all_tags(db: Session = Depends(get_db), user: User = Depends(get_current_user)):
    """Return sorted unique tag strings across all products."""
    rows = db.query(Product.tags).filter(Product.tags != None, Product.tags != "[]").all()
    tag_set = set()
    for (tags_json,) in rows:
        try:
            tags = json.loads(tags_json) if tags_json else []
            tag_set.update(tags)
        except Exception:
            pass
    return sorted(tag_set)


@router.get("/categories", response_model=List[CategoryOut])
def list_categories(db: Session = Depends(get_db), user: User = Depends(get_current_user)):
    return db.query(Category).order_by(Category.sort_order).all()


@router.post("/categories", response_model=CategoryOut, status_code=status.HTTP_201_CREATED)
def create_category(body: CategoryCreate, db: Session = Depends(get_db), user: User = Depends(require_menu_manager)):
    # sort_order is among siblings (same parent_id level)
    sibling_count = db.query(Category).filter(Category.parent_id == body.parent_id).count()
    cat = Category(
        name=body.name,
        color=body.color,
        sort_order=sibling_count,
        parent_id=body.parent_id,
        general_sort_order=body.general_sort_order,
    )
    db.add(cat)
    db.commit()
    db.refresh(cat)
    _broadcast_products_changed()
    return cat


@router.put("/categories/reorder", status_code=status.HTTP_204_NO_CONTENT)
def reorder_categories(items: List[CategoryReorderItem], db: Session = Depends(get_db), user: User = Depends(require_menu_manager)):
    for item in items:
        cat = db.query(Category).filter(Category.id == item.id).first()
        if cat:
            cat.sort_order = item.sort_order
    db.commit()
    _broadcast_products_changed()


@router.put("/categories/reorder-subcategories", status_code=status.HTTP_204_NO_CONTENT)
def reorder_subcategories(items: List[SubcategoryReorderItem], db: Session = Depends(get_db), user: User = Depends(require_menu_manager)):
    """Reorder sub-categories within their parent (sort_order among siblings)."""
    for item in items:
        cat = db.query(Category).filter(Category.id == item.id).first()
        if cat:
            cat.sort_order = item.sort_order
    db.commit()
    _broadcast_products_changed()


@router.put("/categories/reorder-general", status_code=status.HTTP_204_NO_CONTENT)
def reorder_general(items: List[ParentGeneralReorderItem], db: Session = Depends(get_db), user: User = Depends(require_menu_manager)):
    """Update general_sort_order on parent categories (position of the General group)."""
    for item in items:
        cat = db.query(Category).filter(Category.id == item.id).first()
        if cat:
            cat.general_sort_order = item.general_sort_order
    db.commit()
    _broadcast_products_changed()


@router.put("/categories/{category_id}/reparent", response_model=CategoryOut)
def reparent_category(category_id: int, body: CategoryReparentRequest, db: Session = Depends(get_db), user: User = Depends(require_menu_manager)):
    """Move a category to a new parent (or promote to top-level if parent_id is null).
    All products assigned to this category follow it automatically (no product updates needed).
    """
    cat = db.query(Category).filter(Category.id == category_id).first()
    if not cat:
        raise HTTPException(status_code=404, detail="Category not found")
    if body.parent_id is not None:
        new_parent = db.query(Category).filter(Category.id == body.parent_id).first()
        if not new_parent:
            raise HTTPException(status_code=404, detail="Target parent category not found")
        if new_parent.parent_id is not None:
            raise HTTPException(status_code=400, detail="Cannot nest more than two levels deep")
        if body.parent_id == category_id:
            raise HTTPException(status_code=400, detail="A category cannot be its own parent")
    # If cat currently has children and is being made a sub, block it
    has_children = db.query(Category).filter(Category.parent_id == category_id).count() > 0
    if has_children and body.parent_id is not None:
        raise HTTPException(status_code=400, detail="Cannot nest a category that has subcategories")
    # Assign new sort_order at the end of the destination level
    sibling_count = db.query(Category).filter(Category.parent_id == body.parent_id).count()
    cat.parent_id = body.parent_id
    cat.sort_order = sibling_count
    db.commit()
    db.refresh(cat)
    _broadcast_products_changed()
    return cat


@router.put("/categories/{category_id}", response_model=CategoryOut)
def update_category(category_id: int, body: CategoryUpdate, db: Session = Depends(get_db), user: User = Depends(require_menu_manager)):
    cat = db.query(Category).filter(Category.id == category_id).first()
    if not cat:
        raise HTTPException(status_code=404, detail="Category not found")
    for field, value in body.model_dump(exclude_none=True).items():
        setattr(cat, field, value)
    db.commit()
    db.refresh(cat)
    _broadcast_products_changed()
    return cat


@router.delete("/categories/{category_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_category(category_id: int, db: Session = Depends(get_db), user: User = Depends(require_menu_manager)):
    cat = db.query(Category).filter(Category.id == category_id).first()
    if not cat:
        raise HTTPException(status_code=404, detail="Category not found")
    db.delete(cat)
    db.commit()
    _broadcast_products_changed()


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

@router.get("/", response_model=List[ProductOut])
def list_products(all: bool = False, db: Session = Depends(get_db), user: User = Depends(get_current_user)):
    q = db.query(Product)
    has_dashboard = user.role in ("superadmin", "manager") or getattr(user, "perm_access_dashboard", False)
    if not all or not has_dashboard:
        # Waiters only see active, available products
        q = q.filter(Product.is_available == True, Product.lifecycle_status == "active")
    return q.order_by(Product.sort_order, Product.id).all()


class BulkPrepZoneBody(BaseModel):
    product_ids: List[int]
    prep_zone_ids: List[int]


@router.post("/bulk-prep-zones", status_code=status.HTTP_204_NO_CONTENT)
def bulk_assign_prep_zones(body: BulkPrepZoneBody, db: Session = Depends(get_db), user: User = Depends(require_menu_manager)):
    """Replace the prep zone assignments for a list of products at once."""
    zones = db.query(PrepZone).filter(PrepZone.id.in_(body.prep_zone_ids)).all()
    products = db.query(Product).filter(Product.id.in_(body.product_ids)).all()
    for p in products:
        p.prep_zones = zones
    db.commit()
    _broadcast_products_changed()


@router.put("/reorder", status_code=status.HTTP_204_NO_CONTENT)
def reorder_products(items: List[ProductReorderItem], db: Session = Depends(get_db), user: User = Depends(require_menu_manager)):
    for item in items:
        product = db.query(Product).filter(Product.id == item.id).first()
        if product:
            product.sort_order = item.sort_order
    db.commit()
    _broadcast_products_changed()


@router.post("/", response_model=ProductOut, status_code=status.HTTP_201_CREATED)
def create_product(body: ProductCreate, db: Session = Depends(get_db), user: User = Depends(require_menu_manager)):
    data = body.model_dump(exclude={"quick_options", "options", "ingredients", "preference_sets", "cost_breakdown", "prep_zone_ids", "tags"})
    if data.get("sort_order") == 0:
        data["sort_order"] = db.query(Product).count()
    if body.cost_breakdown is not None:
        data["cost_breakdown"] = json.dumps([item.model_dump() for item in body.cost_breakdown])
    data["tags"] = json.dumps(body.tags) if body.tags is not None else None
    product = Product(**data)
    db.add(product)
    db.flush()
    # Assign prep zones
    if body.prep_zone_ids:
        zones = db.query(PrepZone).filter(PrepZone.id.in_(body.prep_zone_ids)).all()
        product.prep_zones = zones
    for i, qo in enumerate(body.quick_options):
        db.add(ProductQuickOption(
            product_id=product.id,
            name=qo.name,
            price=qo.price,
            allow_multiple=qo.allow_multiple,
            sort_order=qo.sort_order if qo.sort_order else i,
            is_favorite=qo.is_favorite,
            favorite_sort_order=qo.favorite_sort_order,
            is_compact=qo.is_compact,
        ))
    created_groups = _replace_modifier_groups(db, product, body.modifier_groups)
    group_id_map = [g.id for g in created_groups]
    for opt in body.options:
        sub_json = json.dumps([s.model_dump() for s in opt.sub_choices]) if opt.sub_choices else None
        resolved_group = group_id_map[opt.group_id] if (opt.group_id is not None and opt.group_id < len(group_id_map)) else None
        db.add(ProductOption(
            product_id=product.id,
            name=opt.name,
            extra_cost=opt.extra_cost,
            allow_multiple=opt.allow_multiple,
            multi_select=opt.multi_select,
            sub_choices=sub_json,
            is_favorite=opt.is_favorite,
            favorite_sort_order=opt.favorite_sort_order,
            is_compact=opt.is_compact,
            group_id=resolved_group,
        ))
    for ing in body.ingredients:
        resolved_group = group_id_map[ing.group_id] if (ing.group_id is not None and ing.group_id < len(group_id_map)) else None
        db.add(ProductIngredient(product_id=product.id, **ing.model_dump(exclude={'group_id'}), group_id=resolved_group))
    _replace_preference_sets(db, product, body.preference_sets, group_id_map)
    db.commit()
    db.refresh(product)
    _broadcast_products_changed()
    return product


@router.put("/{product_id}", response_model=ProductOut)
def update_product(product_id: int, body: ProductUpdate, db: Session = Depends(get_db), user: User = Depends(require_menu_manager)):
    product = db.query(Product).filter(Product.id == product_id).first()
    if not product:
        raise HTTPException(status_code=404, detail="Product not found")
    scalar_fields = body.model_dump(
        exclude_none=True,
        exclude={"quick_options", "options", "ingredients", "preference_sets", "cost_breakdown", "prep_zone_ids", "tags", "modifier_groups"},
    )
    for field, value in scalar_fields.items():
        setattr(product, field, value)
    # Always clear legacy per-product printer — routing is now via prep zones only
    product.printer_zone_id = None
    # Update prep zones if provided
    if body.prep_zone_ids is not None:
        zones = db.query(PrepZone).filter(PrepZone.id.in_(body.prep_zone_ids)).all()
        product.prep_zones = zones
    # cost_breakdown is a list of objects — serialize to JSON for storage
    if body.cost_breakdown is not None:
        product.cost_breakdown = json.dumps([item.model_dump() for item in body.cost_breakdown])
    elif "cost_breakdown" in body.model_fields_set:
        # explicitly set to null — clear it
        product.cost_breakdown = None
    # tags is a list of strings — serialize to JSON for storage
    if body.tags is not None:
        product.tags = json.dumps(body.tags)
    elif "tags" in body.model_fields_set:
        product.tags = None
    if body.quick_options is not None:
        _replace_quick_options(db, product, body.quick_options)
    # Modifier groups must be recreated before items so we have the ID map
    group_id_map = None
    if body.modifier_groups is not None:
        created_groups = _replace_modifier_groups(db, product, body.modifier_groups)
        group_id_map = [g.id for g in created_groups]
    if body.options is not None:
        _replace_options(db, product, body.options, group_id_map)
    if body.ingredients is not None:
        _replace_ingredients(db, product, body.ingredients, group_id_map)
    if body.preference_sets is not None:
        _replace_preference_sets(db, product, body.preference_sets, group_id_map)
    db.commit()
    db.refresh(product)
    _broadcast_products_changed()
    return product


@router.post("/{product_id}/image", response_model=ProductOut)
async def upload_product_image(product_id: int, file: UploadFile = File(...), db: Session = Depends(get_db), user: User = Depends(require_menu_manager)):
    product = db.query(Product).filter(Product.id == product_id).first()
    if not product:
        raise HTTPException(status_code=404, detail="Product not found")

    if not file.content_type.startswith("image/"):
        raise HTTPException(status_code=400, detail="File must be an image")

    os.makedirs(IMAGE_DIR, exist_ok=True)

    if product.image_url:
        old_path = os.path.join(IMAGE_DIR, os.path.basename(product.image_url))
        if os.path.exists(old_path):
            os.remove(old_path)

    ext = file.filename.rsplit(".", 1)[-1].lower() if "." in file.filename else "jpg"
    filename = f"{product_id}_{uuid.uuid4().hex[:8]}.{ext}"
    filepath = os.path.join(IMAGE_DIR, filename)

    contents = await file.read()
    with open(filepath, "wb") as f:
        f.write(contents)

    product.image_url = f"/static/product_images/{filename}"
    db.commit()
    db.refresh(product)
    _broadcast_products_changed()
    return product


# ── Modifier Groups ───────────────────────────────────────────────────────────

@router.get("/{product_id}/modifier-groups", response_model=List[ModifierGroupOut])
def list_modifier_groups(product_id: int, db: Session = Depends(get_db), user: User = Depends(get_current_user)):
    return db.query(ProductModifierGroup).filter(ProductModifierGroup.product_id == product_id).order_by(ProductModifierGroup.sort_order).all()


@router.post("/{product_id}/modifier-groups", response_model=ModifierGroupOut, status_code=status.HTTP_201_CREATED)
def create_modifier_group(product_id: int, body: ModifierGroupCreate, db: Session = Depends(get_db), user: User = Depends(require_menu_manager)):
    product = db.query(Product).filter(Product.id == product_id).first()
    if not product:
        raise HTTPException(status_code=404, detail="Product not found")
    g = ProductModifierGroup(product_id=product_id, modifier_type=body.modifier_type, name=body.name, sort_order=body.sort_order)
    db.add(g)
    db.commit()
    db.refresh(g)
    _broadcast_products_changed()
    return g


@router.put("/{product_id}/modifier-groups/{group_id}", response_model=ModifierGroupOut)
def update_modifier_group(product_id: int, group_id: int, body: ModifierGroupCreate, db: Session = Depends(get_db), user: User = Depends(require_menu_manager)):
    g = db.query(ProductModifierGroup).filter(ProductModifierGroup.id == group_id, ProductModifierGroup.product_id == product_id).first()
    if not g:
        raise HTTPException(status_code=404, detail="Group not found")
    g.name = body.name
    g.sort_order = body.sort_order
    db.commit()
    db.refresh(g)
    _broadcast_products_changed()
    return g


@router.delete("/{product_id}/modifier-groups/{group_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_modifier_group(product_id: int, group_id: int, db: Session = Depends(get_db), user: User = Depends(require_menu_manager)):
    g = db.query(ProductModifierGroup).filter(ProductModifierGroup.id == group_id, ProductModifierGroup.product_id == product_id).first()
    if not g:
        raise HTTPException(status_code=404, detail="Group not found")
    # Un-group all items in this group before deleting
    db.query(ProductOption).filter(ProductOption.group_id == group_id).update({"group_id": None})
    db.query(ProductIngredient).filter(ProductIngredient.group_id == group_id).update({"group_id": None})
    db.query(ProductPreferenceSet).filter(ProductPreferenceSet.group_id == group_id).update({"group_id": None})
    db.delete(g)
    db.commit()
    _broadcast_products_changed()


@router.delete("/{product_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_product(product_id: int, hard: bool = False, db: Session = Depends(get_db), user: User = Depends(require_menu_manager)):
    product = db.query(Product).filter(Product.id == product_id).first()
    if not product:
        raise HTTPException(status_code=404, detail="Product not found")
    if hard:
        has_orders = db.query(OrderItem).filter(OrderItem.product_id == product_id).first()
        if has_orders:
            raise HTTPException(
                status_code=400,
                detail="Cannot permanently delete a product that appears in past orders. Archive it instead."
            )
        db.delete(product)
    else:
        # If product has order history, archive it; otherwise hard delete
        has_orders = db.query(OrderItem).filter(OrderItem.product_id == product_id).first()
        if has_orders:
            product.lifecycle_status = "archived"
        else:
            db.delete(product)
    db.commit()
    _broadcast_products_changed()
