"""
Accounting posting engine — operational events → balanced GL journal lines.

Golden rule: operational modules emit business events; this module resolves
AccountMapping rules and writes to AccountHistory (and ManualJournalEntry when
a formal journal document is required).

Only approved / finalized documents should call post_event().
"""
from __future__ import annotations

from datetime import date
from decimal import Decimal

from django.db import transaction

from .models import AccountHistory, AccountMapping, ChartOfAccount, ManualJournalEntry, ManualJournalLine
from .journal_posting import PostingError, assert_period_open, post_journal_entry_to_ledger


def _account_by_number(branch, account_number: str) -> ChartOfAccount:
    account = ChartOfAccount.objects.filter(
        company_branch=branch,
        account_number=account_number,
        recycle_bin=False,
        is_active=True,
    ).first()
    if not account:
        raise PostingError(f"GL account {account_number} not found for branch.")
    return account


def resolve_posting_rule(branch, mapping_type: str, source_key: str) -> AccountMapping | None:
    return AccountMapping.objects.filter(
        company_branch=branch,
        mapping_type=mapping_type,
        source_key=source_key,
    ).select_related("debit_account", "credit_account").first()


@transaction.atomic
def post_event(
    *,
    branch,
    staff,
    mapping_type: str,
    source_key: str,
    amount: Decimal | float,
    transaction_date: date | None = None,
    description: str = "",
    rel_type: str = "",
    rel_id: str = "",
    reference: str = "",
    create_journal_document: bool = True,
) -> dict:
    """
    Post a two-line balanced entry from an AccountMapping rule.

    Returns summary dict with journal entry id (if created) and history ids.
    """
    amount = Decimal(str(amount))
    if amount <= 0:
        raise PostingError("Posting amount must be positive.")

    mapping = resolve_posting_rule(branch, mapping_type, source_key)
    if not mapping:
        raise PostingError(f"No posting rule for {mapping_type}:{source_key}")
    if not mapping.debit_account_id or not mapping.credit_account_id:
        raise PostingError(f"Incomplete posting rule for {mapping_type}:{source_key}")

    txn_date = transaction_date or date.today()
    memo = description or mapping.description or mapping.source_label

    assert_period_open(branch, txn_date)

    journal_entry = None
    if create_journal_document:
        journal_entry = ManualJournalEntry.objects.create(
            company_branch=branch,
            entry_date=txn_date,
            description=memo,
            reference=reference or rel_id,
            status="draft",
            source_type=_source_type_from_rel(rel_type or mapping_type),
            source_id=str(rel_id),
            created_by=staff,
            last_updated_by=staff,
        )
        ManualJournalLine.objects.create(
            journal_entry=journal_entry,
            account=mapping.debit_account,
            debit=amount,
            credit=Decimal("0"),
            description=memo,
            line_order=0,
        )
        ManualJournalLine.objects.create(
            journal_entry=journal_entry,
            account=mapping.credit_account,
            debit=Decimal("0"),
            credit=amount,
            description=memo,
            line_order=1,
        )
        post_journal_entry_to_ledger(journal_entry, staff)

    rel = rel_type or mapping_type
    if not create_journal_document:
        dr_history = AccountHistory.objects.create(
            company_branch=branch,
            account=mapping.debit_account,
            debit=amount,
            credit=Decimal("0"),
            transaction_date=txn_date,
            description=memo,
            rel_type=rel,
            rel_id=str(rel_id),
            created_by=staff,
        )
        cr_history = AccountHistory.objects.create(
            company_branch=branch,
            account=mapping.credit_account,
            debit=Decimal("0"),
            credit=amount,
            transaction_date=txn_date,
            description=memo,
            rel_type=rel,
            rel_id=str(rel_id),
            created_by=staff,
        )
        return {
            "journal_entry_id": None,
            "journal_entry_number": None,
            "debit_history_id": dr_history.id,
            "credit_history_id": cr_history.id,
            "amount": float(amount),
        }

    return {
        "journal_entry_id": journal_entry.id if journal_entry else None,
        "journal_entry_number": journal_entry.entry_number if journal_entry else None,
        "debit_history_id": None,
        "credit_history_id": None,
        "amount": float(amount),
    }


@transaction.atomic
def post_balanced_lines(
    *,
    branch,
    staff,
    lines: list[dict],
    transaction_date: date | None = None,
    description: str = "",
    rel_type: str = "",
    rel_id: str = "",
    reference: str = "",
    create_journal_document: bool = True,
) -> dict:
    """
    Post a balanced multi-line journal entry.

    Each line dict: {"account": ChartOfAccount, "debit": Decimal, "credit": Decimal}
    """
    if not lines:
        raise PostingError("No journal lines supplied.")

    txn_date = transaction_date or date.today()
    memo = description or "Journal entry"
    total_debit = Decimal("0")
    total_credit = Decimal("0")
    normalized = []

    for line in lines:
        account = line["account"]
        debit = Decimal(str(line.get("debit") or 0))
        credit = Decimal(str(line.get("credit") or 0))
        if debit < 0 or credit < 0:
            raise PostingError("Line amounts cannot be negative.")
        if debit > 0 and credit > 0:
            raise PostingError("A line cannot have both debit and credit.")
        if debit == 0 and credit == 0:
            continue
        total_debit += debit
        total_credit += credit
        normalized.append({"account": account, "debit": debit, "credit": credit})

    if not normalized:
        raise PostingError("No non-zero journal lines.")
    if total_debit != total_credit:
        raise PostingError(f"Unbalanced entry: DR {total_debit} != CR {total_credit}.")
    if total_debit <= 0:
        raise PostingError("Posting amount must be positive.")

    assert_period_open(branch, txn_date)

    journal_entry = None
    if create_journal_document:
        journal_entry = ManualJournalEntry.objects.create(
            company_branch=branch,
            entry_date=txn_date,
            description=memo,
            reference=reference or rel_id,
            status="draft",
            source_type=_source_type_from_rel(rel_type or "journal"),
            source_id=str(rel_id),
            created_by=staff,
            last_updated_by=staff,
        )
        for order, line in enumerate(normalized):
            ManualJournalLine.objects.create(
                journal_entry=journal_entry,
                account=line["account"],
                debit=line["debit"],
                credit=line["credit"],
                description=memo,
                line_order=order,
            )
        post_journal_entry_to_ledger(journal_entry, staff)
        return {
            "journal_entry_id": journal_entry.id,
            "journal_entry_number": journal_entry.entry_number,
            "history_ids": list(journal_entry.ledger_lines.values_list("id", flat=True)),
            "amount": float(total_debit),
        }

    history_ids = []
    rel = rel_type or "journal"
    for line in normalized:
        history = AccountHistory.objects.create(
            company_branch=branch,
            account=line["account"],
            debit=line["debit"],
            credit=line["credit"],
            transaction_date=txn_date,
            description=memo,
            rel_type=rel,
            rel_id=str(rel_id),
            created_by=staff,
        )
        history_ids.append(history.id)

    return {
        "journal_entry_id": None,
        "journal_entry_number": None,
        "history_ids": history_ids,
        "amount": float(total_debit),
    }


@transaction.atomic
def post_direct_entry(
    *,
    branch,
    staff,
    debit_account: ChartOfAccount,
    credit_account: ChartOfAccount,
    amount: Decimal | float,
    transaction_date: date | None = None,
    description: str = "",
    rel_type: str = "",
    rel_id: str = "",
    reference: str = "",
    create_journal_document: bool = True,
) -> dict:
    """Post a simple two-line entry between explicit GL accounts."""
    amount = Decimal(str(amount).replace(",", ""))
    if amount <= 0:
        raise PostingError("Posting amount must be positive.")
    return post_balanced_lines(
        branch=branch,
        staff=staff,
        lines=[
            {"account": debit_account, "debit": amount, "credit": Decimal("0")},
            {"account": credit_account, "debit": Decimal("0"), "credit": amount},
        ],
        transaction_date=transaction_date,
        description=description,
        rel_type=rel_type,
        rel_id=rel_id,
        reference=reference,
        create_journal_document=create_journal_document,
    )


def _source_type_from_rel(rel_type: str) -> str:
    mapping = {
        "journal_entry": "manual",
        "journal": "manual",
        "invoice": "sales_invoice",
        "payment": "customer_payment",
        "expense": "expense",
        "deposit": "deposit",
        "payroll": "payroll",
        "purchase_order": "purchase",
        "delivery_posting": "inventory",
        "purchase_posting": "purchase",
        "invoice_posting": "sales_invoice",
        "payment_posting_sale": "customer_payment",
        "payment_posting_expense": "expense",
        "payroll_posting": "payroll",
    }
    return mapping.get(rel_type, "other")
