"""
inventory/models.py
───────────────────
Financial backbone of Tai AfroPress.

Models
------
  StockLocation        – physical places stock lives
  InventoryTransaction – immutable stock ledger (source of truth)
  InventoryBalance     – cached quantity per book per location
  Bookstore            – consignment partners
  BookstoreListing     – versioned distribution terms for a book at a bookstore
  Event                – book fairs / exhibitions
  Sale                 – a completed sales event
  SaleItem             – per-book line inside a sale (stores margin snapshot)
  AuthorPayout         – aggregated payment to an author OR publisher for a period
  AuthorPayoutItem     – per-book earnings row inside a payout
  ExpenseCategory      – taxonomy for expenses
  Expense              – a single cost incurred by the business
  AuditLog             – immutable audit trail for all financial actions
"""

import uuid
from decimal import Decimal
from datetime import date

from django.conf import settings
from django.db import models
from django.utils import timezone


# ---------------------------------------------------------------------------
# Stock Locations
# ---------------------------------------------------------------------------

class StockLocation(models.Model):
    TYPE_WAREHOUSE = "warehouse"
    TYPE_OFFICE    = "office"
    TYPE_DEPOT     = "depot"
    TYPE_BOOKSTORE = "bookstore"
    TYPE_EVENT     = "event"
    TYPE_AUTHOR    = "author"
    TYPE_DAMAGED   = "damaged"
    TYPE_OTHER     = "other"

    LOCATION_TYPES = [
        (TYPE_WAREHOUSE, "Main Warehouse"),
        (TYPE_OFFICE,    "Office"),
        (TYPE_DEPOT,     "Depot"),
        (TYPE_BOOKSTORE, "Bookstore"),
        (TYPE_EVENT,     "Event"),
        (TYPE_AUTHOR,    "Author"),
        (TYPE_DAMAGED,   "Damaged Stock"),
        (TYPE_OTHER,     "Other"),
    ]

    name          = models.CharField(max_length=255)
    code          = models.CharField(max_length=20, unique=True, db_index=True)
    location_type = models.CharField(max_length=30, choices=LOCATION_TYPES)
    address       = models.TextField(blank=True)
    is_active     = models.BooleanField(default=True)
    created_at    = models.DateTimeField(auto_now_add=True)

    class Meta:
        ordering = ["name"]
        verbose_name = "Stock Location"
        verbose_name_plural = "Stock Locations"

    def __str__(self) -> str:
        return f"{self.name} ({self.code})"


# ---------------------------------------------------------------------------
# Inventory Transaction  (immutable ledger)
# ---------------------------------------------------------------------------

class InventoryTransaction(models.Model):
    TYPE_RECEIPT_FROM_AUTHOR    = "receipt_from_author"
    TYPE_TRANSFER               = "transfer"
    TYPE_DISPATCH_TO_BOOKSTORE  = "dispatch_to_bookstore"
    TYPE_DISPATCH_TO_EVENT      = "dispatch_to_event"
    TYPE_RETURN_FROM_BOOKSTORE  = "return_from_bookstore"
    TYPE_RETURN_FROM_EVENT      = "return_from_event"
    TYPE_WRITE_OFF              = "write_off"
    TYPE_ADJUSTMENT             = "adjustment"
    TYPE_SALE                   = "sale"
    TYPE_REVERSAL               = "reversal"

    TRANSACTION_TYPES = [
        (TYPE_RECEIPT_FROM_AUTHOR,   "Receipt from Author"),
        (TYPE_TRANSFER,              "Internal Transfer"),
        (TYPE_DISPATCH_TO_BOOKSTORE, "Dispatch to Bookstore"),
        (TYPE_DISPATCH_TO_EVENT,     "Dispatch to Event"),
        (TYPE_RETURN_FROM_BOOKSTORE, "Return from Bookstore"),
        (TYPE_RETURN_FROM_EVENT,     "Return from Event"),
        (TYPE_WRITE_OFF,             "Write Off"),
        (TYPE_ADJUSTMENT,            "Stock Adjustment"),
        (TYPE_SALE,                  "Sale"),
        (TYPE_REVERSAL,              "Reversal"),
    ]

    book                  = models.ForeignKey(
        "catalog_app.Book", on_delete=models.PROTECT,
        related_name="inventory_transactions"
    )
    source_location       = models.ForeignKey(
        StockLocation, on_delete=models.PROTECT,
        null=True, blank=True, related_name="outgoing_transactions"
    )
    destination_location  = models.ForeignKey(
        StockLocation, on_delete=models.PROTECT,
        null=True, blank=True, related_name="incoming_transactions"
    )
    quantity              = models.PositiveIntegerField()
    transaction_type      = models.CharField(max_length=30, choices=TRANSACTION_TYPES, db_index=True)
    transaction_reference = models.CharField(max_length=100, blank=True, db_index=True)
    # Reversal support: never delete, create a reversal row instead
    is_reversed           = models.BooleanField(default=False)
    reversed_by           = models.ForeignKey(
        "self", on_delete=models.SET_NULL,
        null=True, blank=True, related_name="reverses"
    )
    notes                 = models.TextField(blank=True)
    performed_by          = models.ForeignKey(
        settings.AUTH_USER_MODEL, on_delete=models.SET_NULL,
        null=True, blank=True, related_name="inventory_transactions"
    )
    created_at            = models.DateTimeField(auto_now_add=True, db_index=True)

    class Meta:
        ordering = ["-created_at"]
        verbose_name = "Inventory Transaction"
        verbose_name_plural = "Inventory Transactions"
        indexes = [
            models.Index(fields=["book", "created_at"]),
            models.Index(fields=["transaction_type", "created_at"]),
        ]

    def __str__(self) -> str:
        return (
            f"{self.get_transaction_type_display()} | "
            f"{self.book.title} × {self.quantity} | {self.created_at:%Y-%m-%d}"
        )


# ---------------------------------------------------------------------------
# Inventory Balance  (cached, always re-derivable from transactions)
# ---------------------------------------------------------------------------

class InventoryBalance(models.Model):
    book              = models.ForeignKey(
        "catalog_app.Book", on_delete=models.CASCADE,
        related_name="inventory_balances"
    )
    location          = models.ForeignKey(
        StockLocation, on_delete=models.CASCADE,
        related_name="inventory_balances"
    )
    quantity_on_hand  = models.IntegerField(default=0)
    quantity_reserved = models.IntegerField(default=0)

    class Meta:
        unique_together = (("book", "location"),)
        verbose_name = "Inventory Balance"
        verbose_name_plural = "Inventory Balances"
        indexes = [
            models.Index(fields=["book"]),
            models.Index(fields=["location"]),
        ]

    @property
    def quantity_available(self) -> int:
        return max(self.quantity_on_hand - self.quantity_reserved, 0)

    def __str__(self) -> str:
        return (
            f"{self.book.title} @ {self.location.name}: "
            f"{self.quantity_on_hand} on hand"
        )


# ---------------------------------------------------------------------------
# Bookstore
# ---------------------------------------------------------------------------

class Bookstore(models.Model):
    name           = models.CharField(max_length=255)
    contact_person = models.CharField(max_length=255, blank=True)
    phone          = models.CharField(max_length=20, blank=True)
    email          = models.EmailField(blank=True)
    address        = models.TextField(blank=True)
    stock_location = models.OneToOneField(
        StockLocation, on_delete=models.SET_NULL,
        null=True, blank=True, related_name="bookstore"
    )
    default_bookstore_cut_percent = models.DecimalField(
        max_digits=5, decimal_places=2, default=Decimal("30.00"),
        help_text="Default % of the sale amount this bookstore keeps — used as the "
                   "bookstore's cut when a book is auto-listed here on first stock receipt. "
                   "Override per-book via BookstoreListing if a specific title's terms differ.",
    )
    is_internal    = models.BooleanField(
        default=False,
        help_text="This is Tai Bookstore/the website itself, not a real external consignment "
                   "partner — there should only ever be one such row. Used to auto-resolve "
                   "walk-in/website sales to their BookstoreListing terms, and excluded from "
                   "staff-facing pickers for recording external-bookstore sales/dispatches.",
    )
    is_active      = models.BooleanField(default=True)
    created_at     = models.DateTimeField(auto_now_add=True)
    updated_at     = models.DateTimeField(auto_now=True)

    class Meta:
        ordering = ["name"]
        verbose_name = "Bookstore"
        verbose_name_plural = "Bookstores"

    def __str__(self) -> str:
        return self.name


# ---------------------------------------------------------------------------
# BookstoreListing  (versioned distribution terms — book × bookstore)
# ---------------------------------------------------------------------------

class BookstoreListing(models.Model):
    """
    Distribution terms for one book at one external bookstore. Append-only:
    when terms change, create a new row with a later effective_from rather
    than editing an existing one, so historical sales can always be traced
    back to the terms that actually applied at the time.
    """
    book                  = models.ForeignKey(
        "catalog_app.Book", on_delete=models.CASCADE, related_name="bookstore_listings"
    )
    bookstore             = models.ForeignKey(
        Bookstore, on_delete=models.CASCADE, related_name="listings"
    )
    agreed_unit_price     = models.DecimalField(
        max_digits=10, decimal_places=2,
        help_text="The price the payee's earnings are computed from — independent of what "
                   "the book actually sells for at this bookstore.",
    )
    bookstore_cut_percent = models.DecimalField(
        max_digits=5, decimal_places=2,
        help_text="% of the actual sale amount the bookstore keeps.",
    )
    payee_cut_percent     = models.DecimalField(
        max_digits=5, decimal_places=2,
        help_text="% of the agreed price paid to the author/publisher. Tai AfroPress keeps "
                   "the remainder, including any markup above the agreed price.",
    )
    effective_from        = models.DateField(
        default=date.today,
        help_text="Terms apply to sales on or after this date, until a newer listing's effective_from.",
    )
    notes                 = models.TextField(blank=True)
    created_by            = models.ForeignKey(
        settings.AUTH_USER_MODEL, on_delete=models.SET_NULL,
        null=True, blank=True, related_name="bookstore_listings_created"
    )
    created_at             = models.DateTimeField(auto_now_add=True)

    class Meta:
        ordering = ["book", "bookstore", "-effective_from"]
        verbose_name = "Bookstore Listing"
        verbose_name_plural = "Bookstore Listings"
        indexes = [
            models.Index(fields=["book", "bookstore", "effective_from"]),
        ]

    def __str__(self) -> str:
        return f"{self.book.title} @ {self.bookstore.name} (from {self.effective_from})"

    @classmethod
    def active_for(cls, book, bookstore, as_of):
        """The terms that applied on a given date, or None if never listed by then."""
        return (
            cls.objects
            .filter(book=book, bookstore=bookstore, effective_from__lte=as_of)
            .order_by("-effective_from")
            .first()
        )


# ---------------------------------------------------------------------------
# Event
# ---------------------------------------------------------------------------

class Event(models.Model):
    name           = models.CharField(max_length=255)
    venue          = models.CharField(max_length=255)
    start_date     = models.DateField(db_index=True)
    end_date       = models.DateField()
    stock_location = models.OneToOneField(
        StockLocation, on_delete=models.SET_NULL,
        null=True, blank=True, related_name="event"
    )
    notes          = models.TextField(blank=True)
    created_at     = models.DateTimeField(auto_now_add=True)

    class Meta:
        ordering = ["-start_date"]
        verbose_name = "Event / Book Fair"
        verbose_name_plural = "Events / Book Fairs"

    def __str__(self) -> str:
        return f"{self.name} ({self.start_date})"


# ---------------------------------------------------------------------------
# Customer  (repeat-buyer lookup for POS)
# ---------------------------------------------------------------------------

class Customer(models.Model):
    name       = models.CharField(max_length=255)
    phone      = models.CharField(max_length=20, unique=True)
    email      = models.EmailField(blank=True)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        ordering = ["name"]
        verbose_name = "Customer"
        verbose_name_plural = "Customers"

    def __str__(self) -> str:
        return f"{self.name} ({self.phone})"


# ---------------------------------------------------------------------------
# Sale
# ---------------------------------------------------------------------------

def _generate_invoice_number() -> str:
    today = date.today().strftime("%Y%m%d")
    suffix = uuid.uuid4().hex[:6].upper()
    return f"INV-{today}-{suffix}"


class Sale(models.Model):
    CHANNEL_WEBSITE   = "website"
    CHANNEL_BOOKSTORE = "bookstore"
    CHANNEL_EVENT     = "event"
    CHANNEL_WHATSAPP  = "whatsapp"
    CHANNEL_PHONE     = "phone"
    CHANNEL_WALK_IN   = "walk_in"
    CHANNEL_DIRECT    = "direct"
    CHANNEL_BULK      = "bulk"
    CHANNEL_OTHER     = "other"

    CHANNELS = [
        (CHANNEL_WEBSITE,   "Website"),
        (CHANNEL_BOOKSTORE, "Bookstore"),
        (CHANNEL_EVENT,     "Event / Book Fair"),
        (CHANNEL_WHATSAPP,  "WhatsApp"),
        (CHANNEL_PHONE,     "Phone"),
        (CHANNEL_WALK_IN,   "Walk-in"),
        (CHANNEL_DIRECT,    "Direct"),
        (CHANNEL_BULK,      "Bulk Order"),
        (CHANNEL_OTHER,     "Other"),
    ]

    PAYMENT_CASH  = "cash"
    PAYMENT_MPESA = "mpesa"
    PAYMENT_CARD  = "card"

    PAYMENT_METHODS = [
        (PAYMENT_CASH,  "Cash"),
        (PAYMENT_MPESA, "M-Pesa"),
        (PAYMENT_CARD,  "Card / Bank Transfer"),
    ]

    invoice_number      = models.CharField(
        max_length=30, unique=True, db_index=True,
        default=_generate_invoice_number
    )
    sale_channel        = models.CharField(max_length=30, choices=CHANNELS, db_index=True)
    customer_name       = models.CharField(max_length=255, blank=True)
    customer_phone      = models.CharField(max_length=20, blank=True)
    customer            = models.ForeignKey(
        Customer, on_delete=models.SET_NULL,
        null=True, blank=True, related_name="sales"
    )
    customer_email      = models.EmailField(blank=True)
    sale_date           = models.DateField(db_index=True)

    # Optional links to related objects
    bookstore           = models.ForeignKey(
        Bookstore, on_delete=models.SET_NULL,
        null=True, blank=True, related_name="sales"
    )
    event               = models.ForeignKey(
        Event, on_delete=models.SET_NULL,
        null=True, blank=True, related_name="sales"
    )
    linked_order        = models.OneToOneField(
        "store.Order", on_delete=models.SET_NULL,
        null=True, blank=True, related_name="inventory_sale"
    )

    # Aggregated financials (denormalised for fast reporting)
    total_gross         = models.DecimalField(max_digits=12, decimal_places=2, default=Decimal("0"))
    total_discount      = models.DecimalField(max_digits=12, decimal_places=2, default=Decimal("0"))
    total_net           = models.DecimalField(max_digits=12, decimal_places=2, default=Decimal("0"))
    total_profit        = models.DecimalField(max_digits=12, decimal_places=2, default=Decimal("0"))
    total_author_payout = models.DecimalField(max_digits=12, decimal_places=2, default=Decimal("0"))
    total_bookstore_cut = models.DecimalField(
        max_digits=12, decimal_places=2, default=Decimal("0"),
        help_text="Sum of external bookstores' cut, for distribution-channel sales only.",
    )

    notes               = models.TextField(blank=True)
    payment_method      = models.CharField(max_length=10, choices=PAYMENT_METHODS, blank=True)
    mpesa_code          = models.CharField(max_length=20, blank=True)
    amount_tendered     = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True)
    receipt_sent        = models.BooleanField(default=False)
    recorded_by         = models.ForeignKey(
        settings.AUTH_USER_MODEL, on_delete=models.SET_NULL,
        null=True, blank=True, related_name="recorded_sales"
    )
    created_at          = models.DateTimeField(auto_now_add=True)
    updated_at          = models.DateTimeField(auto_now=True)

    # Voiding (not deleting) a sale — see sales_service.void_sale(). A voided
    # sale stays in place for audit purposes but drops out of revenue/payout
    # reporting, and its stock/ledger impact gets reversed, not erased.
    is_voided           = models.BooleanField(default=False, db_index=True)
    voided_at           = models.DateTimeField(null=True, blank=True)
    voided_by           = models.ForeignKey(
        settings.AUTH_USER_MODEL, on_delete=models.SET_NULL,
        null=True, blank=True, related_name="voided_sales"
    )
    void_reason         = models.CharField(max_length=255, blank=True)

    class Meta:
        ordering = ["-sale_date", "-created_at"]
        verbose_name = "Sale"
        verbose_name_plural = "Sales"
        indexes = [
            models.Index(fields=["sale_date", "sale_channel"]),
        ]

    def __str__(self) -> str:
        return f"{self.invoice_number} | {self.sale_date} | KES {self.total_net}"

    @property
    def sold_at_label(self) -> str:
        """Where this sale actually happened, for payee-facing statements."""
        if self.sale_channel == self.CHANNEL_WALK_IN:
            return "Tai Bookstore"
        if self.sale_channel == self.CHANNEL_BOOKSTORE and self.bookstore_id:
            return self.bookstore.name
        if self.sale_channel == self.CHANNEL_EVENT and self.event_id:
            return self.event.name
        return self.get_sale_channel_display()


# ---------------------------------------------------------------------------
# SaleItem
# ---------------------------------------------------------------------------

class SaleItem(models.Model):
    sale                  = models.ForeignKey(
        Sale, on_delete=models.CASCADE, related_name="items"
    )
    book                  = models.ForeignKey(
        "catalog_app.Book", on_delete=models.PROTECT,
        related_name="sale_items"
    )
    stock_location        = models.ForeignKey(
        StockLocation, on_delete=models.SET_NULL,
        null=True, blank=True, related_name="sale_items"
    )

    quantity              = models.PositiveIntegerField()
    unit_price            = models.DecimalField(max_digits=10, decimal_places=2)
    discount_percent      = models.DecimalField(max_digits=5, decimal_places=2, default=Decimal("0"))

    # Derived amounts — stored as snapshot, never recalculated
    gross_amount          = models.DecimalField(max_digits=12, decimal_places=2)
    discount_amount       = models.DecimalField(max_digits=12, decimal_places=2)
    net_amount            = models.DecimalField(max_digits=12, decimal_places=2)

    # Margin snapshot at time of sale — immutable
    my_keep_percent       = models.DecimalField(max_digits=5, decimal_places=2)
    author_pay_percent    = models.DecimalField(max_digits=5, decimal_places=2)
    my_profit_amount      = models.DecimalField(max_digits=12, decimal_places=2)
    author_payout_amount  = models.DecimalField(max_digits=12, decimal_places=2)

    # External bookstore distribution — set only when this line went through
    # a BookstoreListing (3-way split). my_keep_percent/author_pay_percent
    # above still hold the *effective* platform/payee shares of net_amount;
    # bookstore_cut_percent is the third, contractual share.
    bookstore_listing      = models.ForeignKey(
        "inventory.BookstoreListing", on_delete=models.SET_NULL,
        null=True, blank=True, related_name="sale_items",
    )
    bookstore_cut_percent  = models.DecimalField(max_digits=5, decimal_places=2, null=True, blank=True)
    bookstore_cut_amount   = models.DecimalField(max_digits=12, decimal_places=2, null=True, blank=True)

    # Payout tracking
    payout_included       = models.BooleanField(default=False, db_index=True)

    # Voiding a single line of a multi-item invoice — see
    # sales_service.void_sale_item(). Mirrors Sale.is_voided/voided_at/
    # voided_by/void_reason, just scoped to one line instead of the whole
    # invoice.
    is_voided              = models.BooleanField(default=False, db_index=True)
    voided_at              = models.DateTimeField(null=True, blank=True)
    voided_by              = models.ForeignKey(
        settings.AUTH_USER_MODEL, on_delete=models.SET_NULL,
        null=True, blank=True, related_name="voided_sale_items"
    )
    void_reason            = models.CharField(max_length=255, blank=True)

    class Meta:
        ordering = ["id"]
        verbose_name = "Sale Item"
        verbose_name_plural = "Sale Items"
        indexes = [
            models.Index(fields=["book", "payout_included"]),
        ]

    def __str__(self) -> str:
        return f"{self.quantity}× {self.book.title} (KES {self.net_amount})"


# ---------------------------------------------------------------------------
# AuthorPayout
# ---------------------------------------------------------------------------

class AuthorPayout(models.Model):
    STATUS_PENDING   = "pending"
    STATUS_APPROVED  = "approved"
    STATUS_PAID      = "paid"
    STATUS_CANCELLED = "cancelled"

    STATUSES = [
        (STATUS_PENDING,   "Pending"),
        (STATUS_APPROVED,  "Approved"),
        (STATUS_PAID,      "Paid"),
        (STATUS_CANCELLED, "Cancelled"),
    ]

    PERIOD_MONTHLY   = "monthly"
    PERIOD_QUARTERLY = "quarterly"
    PERIOD_CUSTOM    = "custom"

    PERIODS = [
        (PERIOD_MONTHLY,   "Monthly"),
        (PERIOD_QUARTERLY, "Quarterly"),
        (PERIOD_CUSTOM,    "Custom"),
    ]

    author              = models.ForeignKey(
        "catalog_app.Author", on_delete=models.PROTECT,
        null=True, blank=True, related_name="payouts"
    )
    publisher           = models.ForeignKey(
        "catalog_app.Publisher", on_delete=models.PROTECT,
        null=True, blank=True, related_name="payouts"
    )
    period_type         = models.CharField(max_length=20, choices=PERIODS)
    period_start        = models.DateField()
    period_end          = models.DateField()

    total_earnings      = models.DecimalField(max_digits=12, decimal_places=2)
    total_adjustments   = models.DecimalField(max_digits=12, decimal_places=2, default=Decimal("0"))
    total_paid          = models.DecimalField(max_digits=12, decimal_places=2, default=Decimal("0"))
    outstanding_balance = models.DecimalField(max_digits=12, decimal_places=2)

    status              = models.CharField(max_length=20, choices=STATUSES, default=STATUS_PENDING, db_index=True)
    payment_method      = models.CharField(max_length=100, blank=True)
    payment_reference   = models.CharField(max_length=100, blank=True)
    payment_date        = models.DateField(null=True, blank=True)
    notes               = models.TextField(blank=True)

    prepared_by         = models.ForeignKey(
        settings.AUTH_USER_MODEL, on_delete=models.SET_NULL,
        null=True, blank=True, related_name="prepared_payouts"
    )
    approved_by         = models.ForeignKey(
        settings.AUTH_USER_MODEL, on_delete=models.SET_NULL,
        null=True, blank=True, related_name="approved_payouts"
    )
    approved_at         = models.DateTimeField(null=True, blank=True)
    paid_at             = models.DateTimeField(null=True, blank=True)

    created_at          = models.DateTimeField(auto_now_add=True)
    updated_at          = models.DateTimeField(auto_now=True)

    class Meta:
        ordering = ["-period_end", "author"]
        verbose_name = "Author Payout"
        verbose_name_plural = "Author Payouts"
        indexes = [
            models.Index(fields=["author", "status"]),
            models.Index(fields=["publisher", "status"]),
            models.Index(fields=["period_start", "period_end"]),
        ]
        constraints = [
            models.CheckConstraint(
                check=(
                    models.Q(author__isnull=False, publisher__isnull=True)
                    | models.Q(author__isnull=True, publisher__isnull=False)
                ),
                name="authorpayout_exactly_one_payee",
            ),
        ]

    def clean(self):
        from django.core.exceptions import ValidationError
        if bool(self.author_id) == bool(self.publisher_id):
            raise ValidationError("Set exactly one of author or publisher, not both or neither.")

    @property
    def payee(self):
        return self.author or self.publisher

    @property
    def payee_label(self) -> str:
        return "Publisher" if self.publisher_id else "Author"

    def __str__(self) -> str:
        return (
            f"{self.payee.name} | {self.period_start} → {self.period_end} | "
            f"KES {self.total_earnings} | {self.get_status_display()}"
        )


# ---------------------------------------------------------------------------
# AuthorPayoutItem
# ---------------------------------------------------------------------------

class AuthorPayoutItem(models.Model):
    payout             = models.ForeignKey(
        AuthorPayout, on_delete=models.CASCADE, related_name="items"
    )
    sale_item          = models.ForeignKey(
        SaleItem, on_delete=models.PROTECT, related_name="payout_items"
    )
    book               = models.ForeignKey(
        "catalog_app.Book", on_delete=models.PROTECT,
        related_name="payout_items"
    )
    gross_earnings     = models.DecimalField(max_digits=12, decimal_places=2)
    adjustment_amount  = models.DecimalField(max_digits=12, decimal_places=2, default=Decimal("0"))
    adjustment_notes   = models.TextField(blank=True)
    net_earnings       = models.DecimalField(max_digits=12, decimal_places=2)

    class Meta:
        ordering = ["book__title"]
        verbose_name = "Payout Item"
        verbose_name_plural = "Payout Items"

    def __str__(self) -> str:
        return f"{self.book.title}: KES {self.net_earnings}"


# ---------------------------------------------------------------------------
# Expense
# ---------------------------------------------------------------------------

class ExpenseCategory(models.Model):
    name        = models.CharField(max_length=100, unique=True)
    description = models.TextField(blank=True)

    class Meta:
        ordering = ["name"]
        verbose_name = "Expense Category"
        verbose_name_plural = "Expense Categories"

    def __str__(self) -> str:
        return self.name


class Expense(models.Model):
    category      = models.ForeignKey(
        ExpenseCategory, on_delete=models.PROTECT, related_name="expenses"
    )
    amount        = models.DecimalField(max_digits=12, decimal_places=2)
    date          = models.DateField(db_index=True)
    description   = models.TextField()
    linked_book   = models.ForeignKey(
        "catalog_app.Book", on_delete=models.SET_NULL,
        null=True, blank=True, related_name="expenses"
    )
    linked_event  = models.ForeignKey(
        Event, on_delete=models.SET_NULL,
        null=True, blank=True, related_name="expenses"
    )
    linked_sale   = models.ForeignKey(
        Sale, on_delete=models.SET_NULL,
        null=True, blank=True, related_name="expenses"
    )
    receipt_image = models.FileField(
        upload_to="expense_receipts/", null=True, blank=True
    )
    recorded_by   = models.ForeignKey(
        settings.AUTH_USER_MODEL, on_delete=models.SET_NULL,
        null=True, blank=True, related_name="recorded_expenses"
    )
    notes         = models.TextField(blank=True)
    created_at    = models.DateTimeField(auto_now_add=True)

    class Meta:
        ordering = ["-date", "-created_at"]
        verbose_name = "Expense"
        verbose_name_plural = "Expenses"
        indexes = [
            models.Index(fields=["date", "category"]),
        ]

    def __str__(self) -> str:
        return f"{self.category.name} | KES {self.amount} | {self.date}"


# ---------------------------------------------------------------------------
# PendingInventoryTransaction  (maker-checker staging record)
# ---------------------------------------------------------------------------

class PendingInventoryTransaction(models.Model):
    OP_RECEIVE  = "receive"
    OP_TRANSFER = "transfer"
    OP_ADJUST   = "adjust"

    OP_TYPES = [
        (OP_RECEIVE,  "Receive from Author / Supplier"),
        (OP_TRANSFER, "Internal Transfer"),
        (OP_ADJUST,   "Stock Adjustment"),
    ]

    STATUS_PENDING  = "pending"
    STATUS_APPROVED = "approved"
    STATUS_REJECTED = "rejected"

    STATUSES = [
        (STATUS_PENDING,  "Pending"),
        (STATUS_APPROVED, "Approved"),
        (STATUS_REJECTED, "Rejected"),
    ]

    op_type          = models.CharField(max_length=20, choices=OP_TYPES, db_index=True)
    book             = models.ForeignKey(
        "catalog_app.Book", on_delete=models.PROTECT,
        related_name="pending_transactions"
    )
    source_location  = models.ForeignKey(
        StockLocation, on_delete=models.PROTECT,
        null=True, blank=True, related_name="pending_outgoing"
    )
    dest_location    = models.ForeignKey(
        StockLocation, on_delete=models.PROTECT,
        null=True, blank=True, related_name="pending_incoming"
    )
    quantity         = models.PositiveIntegerField(null=True, blank=True)
    new_quantity     = models.IntegerField(null=True, blank=True)  # adjust only
    reference        = models.CharField(max_length=100, blank=True)
    notes            = models.TextField(blank=True)

    status           = models.CharField(max_length=20, choices=STATUSES, default=STATUS_PENDING, db_index=True)
    submitted_by     = models.ForeignKey(
        settings.AUTH_USER_MODEL, on_delete=models.SET_NULL,
        null=True, related_name="submitted_pending_transactions"
    )
    submitted_at     = models.DateTimeField(auto_now_add=True)
    reviewed_by      = models.ForeignKey(
        settings.AUTH_USER_MODEL, on_delete=models.SET_NULL,
        null=True, blank=True, related_name="reviewed_pending_transactions"
    )
    reviewed_at      = models.DateTimeField(null=True, blank=True)
    rejection_reason = models.TextField(blank=True)
    linked_tx        = models.OneToOneField(
        InventoryTransaction, on_delete=models.SET_NULL,
        null=True, blank=True, related_name="pending_source"
    )

    class Meta:
        ordering = ["-submitted_at"]
        verbose_name = "Pending Inventory Transaction"
        verbose_name_plural = "Pending Inventory Transactions"
        indexes = [
            models.Index(fields=["status", "submitted_at"]),
        ]

    def __str__(self) -> str:
        return (
            f"{self.get_op_type_display()} | {self.book.title} | "
            f"{self.get_status_display()} | {self.submitted_at:%Y-%m-%d}"
        )


# ---------------------------------------------------------------------------
# AuditLog  (immutable; no updated_at)
# ---------------------------------------------------------------------------

class AuditLog(models.Model):
    ACTION_CREATE  = "CREATE"
    ACTION_UPDATE  = "UPDATE"
    ACTION_DELETE  = "DELETE"
    ACTION_APPROVE = "APPROVE"
    ACTION_REJECT  = "REJECT"
    ACTION_PAY     = "PAY"
    ACTION_REVERSE = "REVERSE"

    ACTIONS = [
        (ACTION_CREATE,  "Create"),
        (ACTION_UPDATE,  "Update"),
        (ACTION_DELETE,  "Delete"),
        (ACTION_APPROVE, "Approve"),
        (ACTION_REJECT,  "Reject"),
        (ACTION_PAY,     "Pay"),
        (ACTION_REVERSE, "Reverse"),
    ]

    user         = models.ForeignKey(
        settings.AUTH_USER_MODEL, on_delete=models.SET_NULL,
        null=True, blank=True, related_name="inventory_audit_logs"
    )
    action       = models.CharField(max_length=20, choices=ACTIONS, db_index=True)
    model_name   = models.CharField(max_length=100, db_index=True)
    object_id    = models.CharField(max_length=50)
    object_repr  = models.CharField(max_length=500)
    old_values   = models.JSONField(null=True, blank=True)
    new_values   = models.JSONField(null=True, blank=True)
    ip_address   = models.GenericIPAddressField(null=True, blank=True)
    timestamp    = models.DateTimeField(auto_now_add=True, db_index=True)

    class Meta:
        ordering = ["-timestamp"]
        verbose_name = "Audit Log"
        verbose_name_plural = "Audit Logs"
        indexes = [
            models.Index(fields=["model_name", "object_id"]),
            models.Index(fields=["user", "timestamp"]),
        ]

    def __str__(self) -> str:
        user_str = self.user.username if self.user else "system"
        return f"{self.action} {self.model_name}#{self.object_id} by {user_str} @ {self.timestamp:%Y-%m-%d %H:%M}"


# ---------------------------------------------------------------------------
# PosMpesaPayment — an M-Pesa STK prompt sent from the POS
# ---------------------------------------------------------------------------

class PosMpesaPayment(models.Model):
    """
    One STK prompt sent to a walk-in customer's phone from the POS. Updated by
    Safaricom's callback (shared with store orders) or a status query, and
    linked to the Sale once the cashier completes it — a payment can back
    only one sale.
    """
    STATUS_PENDING = "pending"
    STATUS_PAID    = "paid"
    STATUS_FAILED  = "failed"
    STATUSES = [
        (STATUS_PENDING, "Waiting for customer"),
        (STATUS_PAID,    "Paid"),
        (STATUS_FAILED,  "Failed / cancelled"),
    ]

    phone_number         = models.CharField(max_length=20)
    amount               = models.DecimalField(
        max_digits=12, decimal_places=2,
        help_text="Cart total the prompt was sent for (sandbox charges KES 1).",
    )
    checkout_request_id  = models.CharField(max_length=100, blank=True, db_index=True)
    merchant_request_id  = models.CharField(max_length=100, blank=True)
    status               = models.CharField(max_length=20, choices=STATUSES, default=STATUS_PENDING, db_index=True)
    mpesa_receipt_number = models.CharField(max_length=50, blank=True, db_index=True)
    result_code          = models.IntegerField(null=True, blank=True)
    result_desc          = models.TextField(blank=True)
    raw_callback         = models.JSONField(null=True, blank=True)
    sale                 = models.OneToOneField(
        "inventory.Sale", on_delete=models.SET_NULL,
        null=True, blank=True, related_name="mpesa_stk_payment",
    )
    created_by           = models.ForeignKey(
        settings.AUTH_USER_MODEL, on_delete=models.SET_NULL,
        null=True, blank=True, related_name="pos_mpesa_payments",
    )
    created_at           = models.DateTimeField(auto_now_add=True)
    updated_at           = models.DateTimeField(auto_now=True)

    class Meta:
        ordering = ["-created_at"]
        verbose_name = "POS M-Pesa Payment"
        verbose_name_plural = "POS M-Pesa Payments"

    def __str__(self) -> str:
        return f"{self.phone_number} | KES {self.amount} | {self.get_status_display()}"


# ---------------------------------------------------------------------------
# ReportsHub  (unmanaged — hangs the "Reports & Dashboard" admin section off
# a proper nav entry; no table, no rows, view-only)
# ---------------------------------------------------------------------------

class ReportsHub(models.Model):
    class Meta:
        managed = False
        default_permissions = ()
        verbose_name = "Reports & Dashboard"
        verbose_name_plural = "Reports & Dashboard"
