"""
Journal Entry → General Ledger posting service.

Business events create journal entries (transaction level).
Posting writes balanced lines into AccountHistory (GL sub-ledger).
Modules must never mutate account balances directly — only post through here.
"""
from __future__ import annotations

from datetime import date
from decimal import Decimal

from django.db import transaction
from django.utils import timezone

from .models import (
    AccountHistory,
    AccountingPeriod,
    AccountingSettings,
    ManualJournalEntry,
    ManualJournalLine,
)


class PostingError(Exception):
    pass


def assert_period_open(branch, transaction_date: date) -> None:
    """Reject postings into locked fiscal periods or when books are closed."""
    if not branch:
        return

    settings = AccountingSettings.objects.filter(company_branch=branch).first()
    if settings and settings.close_books and settings.closing_date and transaction_date <= settings.closing_date:
        raise PostingError("Books are closed for this date.")

    locked = AccountingPeriod.objects.filter(
        company_branch=branch,
        recycle_bin=False,
        period_locked=True,
        period_start_date__lte=transaction_date,
        period_end_date__gte=transaction_date,
    ).exists()
    if locked:
        raise PostingError("The accounting period for this date is locked.")


def _journal_lines_balanced(lines) -> tuple[Decimal, Decimal]:
    total_debit = Decimal("0")
    total_credit = Decimal("0")
    for line in lines:
        total_debit += Decimal(str(line.debit or 0))
        total_credit += Decimal(str(line.credit or 0))
    return total_debit, total_credit


@transaction.atomic
def post_journal_entry_to_ledger(entry: ManualJournalEntry, staff) -> ManualJournalEntry:
    """
    Post a journal entry into the General Ledger (AccountHistory).
    Draft → Posted. Idempotent if already posted.
    """
    if entry.status == "posted":
        return entry
    if entry.status in ("reversed", "cancelled"):
        raise PostingError("Cannot post a reversed or cancelled journal entry.")

    lines = list(entry.journal_lines.select_related("account"))
    if len(lines) < 2:
        raise PostingError("Journal entry requires at least two lines.")

    total_debit, total_credit = _journal_lines_balanced(lines)
    if total_debit != total_credit:
        raise PostingError(f"Unbalanced journal: DR {total_debit} != CR {total_credit}.")
    if total_debit <= 0:
        raise PostingError("Journal total must be greater than zero.")

    assert_period_open(entry.company_branch, entry.entry_date)

    if AccountHistory.objects.filter(journal_entry=entry).exists():
        entry.status = "posted"
        entry.posted_at = entry.posted_at or timezone.now()
        entry.posted_by = entry.posted_by or staff
        entry.save(update_fields=["status", "posted_at", "posted_by", "last_updated_on"])
        return entry

    history_bulk = []
    for line in lines:
        history_bulk.append(AccountHistory(
            account=line.account,
            debit=line.debit,
            credit=line.credit,
            description=line.description or entry.description,
            transaction_date=entry.entry_date,
            rel_id=entry.id,
            rel_type="journal_entry",
            journal_entry=entry,
            company_branch=entry.company_branch,
            created_by=staff,
        ))
    AccountHistory.objects.bulk_create(history_bulk)

    entry.status = "posted"
    entry.posted_at = timezone.now()
    entry.posted_by = staff
    entry.last_updated_by = staff
    entry.save(update_fields=["status", "posted_at", "posted_by", "last_updated_on"])
    return entry


@transaction.atomic
def reverse_journal_entry(
    entry: ManualJournalEntry,
    staff,
    *,
    reversal_date: date | None = None,
    description: str = "",
) -> ManualJournalEntry:
    """Create and post a reversing journal entry; mark original as reversed."""
    if entry.status != "posted":
        raise PostingError("Only posted journal entries can be reversed.")
    if entry.reversal_entries.filter(status="posted", recycle_bin=False).exists():
        raise PostingError("This journal entry has already been reversed.")

    txn_date = reversal_date or date.today()
    memo = description or f"Reversal of {entry.entry_number}"

    reversal = ManualJournalEntry.objects.create(
        company_branch=entry.company_branch,
        entry_date=txn_date,
        description=memo,
        reference=entry.entry_number,
        status="draft",
        source_type="reversal",
        source_id=str(entry.id),
        reversal_of=entry,
        created_by=staff,
        last_updated_by=staff,
    )

    for order, line in enumerate(entry.journal_lines.select_related("account")):
        ManualJournalLine.objects.create(
            journal_entry=reversal,
            account=line.account,
            debit=line.credit,
            credit=line.debit,
            description=line.description or memo,
            line_order=order,
        )

    post_journal_entry_to_ledger(reversal, staff)

    entry.status = "reversed"
    entry.last_updated_by = staff
    entry.save(update_fields=["status", "last_updated_on"])
    return reversal


@transaction.atomic
def cancel_journal_draft(entry: ManualJournalEntry, staff) -> None:
    """Cancel a draft journal without GL impact."""
    if entry.status != "draft":
        raise PostingError("Only draft journal entries can be cancelled.")
    entry.status = "cancelled"
    entry.last_updated_by = staff
    entry.recycle_bin = True
    entry.save(update_fields=["status", "last_updated_by", "recycle_bin", "last_updated_on"])
