from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy import func
from sqlalchemy.orm import Session
from typing import Optional
from datetime import datetime, timezone
from pydantic import BaseModel

from database import get_db
from models.business_day import BusinessDay
from models.order import Order, OrderItem, OrderAuditLog, PrintLog
from models.shift import WaiterShift
from models.flag import TableFlagAssignment
from models.message import StaffMessage, StaffMessageAck
from models.table import Table, TableGroup
from models.printer import Printer
from schemas.business_day import BusinessDayOut, OpenBusinessDayRequest, CloseBusinessDayRequest
from routers.deps import get_current_user, require_manager
from models.user import User
from middleware.license_check import license_state

router = APIRouter()


def _dt(dt):
    if dt is None:
        return None
    return (dt.isoformat() + "Z") if dt.tzinfo is None else dt.isoformat()


@router.get("/current", response_model=Optional[BusinessDayOut])
def get_current_business_day(
    db: Session = Depends(get_db),
    user: User = Depends(get_current_user),
):
    return db.query(BusinessDay).filter(BusinessDay.status == "open").first()


@router.post("/open", response_model=BusinessDayOut, status_code=status.HTTP_201_CREATED)
def open_business_day(
    body: OpenBusinessDayRequest,
    db: Session = Depends(get_db),
    user: User = Depends(require_manager),
):
    existing = db.query(BusinessDay).filter(BusinessDay.status == "open").first()
    if existing:
        raise HTTPException(status_code=400, detail="A business day is already open")

    # Gate: admin lock (already enforced or pending)
    if license_state.get("locked") or license_state.get("lock_pending"):
        raise HTTPException(
            status_code=423,
            detail={
                "code": "SYSTEM_LOCKED",
                "message": "Το σύστημα έχει κλειδωθεί από διαχειριστή. Επικοινωνήστε με την υποστήριξη.",
            },
        )

    # Gate: license expired and expiry grace period also over
    if not license_state.get("licensed", True):
        expires_at = license_state.get("expires_at", "")
        raise HTTPException(
            status_code=402,
            detail={
                "code": "LICENSE_EXPIRED",
                "message": "Η άδεια χρήσης έχει λήξει. Ανανεώστε την άδεια ή επικοινωνήστε με την υποστήριξη.",
                "expires_at": expires_at,
            },
        )

    day = BusinessDay(opened_by_id=user.id, notes=body.notes)
    db.add(day)
    db.commit()
    db.refresh(day)
    return day


@router.post("/close", response_model=BusinessDayOut)
def close_business_day(
    body: CloseBusinessDayRequest,
    db: Session = Depends(get_db),
    user: User = Depends(require_manager),
):
    day = db.query(BusinessDay).filter(BusinessDay.status == "open").first()
    if not day:
        raise HTTPException(status_code=404, detail="No open business day")

    open_orders = db.query(Order).filter(
        Order.business_day_id == day.id,
        Order.status.in_(["open", "partially_paid"]),
    ).all()

    if open_orders and not body.force:
        # Count orders that have at least one active (unpaid) item — covers both
        # "open" (fully unpaid) and "partially_paid" (partially unpaid) orders.
        with_pending = sum(
            1 for o in open_orders
            if db.query(OrderItem).filter(
                OrderItem.order_id == o.id,
                OrderItem.status == "active",
            ).first() is not None
        )
        raise HTTPException(
            status_code=409,
            detail={
                "message": f"{len(open_orders)} table(s) still open, {with_pending} with unpaid items.",
                "open_orders": len(open_orders),
                "partially_paid": with_pending,
            },
        )

    now = datetime.now(timezone.utc)

    # Close all non-terminal orders for this business day (open, partially_paid, paid)
    all_unclosed = db.query(Order).filter(
        Order.business_day_id == day.id,
        Order.status.in_(["open", "partially_paid", "paid"]),
    ).all()
    for order in all_unclosed:
        was_unpaid = order.status in ("open", "partially_paid")
        order.status = "closed"
        order.closed_at = now
        order.closed_by = user.id
        if was_unpaid:
            db.add(OrderAuditLog(
                order_id=order.id,
                event_type="ORDER_CLOSED",
                waiter_id=user.id,
                note="Force-closed at end of business day",
            ))

    active_shifts = db.query(WaiterShift).filter(
        WaiterShift.business_day_id == day.id,
        WaiterShift.ended_at == None,
    ).all()
    for shift in active_shifts:
        items = db.query(OrderItem).filter(
            OrderItem.paid_in_shift_id == shift.id,
            OrderItem.status == "paid",
        ).all()
        shift.total_collected = sum(i.unit_price * i.quantity for i in items)
        shift.ended_at = now

    # Clear all table flags and staff messages — fresh slate for the next day
    db.query(TableFlagAssignment).delete(synchronize_session=False)
    db.query(StaffMessageAck).delete(synchronize_session=False)
    db.query(StaffMessage).delete(synchronize_session=False)

    day.status = "closed"
    day.closed_at = now
    day.closed_by_id = user.id
    if body.notes:
        day.notes = body.notes

    # Phase 2E: store-level cash reconciliation
    if body.store_closing_cash is not None:
        day.store_closing_cash = body.store_closing_cash
        # expected = store_opening_cash + sum of all waiter collected amounts this day
        all_shifts = db.query(WaiterShift).filter(WaiterShift.business_day_id == day.id).all()
        total_collected = sum(s.total_collected or 0.0 for s in all_shifts)
        expected_closing = (day.store_opening_cash or 0.0) + total_collected
        day.store_cash_discrepancy = round(body.store_closing_cash - expected_closing, 2)

    db.commit()
    db.refresh(day)

    # Deferred lock: if cloud requested a lock while the workday was open,
    # enforce it now that the day has closed.
    if license_state.get("lock_pending"):
        license_state["lock_pending"] = False
        license_state["locked"] = True
        from services.cloud_sync import _persist_state
        _persist_state()

    return day


class PatchBusinessDayRequest(BaseModel):
    closed_at: str  # ISO-8601 datetime string


@router.patch("/{day_id}", status_code=status.HTTP_200_OK)
def patch_business_day(
    day_id: int,
    body: "PatchBusinessDayRequest",
    db: Session = Depends(get_db),
    user: User = Depends(require_manager),
):
    """Edit the close-time of a past (closed) business day.
    Constraints:
    - Day must already be closed.
    - new closed_at must be ≥ the latest order closed_at in this day.
    - new closed_at must be ≤ now (UTC).
    """
    day = db.query(BusinessDay).filter(BusinessDay.id == day_id).first()
    if not day:
        raise HTTPException(status_code=404, detail="Business day not found")
    if day.status != "closed":
        raise HTTPException(status_code=400, detail="Can only edit a closed business day")

    try:
        new_closed = datetime.fromisoformat(body.closed_at.replace("Z", "+00:00"))
        if new_closed.tzinfo is None:
            new_closed = new_closed.replace(tzinfo=timezone.utc)
    except ValueError:
        raise HTTPException(status_code=422, detail="Invalid datetime format")

    now = datetime.now(timezone.utc)
    if new_closed > now:
        raise HTTPException(status_code=400, detail="Close time cannot be in the future")

    # Must not be earlier than the latest order closed_at in this day
    last_order_close = (
        db.query(func.max(Order.closed_at))
        .filter(Order.business_day_id == day_id, Order.closed_at != None)
        .scalar()
    )
    if last_order_close:
        if last_order_close.tzinfo is None:
            last_order_close = last_order_close.replace(tzinfo=timezone.utc)
        if new_closed < last_order_close:
            raise HTTPException(
                status_code=400,
                detail=f"Close time cannot be earlier than the last order's close time ({_dt(last_order_close)})"
            )

    day.closed_at = new_closed
    db.commit()
    return {"id": day.id, "closed_at": _dt(day.closed_at)}


@router.delete("/{day_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_business_day(
    day_id: int,
    db: Session = Depends(get_db),
    user: User = Depends(require_manager),
):
    """Permanently delete a business day. Only allowed when it has no shifts."""
    day = db.query(BusinessDay).filter(BusinessDay.id == day_id).first()
    if not day:
        raise HTTPException(status_code=404, detail="Business day not found")
    if day.status == "open":
        raise HTTPException(status_code=400, detail="Cannot delete an open business day — close it first")

    shift_count = db.query(WaiterShift).filter(WaiterShift.business_day_id == day_id).count()
    if shift_count > 0:
        raise HTTPException(
            status_code=409,
            detail=f"Δεν είναι δυνατή η διαγραφή: η εργάσιμη μέρα έχει {shift_count} βάρδια/ες. Διαγράψτε πρώτα όλες τις βάρδιες.",
        )

    db.query(Order).filter(Order.business_day_id == day_id).update(
        {"business_day_id": None}, synchronize_session=False
    )
    db.delete(day)
    db.commit()


@router.get("/summary")
def business_day_summary(
    day_id: Optional[int] = None,
    db: Session = Depends(get_db),
    user: User = Depends(require_manager),
):
    """Full summary for a business day: per-waiter, per-zone, per-printer, and checks.
    If day_id is omitted, uses the currently open business day."""
    if day_id:
        day = db.query(BusinessDay).filter(BusinessDay.id == day_id).first()
        if not day:
            raise HTTPException(status_code=404, detail="Business day not found")
    else:
        day = db.query(BusinessDay).filter(BusinessDay.status == "open").first()
        if not day:
            raise HTTPException(status_code=404, detail="No open business day")

    orders = db.query(Order).filter(Order.business_day_id == day.id).all()
    order_ids = [o.id for o in orders]

    all_items = db.query(OrderItem).filter(OrderItem.order_id.in_(order_ids)).all() if order_ids else []
    paid_items = [i for i in all_items if i.status == "paid"]
    active_items = [i for i in all_items if i.status == "active"]

    shifts = db.query(WaiterShift).filter(WaiterShift.business_day_id == day.id).all()
    users_map = {u.id: u for u in db.query(User).all()}
    tables_map = {t.id: t for t in db.query(Table).all()}
    groups_map = {g.id: g for g in db.query(TableGroup).all()}
    printers_map = {p.id: p for p in db.query(Printer).all()}

    # ── Per-waiter summary ──────────────────────────────────────────────────
    waiter_stats: dict = {}

    def _waiter_entry(uid):
        if uid not in waiter_stats:
            u = users_map.get(uid)
            name = (u.full_name or u.nickname or u.username) if u else f"#{uid}"
            role = u.role if u else "waiter"
            waiter_stats[uid] = {
                "waiter_id": uid,
                "waiter_name": name,
                "role": role,
                "orders_opened": 0,
                "items_ordered": 0,
                "total_items_value": 0.0,
                "items_paid": 0,
                "total_collected": 0.0,
                "cash": 0.0,
                "card": 0.0,
                "transfer": 0.0,
                "treat": 0.0,
                "other": 0.0,
                "shift_started_at": None,
                "shift_ended_at": None,
                "starting_cash": None,
                # Phase 2B payroll
                "hourly_rate_snapshot": None,
                "duration_hours": None,
                "shift_pay": None,
                # distinct tables served (not order count)
                "tables_served": 0,
            }
        return waiter_stats[uid]

    # track unique tables per waiter (opened_by)
    waiter_tables: dict = {}  # uid → set of table_ids

    for o in orders:
        if o.status == "cancelled":
            continue
        e = _waiter_entry(o.opened_by)
        e["orders_opened"] += 1
        if o.table_id:
            waiter_tables.setdefault(o.opened_by, set()).add(o.table_id)

    for item in all_items:
        if item.status == "cancelled":
            continue
        e = _waiter_entry(item.added_by)
        e["items_ordered"] += item.quantity
        e["total_items_value"] += item.unit_price * item.quantity

    for item in paid_items:
        if item.paid_by:
            e = _waiter_entry(item.paid_by)
            val = item.unit_price * item.quantity
            e["items_paid"] += item.quantity
            e["total_collected"] += val
            method = (item.payment_method or "cash").lower()
            if method == "cash":
                e["cash"] += val
            elif method == "card":
                e["card"] += val
            elif method == "transfer":
                e["transfer"] += val
            elif method in ("treat", "comp"):
                e["treat"] += val
            else:
                e["other"] += val

    from routers.shifts import compute_shift_pay
    for shift in shifts:
        e = _waiter_entry(shift.waiter_id)
        if e["shift_started_at"] is None or (shift.started_at and _dt(shift.started_at) < e["shift_started_at"]):
            e["shift_started_at"] = _dt(shift.started_at)
            if shift.starting_cash is not None:
                e["starting_cash"] = shift.starting_cash
        if shift.ended_at:
            ended = _dt(shift.ended_at)
            if e["shift_ended_at"] is None or ended > e["shift_ended_at"]:
                e["shift_ended_at"] = ended
        # payroll — take the most recent non-null snapshot
        if shift.hourly_rate_snapshot is not None:
            e["hourly_rate_snapshot"] = shift.hourly_rate_snapshot
        pay_data = compute_shift_pay(shift)
        e["duration_hours"] = round((e["duration_hours"] or 0) + pay_data["duration_hours"], 2)
        if pay_data["shift_pay"] is not None:
            e["shift_pay"] = round((e["shift_pay"] or 0) + pay_data["shift_pay"], 2)

    # write tables_served counts
    for uid, table_set in waiter_tables.items():
        if uid in waiter_stats:
            waiter_stats[uid]["tables_served"] = len(table_set)

    # manager gets shift = day open/close
    manager_opener = users_map.get(day.opened_by_id)
    if manager_opener:
        e = _waiter_entry(day.opened_by_id)
        if e["shift_started_at"] is None:
            e["shift_started_at"] = _dt(day.opened_at)
        if e["shift_ended_at"] is None and day.closed_at:
            e["shift_ended_at"] = _dt(day.closed_at)

    # ── Per-zone summary ────────────────────────────────────────────────────
    zone_stats: dict = {}  # group_id → stats

    def _zone_entry(gid, gname, gcolor):
        if gid not in zone_stats:
            zone_stats[gid] = {
                "group_id": gid,
                "group_name": gname,
                "color": gcolor,
                "orders": 0,
                "items": 0,
                "revenue": 0.0,
            }
        return zone_stats[gid]

    for o in orders:
        if o.status == "cancelled" or not o.table_id:
            continue
        table = tables_map.get(o.table_id)
        group = groups_map.get(table.group_id) if table and table.group_id else None
        gid = group.id if group else 0
        gname = group.name if group else "Χωρίς Ζώνη"
        gcolor = group.color if group else "#9ca3af"
        entry = _zone_entry(gid, gname, gcolor)
        entry["orders"] += 1
        for item in o.items:
            if item.status == "cancelled":
                continue
            entry["items"] += item.quantity
            if item.status == "paid":
                entry["revenue"] += item.unit_price * item.quantity

    # ── Per-printer summary ─────────────────────────────────────────────────
    # Build item_id → value lookup for revenue-per-printer calculation
    items_value_map = {i.id: i.unit_price * i.quantity for i in all_items if i.status != "cancelled"}

    printer_stats: dict = {}
    if order_ids:
        logs = db.query(PrintLog).filter(PrintLog.order_id.in_(order_ids)).all()
        for log in logs:
            pid = log.printer_id
            if pid not in printer_stats:
                pr = printers_map.get(pid)
                printer_stats[pid] = {
                    "printer_id": pid,
                    "printer_name": pr.name if pr else f"#{pid}",
                    "ip_address": pr.ip_address if pr else "",
                    "jobs": 0,
                    "success": 0,
                    "failed": 0,
                    "items_value": 0.0,
                }
            printer_stats[pid]["jobs"] += 1
            if log.success:
                printer_stats[pid]["success"] += 1
                try:
                    import json as _json
                    item_ids = _json.loads(log.item_ids) if log.item_ids else []
                    for iid in item_ids:
                        printer_stats[pid]["items_value"] += items_value_map.get(iid, 0.0)
                except Exception:
                    pass
            else:
                printer_stats[pid]["failed"] += 1

    for pid in printer_stats:
        printer_stats[pid]["items_value"] = round(printer_stats[pid]["items_value"], 2)

    # ── Checks ──────────────────────────────────────────────────────────────
    open_orders = [o for o in orders if o.status in ("open", "partially_paid")]
    with_unpaid_items = sum(
        1 for o in open_orders
        if any(i.status == "active" for i in o.items)
    )
    active_shift_count = sum(1 for s in shifts if s.ended_at is None)

    total_revenue = sum(i.unit_price * i.quantity for i in paid_items)
    total_items_ordered = sum(i.quantity for i in all_items if i.status != "cancelled")
    total_orders = sum(1 for o in orders if o.status != "cancelled")

    # Round float fields in waiter stats
    for w in waiter_stats.values():
        for key in ("total_collected", "total_items_value", "cash", "card", "transfer", "treat", "other"):
            w[key] = round(w[key], 2)

    # Phase 2E: store-level cash totals for summary view
    total_waiter_collected = sum(s.total_collected or 0.0 for s in shifts)
    store_expected_closing = (day.store_opening_cash or 0.0) + total_waiter_collected

    # ── Phase 2K: full financial summary ──────────────��─────────────────────

    # COGS from Phase 2A cost snapshots on paid items
    from models.waste import WasteLog
    from models.expenses import Expense
    from models.tabs import Tab, TabEntry

    cogs_trackable = 0.0
    cogs_uncosted_revenue = 0.0
    cogs_uncosted_count = 0
    for item in paid_items:
        rev = item.unit_price * item.quantity
        if item.unit_cost is not None:
            cogs_trackable += item.unit_cost * item.quantity
        else:
            cogs_uncosted_revenue += rev
            cogs_uncosted_count += 1
    gross_profit = round(total_revenue - cogs_trackable, 2)
    cogs_has_gap = cogs_uncosted_count > 0

    # LABOR from shifts
    total_labor_cost = 0.0
    labor_untracked_shifts = 0
    for shift in shifts:
        pay_data = compute_shift_pay(shift)
        if pay_data["shift_pay"] is not None:
            total_labor_cost += pay_data["shift_pay"]
        else:
            labor_untracked_shifts += 1

    # WASTE from Phase 2H
    waste_entries = db.query(WasteLog).filter(WasteLog.business_day_id == day.id).all()
    waste_item_count = len(waste_entries)
    waste_estimated_cost = round(sum(w.total_cost for w in waste_entries if w.total_cost is not None), 2)

    # EXPENSES logged today
    expenses_today = db.query(Expense).filter(Expense.business_day_id == day.id).all()
    expenses_total = round(sum(e.total_amount for e in expenses_today), 2)
    expenses_paid_today = round(sum(e.paid_amount for e in expenses_today), 2)
    expenses_due = round(expenses_total - expenses_paid_today, 2)

    # TABBED REVENUE (items moved to tabs — not yet collected)
    tabbed_items = [i for i in all_items if i.status == "tabbed"]
    tabbed_revenue = round(sum(i.unit_price * i.quantity for i in tabbed_items), 2)

    # NET estimate (revenue - COGS - labor - waste - expenses paid)
    net_estimate = round(
        total_revenue
        - cogs_trackable
        - total_labor_cost
        - waste_estimated_cost
        - expenses_paid_today,
        2
    )
    net_has_unknowns = cogs_has_gap or labor_untracked_shifts > 0

    return {
        "day_id": day.id,
        "status": day.status,
        "opened_at": _dt(day.opened_at),
        "closed_at": _dt(day.closed_at),
        "total_revenue": round(total_revenue, 2),
        "total_orders": total_orders,
        "total_items_ordered": total_items_ordered,
        "waiters": sorted(waiter_stats.values(), key=lambda x: -x["total_collected"]),
        "zones": sorted(zone_stats.values(), key=lambda x: -x["revenue"]),
        "printers": list(printer_stats.values()),
        "checks": {
            "open_orders": len(open_orders),
            "with_unpaid_items": with_unpaid_items,
            "active_shifts": active_shift_count,
        },
        # Phase 2E — cash reconciliation
        "store_opening_cash": day.store_opening_cash,
        "store_closing_cash": day.store_closing_cash,
        "store_cash_discrepancy": day.store_cash_discrepancy,
        "store_expected_closing": round(store_expected_closing, 2),
        "total_waiter_collected": round(total_waiter_collected, 2),
        # Phase 2K — full financial summary
        "cogs_trackable": round(cogs_trackable, 2),
        "cogs_uncosted_revenue": round(cogs_uncosted_revenue, 2),
        "cogs_uncosted_count": cogs_uncosted_count,
        "cogs_has_gap": cogs_has_gap,
        "gross_profit": gross_profit,
        "total_labor_cost": round(total_labor_cost, 2),
        "labor_untracked_shifts": labor_untracked_shifts,
        "waste_item_count": waste_item_count,
        "waste_estimated_cost": waste_estimated_cost,
        "expenses_total": expenses_total,
        "expenses_paid_today": expenses_paid_today,
        "expenses_due": expenses_due,
        "tabbed_revenue": tabbed_revenue,
        "net_estimate": net_estimate,
        "net_has_unknowns": net_has_unknowns,
    }


@router.get("/history")
def business_day_history(
    db: Session = Depends(get_db),
    user: User = Depends(require_manager),
):
    days = db.query(BusinessDay).order_by(BusinessDay.opened_at.desc()).all()
    result = []
    for day in days:
        order_count = db.query(Order).filter(Order.business_day_id == day.id).count()
        revenue = (
            db.query(func.sum(OrderItem.unit_price * OrderItem.quantity))
            .join(Order)
            .filter(Order.business_day_id == day.id, OrderItem.status == "paid")
            .scalar() or 0.0
        )
        w_opener = day.opener
        w_closer = day.closer
        result.append({
            "id": day.id,
            "status": day.status,
            "opened_at": _dt(day.opened_at),
            "closed_at": _dt(day.closed_at),
            "opened_by_id": day.opened_by_id,
            "opened_by_name": (w_opener.full_name or w_opener.username) if w_opener else None,
            "closed_by_id": day.closed_by_id,
            "closed_by_name": (w_closer.full_name or w_closer.username) if w_closer else None,
            "notes": day.notes,
            "order_count": order_count,
            "revenue": round(revenue, 2),
        })
    return {"business_days": result}
