"""Customer-facing B2B ordering API.

Every endpoint is gated by the ``ordering`` module and scoped to the caller's
own company (``tenant_id`` + ``client_account_id``). Customers can never see
another company's catalogue visibility, prices, or orders.
"""
from __future__ import annotations

from datetime import datetime

from fastapi import APIRouter, Depends, HTTPException, Query, Response, status
from sqlalchemy import select
from sqlalchemy.orm import Session, selectinload

from app.api.deps import AuthSession, require_client_module_access
from app.db.session import get_db
from app.models.ordering import CatalogueProduct, Order, OrderLine
from app.schemas.ordering import (
    DraftOrderCreate,
    DraftOrderUpdate,
    OrderLineInput,
    OrderSubmitRequest,
)
from app.services import ordering_service as svc
from app.services.order_notifications import send_order_submitted_notifications
from app.services.order_pdf import OrderPdfUnavailableError, build_order_confirmation_pdf
from app.services.ordering_pricing import resolve_price

router = APIRouter(prefix="/api/ordering", tags=["ordering"])


# --- Helpers -----------------------------------------------------------------


def _require_active_customer(db: Session, session: AuthSession):
    account = svc.get_customer_account(db, client_account_id=session.client_account_id)
    svc.ensure_customer_active(account)
    return account


def _get_visible_product(db: Session, session: AuthSession, product_id: int) -> CatalogueProduct:
    hidden = svc.visible_product_ids_for_customer(
        db, tenant_id=svc.ORDERING_TENANT, client_account_id=session.client_account_id
    )
    product = db.scalar(
        select(CatalogueProduct).where(
            CatalogueProduct.id == product_id,
            CatalogueProduct.tenant_id == svc.ORDERING_TENANT,
            CatalogueProduct.active.is_(True),
        )
    )
    if product is None or product.id in hidden:
        raise HTTPException(status_code=404, detail="Product not available")
    return product


def _load_own_order(db: Session, session: AuthSession, order_id: int) -> Order:
    order = db.scalar(
        select(Order)
        .where(
            Order.id == order_id,
            Order.tenant_id == svc.ORDERING_TENANT,
            Order.client_account_id == session.client_account_id,
        )
        .options(selectinload(Order.lines))
    )
    if order is None:
        raise HTTPException(status_code=404, detail="Order not found")
    return order


def _rebuild_lines(db: Session, session: AuthSession, order: Order, lines: list[OrderLineInput]) -> None:
    """Replace an order's lines with freshly priced lines (server-side pricing).

    Validates product availability and minimum order quantity. Raises 422 on
    invalid input.
    """
    order.lines.clear()
    db.flush()
    for index, line_input in enumerate(lines):
        product = _get_visible_product(db, session, line_input.product_id)
        if line_input.quantity < product.min_order_quantity:
            raise HTTPException(
                status_code=422,
                detail=(
                    f"{product.name}: minimum order quantity is "
                    f"{product.min_order_quantity:g} {product.unit_of_measure}"
                ),
            )
        resolution = resolve_price(
            db,
            client_account_id=session.client_account_id,
            product=product,
            quantity=line_input.quantity,
        )
        order.lines.append(
            OrderLine(
                tenant_id=svc.ORDERING_TENANT,
                product_id=product.id,
                product_name=product.name,
                product_sku=product.sku,
                quantity=line_input.quantity,
                unit_price=resolution.unit_price,
                requires_quote=resolution.requires_quote,
                price_source=resolution.price_source,
                price_rule_id=resolution.price_rule_id,
                discount_percent=resolution.discount_percent,
                sort_order=index,
                notes=line_input.notes,
            )
        )
    db.flush()
    svc.recompute_order_totals(order)


# --- Catalogue ---------------------------------------------------------------


@router.get("/catalogue")
def list_catalogue(
    category: str | None = Query(default=None),
    q: str | None = Query(default=None),
    session: AuthSession = Depends(require_client_module_access("ordering")),
    db: Session = Depends(get_db),
):
    _require_active_customer(db, session)
    products = svc.list_visible_products(
        db, tenant_id=svc.ORDERING_TENANT, client_account_id=session.client_account_id
    )
    if category:
        products = [p for p in products if p.category == category]
    if q:
        needle = q.lower()
        products = [
            p
            for p in products
            if needle in p.name.lower()
            or needle in p.sku.lower()
            or (p.description and needle in p.description.lower())
        ]
    return [
        svc.serialize_product(p, db=db, client_account_id=session.client_account_id)
        for p in products
    ]


@router.get("/catalogue/{product_id}")
def get_catalogue_product(
    product_id: int,
    quantity: float = Query(default=1.0, gt=0),
    session: AuthSession = Depends(require_client_module_access("ordering")),
    db: Session = Depends(get_db),
):
    _require_active_customer(db, session)
    product = _get_visible_product(db, session, product_id)
    return svc.serialize_product(
        product, db=db, client_account_id=session.client_account_id, quantity=quantity
    )


# --- Orders ------------------------------------------------------------------


@router.get("/orders")
def list_orders(
    status_filter: str | None = Query(default=None, alias="status"),
    session: AuthSession = Depends(require_client_module_access("ordering")),
    db: Session = Depends(get_db),
):
    stmt = (
        select(Order)
        .where(
            Order.tenant_id == svc.ORDERING_TENANT,
            Order.client_account_id == session.client_account_id,
        )
        .options(selectinload(Order.lines))
        .order_by(Order.created_at.desc())
    )
    orders = db.scalars(stmt).all()
    if status_filter == "draft":
        orders = [o for o in orders if o.status == "draft"]
    elif status_filter == "submitted":
        orders = [o for o in orders if o.status != "draft"]
    return [svc.serialize_order(o, for_admin=False) for o in orders]


@router.get("/orders/{order_id}")
def get_order(
    order_id: int,
    session: AuthSession = Depends(require_client_module_access("ordering")),
    db: Session = Depends(get_db),
):
    order = _load_own_order(db, session, order_id)
    return svc.serialize_order(order, for_admin=False)


@router.post("/orders", status_code=status.HTTP_201_CREATED)
def create_draft_order(
    payload: DraftOrderCreate,
    session: AuthSession = Depends(require_client_module_access("ordering", "edit")),
    db: Session = Depends(get_db),
):
    _require_active_customer(db, session)
    order = Order(
        tenant_id=svc.ORDERING_TENANT,
        client_account_id=session.client_account_id,
        status="draft",
        created_by_user_id=session.user_id,
        created_by_name=session.name,
        purchase_order_number=payload.purchase_order_number,
        delivery_notes=payload.delivery_notes,
        requested_delivery_date=payload.requested_delivery_date,
        fulfilment_method=payload.fulfilment_method,
    )
    db.add(order)
    db.flush()
    _rebuild_lines(db, session, order, payload.lines)
    svc.record_status_change(
        db, order, to_status="draft", actor_type="customer", actor_name=session.name, note="Draft created"
    )
    svc.audit_order_event(
        db, session=session, order=order, action="order.created", summary="Draft order created."
    )
    db.commit()
    db.refresh(order)
    return svc.serialize_order(order, for_admin=False)


@router.patch("/orders/{order_id}")
def update_draft_order(
    order_id: int,
    payload: DraftOrderUpdate,
    session: AuthSession = Depends(require_client_module_access("ordering", "edit")),
    db: Session = Depends(get_db),
):
    order = _load_own_order(db, session, order_id)
    if order.status not in svc.CUSTOMER_EDITABLE_STATUSES:
        raise HTTPException(
            status_code=409,
            detail="This order has been submitted and can no longer be edited. Ask an admin to reopen it.",
        )
    changes = payload.model_dump(exclude_unset=True)
    for field_name in ("purchase_order_number", "delivery_notes", "requested_delivery_date", "fulfilment_method"):
        if field_name in changes:
            setattr(order, field_name, changes[field_name])
    if payload.lines is not None:
        _rebuild_lines(db, session, order, payload.lines)
    svc.audit_order_event(
        db, session=session, order=order, action="order.updated", summary="Draft order updated."
    )
    db.commit()
    db.refresh(order)
    return svc.serialize_order(order, for_admin=False)


@router.delete("/orders/{order_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_draft_order(
    order_id: int,
    session: AuthSession = Depends(require_client_module_access("ordering", "edit")),
    db: Session = Depends(get_db),
):
    order = _load_own_order(db, session, order_id)
    if order.status != "draft":
        raise HTTPException(status_code=409, detail="Only draft orders can be deleted")
    db.delete(order)
    db.commit()
    return Response(status_code=status.HTTP_204_NO_CONTENT)


@router.post("/orders/{order_id}/submit")
def submit_order(
    order_id: int,
    payload: OrderSubmitRequest,
    session: AuthSession = Depends(require_client_module_access("ordering", "edit")),
    db: Session = Depends(get_db),
):
    account = _require_active_customer(db, session)
    order = _load_own_order(db, session, order_id)
    if order.status != "draft":
        raise HTTPException(status_code=409, detail="Only draft orders can be submitted")
    if not order.lines:
        raise HTTPException(status_code=422, detail="Cannot submit an empty order")

    # Apply any last-minute header changes supplied on submit.
    changes = payload.model_dump(exclude_unset=True)
    for field_name in ("purchase_order_number", "delivery_notes", "requested_delivery_date", "fulfilment_method"):
        if field_name in changes and changes[field_name] is not None:
            setattr(order, field_name, changes[field_name])

    # Required PO number, if the tenant configured it.
    from app.services.order_notifications import get_or_create_settings

    settings = get_or_create_settings(db, order.tenant_id)
    if settings.require_po_number and not (order.purchase_order_number or "").strip():
        raise HTTPException(status_code=422, detail="A purchase order number is required to submit this order")

    # Re-resolve and freeze the exact price used at submission time.
    line_inputs = [
        OrderLineInput(product_id=line.product_id, quantity=line.quantity, notes=line.notes)
        for line in order.lines
    ]
    _rebuild_lines(db, session, order, line_inputs)

    order.order_number = svc.next_order_number(db, order.tenant_id)
    order.submitted_at = datetime.utcnow()
    svc.record_status_change(
        db, order, to_status="submitted", actor_type="customer", actor_name=session.name, note="Submitted by customer"
    )
    svc.audit_order_event(
        db, session=session, order=order, action="order.submitted",
        summary=f"Order {order.order_number} submitted (subtotal ex GST {order.subtotal_ex_gst:.2f}).",
    )

    notifications = send_order_submitted_notifications(db, order)
    db.commit()
    db.refresh(order)
    result = svc.serialize_order(order, for_admin=False)
    result["notifications"] = [
        {"channel": n.channel, "recipients": n.recipients, "delivered": n.delivered, "detail": n.detail}
        for n in notifications
    ]
    return result


@router.post("/orders/{order_id}/reorder", status_code=status.HTTP_201_CREATED)
def reorder(
    order_id: int,
    session: AuthSession = Depends(require_client_module_access("ordering", "edit")),
    db: Session = Depends(get_db),
):
    _require_active_customer(db, session)
    source = _load_own_order(db, session, order_id)
    new_order = Order(
        tenant_id=svc.ORDERING_TENANT,
        client_account_id=session.client_account_id,
        status="draft",
        created_by_user_id=session.user_id,
        created_by_name=session.name,
        fulfilment_method=source.fulfilment_method,
    )
    db.add(new_order)
    db.flush()
    line_inputs = [
        OrderLineInput(product_id=line.product_id, quantity=line.quantity, notes=line.notes)
        for line in source.lines
    ]
    # Skip lines whose product is no longer available rather than failing.
    available: list[OrderLineInput] = []
    for line_input in line_inputs:
        product = db.scalar(
            select(CatalogueProduct).where(
                CatalogueProduct.id == line_input.product_id,
                CatalogueProduct.tenant_id == svc.ORDERING_TENANT,
                CatalogueProduct.active.is_(True),
            )
        )
        if product is not None and line_input.quantity >= product.min_order_quantity:
            available.append(line_input)
    _rebuild_lines(db, session, new_order, available)
    svc.record_status_change(
        db, new_order, to_status="draft", actor_type="customer", actor_name=session.name,
        note=f"Reordered from {source.order_number or source.id}",
    )
    svc.audit_order_event(
        db, session=session, order=new_order, action="order.reordered",
        summary=f"Draft created by reordering from {source.order_number or source.id}.",
    )
    db.commit()
    db.refresh(new_order)
    return svc.serialize_order(new_order, for_admin=False)


@router.get("/orders/{order_id}/confirmation.pdf")
def order_confirmation_pdf(
    order_id: int,
    session: AuthSession = Depends(require_client_module_access("ordering")),
    db: Session = Depends(get_db),
):
    order = _load_own_order(db, session, order_id)
    account = svc.get_customer_account(db, client_account_id=session.client_account_id)
    try:
        pdf_bytes = build_order_confirmation_pdf(order, account)
    except OrderPdfUnavailableError as exc:
        raise HTTPException(status_code=503, detail=str(exc)) from exc
    filename = f"order-{order.order_number or order.id}.pdf"
    return Response(
        content=pdf_bytes,
        media_type="application/pdf",
        headers={"Content-Disposition": f'attachment; filename="{filename}"'},
    )
