"""
Operational posting hooks — bridge business events to the GL posting engine.
"""
from __future__ import annotations

import logging
from decimal import Decimal

from finance_and_accounting.models import AccountHistory, ChartOfAccount
from finance_and_accounting.posting_engine import (
    PostingError,
    _account_by_number,
    post_balanced_lines,
    post_direct_entry,
    post_event,
    resolve_posting_rule,
)

logger = logging.getLogger(__name__)


def already_posted(rel_type: str, rel_id) -> bool:
    return AccountHistory.objects.filter(
        rel_type=rel_type,
        rel_id=str(rel_id),
    ).exists()


def try_post_operational_event(
    *,
    branch,
    staff,
    mapping_type: str,
    source_key: str,
    amount,
    rel_type: str,
    rel_id,
    description: str = "",
    reference: str = "",
) -> dict:
    """
    Post a balanced GL entry from AccountMapping rules.
    Idempotent: skips if rel_type+rel_id already exists in AccountHistory.
    """
    if already_posted(rel_type, rel_id):
        return {"posted": False, "skipped": True, "reason": "already_posted"}

    try:
        result = post_event(
            branch=branch,
            staff=staff,
            mapping_type=mapping_type,
            source_key=source_key,
            amount=amount,
            transaction_date=None,
            description=description,
            rel_type=rel_type,
            rel_id=str(rel_id),
            reference=reference,
            create_journal_document=True,
        )
        return {"posted": True, **result}
    except PostingError as exc:
        logger.warning(
            "GL posting skipped for %s:%s rel=%s/%s — %s",
            mapping_type, source_key, rel_type, rel_id, exc,
        )
        return {"posted": False, "error": str(exc)}
    except Exception as exc:
        logger.exception(
            "GL posting failed for %s:%s rel=%s/%s",
            mapping_type, source_key, rel_type, rel_id,
        )
        return {"posted": False, "error": str(exc)}


def estimate_stock_transaction_cogs(stock_transaction) -> Decimal:
    """Estimate COGS for outbound stock moves from latest purchase prices."""
    total = Decimal("0")
    try:
        from procurement.models import ProductPurchaseInstance
    except ImportError:
        return total

    for line in stock_transaction.stock_transaction_instances.all():
        qty = Decimal(str(line.quantity or "0").replace(",", "") or "0")
        if qty <= 0 or not line.product_id:
            continue
        purchase = ProductPurchaseInstance.objects.filter(
            purchase_product_id=line.product_id,
        ).order_by("-id").first()
        if purchase and purchase.purchase_value_per_unit:
            unit = Decimal(str(purchase.purchase_value_per_unit).replace(",", "") or "0")
            total += unit * qty
    return total.quantize(Decimal("0.01"))


def _parse_amount(value) -> Decimal:
    return Decimal(str(value or 0).replace(",", "") or "0")


def compute_customer_order_vat_totals(customer_order) -> dict:
    """Return net, VAT, and gross totals for a customer order."""
    from sales_and_marketing.models import ProductVAT

    net = Decimal("0")
    vat = Decimal("0")
    for item in customer_order.customer_order_items.all():
        line_net = _parse_amount(item.net_subtotal)
        net += line_net
        try:
            pv = ProductVAT.objects.get(product=item.product)
            if pv.vat_category == "standard":
                rate = _parse_amount(pv.vat_percentage_value)
                vat += (line_net * rate / Decimal("100")).quantize(Decimal("0.01"))
        except ProductVAT.DoesNotExist:
            pass
    return {"net": net, "vat": vat, "gross": net + vat}


def post_sales_invoice(branch, staff, invoice, *, amount=None) -> dict:
    order = invoice.customer_order
    totals = compute_customer_order_vat_totals(order)
    net = totals["net"]
    vat = totals["vat"]
    gross = totals["gross"]

    if gross <= 0:
        gross = _parse_amount(amount if amount is not None else invoice.invoice_amount_due)
        net = gross
        vat = Decimal("0")

    if already_posted("sales_invoice", invoice.id):
        return {"posted": False, "skipped": True, "reason": "already_posted"}

    try:
        ar = _account_by_number(branch, "1100")
        revenue = _account_by_number(branch, "4000")

        if vat > 0:
            tax = _account_by_number(branch, "2100")
            result = post_balanced_lines(
                branch=branch,
                staff=staff,
                lines=[
                    {"account": ar, "debit": gross, "credit": Decimal("0")},
                    {"account": revenue, "debit": Decimal("0"), "credit": net},
                    {"account": tax, "debit": Decimal("0"), "credit": vat},
                ],
                description=f"Sales invoice {invoice.invoice_number}",
                rel_type="sales_invoice",
                rel_id=invoice.id,
                reference=invoice.invoice_number,
            )
            return {
                "posted": True,
                **result,
                "net": float(net),
                "vat": float(vat),
                "gross": float(gross),
            }

        return try_post_operational_event(
            branch=branch,
            staff=staff,
            mapping_type="invoice_posting",
            source_key="sales_invoice",
            amount=net if net > 0 else gross,
            rel_type="sales_invoice",
            rel_id=invoice.id,
            description=f"Sales invoice {invoice.invoice_number}",
            reference=invoice.invoice_number,
        )
    except PostingError as exc:
        logger.warning("Sales invoice GL posting skipped for %s — %s", invoice.id, exc)
        return {"posted": False, "error": str(exc)}
    except Exception as exc:
        logger.exception("Sales invoice GL posting failed for %s", invoice.id)
        return {"posted": False, "error": str(exc)}


def post_customer_payment(branch, staff, payment, invoice) -> dict:
    return try_post_operational_event(
        branch=branch,
        staff=staff,
        mapping_type="payment_posting_sale",
        source_key="customer_payment",
        amount=payment.payment_amount,
        rel_type="customer_payment",
        rel_id=payment.id,
        description=f"Customer payment for {invoice.invoice_number}",
        reference=getattr(payment, "payment_number", "") or str(payment.id),
    )


def post_goods_delivery(branch, staff, stock_transaction, *, amount=None) -> dict:
    amt = amount if amount is not None else estimate_stock_transaction_cogs(stock_transaction)
    if amt <= 0:
        return {"posted": False, "skipped": True, "reason": "zero_cogs"}
    return try_post_operational_event(
        branch=branch,
        staff=staff,
        mapping_type="delivery_posting",
        source_key="goods_delivery",
        amount=amt,
        rel_type="goods_delivery",
        rel_id=stock_transaction.id,
        description=f"Goods issue {stock_transaction.stock_transaction_number}",
        reference=stock_transaction.stock_transaction_reference or stock_transaction.stock_transaction_number,
    )


def post_supplier_payment(branch, staff, expense, credit_account: ChartOfAccount | None = None) -> dict:
    if already_posted("supplier_payment", expense.id):
        return {"posted": False, "skipped": True, "reason": "already_posted"}
    amount = _parse_amount(expense.expense_amount)
    if amount <= 0:
        return {"posted": False, "skipped": True, "reason": "zero_amount"}
    if credit_account is not None:
        try:
            mapping = resolve_posting_rule(branch, "payment_posting_expense", "supplier_payment")
            debit_account = (
                mapping.debit_account
                if mapping and mapping.debit_account_id
                else _account_by_number(branch, "2000")
            )
            result = post_direct_entry(
                branch=branch,
                staff=staff,
                debit_account=debit_account,
                credit_account=credit_account,
                amount=amount,
                description=f"Supplier payment {getattr(expense, 'expense_number', expense.id)}",
                rel_type="supplier_payment",
                rel_id=expense.id,
                reference=getattr(expense, "expense_number", "") or str(expense.id),
            )
            return {"posted": True, **result}
        except PostingError as exc:
            logger.warning("Supplier payment GL posting skipped for expense %s — %s", expense.id, exc)
            return {"posted": False, "error": str(exc)}
    return try_post_operational_event(
        branch=branch,
        staff=staff,
        mapping_type="payment_posting_expense",
        source_key="supplier_payment",
        amount=amount,
        rel_type="supplier_payment",
        rel_id=expense.id,
        description=f"Supplier payment {getattr(expense, 'expense_number', expense.id)}",
        reference=getattr(expense, "expense_number", "") or str(expense.id),
    )


def post_goods_receipt(branch, staff, stock_transaction, *, amount=None) -> dict:
    amt = amount if amount is not None else estimate_stock_transaction_cogs(stock_transaction)
    if amt <= 0:
        ref = (stock_transaction.stock_transaction_reference or "").strip()
        if ref:
            try:
                from procurement.models import PurchaseOrder
                po = PurchaseOrder.objects.filter(purchase_order_number=ref).first()
                if po and po.purchase_value_overall:
                    amt = Decimal(str(po.purchase_value_overall).replace(",", "") or "0")
            except Exception:
                pass
    if amt <= 0:
        return {"posted": False, "skipped": True, "reason": "zero_receipt_value"}
    return try_post_operational_event(
        branch=branch,
        staff=staff,
        mapping_type="purchase_posting",
        source_key="goods_receipt",
        amount=amt,
        rel_type="goods_receipt",
        rel_id=stock_transaction.id,
        description=f"Goods receipt {stock_transaction.stock_transaction_number}",
        reference=stock_transaction.stock_transaction_reference or stock_transaction.stock_transaction_number,
    )


def post_vendor_invoice(branch, staff, vendor_invoice) -> dict:
    if vendor_invoice.match_status not in ("matched", "partial"):
        return {
            "posted": False,
            "skipped": True,
            "reason": f"match_status_{vendor_invoice.match_status}",
        }
    amount = _parse_amount(vendor_invoice.invoice_amount)
    if amount <= 0:
        return {"posted": False, "skipped": True, "reason": "zero_amount"}
    ref = vendor_invoice.supplier_invoice_number or str(vendor_invoice.id)
    return try_post_operational_event(
        branch=branch,
        staff=staff,
        mapping_type="purchase_posting",
        source_key="vendor_invoice",
        amount=amount,
        rel_type="vendor_invoice",
        rel_id=vendor_invoice.id,
        description=f"Vendor invoice {ref}",
        reference=ref,
    )


def post_payroll_accrual(branch, staff, payroll_sheet) -> dict:
    amount = _parse_amount(payroll_sheet.payroll_sheet_value)
    if amount <= 0:
        amount = _parse_amount(payroll_sheet.payroll_sheet_total_net_pay_value)
    if amount <= 0:
        return {"posted": False, "skipped": True, "reason": "zero_amount"}
    return try_post_operational_event(
        branch=branch,
        staff=staff,
        mapping_type="payroll_posting",
        source_key="payroll_accrual",
        amount=amount,
        rel_type="payroll_accrual",
        rel_id=payroll_sheet.id,
        description=f"Payroll accrual {payroll_sheet.payroll_sheet_number}",
        reference=payroll_sheet.payroll_sheet_number,
    )


def post_payroll_payment(branch, staff, expense, credit_account: ChartOfAccount) -> dict:
    if already_posted("payroll_payment", expense.id):
        return {"posted": False, "skipped": True, "reason": "already_posted"}
    amount = _parse_amount(expense.expense_amount)
    if amount <= 0:
        return {"posted": False, "skipped": True, "reason": "zero_amount"}
    try:
        mapping = resolve_posting_rule(branch, "payroll_posting", "payroll_payment")
        debit_account = (
            mapping.debit_account
            if mapping and mapping.debit_account_id
            else _account_by_number(branch, "2200")
        )
        result = post_direct_entry(
            branch=branch,
            staff=staff,
            debit_account=debit_account,
            credit_account=credit_account,
            amount=amount,
            description=f"Payroll payment {getattr(expense, 'expense_number', expense.id)}",
            rel_type="payroll_payment",
            rel_id=expense.id,
            reference=getattr(expense, "expense_number", "") or str(expense.id),
        )
        return {"posted": True, **result}
    except PostingError as exc:
        logger.warning("Payroll payment GL posting skipped for expense %s — %s", expense.id, exc)
        return {"posted": False, "error": str(exc)}
    except Exception as exc:
        logger.exception("Payroll payment GL posting failed for expense %s", expense.id)
        return {"posted": False, "error": str(exc)}


def post_operating_expense(branch, staff, expense, expense_type: str, credit_account: ChartOfAccount) -> dict:
    rel_type = f"expense_{expense_type}"
    if already_posted(rel_type, expense.id):
        return {"posted": False, "skipped": True, "reason": "already_posted"}
    amount = _parse_amount(expense.expense_amount)
    if amount <= 0:
        return {"posted": False, "skipped": True, "reason": "zero_amount"}
    try:
        mapping = resolve_posting_rule(branch, "expense_category", expense_type)
        if not mapping or not mapping.debit_account_id:
            raise PostingError(f"No expense category mapping for {expense_type}")
        result = post_direct_entry(
            branch=branch,
            staff=staff,
            debit_account=mapping.debit_account,
            credit_account=credit_account,
            amount=amount,
            description=f"{expense_type.replace('_', ' ').title()} {getattr(expense, 'expense_number', expense.id)}",
            rel_type=rel_type,
            rel_id=expense.id,
            reference=getattr(expense, "expense_number", "") or str(expense.id),
        )
        return {"posted": True, **result}
    except PostingError as exc:
        logger.warning(
            "Operating expense GL posting skipped for %s expense %s — %s",
            expense_type, expense.id, exc,
        )
        return {"posted": False, "error": str(exc)}
    except Exception as exc:
        logger.exception("Operating expense GL posting failed for %s expense %s", expense_type, expense.id)
        return {"posted": False, "error": str(exc)}


def post_expense_gl(branch, staff, expense, expense_type: str, credit_account: ChartOfAccount) -> dict:
    """Route expense payments to the correct GL posting hook."""
    if expense_type == "purchase_expense":
        return post_supplier_payment(branch, staff, expense, credit_account)
    if expense_type == "salary_expense":
        return post_payroll_payment(branch, staff, expense, credit_account)

    mappable_types = {
        "utility_expense",
        "rent_expense",
        "operations_expense",
        "tax_and_license_expense",
        "entertainment_and_private_expense",
        "other",
        "refund_expense",
        "capital_expenditure",
    }
    if expense_type in mappable_types:
        return post_operating_expense(branch, staff, expense, expense_type, credit_account)
    return {"posted": False, "skipped": True, "reason": "unsupported_expense_type"}
