"""Centralized Posting Engine — sole creator of Journal Entries (ADR-010)."""
from __future__ import annotations

import uuid
from datetime import date, datetime, timezone
from decimal import Decimal
from uuid import UUID

from sqlalchemy.ext.asyncio import AsyncSession

from app.models.foundation import Account, FiscalPeriod
from app.models.posting import (
    AccountingAuditLog,
    JournalEntry,
    PostingError,
    PostingLog,
    Voucher,
    VoucherLine,
)
from app.models.types import (
    AccountStatus,
    FiscalPeriodStatus,
    JournalStatus,
    PostingStatus,
    VoucherStatus,
)
from app.repositories.foundation import AccountRepository, FiscalPeriodRepository
from app.repositories.posting import (
    AuditLogRepository,
    JournalEntryRepository,
    PostingErrorRepository,
    PostingLogRepository,
    VoucherRepository,
)
from shared.exceptions import AppError, NotFoundError


class PostingValidationError(AppError):
    status_code = 422
    error_code = "posting_validation_failed"


class PostingEngine:
    """The ONLY component allowed to create Journal Entries."""

    def __init__(self, session: AsyncSession) -> None:
        self.session = session
        self.voucher_repo = VoucherRepository(session)
        self.account_repo = AccountRepository(session)
        self.period_repo = FiscalPeriodRepository(session)
        self.journal_repo = JournalEntryRepository(session)
        self.log_repo = PostingLogRepository(session)
        self.error_repo = PostingErrorRepository(session)
        self.audit_repo = AuditLogRepository(session)

    async def validate_voucher(self, tenant_id: UUID, voucher_id: UUID) -> Voucher:
        voucher = await self.voucher_repo.get_with_lines(tenant_id, voucher_id)
        if voucher is None:
            raise NotFoundError("سند یافت نشد", error_code="voucher_not_found")

        errors: list[PostingError] = []

        if voucher.status not in (VoucherStatus.DRAFT, VoucherStatus.VALIDATED):
            errors.append(self._make_error(tenant_id, voucher_id, "invalid_status", "وضعیت سند نامعتبر است"))

        period = await self.period_repo.get(tenant_id, voucher.fiscal_period_id)
        if period is None:
            errors.append(self._make_error(tenant_id, voucher_id, "period_not_found", "دوره مالی یافت نشد"))
        elif period.status != FiscalPeriodStatus.OPEN:
            errors.append(self._make_error(tenant_id, voucher_id, "period_locked", "دوره مالی بسته یا قفل است"))

        if not voucher.lines:
            errors.append(self._make_error(tenant_id, voucher_id, "no_lines", "سند فاقد سطر است"))

        total_debit = Decimal("0")
        total_credit = Decimal("0")
        for line in voucher.lines:
            total_debit += line.debit
            total_credit += line.credit
            account = await self.account_repo.get(tenant_id, line.account_id)
            if account is None:
                errors.append(self._make_error(tenant_id, voucher_id, "account_not_found", f"حساب {line.account_id} یافت نشد", "account_id"))
            elif account.status != AccountStatus.ACTIVE:
                errors.append(self._make_error(tenant_id, voucher_id, "account_inactive", f"حساب {account.code} غیرفعال است", "account_id"))
            elif not account.is_postable:
                errors.append(self._make_error(tenant_id, voucher_id, "account_not_postable", f"حساب {account.code} قابل ثبت نیست", "account_id"))

        if total_debit != total_credit:
            errors.append(self._make_error(tenant_id, voucher_id, "unbalanced", f"عدم تراز: بدهکار={total_debit} بستانکار={total_credit}"))

        if errors:
            for err in errors:
                await self.error_repo.add(err)
            await self._log(tenant_id, voucher_id, "validate", PostingStatus.FAILED, "Validation failed")
            raise PostingValidationError("اعتبارسنجی سند ناموفق بود", details={"errors": len(errors)})

        voucher.total_debit = total_debit
        voucher.total_credit = total_credit
        voucher.status = VoucherStatus.VALIDATED
        await self._log(tenant_id, voucher_id, "validate", PostingStatus.COMPLETED, "Validated")
        return voucher

    async def post_voucher(
        self,
        tenant_id: UUID,
        voucher_id: UUID,
        *,
        actor_user_id: str,
        source_module: str | None = None,
    ) -> Voucher:
        voucher = await self.validate_voucher(tenant_id, voucher_id)

        if voucher.status == VoucherStatus.POSTED:
            raise PostingValidationError("سند قبلاً ثبت شده است")

        entry_num = 0
        for line in voucher.lines:
            entry_num += 1
            entry = JournalEntry(
                tenant_id=tenant_id,
                voucher_id=voucher.id,
                fiscal_period_id=voucher.fiscal_period_id,
                entry_number=f"{voucher.voucher_number}-{entry_num}",
                entry_date=voucher.voucher_date,
                status=JournalStatus.POSTED,
                account_id=line.account_id,
                debit=line.debit,
                credit=line.credit,
                description=line.description,
                cost_center_id=line.cost_center_id,
                project_id=line.project_id,
                posted_at=datetime.now(timezone.utc),
                posted_by=actor_user_id,
            )
            await self.journal_repo.add(entry)

        now = datetime.now(timezone.utc)
        voucher.status = VoucherStatus.POSTED
        voucher.posted_at = now
        voucher.posted_by = actor_user_id
        if source_module:
            voucher.source_module = source_module

        await self._audit(
            tenant_id=tenant_id,
            operation="post",
            actor_user_id=actor_user_id,
            voucher=voucher,
            source_module=source_module,
        )
        await self._log(tenant_id, voucher_id, "post", PostingStatus.COMPLETED, "Posted", actor_user_id)
        return voucher

    async def reverse_voucher(
        self,
        tenant_id: UUID,
        voucher_id: UUID,
        *,
        actor_user_id: str,
        reversal_date: date | None = None,
    ) -> Voucher:
        original = await self.voucher_repo.get_with_lines(tenant_id, voucher_id)
        if original is None:
            raise NotFoundError("سند یافت نشد", error_code="voucher_not_found")
        if original.status != VoucherStatus.POSTED:
            raise PostingValidationError("فقط اسناد ثبت‌شده قابل برگشت هستند")

        rev_number = f"REV-{original.voucher_number}"
        reversal = Voucher(
            tenant_id=tenant_id,
            fiscal_period_id=original.fiscal_period_id,
            voucher_number=rev_number,
            voucher_date=reversal_date or date.today(),
            status=VoucherStatus.DRAFT,
            description=f"برگشت سند {original.voucher_number}",
            reference_number=original.voucher_number,
            source_module=original.source_module,
            reversed_voucher_id=original.id,
            created_by=actor_user_id,
        )
        await self.voucher_repo.add(reversal)

        for i, line in enumerate(original.lines, start=1):
            rev_line = VoucherLine(
                tenant_id=tenant_id,
                voucher_id=reversal.id,
                line_number=i,
                account_id=line.account_id,
                debit=line.credit,
                credit=line.debit,
                description=f"برگشت: {line.description or ''}",
                cost_center_id=line.cost_center_id,
                project_id=line.project_id,
            )
            self.session.add(rev_line)

        await self.session.flush()
        reversal = await self.voucher_repo.get_with_lines(tenant_id, reversal.id)
        await self.post_voucher(tenant_id, reversal.id, actor_user_id=actor_user_id)

        original.status = VoucherStatus.REVERSED
        await self._audit(
            tenant_id=tenant_id,
            operation="reverse",
            actor_user_id=actor_user_id,
            voucher=original,
        )
        return reversal

    async def reverse_and_reopen_for_edit(
        self,
        tenant_id: UUID,
        voucher_id: UUID,
        *,
        actor_user_id: str,
    ) -> Voucher:
        """Reverse a posted voucher and open a new draft copy for correction."""
        from app.services.document_number_service import DocumentNumberService

        original = await self.voucher_repo.get_with_lines(tenant_id, voucher_id)
        if original is None:
            raise NotFoundError("سند یافت نشد", error_code="voucher_not_found")
        if original.status != VoucherStatus.POSTED:
            raise PostingValidationError("فقط اسناد ثبت‌شده قابل برگشت و ویرایش هستند")

        await self.reverse_voucher(tenant_id, voucher_id, actor_user_id=actor_user_id)

        number = await DocumentNumberService(self.session).allocate(tenant_id, "voucher")
        draft = Voucher(
            tenant_id=tenant_id,
            fiscal_period_id=original.fiscal_period_id,
            voucher_number=number,
            voucher_date=original.voucher_date,
            status=VoucherStatus.DRAFT,
            description=original.description or f"اصلاح سند {original.voucher_number}",
            reference_number=original.voucher_number,
            source_module=original.source_module,
            created_by=actor_user_id,
        )
        await self.voucher_repo.add(draft)
        for i, line in enumerate(original.lines, start=1):
            self.session.add(
                VoucherLine(
                    tenant_id=tenant_id,
                    voucher_id=draft.id,
                    line_number=i,
                    account_id=line.account_id,
                    debit=line.debit,
                    credit=line.credit,
                    description=line.description,
                    cost_center_id=line.cost_center_id,
                    project_id=line.project_id,
                )
            )
        await self.session.flush()
        await self._audit(
            tenant_id=tenant_id,
            operation="reopen_for_edit",
            actor_user_id=actor_user_id,
            voucher=draft,
        )
        return await self.voucher_repo.get_with_lines(tenant_id, draft.id)

    async def cancel_voucher(
        self, tenant_id: UUID, voucher_id: UUID, *, actor_user_id: str
    ) -> Voucher:
        voucher = await self.voucher_repo.get(tenant_id, voucher_id)
        if voucher is None:
            raise NotFoundError("سند یافت نشد", error_code="voucher_not_found")
        if voucher.status == VoucherStatus.POSTED:
            raise PostingValidationError("سند ثبت‌شده قابل لغو نیست — از برگشت استفاده کنید")
        voucher.status = VoucherStatus.CANCELLED
        await self._audit(tenant_id=tenant_id, operation="cancel", actor_user_id=actor_user_id, voucher=voucher)
        return voucher

    def _make_error(
        self, tenant_id: UUID, voucher_id: UUID, code: str, message: str, field: str | None = None
    ) -> PostingError:
        return PostingError(
            tenant_id=tenant_id,
            voucher_id=voucher_id,
            error_code=code,
            error_message=message,
            field_name=field,
        )

    async def _log(
        self,
        tenant_id: UUID,
        voucher_id: UUID,
        operation: str,
        status: PostingStatus,
        message: str,
        actor: str | None = None,
    ) -> None:
        await self.log_repo.add(
            PostingLog(
                tenant_id=tenant_id,
                voucher_id=voucher_id,
                operation=operation,
                status=status,
                actor_user_id=actor,
                message=message,
            )
        )

    async def _audit(
        self,
        *,
        tenant_id: UUID,
        operation: str,
        actor_user_id: str,
        voucher: Voucher,
        source_module: str | None = None,
    ) -> None:
        await self.audit_repo.add(
            AccountingAuditLog(
                tenant_id=tenant_id,
                operation=operation,
                actor_user_id=actor_user_id,
                source_module=source_module or voucher.source_module,
                voucher_number=voucher.voucher_number,
                reference_number=voucher.reference_number,
                resource_type="voucher",
                resource_id=str(voucher.id),
            )
        )
