"""
inventory/services/sales_service.py
─────────────────────────────────────
Records sales and calculates margin splits.

Two margin modes, both quantized/snapshotted on the SaleItem at time of sale:

Direct (no active BookstoreListing for this book+bookstore):
  gross_amount         = quantity × unit_price
  discount_amount      = gross_amount × (discount_percent / 100)
  net_amount           = gross_amount − discount_amount
  my_profit_amount     = net_amount × (my_keep_percent / 100)
  author_payout_amount = net_amount − my_profit_amount
  my_keep_percent resolved: Book override → payee (publisher, else author)
  default → 25%.

Bookstore distribution (book has an active BookstoreListing with the sale's
bookstore as of sale_date) — a 3-way split where the payee's cut is fixed to
the *agreed* price regardless of markup:
  gross_amount / discount_amount / net_amount — same as above, using the
    *actual* unit_price entered (which may be marked up from the agreed price)
  bookstore_cut_amount = net_amount × (listing.bookstore_cut_percent / 100)
  author_payout_amount = quantity × listing.agreed_unit_price × (listing.payee_cut_percent / 100)
  my_profit_amount     = net_amount − bookstore_cut_amount − author_payout_amount
  (absorbs both our agreed share AND any markup above the agreed price)
"""

import logging
import uuid
from datetime import date
from decimal import Decimal, ROUND_HALF_UP

from django.db import transaction
from django.db.models import Sum
from django.utils import timezone

from catalog_app.models import Book
from inventory.models import (
    AuditLog,
    InventoryTransaction,
    Sale,
    SaleItem,
    StockLocation,
)

logger = logging.getLogger(__name__)

_TWO = Decimal("0.01")


# ---------------------------------------------------------------------------
# Margin calculation (pure function — easy to test in isolation)
# ---------------------------------------------------------------------------

def calculate_item_margins(
    *,
    book: Book,
    quantity: int,
    unit_price: Decimal,
    discount_percent: Decimal = Decimal("0"),
    bookstore=None,
    sale_date: date | None = None,
) -> dict:
    """
    Return a fully-calculated margin dict for one sale line.

    If `bookstore` has an active BookstoreListing for this book as of
    `sale_date` (defaults to today), uses the 3-way distribution split.
    Otherwise uses the standard 2-way split, with my_keep_percent resolved:
    Book override → payee (publisher, else author) default → 25%.
    """
    gross_amount    = (Decimal(str(quantity)) * unit_price).quantize(_TWO)
    discount_amount = (gross_amount * discount_percent / Decimal("100")).quantize(_TWO, ROUND_HALF_UP)
    net_amount      = (gross_amount - discount_amount).quantize(_TWO)

    listing = None
    if bookstore is not None:
        from inventory.models import BookstoreListing
        listing = BookstoreListing.active_for(book, bookstore, sale_date or date.today())

    if listing is not None:
        bookstore_cut_amount = (
            net_amount * listing.bookstore_cut_percent / Decimal("100")
        ).quantize(_TWO, ROUND_HALF_UP)
        author_payout_amount = (
            Decimal(str(quantity)) * listing.agreed_unit_price * listing.payee_cut_percent / Decimal("100")
        ).quantize(_TWO, ROUND_HALF_UP)
        my_profit_amount = (net_amount - bookstore_cut_amount - author_payout_amount).quantize(_TWO)

        # Effective shares of net_amount, for display/consistency — the payee's
        # amount is fixed to the agreed price, not literally net_amount × a %.
        my_keep_percent = (
            (my_profit_amount / net_amount * Decimal("100")).quantize(_TWO, ROUND_HALF_UP)
            if net_amount else Decimal("0.00")
        )
        author_pay_percent = (
            (author_payout_amount / net_amount * Decimal("100")).quantize(_TWO, ROUND_HALF_UP)
            if net_amount else Decimal("0.00")
        )

        return {
            "quantity":              quantity,
            "unit_price":            unit_price.quantize(_TWO),
            "discount_percent":      discount_percent.quantize(_TWO),
            "gross_amount":          gross_amount,
            "discount_amount":       discount_amount,
            "net_amount":            net_amount,
            "my_keep_percent":       my_keep_percent,
            "author_pay_percent":    author_pay_percent,
            "my_profit_amount":      my_profit_amount,
            "author_payout_amount":  author_payout_amount,
            "bookstore_listing":     listing,
            "bookstore_cut_percent": listing.bookstore_cut_percent.quantize(_TWO),
            "bookstore_cut_amount":  bookstore_cut_amount,
        }

    # Resolve margin: per-book override → payee (publisher, else author) default → 30%
    payee = book.publisher or book.author
    my_keep_percent = (
        book.my_keep_percent
        if book.my_keep_percent is not None
        else (
            payee.default_my_keep_percent
            if payee and payee.default_my_keep_percent is not None
            else Decimal("30.00")
        )
    )
    author_pay_percent = (Decimal("100") - my_keep_percent).quantize(_TWO)

    my_profit_amount     = (net_amount * my_keep_percent / Decimal("100")).quantize(_TWO, ROUND_HALF_UP)
    author_payout_amount = (net_amount - my_profit_amount).quantize(_TWO)

    return {
        "quantity":              quantity,
        "unit_price":            unit_price.quantize(_TWO),
        "discount_percent":      discount_percent.quantize(_TWO),
        "gross_amount":          gross_amount,
        "discount_amount":       discount_amount,
        "net_amount":            net_amount,
        "my_keep_percent":       my_keep_percent.quantize(_TWO),
        "author_pay_percent":    author_pay_percent,
        "my_profit_amount":      my_profit_amount,
        "author_payout_amount":  author_payout_amount,
        "bookstore_listing":     None,
        "bookstore_cut_percent": None,
        "bookstore_cut_amount":  None,
    }


def _resolve_internal_bookstore(sale_channel: str):
    """
    Walk-in and website sales are never staff-assigned to a bookstore (that
    field is for genuine external consignment partners), but they still need
    to resolve to the internal Tai Bookstore/website BookstoreListing — if
    one has been configured — so the author's payout is fixed to the agreed
    price instead of scaling with whatever markup the sale was made at.
    Returns None (falls back to the plain 2-way split) if that row hasn't
    been set up yet, or for any other channel.
    """
    if sale_channel not in (Sale.CHANNEL_WALK_IN, Sale.CHANNEL_WEBSITE):
        return None
    from inventory.models import Bookstore
    return Bookstore.objects.filter(is_internal=True, is_active=True).first()


# ---------------------------------------------------------------------------
# Record a sale
# ---------------------------------------------------------------------------

@transaction.atomic
def record_sale(
    *,
    items: list[dict],
    sale_channel: str,
    sale_date: date,
    customer_name: str = "",
    customer_phone: str = "",
    bookstore=None,
    event=None,
    linked_order=None,
    stock_location: StockLocation | None = None,
    notes: str = "",
    user=None,
) -> Sale:
    """
    Create a Sale + SaleItems, deduct inventory, and create audit trail.

    items: list of dicts with keys:
        book          (Book instance)
        quantity      (int)
        unit_price    (Decimal)
        discount_percent (Decimal, default 0)
        stock_location (StockLocation | None, overrides sale-level default)
    """
    if not items:
        raise ValueError("A sale must have at least one item.")

    # Aggregate totals
    total_gross   = Decimal("0")
    total_discount = Decimal("0")
    total_net     = Decimal("0")
    total_profit  = Decimal("0")
    total_payout  = Decimal("0")
    total_bookstore_cut = Decimal("0")

    # Only used to resolve each line's BookstoreListing — Sale.bookstore
    # itself (set below) stays whatever the caller passed, so it keeps
    # meaning "an explicit external consignment partner" for filtering/display.
    listing_bookstore = bookstore or _resolve_internal_bookstore(sale_channel)

    calculated = []
    for line in items:
        book           = line["book"]
        quantity       = int(line["quantity"])
        unit_price     = Decimal(str(line["unit_price"]))
        disc_pct       = Decimal(str(line.get("discount_percent", "0")))
        line_location  = line.get("stock_location") or stock_location

        margins = calculate_item_margins(
            book=book,
            quantity=quantity,
            unit_price=unit_price,
            discount_percent=disc_pct,
            bookstore=listing_bookstore,
            sale_date=sale_date,
        )
        margins["stock_location"] = line_location
        margins["book"] = book
        calculated.append(margins)

        total_gross    += margins["gross_amount"]
        total_discount += margins["discount_amount"]
        total_net      += margins["net_amount"]
        total_profit   += margins["my_profit_amount"]
        total_payout   += margins["author_payout_amount"]
        total_bookstore_cut += margins["bookstore_cut_amount"] or Decimal("0")

    sale = Sale.objects.create(
        sale_channel=sale_channel,
        sale_date=sale_date,
        customer_name=customer_name,
        customer_phone=customer_phone,
        bookstore=bookstore,
        event=event,
        linked_order=linked_order,
        total_gross=total_gross,
        total_discount=total_discount,
        total_net=total_net,
        total_profit=total_profit,
        total_author_payout=total_payout,
        total_bookstore_cut=total_bookstore_cut,
        notes=notes,
        recorded_by=user,
    )

    for margins in calculated:
        book = margins["book"]
        line_location = margins["stock_location"]

        SaleItem.objects.create(
            sale=sale,
            book=book,
            stock_location=line_location,
            quantity=margins["quantity"],
            unit_price=margins["unit_price"],
            discount_percent=margins["discount_percent"],
            gross_amount=margins["gross_amount"],
            discount_amount=margins["discount_amount"],
            net_amount=margins["net_amount"],
            my_keep_percent=margins["my_keep_percent"],
            author_pay_percent=margins["author_pay_percent"],
            my_profit_amount=margins["my_profit_amount"],
            author_payout_amount=margins["author_payout_amount"],
            bookstore_listing=margins["bookstore_listing"],
            bookstore_cut_percent=margins["bookstore_cut_percent"],
            bookstore_cut_amount=margins["bookstore_cut_amount"],
        )

        # Deduct from inventory
        if line_location and book.track_inventory:
            from inventory.services.inventory_service import _adjust_balance, _sync_book_stock
            InventoryTransaction.objects.create(
                book=book,
                source_location=line_location,
                quantity=margins["quantity"],
                transaction_type=InventoryTransaction.TYPE_SALE,
                transaction_reference=sale.invoice_number,
                notes=f"Sale: {sale.invoice_number}",
                performed_by=user,
            )
            _adjust_balance(book, line_location, -margins["quantity"])
            _sync_book_stock(book)

    AuditLog.objects.create(
        user=user,
        action=AuditLog.ACTION_CREATE,
        model_name="Sale",
        object_id=str(sale.pk),
        object_repr=str(sale),
        new_values={
            "invoice_number": sale.invoice_number,
            "channel": sale_channel,
            "total_net": str(total_net),
        },
    )
    logger.info("Sale recorded: %s | KES %s | channel=%s", sale.invoice_number, total_net, sale_channel)
    return sale


# ---------------------------------------------------------------------------
# Void a sale, or a single line of one (never hard-delete — see
# inventory_service.reverse_transaction)
# ---------------------------------------------------------------------------

def _reverse_item_stock(item: "SaleItem", *, reason: str, user=None) -> None:
    """Find this line's own TYPE_SALE ledger entry and reverse it, if any."""
    from inventory.services.inventory_service import reverse_transaction

    if not item.stock_location or not item.book.track_inventory:
        return
    tx = InventoryTransaction.objects.filter(
        transaction_type=InventoryTransaction.TYPE_SALE,
        transaction_reference=item.sale.invoice_number,
        book=item.book,
        source_location=item.stock_location,
        quantity=item.quantity,
        is_reversed=False,
    ).first()
    if tx:
        reverse_transaction(
            original=tx,
            notes=reason or f"{item.sale.invoice_number} — {item.book.title} voided",
            user=user,
        )


def _recalculate_sale_totals(sale: Sale) -> None:
    """Recompute Sale's denormalized totals from its still-active (non-voided) items."""
    agg = sale.items.filter(is_voided=False).aggregate(
        total_gross=Sum("gross_amount"),
        total_discount=Sum("discount_amount"),
        total_net=Sum("net_amount"),
        total_profit=Sum("my_profit_amount"),
        total_author_payout=Sum("author_payout_amount"),
        total_bookstore_cut=Sum("bookstore_cut_amount"),
    )
    sale.total_gross = agg["total_gross"] or Decimal("0")
    sale.total_discount = agg["total_discount"] or Decimal("0")
    sale.total_net = agg["total_net"] or Decimal("0")
    sale.total_profit = agg["total_profit"] or Decimal("0")
    sale.total_author_payout = agg["total_author_payout"] or Decimal("0")
    sale.total_bookstore_cut = agg["total_bookstore_cut"] or Decimal("0")
    sale.save(update_fields=[
        "total_gross", "total_discount", "total_net",
        "total_profit", "total_author_payout", "total_bookstore_cut",
    ])


@transaction.atomic
def void_sale(sale: Sale, *, user=None, reason: str = "") -> Sale:
    """
    Void a whole sale instead of deleting it: restores the stock each line
    deducted (by reversing its matching InventoryTransaction, the same
    "never delete, create a reversal" discipline the rest of inventory
    already follows), then flags the sale itself as voided.

    The sale and its items are left in place — voiding only drops them out
    of revenue/payout reporting (see report_service.py / payout_service.py,
    which filter on is_voided) — so the record stays visible for audit.
    """
    if sale.is_voided:
        raise ValueError("This sale is already voided.")

    note = f"Void: {reason}" if reason else f"Sale {sale.invoice_number} voided"
    for item in sale.items.select_related("book", "stock_location").all():
        _reverse_item_stock(item, reason=note, user=user)

    sale.is_voided = True
    sale.voided_at = timezone.now()
    sale.voided_by = user
    sale.void_reason = reason
    sale.save(update_fields=["is_voided", "voided_at", "voided_by", "void_reason"])

    AuditLog.objects.create(
        user=user,
        action=AuditLog.ACTION_REVERSE,
        model_name="Sale",
        object_id=str(sale.pk),
        object_repr=str(sale),
        new_values={"invoice_number": sale.invoice_number, "reason": reason},
    )
    logger.info("Sale voided: %s | reason=%s", sale.invoice_number, reason)
    return sale


@transaction.atomic
def void_sale_item(item: "SaleItem", *, user=None, reason: str = "") -> "SaleItem":
    """
    Void a single line of a multi-item invoice — restores just that line's
    stock, excludes it from revenue/payout reporting, and recalculates the
    parent Sale's totals from its remaining active lines, without touching
    anything else on the invoice.

    If this was the last non-voided line, the whole Sale is marked voided
    too — its stock/ledger impact is already fully reversed line-by-line at
    that point, so there's nothing left to reverse at the Sale level.
    """
    if item.is_voided:
        raise ValueError("This line is already voided.")
    sale = item.sale
    if sale.is_voided:
        raise ValueError("The whole invoice is already voided.")

    note = f"Void line: {reason}" if reason else f"{sale.invoice_number} — {item.book.title} voided"
    _reverse_item_stock(item, reason=note, user=user)

    item.is_voided = True
    item.voided_at = timezone.now()
    item.voided_by = user
    item.void_reason = reason
    item.save(update_fields=["is_voided", "voided_at", "voided_by", "void_reason"])

    _recalculate_sale_totals(sale)

    AuditLog.objects.create(
        user=user,
        action=AuditLog.ACTION_REVERSE,
        model_name="SaleItem",
        object_id=str(item.pk),
        object_repr=f"{sale.invoice_number} — {item.book.title}",
        new_values={"invoice_number": sale.invoice_number, "book": item.book.title, "reason": reason},
    )
    logger.info("Sale line voided: %s | book=%s | reason=%s", sale.invoice_number, item.book.title, reason)

    if not sale.items.filter(is_voided=False).exists():
        sale.is_voided = True
        sale.voided_at = timezone.now()
        sale.voided_by = user
        sale.void_reason = reason or "All line items voided."
        sale.save(update_fields=["is_voided", "voided_at", "voided_by", "void_reason"])
        logger.info("Sale %s auto-voided — all lines voided.", sale.invoice_number)

    return item


# ---------------------------------------------------------------------------
# Link a store.Order to a Sale (called from signal when order is paid)
# ---------------------------------------------------------------------------

@transaction.atomic
def create_sale_from_order(order) -> Sale | None:
    """
    Automatically create an inventory Sale when a store Order is paid.
    Skips if a Sale already exists for this order.
    """
    from inventory.models import Sale as SaleModel

    if SaleModel.objects.filter(linked_order=order).exists():
        return None

    # Website orders fulfill from the same physical stock as every other
    # channel — there's no separate "website" stock pool.
    try:
        fulfillment_location = StockLocation.objects.get(code="MAIN-WH")
    except StockLocation.DoesNotExist:
        fulfillment_location = None

    items = []
    for order_item in order.items.select_related("book__author").all():
        book = order_item.book
        items.append({
            "book": book,
            "quantity": order_item.quantity,
            "unit_price": order_item.unit_price,
            "discount_percent": Decimal("0"),
            "stock_location": fulfillment_location,
        })

    if not items:
        return None

    sale = record_sale(
        items=items,
        sale_channel=Sale.CHANNEL_WEBSITE,
        sale_date=order.payment_confirmed_at.date() if order.payment_confirmed_at else date.today(),
        customer_name=order.customer_name,
        customer_phone=order.phone_number,
        linked_order=order,
        notes=f"Auto-created from order {order.order_reference}",
    )
    return sale
