"""B2B ordering portal data model.

This module backs the private customer ordering portal. It deliberately reuses
the existing tenant/customer primitives rather than introducing parallel ones:

* A **customer/company** is an existing :class:`ClientAccount`.
* A **customer user** is an existing :class:`ClientUser` (owner/buyer/viewer/
  accounts roles map onto the existing ``ordering`` module access levels).

Everything here is private: every row is tenant-scoped, and customer-facing
rows are additionally scoped to a ``client_account_id``. The catalogue itself is
global to the seller (one tenant), with per-customer visibility and pricing
layered on top.

Money is stored GST-exclusive (see ``CLAUDE.MD`` costing conventions) as floats,
matching the rest of the costing platform.
"""
from __future__ import annotations

from datetime import datetime

from sqlalchemy import (
    Boolean,
    DateTime,
    Float,
    ForeignKey,
    Integer,
    String,
    Text,
    UniqueConstraint,
)
from sqlalchemy.orm import Mapped, mapped_column, relationship

from app.db.session import Base

# --- Controlled vocabularies -------------------------------------------------
# Plain string constants (not DB enums) keep SQLite migrations trivial and match
# the existing model style.

PRODUCT_CATEGORIES = (
    "grains",
    "premixed",
    "bags",
    "bulk_loads",
    "custom_blends",
    "services",
)

# Full internal order lifecycle. Customers see a simplified subset (see
# ``CUSTOMER_VISIBLE_STATUS`` in the ordering service).
ORDER_STATUSES = (
    "draft",
    "submitted",
    "under_review",
    "confirmed",
    "sent_to_xero",
    "in_production",
    "ready_for_pickup",
    "dispatched",
    "completed",
    "cancelled",
)

# How a resolved unit price was derived. Stored on each order line so future
# reporting can always explain which rule applied.
PRICE_SOURCES = (
    "fixed",        # CustomerProductPrice with rule_type fixed
    "contract",     # CustomerProductPrice with rule_type contract
    "price_list",   # PriceListItem via an assigned price list
    "tiered",       # a quantity tier won over the base rate
    "base",         # catalogue list price (optionally with customer discount)
    "quote",        # manual quote required; no automatic price
)

FULFILMENT_METHODS = ("delivery", "pickup")


class ProductCategory(Base):
    __tablename__ = "product_categories"
    __table_args__ = (UniqueConstraint("tenant_id", "slug", name="uq_product_category_slug"),)

    id: Mapped[int] = mapped_column(primary_key=True)
    tenant_id: Mapped[str] = mapped_column(String(64), default="default", index=True)
    slug: Mapped[str] = mapped_column(String(64), index=True)
    name: Mapped[str] = mapped_column(String(255))
    description: Mapped[str | None] = mapped_column(Text, nullable=True)
    sort_order: Mapped[int] = mapped_column(Integer, default=0)
    active: Mapped[bool] = mapped_column(Boolean, default=True)
    created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)


class CatalogueProduct(Base):
    """A private catalogue product. Named ``CatalogueProduct`` to avoid clashing
    with the existing costing ``Product`` (a separate concern)."""

    __tablename__ = "catalogue_products"
    __table_args__ = (UniqueConstraint("tenant_id", "sku", name="uq_catalogue_product_sku"),)

    id: Mapped[int] = mapped_column(primary_key=True)
    tenant_id: Mapped[str] = mapped_column(String(64), default="default", index=True)
    name: Mapped[str] = mapped_column(String(255), index=True)
    sku: Mapped[str] = mapped_column(String(64), index=True)
    description: Mapped[str | None] = mapped_column(Text, nullable=True)
    category: Mapped[str] = mapped_column(String(64), default="grains", index=True)
    image_url: Mapped[str | None] = mapped_column(String(512), nullable=True)
    unit_size: Mapped[str | None] = mapped_column(String(64), nullable=True)
    unit_of_measure: Mapped[str] = mapped_column(String(64), default="each")
    min_order_quantity: Mapped[float] = mapped_column(Float, default=1.0)
    # Base/list price, GST-exclusive. Used as the fallback price source and as
    # the basis for a customer discount percentage.
    base_price: Mapped[float | None] = mapped_column(Float, nullable=True)
    stock_status: Mapped[str] = mapped_column(String(32), default="in_stock")
    active: Mapped[bool] = mapped_column(Boolean, default=True)
    requires_quote: Mapped[bool] = mapped_column(Boolean, default=False)
    created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
    updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)


class CustomerProductVisibility(Base):
    """Per-customer override of catalogue visibility.

    Products are visible to every customer by default (the catalogue is
    "standard globally"). A row here with ``visible=False`` hides a product from
    one customer; ``visible=True`` is an explicit allow (no-op unless a future
    default flips to opt-in).
    """

    __tablename__ = "customer_product_visibility"
    __table_args__ = (
        UniqueConstraint("client_account_id", "product_id", name="uq_customer_product_visibility"),
    )

    id: Mapped[int] = mapped_column(primary_key=True)
    tenant_id: Mapped[str] = mapped_column(String(64), default="default", index=True)
    client_account_id: Mapped[int] = mapped_column(ForeignKey("client_accounts.id"), index=True)
    product_id: Mapped[int] = mapped_column(ForeignKey("catalogue_products.id"), index=True)
    visible: Mapped[bool] = mapped_column(Boolean, default=True)
    updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
    created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)


class PriceList(Base):
    __tablename__ = "price_lists"
    __table_args__ = (UniqueConstraint("tenant_id", "code", name="uq_price_list_code"),)

    id: Mapped[int] = mapped_column(primary_key=True)
    tenant_id: Mapped[str] = mapped_column(String(64), default="default", index=True)
    code: Mapped[str] = mapped_column(String(64), index=True)
    name: Mapped[str] = mapped_column(String(255))
    description: Mapped[str | None] = mapped_column(Text, nullable=True)
    active: Mapped[bool] = mapped_column(Boolean, default=True)
    created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)

    items: Mapped[list["PriceListItem"]] = relationship(
        back_populates="price_list",
        cascade="all, delete-orphan",
    )


class PriceListItem(Base):
    __tablename__ = "price_list_items"
    __table_args__ = (UniqueConstraint("price_list_id", "product_id", name="uq_price_list_item"),)

    id: Mapped[int] = mapped_column(primary_key=True)
    tenant_id: Mapped[str] = mapped_column(String(64), default="default", index=True)
    price_list_id: Mapped[int] = mapped_column(ForeignKey("price_lists.id"), index=True)
    product_id: Mapped[int] = mapped_column(ForeignKey("catalogue_products.id"), index=True)
    unit_price: Mapped[float] = mapped_column(Float)
    created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
    updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)

    price_list: Mapped[PriceList] = relationship(back_populates="items")


class CustomerPriceAssignment(Base):
    """Links a customer to an assigned price list and a default discount."""

    __tablename__ = "customer_price_assignments"
    __table_args__ = (
        UniqueConstraint("client_account_id", name="uq_customer_price_assignment"),
    )

    id: Mapped[int] = mapped_column(primary_key=True)
    tenant_id: Mapped[str] = mapped_column(String(64), default="default", index=True)
    client_account_id: Mapped[int] = mapped_column(ForeignKey("client_accounts.id"), index=True)
    price_list_id: Mapped[int | None] = mapped_column(ForeignKey("price_lists.id"), nullable=True, index=True)
    # Default percentage discount applied to base prices for this customer when
    # no more specific rule (fixed/contract/price-list) applies. 0..100.
    discount_percent: Mapped[float] = mapped_column(Float, default=0.0)
    updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
    created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)

    price_list: Mapped[PriceList | None] = relationship()


class CustomerProductPrice(Base):
    """A fixed or contract price for a specific customer + product. Highest
    priority pricing rule."""

    __tablename__ = "customer_product_prices"
    __table_args__ = (
        UniqueConstraint("client_account_id", "product_id", name="uq_customer_product_price"),
    )

    id: Mapped[int] = mapped_column(primary_key=True)
    tenant_id: Mapped[str] = mapped_column(String(64), default="default", index=True)
    client_account_id: Mapped[int] = mapped_column(ForeignKey("client_accounts.id"), index=True)
    product_id: Mapped[int] = mapped_column(ForeignKey("catalogue_products.id"), index=True)
    unit_price: Mapped[float | None] = mapped_column(Float, nullable=True)
    # "fixed" | "contract" | "quote" (quote forces a manual-quote workflow for
    # this customer+product even if the product itself doesn't require one).
    rule_type: Mapped[str] = mapped_column(String(32), default="fixed")
    contract_reference: Mapped[str | None] = mapped_column(String(128), nullable=True)
    notes: Mapped[str | None] = mapped_column(Text, nullable=True)
    active: Mapped[bool] = mapped_column(Boolean, default=True)
    updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
    created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)


class PriceTier(Base):
    """Quantity break for a pricing source. Exactly one of
    ``customer_product_price_id`` / ``price_list_item_id`` is set."""

    __tablename__ = "price_tiers"

    id: Mapped[int] = mapped_column(primary_key=True)
    tenant_id: Mapped[str] = mapped_column(String(64), default="default", index=True)
    customer_product_price_id: Mapped[int | None] = mapped_column(
        ForeignKey("customer_product_prices.id"), nullable=True, index=True
    )
    price_list_item_id: Mapped[int | None] = mapped_column(
        ForeignKey("price_list_items.id"), nullable=True, index=True
    )
    # Tier applies when ordered quantity >= min_quantity. The highest qualifying
    # min_quantity wins.
    min_quantity: Mapped[float] = mapped_column(Float)
    unit_price: Mapped[float] = mapped_column(Float)
    created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)


class Order(Base):
    __tablename__ = "orders"

    id: Mapped[int] = mapped_column(primary_key=True)
    tenant_id: Mapped[str] = mapped_column(String(64), default="default", index=True)
    client_account_id: Mapped[int] = mapped_column(ForeignKey("client_accounts.id"), index=True)
    # Human-friendly order reference, assigned on submit (e.g. ORD-000123).
    order_number: Mapped[str | None] = mapped_column(String(32), nullable=True, index=True)
    status: Mapped[str] = mapped_column(String(32), default="draft", index=True)

    # Who created / submitted it (ClientUser ids; nullable for admin-created).
    created_by_user_id: Mapped[int | None] = mapped_column(ForeignKey("client_users.id"), nullable=True)
    created_by_name: Mapped[str | None] = mapped_column(String(255), nullable=True)
    submitted_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)

    # Customer-supplied order details.
    purchase_order_number: Mapped[str | None] = mapped_column(String(128), nullable=True)
    delivery_notes: Mapped[str | None] = mapped_column(Text, nullable=True)
    requested_delivery_date: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
    fulfilment_method: Mapped[str] = mapped_column(String(32), default="delivery")

    # Admin/internal handling.
    admin_notes: Mapped[str | None] = mapped_column(Text, nullable=True)
    reopened: Mapped[bool] = mapped_column(Boolean, default=False)

    # Cached totals (authoritative figures live on lines; this is for listing).
    subtotal_ex_gst: Mapped[float] = mapped_column(Float, default=0.0)
    requires_quote: Mapped[bool] = mapped_column(Boolean, default=False)

    # Xero integration tracking.
    xero_status: Mapped[str] = mapped_column(String(32), default="not_sent")
    xero_invoice_id: Mapped[str | None] = mapped_column(String(128), nullable=True)

    created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
    updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)

    lines: Mapped[list["OrderLine"]] = relationship(
        back_populates="order",
        cascade="all, delete-orphan",
        order_by="OrderLine.sort_order",
    )
    status_history: Mapped[list["OrderStatusHistory"]] = relationship(
        back_populates="order",
        cascade="all, delete-orphan",
        order_by="OrderStatusHistory.created_at",
    )
    attachments: Mapped[list["OrderAttachment"]] = relationship(
        back_populates="order",
        cascade="all, delete-orphan",
    )


class OrderLine(Base):
    __tablename__ = "order_lines"

    id: Mapped[int] = mapped_column(primary_key=True)
    tenant_id: Mapped[str] = mapped_column(String(64), default="default", index=True)
    order_id: Mapped[int] = mapped_column(ForeignKey("orders.id"), index=True)
    product_id: Mapped[int] = mapped_column(ForeignKey("catalogue_products.id"), index=True)

    # Snapshot of product identity at order time (so later catalogue edits don't
    # rewrite historical orders).
    product_name: Mapped[str] = mapped_column(String(255))
    product_sku: Mapped[str] = mapped_column(String(64))

    quantity: Mapped[float] = mapped_column(Float, default=1.0)

    # Authoritative unit price, GST-exclusive, computed by the backend pricing
    # engine and frozen at submission. Null only while quote-only.
    unit_price: Mapped[float | None] = mapped_column(Float, nullable=True)
    line_total: Mapped[float | None] = mapped_column(Float, nullable=True)
    requires_quote: Mapped[bool] = mapped_column(Boolean, default=False)

    # Pricing provenance — enough to explain which rule applied in reporting.
    price_source: Mapped[str] = mapped_column(String(32), default="base")
    price_rule_id: Mapped[int | None] = mapped_column(Integer, nullable=True)
    discount_percent: Mapped[float] = mapped_column(Float, default=0.0)

    # Admin override of the resolved unit price before confirmation.
    admin_override_price: Mapped[float | None] = mapped_column(Float, nullable=True)
    admin_override_reason: Mapped[str | None] = mapped_column(Text, nullable=True)

    sort_order: Mapped[int] = mapped_column(Integer, default=0)
    notes: Mapped[str | None] = mapped_column(Text, nullable=True)

    order: Mapped[Order] = relationship(back_populates="lines")


class OrderStatusHistory(Base):
    __tablename__ = "order_status_history"

    id: Mapped[int] = mapped_column(primary_key=True)
    tenant_id: Mapped[str] = mapped_column(String(64), default="default", index=True)
    order_id: Mapped[int] = mapped_column(ForeignKey("orders.id"), index=True)
    from_status: Mapped[str | None] = mapped_column(String(32), nullable=True)
    to_status: Mapped[str] = mapped_column(String(32))
    actor_type: Mapped[str] = mapped_column(String(32), default="system")
    actor_name: Mapped[str | None] = mapped_column(String(255), nullable=True)
    note: Mapped[str | None] = mapped_column(Text, nullable=True)
    created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)

    order: Mapped[Order] = relationship(back_populates="status_history")


class OrderAttachment(Base):
    __tablename__ = "order_attachments"

    id: Mapped[int] = mapped_column(primary_key=True)
    tenant_id: Mapped[str] = mapped_column(String(64), default="default", index=True)
    order_id: Mapped[int] = mapped_column(ForeignKey("orders.id"), index=True)
    # "purchase_order" | "confirmation_pdf" | "other"
    kind: Mapped[str] = mapped_column(String(32), default="other")
    filename: Mapped[str] = mapped_column(String(255))
    content_type: Mapped[str | None] = mapped_column(String(128), nullable=True)
    url: Mapped[str | None] = mapped_column(String(512), nullable=True)
    note: Mapped[str | None] = mapped_column(Text, nullable=True)
    created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)

    order: Mapped[Order] = relationship(back_populates="attachments")


class NotificationSetting(Base):
    """Per-tenant notification configuration for the ordering portal."""

    __tablename__ = "notification_settings"
    __table_args__ = (UniqueConstraint("tenant_id", name="uq_notification_settings_tenant"),)

    id: Mapped[int] = mapped_column(primary_key=True)
    tenant_id: Mapped[str] = mapped_column(String(64), default="default", index=True)
    # Comma-separated internal recipients notified on new order submissions.
    internal_recipients: Mapped[str | None] = mapped_column(Text, nullable=True)
    send_customer_confirmation: Mapped[bool] = mapped_column(Boolean, default=True)
    require_po_number: Mapped[bool] = mapped_column(Boolean, default=False)
    from_email: Mapped[str | None] = mapped_column(String(255), nullable=True)
    updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
    created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)


class XeroContactLink(Base):
    """Persistent link between a customer (:class:`ClientAccount`) and a Xero
    contact.

    Maintained from the Integrations console. Once a customer is linked, order
    invoices reference the real Xero ``ContactID`` instead of falling back to the
    client code — which is what lets Xero attach the invoice to the right
    contact rather than creating a duplicate. One link per customer per tenant.
    """

    __tablename__ = "xero_contact_links"
    __table_args__ = (
        UniqueConstraint("tenant_id", "client_account_id", name="uq_xero_contact_link_customer"),
    )

    id: Mapped[int] = mapped_column(primary_key=True)
    tenant_id: Mapped[str] = mapped_column(String(64), default="default", index=True)
    client_account_id: Mapped[int] = mapped_column(ForeignKey("client_accounts.id"), index=True)
    xero_contact_id: Mapped[str] = mapped_column(String(128))
    xero_contact_name: Mapped[str | None] = mapped_column(String(255), nullable=True)
    xero_contact_email: Mapped[str | None] = mapped_column(String(255), nullable=True)
    # When the contact details were last reconciled with Xero (a future live
    # sync can refresh name/email and stamp this).
    last_synced_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
    created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
    updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)


class XeroSyncLog(Base):
    __tablename__ = "xero_sync_log"

    id: Mapped[int] = mapped_column(primary_key=True)
    tenant_id: Mapped[str] = mapped_column(String(64), default="default", index=True)
    order_id: Mapped[int] = mapped_column(ForeignKey("orders.id"), index=True)
    # "queued" | "success" | "failed"
    status: Mapped[str] = mapped_column(String(32), default="queued")
    request_summary: Mapped[str | None] = mapped_column(Text, nullable=True)
    xero_invoice_id: Mapped[str | None] = mapped_column(String(128), nullable=True)
    response_message: Mapped[str | None] = mapped_column(Text, nullable=True)
    created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
