from decimal import Decimal, ROUND_HALF_UP

from django.core.exceptions import ValidationError
from django.db import transaction
from django.utils import timezone

from shop_management.models.data.accounting import JournalEntry


def _money(value):
    return Decimal(value).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)


def _journal_number(entry):
    prefix = f"BR{entry.shop_id}-JRN-{entry.entry_date.year}-"
    last = JournalEntry.objects.filter(journal_number__startswith=prefix).order_by("-journal_number").values_list("journal_number", flat=True).first()
    sequence = int(last.rsplit("-", 1)[-1]) + 1 if last else 1
    return f"{prefix}{sequence:06d}"


def validate_journal(entry):
    lines = list(entry.lines.select_related("ledger_account", "transaction_currency", "customer", "supplier", "financial_account"))
    errors = {}
    if len(lines) < 2:
        errors["lines"] = "At least two journal lines are required."
    total_debit = total_credit = total_base_debit = total_base_credit = Decimal("0")
    for index, line in enumerate(lines):
        if line.ledger_account.shop_id != entry.shop_id:
            errors[f"lines[{index}].ledger_account"] = "Ledger account must belong to the journal branch."
        if not line.ledger_account.is_active or not line.ledger_account.allow_posting:
            errors[f"lines[{index}].ledger_account"] = "Select an active posting account, not a group account."
        if line.transaction_currency.shop_id != entry.shop_id:
            errors[f"lines[{index}].transaction_currency"] = "Currency must belong to the journal branch."
        if line.exchange_rate_used <= 0:
            errors[f"lines[{index}].exchange_rate_used"] = "Exchange rate must be greater than zero."
        if line.debit < 0 or line.credit < 0 or bool(line.debit) == bool(line.credit):
            errors[f"lines[{index}]"] = "Enter either a positive debit or a positive credit, never both."
        for field in ("customer", "supplier", "financial_account"):
            related = getattr(line, field)
            if related and related.shop_id != entry.shop_id:
                errors[f"lines[{index}].{field}"] = f"{field.replace('_', ' ').title()} must belong to the journal branch."
        line.base_debit = _money(line.debit * line.exchange_rate_used)
        line.base_credit = _money(line.credit * line.exchange_rate_used)
        total_debit += line.debit
        total_credit += line.credit
        total_base_debit += line.base_debit
        total_base_credit += line.base_credit
    if _money(total_debit) != _money(total_credit):
        errors["balance"] = f"Debits ({_money(total_debit)}) must equal credits ({_money(total_credit)})."
    if _money(total_base_debit) != _money(total_base_credit):
        errors["base_balance"] = f"Base debits ({_money(total_base_debit)}) must equal base credits ({_money(total_base_credit)})."
    if errors:
        raise ValidationError(errors)
    return lines


@transaction.atomic
def post_journal(entry_id, *, user):
    entry = JournalEntry.objects.select_for_update().select_related("shop").get(pk=entry_id)
    if entry.status != JournalEntry.Status.DRAFT:
        raise ValidationError("Only draft journal entries can be posted.")
    lines = validate_journal(entry)
    for line in lines:
        line.save(update_fields=["base_debit", "base_credit", "updated_at"])
    entry.journal_number = entry.journal_number or _journal_number(entry)
    entry.status = JournalEntry.Status.POSTED
    entry.posted_by = user
    entry.posted_at = timezone.now()
    entry.save(update_fields=["journal_number", "status", "posted_by", "posted_at", "updated_at"])
    return entry


@transaction.atomic
def void_journal(entry_id, *, user, reason):
    entry = JournalEntry.objects.select_for_update().get(pk=entry_id)
    if entry.status != JournalEntry.Status.POSTED:
        raise ValidationError("Only posted journal entries can be cancelled.")
    reason = str(reason or "").strip()
    if not reason:
        raise ValidationError({"void_reason": "A void reason is required."})
    entry.status = JournalEntry.Status.VOID
    entry.voided_by = user
    entry.voided_at = timezone.now()
    entry.void_reason = reason
    entry.save(update_fields=["status", "voided_by", "voided_at", "void_reason", "updated_at"])
    return entry
