"""Rules that must hold everywhere: stock changes, order status moves, audit."""
import secrets
from decimal import Decimal

from fastapi import HTTPException
from sqlalchemy import select
from sqlalchemy.orm import Session

from .models import (AuditLog, DeliveryZone, Order, OrderEvent, OrderItem, Product,
                     Setting, StockMovement, now)

STATUSES = ["pending", "confirmed", "packed", "out_for_delivery", "delivered", "cancelled"]
NEXT = {
    "pending": {"confirmed", "cancelled"},
    "confirmed": {"packed", "cancelled"},
    "packed": {"out_for_delivery", "cancelled"},
    "out_for_delivery": {"delivered", "cancelled"},
    "delivered": set(),
    "cancelled": set(),
}
LABEL = {
    "pending": "Order received", "confirmed": "Confirmed", "packed": "Packed",
    "out_for_delivery": "Out for delivery", "delivered": "Delivered", "cancelled": "Cancelled",
}
MAX_QTY = 50


def audit(db: Session, actor: str, action: str, detail: str = ""):
    db.add(AuditLog(actor=actor, action=action, detail=detail))


def get_setting(db: Session, key: str, default: str = "") -> str:
    row = db.get(Setting, key)
    return row.value if row else default


def set_setting(db: Session, key: str, value: str):
    row = db.get(Setting, key)
    if row:
        row.value = value
    else:
        db.add(Setting(key=key, value=value))


def move_stock(db: Session, product: Product, change: int, reason: str, ref: str = "", by: str = ""):
    """The only place stock changes. Caller must hold the row lock (for_update)."""
    product.stock += change
    if product.stock < 0:
        raise HTTPException(400, f"Only {product.stock - change} of {product.name} left.")
    db.add(StockMovement(product_id=product.id, change=change, after=product.stock,
                         reason=reason, ref=ref, by=by))


def locked_products(db: Session, ids: list[int]) -> dict[int, Product]:
    """Lock in id order so two checkouts can never deadlock or oversell."""
    rows = db.scalars(select(Product).where(Product.id.in_(ids)).order_by(Product.id).with_for_update()).all()
    return {p.id: p for p in rows}


def new_order_number(db: Session) -> str:
    for _ in range(10):
        n = "EM-" + now().strftime("%y%m%d") + "-" + secrets.token_hex(2).upper()
        if not db.scalar(select(Order.id).where(Order.number == n)):
            return n
    raise HTTPException(500, "Could not make an order number. Try again.")


def delivery_fee(db: Session, zone: DeliveryZone, subtotal: Decimal) -> Decimal:
    free_over = Decimal(get_setting(db, "free_delivery_over", "250"))
    if free_over > 0 and subtotal >= free_over:
        return Decimal("0.00")
    return zone.fee


def place_order(db: Session, *, user, name, phone, email, address, zone_id, payment_method, note, items) -> Order:
    if payment_method not in ("momo", "cod", "online"):
        raise HTTPException(400, "Choose how you want to pay.")
    zone = db.get(DeliveryZone, zone_id)
    if not zone or not zone.active:
        raise HTTPException(400, "Choose a delivery area.")
    if not items:
        raise HTTPException(400, "Your basket is empty.")
    merged: dict[int, int] = {}
    for it in items:
        merged[it["product_id"]] = merged.get(it["product_id"], 0) + it["quantity"]
    products = locked_products(db, list(merged))
    subtotal = Decimal("0")
    lines = []
    for pid, qty in merged.items():
        p = products.get(pid)
        if not p or not p.active:
            raise HTTPException(400, "One item is no longer available.")
        if qty > MAX_QTY:
            raise HTTPException(400, f"You can order at most {MAX_QTY} of one item.")
        if p.stock < qty:
            raise HTTPException(400, f"Only {p.stock} of {p.name} left.")
        subtotal += p.price * qty
        lines.append((p, qty))
    fee = delivery_fee(db, zone, subtotal)
    order = Order(number=new_order_number(db), user_id=user.id if user else None, customer_name=name,
                  phone=phone, email=email, address=address, zone_id=zone.id, zone_name=zone.name,
                  subtotal=subtotal, delivery_fee=fee, total=subtotal + fee, payment_method=payment_method,
                  note=note, status="pending", payment_status="unpaid")
    db.add(order)
    db.flush()
    for p, qty in lines:
        db.add(OrderItem(order_id=order.id, product_id=p.id, name=p.name,
                         category=p.category.name if p.category else "", quantity=qty,
                         unit_price=p.price, unit_cost=p.cost))
        move_stock(db, p, -qty, "sale", ref=order.number)
    db.add(OrderEvent(order_id=order.id, status="pending", note="Order placed", by=name))
    return order


def change_status(db: Session, order: Order, new: str, by: str, note: str = ""):
    if new not in STATUSES:
        raise HTTPException(400, "Unknown status.")
    if new not in NEXT[order.status]:
        raise HTTPException(400, f"An order that is '{LABEL[order.status]}' cannot move to '{LABEL[new]}'.")
    if new == "cancelled":
        ids = [i.product_id for i in order.items if i.product_id]
        products = locked_products(db, ids)
        for i in order.items:
            if i.product_id in products:
                move_stock(db, products[i.product_id], i.quantity, "cancel", ref=order.number, by=by)
        if order.payment_status == "paid":  # money paid online must be given back by hand in the Paystack dashboard
            order.payment_status = "refund_due" if order.payment_method == "online" else "refunded"
    order.status = new
    if new == "delivered" and order.payment_method == "cod":
        order.payment_status = "paid"
    db.add(OrderEvent(order_id=order.id, status=new, note=note, by=by))
