import json
from datetime import datetime, timezone
from fastapi import APIRouter, Depends, HTTPException, status, BackgroundTasks
from tz import local_strftime


def _iso(dt) -> str | None:
    """Serialize a datetime to ISO 8601 with Z suffix so browsers parse it as UTC."""
    if dt is None:
        return None
    if dt.tzinfo is None:
        return dt.isoformat() + "Z"
    return dt.isoformat()
from sqlalchemy.orm import Session
from typing import List, Optional

from database import get_db
from models.order import Order, OrderItem, OrderWaiter, OrderAuditLog, PrintJob, OrderDiscount
from models.user import User, WaiterZone
from models.table import Table
from models.product import Product
from schemas.order import OrderCreate, OrderOut, OrderItemOut, AddItemsRequest, AddItemsResponse, PayItemsRequest, OfflinePaymentRequest, AssignWaiterRequest, OrderWaiterOut
from services.pricing import (
    OrderContext, resolve_price, evaluate_deals,
    build_active_price_group_ids, write_price_events,
    check_waiter_discount_limits, _system_round,
)
from pydantic import BaseModel

class PrintOrderRequest(BaseModel):
    printer_id: int

class TransferOrderRequest(BaseModel):
    target_table_id: int

class MergeOrderRequest(BaseModel):
    target_order_id: int

class SplitItemRequest(BaseModel):
    quantity: int  # how many to split off into a new item row

class PrintSynopsisRequest(BaseModel):
    printer_id: int

class MoveItemsRequest(BaseModel):
    item_ids: List[int]
    target_order_id: int

class RevertPaymentRequest(BaseModel):
    item_ids: List[int]

from routers.deps import get_current_user, require_manager
from services.printer_service import route_and_print, route_and_print_sync, print_order_receipt, print_order_synopsis, print_cancellation_ticket
from models.settings import PosSettings
from services.sse_bus import broadcast_sync

router = APIRouter()


def _waiter_can_cancel(user: User, db: Session) -> bool:
    """True only if both the global setting and the per-waiter flag are on."""
    row = db.query(PosSettings).filter(PosSettings.key == "orders.waiter_cancellations_allowed").first()
    global_on = row is not None and row.value == "true"
    return global_on and bool(user.can_cancel_orders)


def _can_access_order(order: Order, user: User, db: Session) -> bool:
    """Zone-based access: any waiter whose zone covers the order's table group may act on it.
    Standalone orders (table_id=None) are accessible to all waiters with any zone assignment."""
    if user.role == "superadmin" or user.perm_access_dashboard:
        return True
    zones = db.query(WaiterZone).filter(WaiterZone.waiter_id == user.id).all()
    if not zones:
        return False
    # Standalone orders (takeaway/delivery) have no table — accessible to all zone-assigned waiters
    if order.table_id is None:
        return True
    if any(z.group_id is None for z in zones):
        return True
    table = db.query(Table).filter(Table.id == order.table_id).first()
    if not table:
        return False
    allowed_group_ids = {z.group_id for z in zones}
    return table.group_id in allowed_group_ids


def _audit(db: Session, order_id: int, event_type: str, waiter_id: int = None,
           item_ids: list = None, amount: float = None, payment_method: str = None, note: str = None):
    db.add(OrderAuditLog(
        order_id=order_id,
        event_type=event_type,
        waiter_id=waiter_id,
        item_ids=json.dumps(item_ids) if item_ids is not None else None,
        amount=amount,
        payment_method=payment_method,
        note=note,
    ))


ACTIVE_STATUSES = ["open", "partially_paid", "paid"]


@router.get("/shift-log")
def shift_order_log(
    db: Session = Depends(get_db),
    user: User = Depends(get_current_user),
):
    """
    Returns all orders placed during the current waiter's active shift, newest first.
    Managers get all orders for the current business day.
    Includes print job status per order.
    """
    from models.shift import WaiterShift
    from models.business_day import BusinessDay

    is_manager = user.role == "superadmin" or user.perm_access_dashboard

    if is_manager:
        bd = db.query(BusinessDay).filter(BusinessDay.closed_at == None).order_by(BusinessDay.opened_at.desc()).first()  # noqa: E711
        since = bd.opened_at if bd else None
        q = db.query(Order).filter(Order.status.notin_(["cancelled"]))
        if since:
            q = q.filter(Order.opened_at >= since)
    else:
        shift = db.query(WaiterShift).filter(
            WaiterShift.waiter_id == user.id,
            WaiterShift.ended_at == None,  # noqa: E711
        ).order_by(WaiterShift.started_at.desc()).first()
        if not shift:
            return {"orders": []}
        from models.order import OrderWaiter
        order_ids = [ow.order_id for ow in db.query(OrderWaiter).filter(OrderWaiter.waiter_id == user.id).all()]
        q = db.query(Order).filter(
            Order.id.in_(order_ids),
            Order.opened_at >= shift.started_at,
            Order.status.notin_(["cancelled"]),
        )

    orders = q.order_by(Order.opened_at.desc()).limit(200).all()

    result = []
    for order in orders:
        table_name = None
        if order.table:
            table_name = order.table.label or f"T{order.table.number}"

        # Collect print jobs for this order
        jobs = db.query(PrintJob).filter(PrintJob.order_id == order.id).all()
        has_pending_print = any(j.status == "pending" for j in jobs)
        print_status = "success"
        if has_pending_print:
            print_status = "pending"
        elif any(j.status == "cancelled" and j.cancel_reason == "staff_cancelled" for j in jobs):
            print_status = "cancelled"
        elif not jobs:
            # No print jobs at all — either no printers configured or bypass mode
            print_status = "success"

        latest_attempt = max((j.last_attempted_at for j in jobs if j.last_attempted_at), default=None)
        max_retries = max((j.retry_count for j in jobs), default=0)

        active_items = [i for i in order.items if i.status == "active"]
        paid_items = [i for i in order.items if i.status == "paid"]
        all_items = active_items + paid_items

        result.append({
            "id": order.id,
            "table_name": table_name,
            "opened_at": order.opened_at.isoformat() if order.opened_at else None,
            "status": order.status,
            "print_status": print_status,
            "fiscal_status": order.fiscal_status,  # None | "pending" | "success" | "failed"
            "print_attempted_at": latest_attempt.isoformat() if latest_attempt else None,
            "print_retry_count": max_retries,
            "print_jobs": [
                {
                    "id": j.id,
                    "status": j.status,
                    "retry_count": j.retry_count,
                    "first_attempted_at": j.first_attempted_at.isoformat() if j.first_attempted_at else None,
                    "last_attempted_at": j.last_attempted_at.isoformat() if j.last_attempted_at else None,
                }
                for j in jobs
            ],
            "items": [
                {
                    "id": i.id,
                    "product_name": i.product.name if i.product else f"#{i.product_id}",
                    "quantity": i.quantity,
                    "unit_price": i.unit_price,
                    "price_adjustment": i.price_adjustment or 0.0,
                    "status": i.status,
                }
                for i in all_items
            ],
            "total": sum((i.unit_price + (i.price_adjustment or 0.0)) * i.quantity for i in all_items),
        })

    return {"orders": result}


@router.get("/cancel-permissions")
def cancel_permissions(db: Session = Depends(get_db), user: User = Depends(get_current_user)):
    """Return whether the current user is allowed to cancel orders."""
    is_manager = user.role == "superadmin" or user.perm_access_dashboard
    if is_manager:
        return {"can_cancel": True, "reason": None}
    can = _waiter_can_cancel(user, db)
    reason = None
    if not can:
        row = db.query(PosSettings).filter(PosSettings.key == "orders.waiter_cancellations_allowed").first()
        global_on = row is not None and row.value == "true"
        if not global_on:
            reason = "global_disabled"
        elif not user.can_cancel_orders:
            reason = "user_disabled"
    return {"can_cancel": can, "reason": reason}


@router.get("/", response_model=List[OrderOut])
def list_orders(
    order_status: Optional[str] = None,
    waiter_id: Optional[int] = None,
    all: bool = False,
    db: Session = Depends(get_db),
    user: User = Depends(require_manager),
):
    q = db.query(Order)
    if order_status:
        q = q.filter(Order.status == order_status)
    elif not all:
        q = q.filter(Order.status.in_(ACTIVE_STATUSES))
    if waiter_id:
        q = q.join(OrderWaiter).filter(OrderWaiter.waiter_id == waiter_id)
    return q.all()


@router.get("/my", response_model=List[OrderOut])
def my_orders(db: Session = Depends(get_db), user: User = Depends(get_current_user)):
    direct = db.query(Order).join(OrderWaiter).filter(
        OrderWaiter.waiter_id == user.id,
        Order.status.in_(["open", "partially_paid"]),
    ).all()
    # Also orders where user is opener but not explicitly assigned
    also_opened = db.query(Order).filter(
        Order.opened_by == user.id,
        Order.status.in_(["open", "partially_paid"]),
    ).all()
    seen = {o.id for o in direct}
    return direct + [o for o in also_opened if o.id not in seen]


class ActiveOrderSlim(BaseModel):
    id: int
    table_id: Optional[int]
    status: str
    order_type: str = "here"
    waiter_ids: List[int]
    model_config = {"from_attributes": True}


@router.get("/active", response_model=List[ActiveOrderSlim])
def list_active_orders(db: Session = Depends(get_db), user: User = Depends(get_current_user)):
    """All currently open/partially-paid/paid orders (lightweight). Accessible to all staff."""
    orders = db.query(Order).filter(Order.status.in_(["open", "partially_paid", "paid"])).all()
    return [
        ActiveOrderSlim(
            id=o.id,
            table_id=o.table_id,
            status=o.status,
            order_type=o.order_type or "here",
            waiter_ids=[w.waiter_id for w in o.waiters],
        )
        for o in orders
    ]


# ─── Pending print jobs count (for PWA header badge) ─────────────────────────

@router.get("/pending-prints")
def get_pending_prints(
    db: Session = Depends(get_db),
    user: User = Depends(get_current_user),
):
    """Returns count of pending print jobs + per-order detail for the current shift."""
    pending_jobs = db.query(PrintJob).filter(PrintJob.status == "pending").all()

    from collections import defaultdict
    by_order: dict = defaultdict(list)
    for pj in pending_jobs:
        by_order[pj.order_id].append(pj)

    orders_data = []
    for order_id, jobs in by_order.items():
        order = db.query(Order).filter(Order.id == order_id).first()
        if not order or order.status in ("closed", "cancelled"):
            continue
        table_name = None
        if order.table:
            table_name = order.table.label or f"T{order.table.number}"
        orders_data.append({
            "order_id": order_id,
            "table_name": table_name,
            "opened_at": order.opened_at.isoformat() if order.opened_at else None,
            "jobs": [
                {
                    "id": pj.id,
                    "printer_id": pj.printer_id,
                    "retry_count": pj.retry_count,
                    "first_attempted_at": pj.first_attempted_at.isoformat() if pj.first_attempted_at else None,
                    "last_attempted_at": pj.last_attempted_at.isoformat() if pj.last_attempted_at else None,
                    "item_ids": json.loads(pj.item_ids or "[]"),
                }
                for pj in jobs
            ],
        })

    return {"count": len(pending_jobs), "orders": orders_data}


@router.get("/{order_id}")
def get_order(order_id: int, db: Session = Depends(get_db), user: User = Depends(get_current_user)):
    order = db.query(Order).filter(Order.id == order_id).first()
    if not order:
        raise HTTPException(status_code=404, detail="Order not found")
    if not _can_access_order(order, user, db):
        raise HTTPException(status_code=403, detail="Access denied")

    # Resolve all user IDs referenced by this order in one query
    user_ids = set()
    user_ids.add(order.opened_by)
    if order.closed_by:
        user_ids.add(order.closed_by)
    for item in order.items:
        user_ids.add(item.added_by)
        if item.paid_by:
            user_ids.add(item.paid_by)
    for log in order.audit_logs:
        if log.waiter_id:
            user_ids.add(log.waiter_id)

    users = db.query(User).filter(User.id.in_(user_ids)).all()
    name_map = {u.id: u.nickname or u.full_name or u.username for u in users}

    def fmt_item(i):
        return {
            "id": i.id,
            "order_id": i.order_id,
            "product_id": i.product_id,
            "product": {"id": i.product.id, "name": i.product.name} if i.product else None,
            "added_by": i.added_by,
            "added_by_name": name_map.get(i.added_by),
            "quantity": i.quantity,
            "unit_price": i.unit_price,
            "selected_options": i.selected_options,
            "removed_ingredients": i.removed_ingredients,
            "notes": i.notes,
            "status": i.status,
            "kds_status": i.kds_status or "pending",
            "added_at": _iso(i.added_at),
            "printed": i.printed,
            "paid_by": i.paid_by,
            "paid_by_name": name_map.get(i.paid_by) if i.paid_by else None,
            "paid_at": _iso(i.paid_at),
            "payment_method": i.payment_method,
            "paid_in_shift_id": i.paid_in_shift_id,
            "price_adjustment": getattr(i, 'price_adjustment', 0.0) or 0.0,
            "unit_type": i.unit_type,
        }

    def fmt_log(l):
        return {
            "id": l.id,
            "order_id": l.order_id,
            "event_type": l.event_type,
            "waiter_id": l.waiter_id,
            "waiter_name": name_map.get(l.waiter_id) if l.waiter_id else None,
            "item_ids": l.item_ids,
            "amount": l.amount,
            "payment_method": l.payment_method,
            "note": l.note,
            "created_at": _iso(l.created_at),
            "offline_at": _iso(l.offline_at) if isinstance(l.offline_at, datetime) else l.offline_at,
            "is_duplicate": l.is_duplicate,
        }

    table = db.query(Table).filter(Table.id == order.table_id).first() if order.table_id else None
    table_name = (table.label or f"T{table.number}") if table else None

    # Phase 2F: resolve customer name for display
    customer_name = None
    customer_phone = None
    if order.customer_id and order.customer:
        customer_name = order.customer.name
        if order.customer.nickname:
            customer_name = f"{order.customer.name} «{order.customer.nickname}»"
        customer_phone = order.customer.phone

    return {
        "id": order.id,
        "table_id": order.table_id,
        "table_name": table_name,
        "order_type": order.order_type or "here",
        "opened_by": order.opened_by,
        "opened_at": _iso(order.opened_at),
        "status": order.status,
        "closed_at": _iso(order.closed_at),
        "closed_by": order.closed_by,
        "notes": order.notes,
        "business_day_id": order.business_day_id,
        "customer_id": order.customer_id,
        "customer_name": customer_name,
        "customer_phone": customer_phone,
        "kds_status": order.kds_status,
        "kds_status_changed_at": _iso(order.kds_status_changed_at),
        "customer_count": order.customer_count,
        "items": [fmt_item(i) for i in order.items],
        "waiters": [{"waiter_id": w.waiter_id} for w in order.waiters],
        "audit_logs": [fmt_log(l) for l in order.audit_logs],
    }


@router.post("/", response_model=OrderOut, status_code=status.HTTP_201_CREATED)
def open_order(body: OrderCreate, db: Session = Depends(get_db), user: User = Depends(get_current_user)):
    from models.business_day import BusinessDay
    from models.shift import WaiterShift

    if not (user.role == "superadmin" or user.perm_access_dashboard or user.perm_open_orders):
        raise HTTPException(status_code=403, detail="Δεν έχετε δικαίωμα ανοίγματος παραγγελιών")

    active_day = db.query(BusinessDay).filter(BusinessDay.status == "open").first()
    if not active_day:
        raise HTTPException(status_code=403, detail="Restaurant is not open — manager must open the business day first")

    if user.perm_access_waiter_app and not user.perm_access_dashboard and user.role != "superadmin":
        active_shift = db.query(WaiterShift).filter(
            WaiterShift.waiter_id == user.id,
            WaiterShift.ended_at == None,
        ).first()
        if not active_shift:
            raise HTTPException(status_code=403, detail="You do not have an active shift")

    order_type = body.order_type or "here"
    if order_type not in ("here", "takeaway", "delivery"):
        raise HTTPException(status_code=400, detail="Invalid order_type")

    if body.table_id is not None:
        existing = db.query(Order).filter(
            Order.table_id == body.table_id,
            Order.status.in_(["open", "partially_paid", "paid"]),
        ).first()
        if existing:
            raise HTTPException(status_code=400, detail="Table already has an open order")

    order = Order(table_id=body.table_id, order_type=order_type, opened_by=user.id, business_day_id=active_day.id)
    db.add(order)
    db.flush()
    db.add(OrderWaiter(order_id=order.id, waiter_id=user.id))
    _audit(db, order.id, "ORDER_OPENED", waiter_id=user.id)
    db.commit()
    db.refresh(order)
    broadcast_sync("order_updated", {"order_id": order.id, "table_id": order.table_id, "status": order.status, "action": "opened"})
    return order


@router.post("/{order_id}/items", response_model=AddItemsResponse)
def add_items(
    order_id: int,
    body: AddItemsRequest,
    db: Session = Depends(get_db),
    user: User = Depends(get_current_user),
):
    order = db.query(Order).filter(Order.id == order_id).first()
    if not order:
        raise HTTPException(status_code=404, detail="Order not found")
    if not _can_access_order(order, user, db):
        raise HTTPException(status_code=403, detail="Access denied")
    if order.status not in ("open", "partially_paid", "paid"):
        raise HTTPException(status_code=400, detail="Order is not open")

    # Adding items to a fully-paid order reopens it — partially_paid since prior items were paid
    if order.status == "paid":
        order.status = "partially_paid"

    new_item_ids = []
    service_print_entries = []  # { product, quantity } — printed but not saved
    pending_price_events = []   # (order_item_id, [PriceEvent]) — flushed after commit

    # Build pricing context once for the whole batch
    now = datetime.now(timezone.utc)
    active_pg_ids = build_active_price_group_ids(db, now)
    # Determine order channel
    _channel_map = {"online": "online", "takeaway": "takeaway", "delivery": "takeaway"}
    order_channel = _channel_map.get(order.order_type, "pos")
    if order.source == "online":
        order_channel = "online"
    # Snapshot existing cart items for condition evaluation
    existing_cart = [
        {
            "product_id": i.product_id,
            "category_id": i.product.category_id if i.product else None,
            "quantity": i.quantity,
            "unit_price": i.unit_price,
        }
        for i in order.items if i.status == "active"
    ]

    for item_in in body.items:
        product = db.query(Product).filter(Product.id == item_in.product_id).first()
        if not product or not product.is_available:
            raise HTTPException(status_code=400, detail=f"Product {item_in.product_id} not available")

        # Priceless service items: print-once, NOT persisted (ephemeral)
        # Priced service items: saved as real order items AND printed in a SERVICE section
        is_svc = item_in.is_service_item or getattr(product, 'is_service_item', False)
        if is_svc and not (product.base_price and product.base_price > 0):
            service_print_entries.append({'product': product, 'quantity': item_in.quantity})
            continue

        extra_cost = sum(
            (o.price_delta or o.extra_cost or 0.0)
            for o in (item_in.selected_options or [])
        )

        # Build OrderContext for this item (includes items added so far in this batch)
        ctx = OrderContext(
            channel=order_channel,
            now=now,
            cart_items=existing_cart + [
                {
                    "product_id": product.id,
                    "category_id": product.category_id,
                    "quantity": item_in.quantity,
                    "unit_price": product.base_price + extra_cost,
                }
            ],
            active_price_group_ids=active_pg_ids,
        )

        # Resolve final unit_price through pricing engine
        unit_price, price_events = resolve_price(product, item_in.quantity, extra_cost, ctx, db)

        # Phase 2A: cost snapshot — breakdown takes priority over simple cost
        unit_cost = None
        if product.cost_breakdown:
            try:
                entries = json.loads(product.cost_breakdown)
                total = sum(e.get("amount", 0.0) for e in entries if isinstance(e, dict))
                if total > 0:
                    unit_cost = total
            except Exception:
                pass
        if unit_cost is None and product.cost_simple:
            unit_cost = product.cost_simple

        item = OrderItem(
            order_id=order_id,
            product_id=item_in.product_id,
            added_by=user.id,
            quantity=item_in.quantity,
            unit_price=unit_price,
            unit_cost=unit_cost,
            price_adjustment=item_in.price_adjustment or 0.0,
            selected_options=json.dumps([o.model_dump() for o in item_in.selected_options]) if item_in.selected_options else None,
            removed_ingredients=json.dumps(item_in.removed_ingredients) if item_in.removed_ingredients else None,
            notes=item_in.notes,
            course_id=item_in.course_id,
        )
        db.add(item)
        db.flush()
        new_item_ids.append(item.id)

        # Stamp order_id on events and queue them — written after commit
        if price_events:
            for ev in price_events:
                ev.order_id = order_id
            pending_price_events.append((item.id, price_events))

        # Update rolling cart snapshot so subsequent items in this batch see correct context
        existing_cart.append({
            "product_id": product.id,
            "category_id": product.category_id,
            "quantity": item_in.quantity,
            "unit_price": unit_price,
        })

    # Attach the whole-order note (only overwrite if a note was explicitly provided)
    if body.order_note is not None:
        order.notes = body.order_note or None  # empty string → clear note

    # Update customer count if provided
    if body.customer_count is not None:
        order.customer_count = body.customer_count

    bypass_setting = db.query(PosSettings).filter(PosSettings.key == "orders.bypass_kds_serve").first()
    global_bypass_kds = bypass_setting and bypass_setting.value == "true"

    if global_bypass_kds:
        # Global setting: all new items go straight to served
        for item in order.items:
            if item.id in new_item_ids:
                item.kds_status = "served"
        order.kds_status = "served"
    else:
        # Apply per-zone bypass for each new item individually
        from models.prep_zone import PrepZone
        new_items_map = {item.id: item for item in order.items if item.id in set(new_item_ids)}
        for item in new_items_map.values():
            product = db.query(Product).filter(Product.id == item.product_id).first()
            if not product or not product.prep_zones:
                continue
            # Zone with strongest bypass wins: bypass_kds > bypass_pending
            zone_bypass_kds = any(bool(z.bypass_kds) for z in product.prep_zones)
            zone_bypass_pending = any(bool(z.bypass_pending) for z in product.prep_zones)
            if zone_bypass_kds:
                item.kds_status = "served"
            elif zone_bypass_pending:
                item.kds_status = "done"

        # Recalculate order-level kds_status from all items
        all_active = [i for i in order.items if i.status != "cancelled"]
        if all_active:
            statuses = {i.kds_status for i in all_active}
            if statuses <= {"served"}:
                order.kds_status = "served"
            elif statuses <= {"done", "served"}:
                order.kds_status = "done"
            elif "pending" in statuses or "preparing" in statuses:
                # Pull back to pending so kitchen sees new items
                if order.kds_status in ("done", "served"):
                    order.kds_status = "pending"

    _audit(db, order_id, "ITEMS_ADDED", waiter_id=user.id, item_ids=new_item_ids)
    db.commit()

    # Write price events now that OrderItems are committed and have real IDs
    if pending_price_events:
        for item_id, events in pending_price_events:
            write_price_events(events, order_id, item_id, db)
        db.commit()

    db.refresh(order)

    # Evaluate deals against updated cart — return offers to surface to waiter
    from models.pricing import PriceEventLog
    already_fired = {
        row.deal_id for row in
        db.query(PriceEventLog.deal_id).filter(
            PriceEventLog.order_id == order_id,
            PriceEventLog.deal_id != None,  # noqa: E711
            PriceEventLog.event_type.in_(["deal_offer_shown", "deal_offer_accepted"]),
        ).all()
        if row.deal_id is not None
    }
    final_cart = [
        {
            "product_id": i.product_id,
            "category_id": i.product.category_id if i.product else None,
            "quantity": i.quantity,
            "unit_price": i.unit_price,
        }
        for i in order.items if i.status == "active"
    ]
    deal_ctx = OrderContext(
        channel=order_channel,
        now=now,
        cart_items=final_cart,
        active_price_group_ids=active_pg_ids,
    )
    deal_offers = evaluate_deals(order_id, deal_ctx, already_fired, db)

    # Log deal_offer_shown for each new offer
    if deal_offers:
        for offer in deal_offers:
            db.add(PriceEventLog(
                order_id=order_id,
                event_type="deal_offer_shown",
                deal_id=offer.deal_id,
                applied_by_user_id=user.id,
            ))
        db.commit()

    print_results = route_and_print_sync(order_id, new_item_ids, db, ephemeral_svc_entries=service_print_entries) if (new_item_ids or service_print_entries) else []
    broadcast_sync("order_updated", {"order_id": order.id, "table_id": order.table_id, "status": order.status, "action": "items_added", "item_ids": new_item_ids})
    broadcast_sync("kds_order_updated", {"order_id": order.id, "kds_status": order.kds_status})

    return {
        "order": order,
        "print_results": print_results,
        "deal_offers": [
            {
                "deal_id": o.deal_id,
                "deal_name": o.deal_name,
                "action_type": o.action_type,
                "free_item_id": o.free_item_id,
                "free_item_name": o.free_item_name,
                "free_choices": o.free_choices,
                "free_quantity": o.free_quantity,
            }
            for o in deal_offers
        ],
    }


@router.post("/{order_id}/retry-print", response_model=AddItemsResponse)
def retry_print(
    order_id: int,
    db: Session = Depends(get_db),
    user: User = Depends(get_current_user),
):
    order = db.query(Order).filter(Order.id == order_id).first()
    if not order:
        raise HTTPException(status_code=404, detail="Order not found")
    if not _can_access_order(order, user, db):
        raise HTTPException(status_code=403, detail="Access denied")

    unprinted_ids = [item.id for item in order.items if not item.printed and item.status == "active"]
    if not unprinted_ids:
        return {"order": order, "print_results": []}

    print_results = route_and_print_sync(order_id, unprinted_ids, db)
    db.refresh(order)
    return {"order": order, "print_results": print_results}


@router.put("/{order_id}/items/{item_id}", response_model=OrderItemOut)
def edit_item(order_id: int, item_id: int, notes: Optional[str] = None, db: Session = Depends(get_db), user: User = Depends(require_manager)):
    item = db.query(OrderItem).filter(OrderItem.id == item_id, OrderItem.order_id == order_id).first()
    if not item:
        raise HTTPException(status_code=404, detail="Item not found")
    if notes is not None:
        item.notes = notes
    db.commit()
    db.refresh(item)
    return item


@router.delete("/{order_id}/items/{item_id}", status_code=status.HTTP_204_NO_CONTENT)
def cancel_item(
    order_id: int,
    item_id: int,
    background_tasks: BackgroundTasks,
    print_cancellation: bool = True,
    db: Session = Depends(get_db),
    user: User = Depends(get_current_user),
):
    order = db.query(Order).filter(Order.id == order_id).first()
    if not order:
        raise HTTPException(status_code=404, detail="Order not found")

    is_manager = user.role == "superadmin" or user.perm_access_dashboard
    if not is_manager and not _waiter_can_cancel(user, db):
        raise HTTPException(status_code=403, detail="Δεν επιτρέπεται η ακύρωση παραγγελίας")
    if not is_manager and not _can_access_order(order, user, db):
        raise HTTPException(status_code=403, detail="Access denied")

    item = db.query(OrderItem).filter(OrderItem.id == item_id, OrderItem.order_id == order_id).first()
    if not item:
        raise HTTPException(status_code=404, detail="Item not found")
    if item.status == "paid":
        raise HTTPException(status_code=400, detail="Το αντικείμενο έχει ήδη πληρωθεί")

    now = datetime.now(timezone.utc)
    item.status = "cancelled"
    item.cancelled_by = user.id
    item.cancelled_at = now
    cancelled_ids = [item_id]

    # Cascade: also cancel any deal-linked items bound to this one (free items that came with it)
    linked = db.query(OrderItem).filter(
        OrderItem.linked_item_id == item_id,
        OrderItem.order_id == order_id,
        OrderItem.status == "active",
    ).all()
    for li in linked:
        li.status = "cancelled"
        li.cancelled_by = user.id
        li.cancelled_at = now
        cancelled_ids.append(li.id)

    _audit(db, order_id, "ITEM_CANCELLED", waiter_id=user.id, item_ids=cancelled_ids)
    db.commit()

    should_print = print_cancellation if is_manager else True
    if should_print:
        background_tasks.add_task(print_cancellation_ticket, order_id, cancelled_ids)


@router.post("/{order_id}/pay")
def pay_items(order_id: int, body: PayItemsRequest, db: Session = Depends(get_db), user: User = Depends(get_current_user)):
    order = db.query(Order).filter(Order.id == order_id).first()
    if not order:
        raise HTTPException(status_code=404, detail="Order not found")
    if not _can_access_order(order, user, db):
        raise HTTPException(status_code=403, detail="Access denied")

    from models.shift import WaiterShift
    from services.fiscal_service import (
        is_fiscal_enabled, validate_items_for_fiscal, build_fiscal_items,
        send_fiscal_receipt_background,
    )

    items = db.query(OrderItem).filter(
        OrderItem.id.in_(body.item_ids),
        OrderItem.order_id == order_id,
        OrderItem.status.in_(["active", "ready"]),  # ready items are also payable
    ).all()

    # Fiscal validation: if enabled, all items must have VAT group assigned before payment
    fiscal_on = is_fiscal_enabled(db)
    if fiscal_on and items:
        missing = validate_items_for_fiscal(items, db)
        if missing:
            names = ", ".join(missing)
            raise HTTPException(
                status_code=422,
                detail=f"Fiscal printing is ON but the following products have no VAT group assigned: {names}"
            )

    now = datetime.now(timezone.utc)
    active_shift = db.query(WaiterShift).filter(
        WaiterShift.waiter_id == user.id,
        WaiterShift.ended_at == None,
    ).first()
    effective_method = body.payment_method or "cash"
    total_paid = 0.0
    for item in items:
        item.status = "paid"
        item.paid_by = user.id
        item.paid_at = now
        item.payment_method = effective_method
        item.paid_in_shift_id = active_shift.id if active_shift else None
        adj = getattr(item, 'price_adjustment', 0.0) or 0.0
        total_paid += (item.unit_price + adj) * item.quantity

    db.flush()  # write item status changes before counting, since autoflush=False
    active_remaining = db.query(OrderItem).filter(
        OrderItem.order_id == order_id, OrderItem.status.in_(["active", "ready"])
    ).count()
    order.status = "paid" if active_remaining == 0 else "partially_paid"

    paid_ids = [i.id for i in items]
    _audit(db, order_id, "PAYMENT", waiter_id=user.id, item_ids=paid_ids,
           amount=total_paid, payment_method=body.payment_method)

    auto_closed = False
    if order.status == "paid":
        setting = db.query(PosSettings).filter(PosSettings.key == "orders.auto_close_on_full_payment").first()
        if setting and setting.value == "true":
            for oi in order.items:
                if oi.kds_status != "served":
                    oi.kds_status = "served"
            order.kds_status = "served"
            order.status = "closed"
            order.closed_at = now
            order.closed_by = user.id
            auto_closed = True

    if fiscal_on and items:
        order.fiscal_status = "pending"

    db.commit()
    broadcast_sync("order_paid", {"order_id": order_id, "table_id": order.table_id, "status": order.status, "paid_item_ids": paid_ids, "amount": total_paid, "payment_method": body.payment_method})
    if auto_closed:
        broadcast_sync("order_closed", {"order_id": order_id, "table_id": order.table_id, "auto_closed": True})

    if fiscal_on and items:
        from database import SessionLocal
        fiscal_items = build_fiscal_items(items)
        send_fiscal_receipt_background(fiscal_items, effective_method, total_paid, db,
                                       order_id=order_id, db_factory=SessionLocal,
                                       timeout_seconds=60)

    return {
        "status": order.status,
        "paid_item_ids": paid_ids,
        "auto_closed": auto_closed,
        "fiscal": "pending" if fiscal_on and items else None,
    }


@router.post("/{order_id}/revert-payment")
def revert_payment(order_id: int, body: RevertPaymentRequest, db: Session = Depends(get_db), user: User = Depends(get_current_user)):
    order = db.query(Order).filter(Order.id == order_id).first()
    if not order:
        raise HTTPException(status_code=404, detail="Order not found")
    if not _can_access_order(order, user, db):
        raise HTTPException(status_code=403, detail="Access denied")

    revert_setting = db.query(PosSettings).filter(PosSettings.key == "payments.waiter_revert_allowed").first()
    if not revert_setting or revert_setting.value != "true":
        raise HTTPException(status_code=403, detail="Payment revert is not enabled")

    items = db.query(OrderItem).filter(
        OrderItem.id.in_(body.item_ids),
        OrderItem.order_id == order_id,
        OrderItem.status == "paid",
    ).all()

    if not items:
        raise HTTPException(status_code=404, detail="No paid items found to revert")

    reverted_ids = []
    for item in items:
        item.status = "active"
        item.paid_by = None
        item.paid_at = None
        item.payment_method = None
        item.paid_in_shift_id = None
        reverted_ids.append(item.id)

    db.flush()
    active_remaining = db.query(OrderItem).filter(
        OrderItem.order_id == order_id, OrderItem.status.in_(["active", "ready"])
    ).count()
    paid_remaining = db.query(OrderItem).filter(
        OrderItem.order_id == order_id, OrderItem.status == "paid"
    ).count()
    if active_remaining > 0 and paid_remaining > 0:
        order.status = "partially_paid"
    elif active_remaining > 0:
        order.status = "open"
    # if somehow all remaining are paid after revert (shouldn't happen), leave status

    _audit(db, order_id, "PAYMENT_REVERTED", waiter_id=user.id, item_ids=reverted_ids)
    db.commit()
    broadcast_sync("order_updated", {"order_id": order_id, "table_id": order.table_id, "status": order.status})
    return {"status": order.status, "reverted_item_ids": reverted_ids}


@router.post("/{order_id}/close")
def close_order(order_id: int, db: Session = Depends(get_db), user: User = Depends(get_current_user)):
    order = db.query(Order).filter(Order.id == order_id).first()
    if not order:
        raise HTTPException(status_code=404, detail="Order not found")
    if not (user.role == "superadmin" or user.perm_access_dashboard or user.perm_close_orders):
        raise HTTPException(status_code=403, detail="Δεν έχετε δικαίωμα κλεισίματος παραγγελιών")
    if not _can_access_order(order, user, db):
        raise HTTPException(status_code=403, detail="Access denied")
    if order.status not in ("paid", "open", "partially_paid"):
        raise HTTPException(status_code=400, detail="Cannot close order in current status")

    now = datetime.now(timezone.utc)

    # Mark all still-active items as 'closed' — unpaid, closed by manager
    active_items = db.query(OrderItem).filter(
        OrderItem.order_id == order_id,
        OrderItem.status == "active",
    ).all()
    closed_item_ids = []
    for item in active_items:
        item.status = "closed"
        closed_item_ids.append(item.id)

    # Mark every non-served item as served on KDS so the order disappears from KDS immediately
    for item in order.items:
        if item.kds_status != "served":
            item.kds_status = "served"
    order.kds_status = "served"

    order.status = "closed"
    order.closed_at = now
    order.closed_by = user.id

    note = f"Κλείσιμο από manager — {len(closed_item_ids)} απλήρωτα αντικείμενα" if closed_item_ids else None
    _audit(db, order_id, "ORDER_CLOSED", waiter_id=user.id,
           item_ids=closed_item_ids if closed_item_ids else None, note=note)
    db.commit()
    broadcast_sync("order_closed", {"order_id": order_id, "table_id": order.table_id})
    return {"status": "closed"}


@router.post("/{order_id}/pay-offline")
def pay_items_offline(
    order_id: int,
    body: OfflinePaymentRequest,
    db: Session = Depends(get_db),
    user: User = Depends(get_current_user),
):
    """
    Sync an emergency payment that was taken while the server was offline.
    The UUID prevents double-processing. If a payment with the same UUID already
    exists on this order, the duplicate is logged in red (is_duplicate=1) rather
    than silently dropped — so managers can reconcile.
    """
    order = db.query(Order).filter(Order.id == order_id).first()
    if not order:
        raise HTTPException(status_code=404, detail="Order not found")
    if not _can_access_order(order, user, db):
        raise HTTPException(status_code=403, detail="Access denied")

    # Check for duplicate UUID on this order
    existing_uuid = db.query(OrderAuditLog).filter(
        OrderAuditLog.order_id == order_id,
        OrderAuditLog.offline_uuid == body.uuid,
    ).first()
    is_duplicate = existing_uuid is not None

    from models.shift import WaiterShift
    items = db.query(OrderItem).filter(
        OrderItem.id.in_(body.item_ids),
        OrderItem.order_id == order_id,
        OrderItem.status == "active",
    ).all()

    # Reject empty payments — client had no offline snapshot for this table
    if not items and not is_duplicate:
        raise HTTPException(status_code=400, detail="No active items found — payment rejected")

    # Use the client-recorded offline timestamp as paid_at so audit reflects real payment time
    try:
        paid_at = datetime.fromisoformat(body.offline_at.replace("Z", "+00:00")) if body.offline_at else datetime.now(timezone.utc)
    except (ValueError, AttributeError):
        paid_at = datetime.now(timezone.utc)

    active_shift = db.query(WaiterShift).filter(
        WaiterShift.waiter_id == user.id,
        WaiterShift.ended_at == None,
    ).first()

    effective_method = body.payment_method or "cash"
    total_paid = 0.0
    paid_ids = []
    if not is_duplicate:
        for item in items:
            item.status = "paid"
            item.paid_by = user.id
            item.paid_at = paid_at
            item.payment_method = effective_method
            item.paid_in_shift_id = active_shift.id if active_shift else None
            adj = getattr(item, 'price_adjustment', 0.0) or 0.0
            total_paid += (item.unit_price + adj) * item.quantity
            paid_ids.append(item.id)

        db.flush()
        active_remaining = db.query(OrderItem).filter(
            OrderItem.order_id == order_id, OrderItem.status == "active"
        ).count()
        order.status = "paid" if active_remaining == 0 else "partially_paid"
    else:
        # Duplicate — compute total for audit record without changing item state
        total_paid = sum((i.unit_price + (getattr(i, 'price_adjustment', 0.0) or 0.0)) * i.quantity for i in items)
        paid_ids = [i.id for i in items]

    # Always write audit log — duplicate flag makes it visible in red in manager dashboard
    db.add(OrderAuditLog(
        order_id=order_id,
        event_type="PAYMENT_OFFLINE",
        waiter_id=user.id,
        item_ids=json.dumps(paid_ids),
        amount=total_paid,
        payment_method=body.payment_method,
        note=f"Emergency offline payment (uuid={body.uuid}){' — DUPLICATE' if is_duplicate else ''}",
        offline_uuid=body.uuid,
        offline_at=body.offline_at,
        is_duplicate=1 if is_duplicate else 0,
    ))
    db.commit()

    if not is_duplicate:
        broadcast_sync("order_paid", {"order_id": order_id, "table_id": order.table_id, "status": order.status, "paid_item_ids": paid_ids, "amount": total_paid, "payment_method": body.payment_method})

    return {
        "status": order.status if not is_duplicate else "duplicate",
        "paid_item_ids": paid_ids,
        "is_duplicate": is_duplicate,
    }


@router.delete("/{order_id}", status_code=status.HTTP_204_NO_CONTENT)
def cancel_order(
    order_id: int,
    background_tasks: BackgroundTasks,
    print_cancellation: bool = True,
    db: Session = Depends(get_db),
    user: User = Depends(get_current_user),
):
    order = db.query(Order).filter(Order.id == order_id).first()
    if not order:
        raise HTTPException(status_code=404, detail="Order not found")

    is_manager = user.role == "superadmin" or user.perm_access_dashboard
    if not is_manager and not _waiter_can_cancel(user, db):
        raise HTTPException(status_code=403, detail="Δεν επιτρέπεται η ακύρωση παραγγελίας")
    if not is_manager and not _can_access_order(order, user, db):
        raise HTTPException(status_code=403, detail="Access denied")

    now = datetime.now(timezone.utc)

    active_items = db.query(OrderItem).filter(
        OrderItem.order_id == order_id,
        OrderItem.status == "active",
    ).all()
    cancelled_item_ids = []
    for item in active_items:
        item.status = "cancelled"
        item.cancelled_by = user.id
        item.cancelled_at = now
        cancelled_item_ids.append(item.id)

    order.status = "cancelled"
    order.closed_at = now
    order.closed_by = user.id

    note = f"Ακύρωση — {len(cancelled_item_ids)} αντικείμενα ακυρώθηκαν" if cancelled_item_ids else None
    _audit(db, order_id, "ORDER_CANCELLED", waiter_id=user.id,
           item_ids=cancelled_item_ids if cancelled_item_ids else None, note=note)
    db.commit()
    broadcast_sync("order_closed", {"order_id": order_id, "table_id": order.table_id})

    should_print = print_cancellation if is_manager else True
    if should_print and cancelled_item_ids:
        background_tasks.add_task(print_cancellation_ticket, order_id, cancelled_item_ids)


class CancelItemsRequest(BaseModel):
    item_ids: List[int]
    print_cancellation: bool = True


@router.post("/{order_id}/cancel-items")
def cancel_items_bulk(
    order_id: int,
    body: CancelItemsRequest,
    background_tasks: BackgroundTasks,
    db: Session = Depends(get_db),
    user: User = Depends(get_current_user),
):
    """Cancel a selection of items by ID. Used by the waiter PWA multi-select cancel."""
    order = db.query(Order).filter(Order.id == order_id).first()
    if not order:
        raise HTTPException(status_code=404, detail="Order not found")

    is_manager = user.role == "superadmin" or user.perm_access_dashboard
    if not is_manager and not _waiter_can_cancel(user, db):
        raise HTTPException(status_code=403, detail="Δεν επιτρέπεται η ακύρωση παραγγελίας")
    if not is_manager and not _can_access_order(order, user, db):
        raise HTTPException(status_code=403, detail="Access denied")

    items = db.query(OrderItem).filter(
        OrderItem.id.in_(body.item_ids),
        OrderItem.order_id == order_id,
    ).all()

    now = datetime.now(timezone.utc)
    cancelled_ids = []
    skipped_paid = []
    for item in items:
        if item.status == "paid":
            skipped_paid.append(item.id)
            continue
        item.status = "cancelled"
        item.cancelled_by = user.id
        item.cancelled_at = now
        cancelled_ids.append(item.id)

        # Cascade: cancel deal-linked items (free items bound to this trigger item)
        linked = db.query(OrderItem).filter(
            OrderItem.linked_item_id == item.id,
            OrderItem.order_id == order_id,
            OrderItem.status == "active",
        ).all()
        for li in linked:
            li.status = "cancelled"
            li.cancelled_by = user.id
            li.cancelled_at = now
            cancelled_ids.append(li.id)

    if cancelled_ids:
        _audit(db, order_id, "ITEM_CANCELLED", waiter_id=user.id, item_ids=cancelled_ids)
    db.commit()

    should_print = body.print_cancellation if is_manager else True
    if should_print and cancelled_ids:
        background_tasks.add_task(print_cancellation_ticket, order_id, cancelled_ids)

    return {"cancelled": cancelled_ids, "skipped_paid": skipped_paid}


@router.put("/{order_id}/assign-waiter")
def assign_waiter(order_id: int, body: AssignWaiterRequest, db: Session = Depends(get_db), user: User = Depends(get_current_user)):
    order = db.query(Order).filter(Order.id == order_id).first()
    if not order:
        raise HTTPException(status_code=404, detail="Order not found")
    existing = db.query(OrderWaiter).filter(
        OrderWaiter.order_id == order_id, OrderWaiter.waiter_id == body.waiter_id
    ).first()
    if existing:
        raise HTTPException(status_code=400, detail="Waiter already assigned")
    db.add(OrderWaiter(order_id=order_id, waiter_id=body.waiter_id))
    db.commit()
    return {"status": "assigned"}


class AssignCustomerBody(BaseModel):
    customer_id: Optional[int] = None  # null = unassign


class TabItemBody(BaseModel):
    pass  # no body needed — tab derived from order's customer


@router.put("/{order_id}/customer")
def assign_customer(order_id: int, body: AssignCustomerBody, db: Session = Depends(get_db), user: User = Depends(require_manager)):
    from models.customers import Customer
    order = db.query(Order).filter(Order.id == order_id).first()
    if not order:
        raise HTTPException(status_code=404, detail="Order not found")
    if body.customer_id is not None:
        customer = db.query(Customer).filter(Customer.id == body.customer_id, Customer.is_active == True).first()
        if not customer:
            raise HTTPException(status_code=404, detail="Customer not found")
    order.customer_id = body.customer_id
    db.commit()
    broadcast_sync("order_updated", {"order_id": order.id, "table_id": order.table_id, "status": order.status, "action": "customer_assigned"})
    return {"status": "ok", "customer_id": order.customer_id}


@router.delete("/{order_id}/waiters/{waiter_id}", status_code=status.HTTP_204_NO_CONTENT)
def remove_waiter(order_id: int, waiter_id: int, db: Session = Depends(get_db), user: User = Depends(require_manager)):
    assignment = db.query(OrderWaiter).filter(
        OrderWaiter.order_id == order_id, OrderWaiter.waiter_id == waiter_id
    ).first()
    if not assignment:
        raise HTTPException(status_code=404, detail="Assignment not found")
    db.delete(assignment)
    db.commit()


@router.post("/{order_id}/print")
def print_order(
    order_id: int,
    body: PrintOrderRequest,
    background_tasks: BackgroundTasks,
    db: Session = Depends(get_db),
    user: User = Depends(require_manager),
):
    from models.printer import Printer

    order = db.query(Order).filter(Order.id == order_id).first()
    if not order:
        raise HTTPException(status_code=404, detail="Order not found")

    printer = db.query(Printer).filter(Printer.id == body.printer_id, Printer.is_active == True).first()
    if not printer:
        raise HTTPException(status_code=404, detail="Printer not found or inactive")

    table = db.query(Table).filter(Table.id == order.table_id).first()
    table_name = (table.label or f"T{table.number}") if table else f"#{order.table_id}"

    opener = db.query(User).filter(User.id == order.opened_by).first()
    waiter_name = opener.username if opener else f"#{order.opened_by}"

    items_data = []
    for item in order.items:
        if item.status == "cancelled":
            continue
        product_name = item.product.name if item.product else f"#{item.product_id}"
        adj = getattr(item, 'price_adjustment', 0.0) or 0.0
        effective_price = item.unit_price + adj
        items_data.append({
            "name": product_name,
            "quantity": item.quantity,
            "unit_type": item.product.unit_type if item.product else "piece",
            "unit_price": effective_price,
            "total": effective_price * item.quantity,
            "status": item.status,
        })

    grand_total = sum(i["total"] for i in items_data)

    receipt = {
        "order_id": order.id,
        "table_name": table_name,
        "waiter_name": waiter_name,
        "opened_at": local_strftime(order.opened_at, "%d/%m/%Y %H:%M"),
        "closed_at": local_strftime(order.closed_at, "%d/%m/%Y %H:%M") if order.closed_at else None,
        "status": order.status,
        "items": items_data,
        "total": grand_total,
        "notes": order.notes,
    }

    background_tasks.add_task(print_order_receipt, printer.ip_address, printer.port, receipt, printer.line_width, printer.codepage_n)
    return {"status": "printing"}


# ─── Transfer order to a different table ─────────────────────────────────────

@router.post("/{order_id}/transfer")
def transfer_order(
    order_id: int,
    body: TransferOrderRequest,
    db: Session = Depends(get_db),
    user: User = Depends(get_current_user),
):
    order = db.query(Order).filter(Order.id == order_id).first()
    if not order:
        raise HTTPException(status_code=404, detail="Order not found")
    if not _can_access_order(order, user, db):
        raise HTTPException(status_code=403, detail="Access denied")
    if order.status not in ("open", "partially_paid", "paid"):
        raise HTTPException(status_code=400, detail="Order is not active")

    target_table = db.query(Table).filter(Table.id == body.target_table_id, Table.is_active == True).first()
    if not target_table:
        raise HTTPException(status_code=404, detail="Target table not found")
    if body.target_table_id == order.table_id:
        raise HTTPException(status_code=400, detail="Table is already assigned to this order")

    conflict = db.query(Order).filter(
        Order.table_id == body.target_table_id,
        Order.status.in_(["open", "partially_paid", "paid"]),
    ).first()
    if conflict:
        raise HTTPException(status_code=400, detail="Target table already has an active order")

    old_table_id = order.table_id
    order.table_id = body.target_table_id
    _audit(db, order_id, "TABLE_TRANSFER", waiter_id=user.id,
           note=f"Transferred from table {old_table_id} to table {body.target_table_id}")
    db.commit()
    db.refresh(order)
    broadcast_sync("order_updated", {"order_id": order.id, "table_id": order.table_id, "old_table_id": old_table_id, "status": order.status, "action": "transferred"})
    return order


# ─── Merge another order into this one ───────────────────────────────────────

@router.post("/{order_id}/merge")
def merge_order(
    order_id: int,
    body: MergeOrderRequest,
    db: Session = Depends(get_db),
    user: User = Depends(get_current_user),
):
    """
    Merge source order (order_id) INTO target order (body.target_order_id).
    All items (paid + active) from the source are reassigned to the target.
    Source waiters are added to the target if not already there.
    Source order is cancelled with audit note.
    """
    source = db.query(Order).filter(Order.id == order_id).first()
    if not source:
        raise HTTPException(status_code=404, detail="Source order not found")
    if not _can_access_order(source, user, db):
        raise HTTPException(status_code=403, detail="Access denied")
    if source.status not in ("open", "partially_paid", "paid"):
        raise HTTPException(status_code=400, detail="Source order is not active")

    target = db.query(Order).filter(Order.id == body.target_order_id).first()
    if not target:
        raise HTTPException(status_code=404, detail="Target order not found")
    if not _can_access_order(target, user, db):
        raise HTTPException(status_code=403, detail="Access denied to target order")
    if target.status not in ("open", "partially_paid", "paid"):
        raise HTTPException(status_code=400, detail="Target order is not active")
    if source.id == target.id:
        raise HTTPException(status_code=400, detail="Cannot merge an order with itself")

    # Move all items to target order
    moved_item_ids = []
    for item in source.items:
        item.order_id = target.id
        moved_item_ids.append(item.id)

    # Copy source waiters to target (no duplicates)
    existing_waiter_ids = {w.waiter_id for w in target.waiters}
    for ow in source.waiters:
        if ow.waiter_id not in existing_waiter_ids:
            db.add(OrderWaiter(order_id=target.id, waiter_id=ow.waiter_id))

    # Recompute target status after flush
    db.flush()
    active_remaining = db.query(OrderItem).filter(
        OrderItem.order_id == target.id, OrderItem.status == "active"
    ).count()
    paid_exists = db.query(OrderItem).filter(
        OrderItem.order_id == target.id, OrderItem.status == "paid"
    ).count()
    if active_remaining > 0:
        target.status = "partially_paid" if paid_exists > 0 else "open"
    else:
        target.status = "paid"

    # Cancel source order
    source.status = "cancelled"
    source.closed_at = datetime.now(timezone.utc)
    source.closed_by = user.id

    _audit(db, source.id, "ORDER_CANCELLED", waiter_id=user.id,
           note=f"Merged into order #{target.id} (table {target.table_id})")
    _audit(db, target.id, "ITEMS_ADDED", waiter_id=user.id, item_ids=moved_item_ids,
           note=f"Items merged from order #{source.id} (table {source.table_id})")

    db.commit()
    db.refresh(target)
    broadcast_sync("order_updated", {"order_id": target.id, "table_id": target.table_id, "status": target.status, "action": "merged"})
    broadcast_sync("order_closed", {"order_id": source.id, "table_id": source.table_id})
    return target


# ─── Split a stacked item into two rows ──────────────────────────────────────

@router.post("/{order_id}/items/{item_id}/split", response_model=List[OrderItemOut])
def split_item(
    order_id: int,
    item_id: int,
    body: SplitItemRequest,
    db: Session = Depends(get_db),
    user: User = Depends(get_current_user),
):
    """
    Split qty units off item_id into a new item row.
    Both rows share all properties (product, price, options, notes).
    Only active items can be split.
    """
    item = db.query(OrderItem).filter(
        OrderItem.id == item_id, OrderItem.order_id == order_id
    ).first()
    if not item:
        raise HTTPException(status_code=404, detail="Item not found")
    if item.status != "active":
        raise HTTPException(status_code=400, detail="Only active items can be split")
    if body.quantity <= 0 or body.quantity >= item.quantity:
        raise HTTPException(
            status_code=400,
            detail=f"Split quantity must be between 1 and {item.quantity - 1}"
        )

    order = db.query(Order).filter(Order.id == order_id).first()
    if not _can_access_order(order, user, db):
        raise HTTPException(status_code=403, detail="Access denied")

    # Reduce original item
    item.quantity -= body.quantity

    # Create split-off item
    new_item = OrderItem(
        order_id=order_id,
        product_id=item.product_id,
        added_by=item.added_by,
        quantity=body.quantity,
        unit_price=item.unit_price,
        selected_options=item.selected_options,
        removed_ingredients=item.removed_ingredients,
        notes=item.notes,
        status="active",
        printed=item.printed,
    )
    db.add(new_item)
    db.commit()
    db.refresh(item)
    db.refresh(new_item)
    return [item, new_item]


# ─── Adjust price on an existing item ────────────────────────────────────────

class PriceAdjustRequest(BaseModel):
    price_adjustment: float  # delta from unit_price (can be negative)


@router.patch("/{order_id}/items/{item_id}/price-adjust", response_model=OrderItemOut)
def adjust_item_price(
    order_id: int,
    item_id: int,
    body: PriceAdjustRequest,
    db: Session = Depends(get_db),
    user: User = Depends(get_current_user),
):
    """Apply an on-the-fly price adjustment to an active order item."""
    if not (user.role == "superadmin" or user.perm_access_dashboard or user.perm_modify_prices):
        raise HTTPException(status_code=403, detail="Δεν έχετε δικαίωμα τροποποίησης τιμών")
    setting = db.query(PosSettings).filter(PosSettings.key == "orders.waiter_price_adjust_allowed").first()
    if not setting or setting.value != "true":
        raise HTTPException(status_code=403, detail="Price adjustment is disabled")

    item = db.query(OrderItem).filter(
        OrderItem.id == item_id, OrderItem.order_id == order_id
    ).first()
    if not item:
        raise HTTPException(status_code=404, detail="Item not found")
    if item.status != "active":
        raise HTTPException(status_code=400, detail="Only active items can have price adjusted")

    order = db.query(Order).filter(Order.id == order_id).first()
    if not _can_access_order(order, user, db):
        raise HTTPException(status_code=403, detail="Access denied")

    item.price_adjustment = body.price_adjustment
    db.commit()
    db.refresh(item)
    broadcast_sync("order_updated", {"order_id": order_id, "table_id": order.table_id, "status": order.status, "action": "item_price_adjusted"})
    return item


# ─── Waiter-applied manual discount ─────────────────────────────────────────

from schemas.pricing import ApplyDiscountRequest, ApplyDiscountResponse


@router.post("/{order_id}/items/discount", response_model=ApplyDiscountResponse)
def apply_waiter_discount(
    order_id: int,
    body: ApplyDiscountRequest,
    db: Session = Depends(get_db),
    user: User = Depends(get_current_user),
):
    """
    Apply a manual percentage discount to one or more active items.
    Enforces per-waiter and global limits. Logs every application to
    both order_discounts and price_event_log.
    Final item price rounds to nearest 0.10.
    """
    order = db.query(Order).filter(Order.id == order_id).first()
    if not order:
        raise HTTPException(status_code=404, detail="Order not found")
    if not (user.role == "superadmin" or user.perm_access_dashboard or user.perm_apply_discounts):
        raise HTTPException(status_code=403, detail="Δεν έχετε δικαίωμα εφαρμογής εκπτώσεων")
    if not _can_access_order(order, user, db):
        raise HTTPException(status_code=403, detail="Access denied")

    if body.discount_percent <= 0 or body.discount_percent > 100:
        raise HTTPException(status_code=400, detail="discount_percent must be between 0 and 100")

    # Fetch all target items
    items = db.query(OrderItem).filter(
        OrderItem.id.in_(body.item_ids),
        OrderItem.order_id == order_id,
        OrderItem.status == "active",
    ).all()
    if not items:
        raise HTTPException(status_code=404, detail="No active items found for given IDs")

    item_unit_prices = {i.id: i.unit_price + i.price_adjustment for i in items}

    # Validate limits before touching anything
    ok, error_msg = check_waiter_discount_limits(
        waiter_id=user.id,
        order_id=order_id,
        item_ids=body.item_ids,
        discount_percent=body.discount_percent,
        item_unit_prices=item_unit_prices,
        db=db,
    )
    if not ok:
        raise HTTPException(status_code=403, detail=error_msg)

    from models.pricing import PriceEventLog

    applied = []
    for item in items:
        effective_before = item.unit_price + item.price_adjustment
        discount_euro = round(effective_before * body.discount_percent / 100.0, 4)
        new_effective = _system_round(effective_before - discount_euro)
        # Store as price_adjustment delta (keeps unit_price as the original resolver result)
        new_adj = round(new_effective - item.unit_price, 4)

        price_before = effective_before
        price_after = new_effective

        item.price_adjustment = new_adj

        # Write to order_discounts (existing table, extended)
        db.add(OrderDiscount(
            order_id=order_id,
            item_id=item.id,
            discount_type="percent",
            discount_value=body.discount_percent,
            applied_by=user.id,
            reason=body.note,
            price_before=price_before,
            price_after=price_after,
        ))

        # Write to price_event_log (immutable audit spine)
        db.add(PriceEventLog(
            order_id=order_id,
            order_item_id=item.id,
            event_type="waiter_discount",
            price_before=price_before,
            price_after=price_after,
            delta_amount=round(price_after - price_before, 4),
            applied_by_user_id=user.id,
            waiter_note=body.note,
        ))

        applied.append({
            "item_id": item.id,
            "price_before": round(price_before, 2),
            "price_after": round(price_after, 2),
            "delta": round(price_after - price_before, 2),
        })

    db.commit()
    broadcast_sync("order_updated", {
        "order_id": order_id,
        "table_id": order.table_id,
        "status": order.status,
        "action": "discount_applied",
    })

    # Return remaining budget info for UX display
    from models.pricing import WaiterDiscountSettings
    wds = db.query(WaiterDiscountSettings).filter(
        WaiterDiscountSettings.user_id == user.id
    ).first()

    remaining_shift = None
    remaining_workday = None

    if wds:
        from models.business_day import BusinessDay
        from models.shift import WaiterShift

        bd = db.query(BusinessDay).filter(BusinessDay.closed_at == None).first()  # noqa: E711
        if bd and wds.max_total_value_workday is not None:
            from models.order import Order as _Order
            wd_order_ids = [o.id for o in db.query(_Order).filter(_Order.business_day_id == bd.id).all()]
            wd_rows = db.query(OrderDiscount).filter(
                OrderDiscount.applied_by == user.id,
                OrderDiscount.order_id.in_(wd_order_ids),
            ).all()
            used = sum(
                max(0, (r.price_before or 0) - (r.price_after or 0))
                for r in wd_rows
            )
            remaining_workday = round(wds.max_total_value_workday - used, 2)

        shift = db.query(WaiterShift).filter(
            WaiterShift.waiter_id == user.id,
            WaiterShift.ended_at == None,  # noqa: E711
        ).order_by(WaiterShift.started_at.desc()).first()
        if shift and wds.max_total_value_shift is not None:
            from models.order import Order as _Order
            shift_order_ids = [
                o.id for o in db.query(_Order).filter(_Order.opened_at >= shift.started_at).all()
            ]
            shift_rows = db.query(OrderDiscount).filter(
                OrderDiscount.applied_by == user.id,
                OrderDiscount.order_id.in_(shift_order_ids),
            ).all()
            used = sum(
                max(0, (r.price_before or 0) - (r.price_after or 0))
                for r in shift_rows
            )
            remaining_shift = round(wds.max_total_value_shift - used, 2)

    return ApplyDiscountResponse(
        applied=applied,
        remaining_budget_shift=remaining_shift,
        remaining_budget_workday=remaining_workday,
    )


# ─── Cancel a pending print job ──────────────────────────────────────────────

@router.post("/{order_id}/print-jobs/{job_id}/cancel")
def cancel_print_job(
    order_id: int,
    job_id: int,
    db: Session = Depends(get_db),
    user: User = Depends(get_current_user),
):
    """Staff cancels a pending print job (failed print, still retrying)."""
    pj = db.query(PrintJob).filter(PrintJob.id == job_id, PrintJob.order_id == order_id).first()
    if not pj:
        raise HTTPException(status_code=404, detail="Print job not found")
    if pj.status != "pending":
        raise HTTPException(status_code=400, detail="Only pending print jobs can be cancelled")

    order = db.query(Order).filter(Order.id == order_id).first()
    if not order or not _can_access_order(order, user, db):
        raise HTTPException(status_code=403, detail="Access denied")

    from datetime import datetime, timezone
    pj.status = "cancelled"
    pj.cancelled_at = datetime.now(timezone.utc)
    pj.cancel_reason = "staff_cancelled"

    # Mark affected items as printed so the UI warning goes away
    item_ids = json.loads(pj.item_ids or "[]")
    for item in db.query(OrderItem).filter(OrderItem.id.in_(item_ids)).all():
        item.printed = True

    db.commit()
    broadcast_sync("order_updated", {"order_id": order_id, "table_id": order.table_id, "status": order.status, "action": "print_job_cancelled"})
    return {"ok": True, "job_id": job_id}


# ─── Update customer count on an order ───────────────────────────────────────

class CustomerCountRequest(BaseModel):
    customer_count: Optional[int] = None


@router.patch("/{order_id}/customer-count")
def update_customer_count(
    order_id: int,
    body: CustomerCountRequest,
    db: Session = Depends(get_db),
    user: User = Depends(get_current_user),
):
    order = db.query(Order).filter(Order.id == order_id).first()
    if not order:
        raise HTTPException(status_code=404, detail="Order not found")
    if not _can_access_order(order, user, db):
        raise HTTPException(status_code=403, detail="Access denied")

    order.customer_count = body.customer_count
    db.commit()
    broadcast_sync("order_updated", {"order_id": order_id, "table_id": order.table_id, "status": order.status, "action": "customer_count_updated"})
    return {"customer_count": order.customer_count}


# ─── Move selected items to another order ────────────────────────────────────

@router.post("/{order_id}/move-items")
def move_items(
    order_id: int,
    body: MoveItemsRequest,
    db: Session = Depends(get_db),
    user: User = Depends(get_current_user),
):
    """Move specific active items from this order to another open order."""
    source = db.query(Order).filter(Order.id == order_id).first()
    if not source:
        raise HTTPException(status_code=404, detail="Source order not found")
    if not _can_access_order(source, user, db):
        raise HTTPException(status_code=403, detail="Access denied")
    if source.status not in ("open", "partially_paid"):
        raise HTTPException(status_code=400, detail="Source order is not active")

    target = db.query(Order).filter(Order.id == body.target_order_id).first()
    if not target:
        raise HTTPException(status_code=404, detail="Target order not found")
    if not _can_access_order(target, user, db):
        raise HTTPException(status_code=403, detail="Access denied to target order")
    if target.status not in ("open", "partially_paid"):
        raise HTTPException(status_code=400, detail="Target order is not active")
    if source.id == target.id:
        raise HTTPException(status_code=400, detail="Source and target orders are the same")

    items = db.query(OrderItem).filter(
        OrderItem.id.in_(body.item_ids),
        OrderItem.order_id == order_id,
        OrderItem.status == "active",
    ).all()
    if not items:
        raise HTTPException(status_code=400, detail="No active items found to move")

    moved_ids = []
    for item in items:
        item.order_id = target.id
        moved_ids.append(item.id)

    # Recompute source status
    db.flush()
    src_active = db.query(OrderItem).filter(OrderItem.order_id == source.id, OrderItem.status == "active").count()
    src_paid = db.query(OrderItem).filter(OrderItem.order_id == source.id, OrderItem.status == "paid").count()
    if src_active == 0 and src_paid == 0:
        source.status = "open"
    elif src_active == 0:
        source.status = "paid"
    else:
        source.status = "partially_paid" if src_paid > 0 else "open"

    # Recompute target status
    tgt_active = db.query(OrderItem).filter(OrderItem.order_id == target.id, OrderItem.status == "active").count()
    tgt_paid = db.query(OrderItem).filter(OrderItem.order_id == target.id, OrderItem.status == "paid").count()
    target.status = "partially_paid" if (tgt_active > 0 and tgt_paid > 0) else ("paid" if tgt_active == 0 else "open")

    _audit(db, source.id, "ITEMS_MOVED_OUT", waiter_id=user.id, item_ids=moved_ids,
           note=f"Moved to order #{target.id} (table {target.table_id})")
    _audit(db, target.id, "ITEMS_MOVED_IN", waiter_id=user.id, item_ids=moved_ids,
           note=f"Moved from order #{source.id} (table {source.table_id})")

    db.commit()
    db.refresh(source)
    return {"moved_item_ids": moved_ids, "source_status": source.status, "target_status": target.status}


# ─── Print order synopsis ─────────────────────────────────────────────────────

@router.post("/{order_id}/print-synopsis")
def print_synopsis(
    order_id: int,
    body: PrintSynopsisRequest,
    background_tasks: BackgroundTasks,
    db: Session = Depends(get_db),
    user: User = Depends(get_current_user),
):
    from models.printer import Printer

    order = db.query(Order).filter(Order.id == order_id).first()
    if not order:
        raise HTTPException(status_code=404, detail="Order not found")
    if not _can_access_order(order, user, db):
        raise HTTPException(status_code=403, detail="Access denied")

    printer = db.query(Printer).filter(Printer.id == body.printer_id, Printer.is_active == True).first()
    if not printer:
        raise HTTPException(status_code=404, detail="Printer not found or inactive")

    table = db.query(Table).filter(Table.id == order.table_id).first()
    table_name = (table.label or f"T{table.number}") if table else f"#{order.table_id}"
    opener = db.query(User).filter(User.id == order.opened_by).first()
    waiter_name = (opener.nickname or opener.username) if opener else f"#{order.opened_by}"

    items_data = []
    for item in order.items:
        if item.status == "cancelled":
            continue
        product_name = item.product.name if item.product else f"#{item.product_id}"
        adj = getattr(item, 'price_adjustment', 0.0) or 0.0
        effective_price = item.unit_price + adj
        items_data.append({
            "name": product_name,
            "quantity": item.quantity,
            "unit_type": item.product.unit_type if item.product else "piece",
            "unit_price": effective_price,
            "total": effective_price * item.quantity,
            "status": item.status,
        })

    total = sum(i["total"] for i in items_data)
    paid_total = sum(i["total"] for i in items_data if i["status"] == "paid")

    synopsis = {
        "order_id": order.id,
        "table_name": table_name,
        "waiter_name": waiter_name,
        "opened_at": local_strftime(order.opened_at, "%d/%m/%Y %H:%M"),
        "items": items_data,
        "total": total,
        "paid_total": paid_total,
        "remaining": total - paid_total,
    }

    background_tasks.add_task(print_order_synopsis, printer.ip_address, printer.port, synopsis, printer.line_width, printer.codepage_n)
    return {"status": "printing"}


# ─── Phase 2G: Put item on customer tab ───────────────────────────────────────

@router.post("/{order_id}/items/{item_id}/tab")
def put_item_on_tab(
    order_id: int,
    item_id: int,
    db: Session = Depends(get_db),
    user: User = Depends(require_manager),
):
    from models.tabs import Tab, TabEntry

    order = db.query(Order).filter(Order.id == order_id).first()
    if not order:
        raise HTTPException(status_code=404, detail="Order not found")
    if not order.customer_id:
        raise HTTPException(status_code=400, detail="Order has no customer assigned — assign a customer first")

    item = db.query(OrderItem).filter(OrderItem.id == item_id, OrderItem.order_id == order_id).first()
    if not item:
        raise HTTPException(status_code=404, detail="Item not found")
    if item.status != "active":
        raise HTTPException(status_code=400, detail=f"Item is already {item.status} — cannot tab it")

    # Find or auto-create the customer's open tab
    tab = db.query(Tab).filter(Tab.customer_id == order.customer_id, Tab.status == "open").first()
    if not tab:
        tab = Tab(customer_id=order.customer_id)
        db.add(tab)
        db.flush()

    # Build description
    product_name = item.product.name if item.product else f"#{item.product_id}"
    description = f"{product_name} ×{item.quantity} @ €{item.unit_price:.2f}"

    entry = TabEntry(
        tab_id=tab.id,
        order_id=order_id,
        order_item_id=item_id,
        amount=round(item.unit_price * item.quantity, 2),
        description=description,
        created_by_id=user.id,
    )
    db.add(entry)

    # Mark item as tabbed — distinct from active/paid/cancelled
    item.status = "tabbed"

    db.commit()
    broadcast_sync("order_updated", {"order_id": order.id, "table_id": order.table_id, "status": order.status, "action": "item_tabbed"})
    return {"status": "tabbed", "tab_id": tab.id, "entry_amount": entry.amount}
