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

from database import get_db
from models.shift import WaiterShift, ShiftBreak
from models.business_day import BusinessDay
from models.order import OrderItem
from models.settings import PosSettings
from models.user import User
from schemas.shift import StartShiftRequest, EndShiftRequest
from routers.deps import get_current_user, require_manager

router = APIRouter()


def _dt(dt):
    """Serialize a naive-UTC datetime to ISO string with Z so JS parses it as UTC."""
    if dt is None:
        return None
    return (dt.isoformat() + "Z") if dt.tzinfo is None else dt.isoformat()


def _get_setting(db: Session, key: str, default: str = "true") -> str:
    s = db.query(PosSettings).filter(PosSettings.key == key).first()
    return s.value if s else default


def compute_shift_total(shift_id: int, db: Session) -> float:
    items = db.query(OrderItem).filter(
        OrderItem.paid_in_shift_id == shift_id,
        OrderItem.status == "paid",
    ).all()
    return round(sum(i.unit_price * i.quantity for i in items), 2)


def compute_shift_payment_split(shift_id: int, db: Session) -> dict:
    """Return cash_sales and card_sales for a shift (items paid in this shift)."""
    items = db.query(OrderItem).filter(
        OrderItem.paid_in_shift_id == shift_id,
        OrderItem.status == "paid",
    ).all()
    cash = 0.0
    card = 0.0
    for i in items:
        amount = float(i.unit_price) * i.quantity
        method = (i.payment_method or "cash").lower()
        if method == "card":
            card += amount
        else:
            cash += amount
    return {"cash_sales": round(cash, 2), "card_sales": round(card, 2)}


def compute_shift_pay(shift: WaiterShift) -> dict:
    """Return duration_hours and shift_pay. shift_pay is None if no rate snapshot."""
    now = datetime.now(timezone.utc)
    start = shift.started_at
    if start and start.tzinfo is None:
        start = start.replace(tzinfo=timezone.utc)
    end = shift.ended_at
    if end and end.tzinfo is None:
        end = end.replace(tzinfo=timezone.utc)
    reference = end or now

    total_seconds = (reference - start).total_seconds() if start else 0

    # Subtract completed breaks
    break_seconds = 0
    for b in shift.breaks:
        bs = b.started_at
        be = b.ended_at
        if bs and be:
            if bs.tzinfo is None:
                bs = bs.replace(tzinfo=timezone.utc)
            if be.tzinfo is None:
                be = be.replace(tzinfo=timezone.utc)
            break_seconds += (be - bs).total_seconds()

    worked_seconds = max(0, total_seconds - break_seconds)
    duration_hours = round(worked_seconds / 3600, 4)

    shift_pay = None
    if shift.hourly_rate_snapshot is not None:
        shift_pay = round(duration_hours * shift.hourly_rate_snapshot, 2)

    return {"duration_hours": round(duration_hours, 2), "shift_pay": shift_pay}


def _enrich_shift(shift: WaiterShift, db: Session) -> dict:
    w = shift.waiter
    wname = (w.full_name or w.username) if w else f"#{shift.waiter_id}"
    total = compute_shift_total(shift.id, db) if shift.ended_at is None else (shift.total_collected or 0.0)
    split = compute_shift_payment_split(shift.id, db)
    pay_data = compute_shift_pay(shift)
    # Count cancelled items and their value attributed to this waiter during shift window
    cancelled_q = db.query(OrderItem).filter(
        OrderItem.status == "cancelled",
        OrderItem.added_by == shift.waiter_id,
        OrderItem.added_at >= shift.started_at,
    )
    if shift.ended_at:
        cancelled_q = cancelled_q.filter(OrderItem.added_at <= shift.ended_at)
    cancelled_rows = cancelled_q.all()
    cancellation_events = len(cancelled_rows)
    cancellations = sum(r.quantity for r in cancelled_rows)
    cancellation_value = round(sum(r.unit_price * r.quantity for r in cancelled_rows), 2)
    return {
        "id": shift.id,
        "waiter_id": shift.waiter_id,
        "waiter_name": wname,
        "business_day_id": shift.business_day_id,
        "started_at": _dt(shift.started_at),
        "ended_at": _dt(shift.ended_at),
        "starting_cash": shift.starting_cash,
        "total_collected": total,
        "cash_sales": split["cash_sales"],
        "card_sales": split["card_sales"],
        "net_to_deliver": round(split["cash_sales"] + (shift.starting_cash or 0.0), 2),
        "is_active": shift.ended_at is None,
        "notes": shift.notes,
        "hourly_rate_snapshot": shift.hourly_rate_snapshot,
        "duration_hours": pay_data["duration_hours"],
        "shift_pay": pay_data["shift_pay"],
        "cancellation_events": cancellation_events,
        "cancellations": cancellations,
        "cancellation_value": cancellation_value,
        # Phase 2E — discrepancy computed live against cash-only expected (starting_cash + cash_sales)
        "counted_cash_end": shift.counted_cash_end,
        "cash_discrepancy": round(
            shift.counted_cash_end - (split["cash_sales"] + (shift.starting_cash or 0.0)), 2
        ) if shift.counted_cash_end is not None else None,
        "breaks": [
            {"id": b.id, "shift_id": b.shift_id, "started_at": _dt(b.started_at), "ended_at": _dt(b.ended_at)}
            for b in shift.breaks
        ],
    }


@router.get("/my")
def my_shift(db: Session = Depends(get_db), user: User = Depends(get_current_user)):
    shift = db.query(WaiterShift).filter(
        WaiterShift.waiter_id == user.id,
        WaiterShift.ended_at == None,
    ).first()
    return _enrich_shift(shift, db) if shift else None


@router.post("/start", status_code=status.HTTP_201_CREATED)
def start_shift(
    body: StartShiftRequest,
    db: Session = Depends(get_db),
    user: User = Depends(get_current_user),
):
    target_id = body.waiter_id

    if target_id and target_id != user.id:
        if not user.perm_access_dashboard and user.role != "superadmin":
            raise HTTPException(status_code=403, detail="Only managers can start shifts for other staff")
        target = db.query(User).filter(User.id == target_id, User.is_active == True).first()
        if not target:
            raise HTTPException(status_code=404, detail="Staff member not found")
    else:
        target_id = user.id
        if not user.perm_access_dashboard and user.role != "superadmin" and _get_setting(db, "shifts.waiter_self_start") != "true":
            raise HTTPException(status_code=403, detail="Shift start requires manager confirmation")

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

    existing = db.query(WaiterShift).filter(
        WaiterShift.waiter_id == target_id,
        WaiterShift.ended_at == None,
    ).first()
    if existing:
        raise HTTPException(status_code=400, detail="Waiter already has an active shift")

    target_user = db.query(User).filter(User.id == target_id).first()
    shift = WaiterShift(
        waiter_id=target_id,
        business_day_id=active_day.id,
        starting_cash=body.starting_cash,
        hourly_rate_snapshot=target_user.hourly_rate if target_user else None,
    )
    db.add(shift)
    db.commit()
    db.refresh(shift)
    return _enrich_shift(shift, db)


@router.post("/end")
def end_shift(
    body: EndShiftRequest,
    db: Session = Depends(get_db),
    user: User = Depends(get_current_user),
):
    if not user.perm_access_dashboard and user.role != "superadmin" and _get_setting(db, "shifts.waiter_self_end") != "true":
        raise HTTPException(status_code=403, detail="Shift end requires manager confirmation")

    shift = db.query(WaiterShift).filter(
        WaiterShift.waiter_id == user.id,
        WaiterShift.ended_at == None,
    ).first()
    if not shift:
        raise HTTPException(status_code=404, detail="No active shift found")

    now = datetime.now(timezone.utc)
    total = compute_shift_total(shift.id, db)
    shift.total_collected = total
    shift.ended_at = now
    if body.notes:
        shift.notes = body.notes
    # Phase 2E: cash reconciliation — store counted amount; discrepancy computed live in _enrich_shift
    if body.counted_cash_end is not None:
        shift.counted_cash_end = body.counted_cash_end

    open_break = db.query(ShiftBreak).filter(
        ShiftBreak.shift_id == shift.id, ShiftBreak.ended_at == None
    ).first()
    if open_break:
        open_break.ended_at = now

    db.commit()
    db.refresh(shift)
    return _enrich_shift(shift, db)


@router.post("/manager/start", status_code=status.HTTP_201_CREATED)
def manager_start_shift(
    body: StartShiftRequest,
    db: Session = Depends(get_db),
    user: User = Depends(require_manager),
):
    if not body.waiter_id:
        raise HTTPException(status_code=400, detail="waiter_id is required")

    target = db.query(User).filter(User.id == body.waiter_id, User.is_active == True).first()
    if not target:
        raise HTTPException(status_code=404, detail="Waiter not found")

    active_day = db.query(BusinessDay).filter(BusinessDay.status == "open").first()
    if not active_day:
        raise HTTPException(status_code=400, detail="No open business day")

    existing = db.query(WaiterShift).filter(
        WaiterShift.waiter_id == body.waiter_id,
        WaiterShift.ended_at == None,
    ).first()
    if existing:
        raise HTTPException(status_code=400, detail="Waiter already has an active shift")

    shift = WaiterShift(
        waiter_id=body.waiter_id,
        business_day_id=active_day.id,
        starting_cash=body.starting_cash,
        hourly_rate_snapshot=target.hourly_rate,
    )
    db.add(shift)
    db.commit()
    db.refresh(shift)
    return _enrich_shift(shift, db)


@router.post("/manager/end/{shift_id}")
def manager_end_shift(
    shift_id: int,
    body: EndShiftRequest,
    db: Session = Depends(get_db),
    user: User = Depends(require_manager),
):
    shift = db.query(WaiterShift).filter(
        WaiterShift.id == shift_id,
        WaiterShift.ended_at == None,
    ).first()
    if not shift:
        raise HTTPException(status_code=404, detail="Active shift not found")

    now = datetime.now(timezone.utc)
    total = compute_shift_total(shift.id, db)
    shift.total_collected = total
    shift.ended_at = now
    if body.notes:
        shift.notes = body.notes
    # Phase 2E: cash reconciliation — store counted amount; discrepancy computed live in _enrich_shift
    if body.counted_cash_end is not None:
        shift.counted_cash_end = body.counted_cash_end

    open_break = db.query(ShiftBreak).filter(
        ShiftBreak.shift_id == shift.id, ShiftBreak.ended_at == None
    ).first()
    if open_break:
        open_break.ended_at = now

    db.commit()
    db.refresh(shift)
    return _enrich_shift(shift, db)


@router.post("/{shift_id}/break/start")
def start_break(
    shift_id: int,
    db: Session = Depends(get_db),
    user: User = Depends(get_current_user),
):
    shift = db.query(WaiterShift).filter(WaiterShift.id == shift_id).first()
    if not shift:
        raise HTTPException(status_code=404, detail="Shift not found")
    if shift.waiter_id != user.id and not user.perm_access_dashboard and user.role != "superadmin":
        raise HTTPException(status_code=403, detail="Access denied")
    if shift.ended_at:
        raise HTTPException(status_code=400, detail="Shift already ended")

    open_break = db.query(ShiftBreak).filter(
        ShiftBreak.shift_id == shift_id, ShiftBreak.ended_at == None
    ).first()
    if open_break:
        raise HTTPException(status_code=400, detail="Break already in progress")

    b = ShiftBreak(shift_id=shift_id)
    db.add(b)
    db.commit()
    db.refresh(b)
    return {"id": b.id, "shift_id": b.shift_id, "started_at": _dt(b.started_at), "ended_at": _dt(b.ended_at)}


@router.post("/{shift_id}/break/end")
def end_break(
    shift_id: int,
    db: Session = Depends(get_db),
    user: User = Depends(get_current_user),
):
    shift = db.query(WaiterShift).filter(WaiterShift.id == shift_id).first()
    if not shift:
        raise HTTPException(status_code=404, detail="Shift not found")
    if shift.waiter_id != user.id and not user.perm_access_dashboard and user.role != "superadmin":
        raise HTTPException(status_code=403, detail="Access denied")

    open_break = db.query(ShiftBreak).filter(
        ShiftBreak.shift_id == shift_id, ShiftBreak.ended_at == None
    ).first()
    if not open_break:
        raise HTTPException(status_code=404, detail="No active break found")

    open_break.ended_at = datetime.now(timezone.utc)
    db.commit()
    db.refresh(open_break)
    return {"id": open_break.id, "shift_id": open_break.shift_id, "started_at": _dt(open_break.started_at), "ended_at": _dt(open_break.ended_at)}


@router.get("/")
def list_shifts(
    waiter_id: Optional[int] = None,
    business_day_id: Optional[int] = None,
    active_only: bool = False,
    db: Session = Depends(get_db),
    user: User = Depends(require_manager),
):
    q = db.query(WaiterShift)
    if waiter_id:
        q = q.filter(WaiterShift.waiter_id == waiter_id)
    if business_day_id:
        q = q.filter(WaiterShift.business_day_id == business_day_id)
    if active_only:
        q = q.filter(WaiterShift.ended_at == None)
    shifts = q.order_by(WaiterShift.started_at.desc()).all()
    return {"shifts": [_enrich_shift(s, db) for s in shifts]}


@router.get("/{shift_id}")
def get_shift(
    shift_id: int,
    db: Session = Depends(get_db),
    user: User = Depends(require_manager),
):
    shift = db.query(WaiterShift).filter(WaiterShift.id == shift_id).first()
    if not shift:
        raise HTTPException(status_code=404, detail="Shift not found")
    return _enrich_shift(shift, db)


@router.delete("/{shift_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_shift(
    shift_id: int,
    db: Session = Depends(get_db),
    user: User = Depends(require_manager),
):
    """Permanently delete a shift, its breaks, and all orders opened during that shift."""
    from models.order import Order, OrderItem, OrderAuditLog, OrderWaiter
    import json as _json

    shift = db.query(WaiterShift).filter(WaiterShift.id == shift_id).first()
    if not shift:
        raise HTTPException(status_code=404, detail="Shift not found")
    if shift.ended_at is None:
        raise HTTPException(status_code=400, detail="Cannot delete an active shift — end it first")

    # Find all orders opened during this shift's time window by this waiter
    orders = db.query(Order).filter(
        Order.opened_by == shift.waiter_id,
        Order.opened_at >= shift.started_at,
        Order.opened_at <= shift.ended_at,
    ).all()

    for order in orders:
        db.query(OrderAuditLog).filter(OrderAuditLog.order_id == order.id).delete(synchronize_session=False)
        db.query(OrderWaiter).filter(OrderWaiter.order_id == order.id).delete(synchronize_session=False)
        db.query(OrderItem).filter(OrderItem.order_id == order.id).delete(synchronize_session=False)
        db.delete(order)

    db.delete(shift)
    db.commit()


@router.get("/{shift_id}/summary")
def get_shift_summary(
    shift_id: int,
    db: Session = Depends(get_db),
    user: User = Depends(require_manager),
):
    """Full shift summary with complete per-item waiter attribution.

    Returns all items that either:
    - were paid in this shift (paid_in_shift_id == shift_id), OR
    - were added by this waiter (added_by == waiter_id) and are active/unpaid or paid in another shift.

    Each item includes added_by_name, paid_by_name, timestamps, and order context.
    Orders include opener_name and closer_name.
    """
    from models.order import Order
    from models.table import Table
    from sqlalchemy.orm import joinedload

    shift = db.query(WaiterShift).filter(WaiterShift.id == shift_id).first()
    if not shift:
        raise HTTPException(status_code=404, detail="Shift not found")

    waiter_id = shift.waiter_id

    # Collect all relevant items: paid in this shift OR added by this waiter (any status)
    paid_items = db.query(OrderItem).options(
        joinedload(OrderItem.product),
        joinedload(OrderItem.order),
    ).filter(
        OrderItem.paid_in_shift_id == shift_id,
        OrderItem.status == "paid",
    ).all()

    from datetime import timezone as tz
    upper_bound = shift.ended_at or datetime.now(tz.utc)
    ordered_items = db.query(OrderItem).options(
        joinedload(OrderItem.product),
        joinedload(OrderItem.order),
    ).filter(
        OrderItem.added_by == waiter_id,
        OrderItem.added_at >= shift.started_at,
        OrderItem.added_at <= upper_bound,
        (OrderItem.paid_in_shift_id != shift_id) | (OrderItem.paid_in_shift_id == None),
    ).all()

    # Deduplicate: union by item id, paid_items takes precedence
    items_by_id: dict[int, OrderItem] = {}
    for item in ordered_items:
        items_by_id[item.id] = item
    for item in paid_items:
        items_by_id[item.id] = item
    all_items = list(items_by_id.values())

    # Also include items cancelled within this shift window (may not be in ordered_items
    # if they were cancelled after the shift ended or deduped out)
    cancelled_items_for_summary = db.query(OrderItem).options(
        joinedload(OrderItem.product),
        joinedload(OrderItem.order),
    ).filter(
        OrderItem.added_by == waiter_id,
        OrderItem.status == "cancelled",
        OrderItem.added_at >= shift.started_at,
        OrderItem.added_at <= upper_bound,
    ).all()
    for item in cancelled_items_for_summary:
        if item.id not in items_by_id:
            items_by_id[item.id] = item
    all_items = list(items_by_id.values())

    # Build lookup maps
    all_waiter_ids = set()
    for item in all_items:
        all_waiter_ids.add(item.added_by)
        if item.paid_by:
            all_waiter_ids.add(item.paid_by)
    all_order_ids = {item.order_id for item in all_items}

    # Load orders with opener/closer
    orders_db: dict[int, Order] = {}
    if all_order_ids:
        for o in db.query(Order).filter(Order.id.in_(all_order_ids)).all():
            orders_db[o.id] = o
            if o.opened_by:
                all_waiter_ids.add(o.opened_by)
            if o.closed_by:
                all_waiter_ids.add(o.closed_by)

    # Load table names
    table_ids = {o.table_id for o in orders_db.values() if o.table_id}
    tables_map: dict[int, str] = {}
    if table_ids:
        for t in db.query(Table).filter(Table.id.in_(table_ids)).all():
            prefix = (t.group.prefix if t.group and t.group.prefix else "") if t.group else ""
            tables_map[t.id] = t.label if t.label else f"{prefix}{t.number}"

    # Load waiter names
    waiters_map: dict[int, str] = {}
    if all_waiter_ids:
        for w in db.query(User).filter(User.id.in_(all_waiter_ids)).all():
            waiters_map[w.id] = w.full_name or w.username

    def _wname(wid):
        if wid is None:
            return None
        return waiters_map.get(wid, f"#{wid}")

    # Build orders dict
    orders_out: dict[int, dict] = {}
    for oid, o in orders_db.items():
        tid = o.table_id
        orders_out[oid] = {
            "order_id": oid,
            "table_id": tid,
            "table_name": tables_map.get(tid) if tid else None,
            "opened_at": _dt(o.opened_at),
            "closed_at": _dt(o.closed_at),
            "opener_name": _wname(o.opened_by),
            "closer_name": _wname(o.closed_by),
            "status": o.status,
            "items": [],
        }

    # Attach items to their orders
    for item in all_items:
        oid = item.order_id
        if oid not in orders_out:
            continue
        orders_out[oid]["items"].append({
            "id": item.id,
            "product_name": item.product.name if item.product else f"#{item.product_id}",
            "quantity": item.quantity,
            "unit_price": float(item.unit_price),
            "subtotal": round(float(item.unit_price) * item.quantity, 2),
            "status": item.status,
            "added_by_id": item.added_by,
            "added_by_name": _wname(item.added_by),
            "added_at": _dt(item.added_at),
            "paid_by_id": item.paid_by,
            "paid_by_name": _wname(item.paid_by),
            "paid_at": _dt(item.paid_at),
            "paid_in_shift_id": item.paid_in_shift_id,
            "payment_method": item.payment_method,
        })

    # Remove orders with no items (shouldn't happen but safety net)
    populated_orders = [o for o in orders_out.values() if o["items"]]

    # Compute duration
    started = shift.started_at
    ended = shift.ended_at
    duration_minutes = None
    if started and ended:
        duration_minutes = int((ended - started).total_seconds() / 60)
    elif started:
        from datetime import timezone as tz
        now = datetime.now(tz.utc)
        s = started if started.tzinfo else started.replace(tzinfo=tz.utc)
        duration_minutes = int((now - s).total_seconds() / 60)

    enriched = _enrich_shift(shift, db)
    enriched["orders"] = populated_orders
    enriched["duration_minutes"] = duration_minutes
    return enriched
