"""Treasury service — integrates with Posting Engine only."""
from __future__ import annotations

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

from sqlalchemy.ext.asyncio import AsyncSession

from app.models.posting import Voucher, VoucherLine
from app.models.treasury import CashBox, CashTransaction, BankAccount, BankTransaction
from app.models.types import TreasuryTransactionType, VoucherStatus
from app.repositories.base import TenantBaseRepository
from app.repositories.foundation import FiscalPeriodRepository
from app.repositories.posting import VoucherRepository
from app.services.posting_engine import PostingEngine
from shared.exceptions import AppError, NotFoundError


class TreasuryError(AppError):
    status_code = 422
    error_code = "treasury_error"


class CashBoxRepository(TenantBaseRepository[CashBox]):
    model = CashBox


class CashTransactionRepository(TenantBaseRepository[CashTransaction]):
    model = CashTransaction


class BankAccountRepository(TenantBaseRepository[BankAccount]):
    model = BankAccount


class TreasuryService:
    def __init__(self, session: AsyncSession) -> None:
        self.session = session
        self.cash_box_repo = CashBoxRepository(session)
        self.cash_tx_repo = CashTransactionRepository(session)
        self.bank_account_repo = BankAccountRepository(session)
        self.voucher_repo = VoucherRepository(session)
        self.period_repo = FiscalPeriodRepository(session)
        self.posting_engine = PostingEngine(session)

    async def record_cash_receipt(
        self,
        tenant_id: UUID,
        cash_box_id: UUID,
        amount: Decimal,
        *,
        debit_account_id: UUID,
        credit_account_id: UUID,
        actor_user_id: str,
        description: str | None = None,
        transaction_date: date | None = None,
    ) -> CashTransaction:
        cash_box = await self.cash_box_repo.get(tenant_id, cash_box_id)
        if cash_box is None:
            raise NotFoundError("صندوق یافت نشد", error_code="cash_box_not_found")

        period = await self.period_repo.get_current(tenant_id)
        if period is None:
            raise NotFoundError("دوره مالی جاری یافت نشد", error_code="period_not_found")

        tx_date = transaction_date or date.today()
        ref = f"CASH-R-{tx_date.isoformat()}-{cash_box.code}"

        voucher = await self._create_treasury_voucher(
            tenant_id, period.id, ref, tx_date,
            debit_account_id, credit_account_id, amount,
            description or "دریافت نقدی", actor_user_id, "treasury",
        )

        tx = CashTransaction(
            tenant_id=tenant_id,
            cash_box_id=cash_box_id,
            transaction_type=TreasuryTransactionType.CASH_RECEIPT,
            transaction_date=tx_date,
            amount=amount,
            description=description,
            voucher_id=voucher.id,
            reference_number=ref,
        )
        await self.cash_tx_repo.add(tx)
        cash_box.current_balance += amount
        return tx

    async def record_cash_payment(
        self,
        tenant_id: UUID,
        cash_box_id: UUID,
        amount: Decimal,
        *,
        debit_account_id: UUID,
        credit_account_id: UUID,
        actor_user_id: str,
        description: str | None = None,
        transaction_date: date | None = None,
    ) -> CashTransaction:
        cash_box = await self.cash_box_repo.get(tenant_id, cash_box_id)
        if cash_box is None:
            raise NotFoundError("صندوق یافت نشد", error_code="cash_box_not_found")
        if cash_box.current_balance < amount:
            raise TreasuryError("موجودی صندوق کافی نیست")

        period = await self.period_repo.get_current(tenant_id)
        if period is None:
            raise NotFoundError("دوره مالی جاری یافت نشد", error_code="period_not_found")

        tx_date = transaction_date or date.today()
        ref = f"CASH-P-{tx_date.isoformat()}-{cash_box.code}"

        voucher = await self._create_treasury_voucher(
            tenant_id, period.id, ref, tx_date,
            debit_account_id, credit_account_id, amount,
            description or "پرداخت نقدی", actor_user_id, "treasury",
        )

        tx = CashTransaction(
            tenant_id=tenant_id,
            cash_box_id=cash_box_id,
            transaction_type=TreasuryTransactionType.CASH_PAYMENT,
            transaction_date=tx_date,
            amount=amount,
            description=description,
            voucher_id=voucher.id,
            reference_number=ref,
        )
        await self.cash_tx_repo.add(tx)
        cash_box.current_balance -= amount
        return tx

    async def post_gl_transfer(
        self,
        tenant_id: UUID,
        *,
        amount: Decimal,
        from_account_id: UUID,
        to_account_id: UUID,
        actor_user_id: str,
        description: str,
        transaction_date: date | None = None,
        reference: str | None = None,
    ) -> Voucher:
        """Any GL → any GL: Dr destination (to), Cr source (from)."""
        from app.services.document_number_service import DocumentNumberService
        from app.services.source_posting_guard import SourcePostingGuard
        from shared.exceptions import AppError

        if from_account_id == to_account_id:
            raise AppError(
                "حساب مبدأ و مقصد نباید یکسان باشند",
                status_code=422,
                error_code="transfer_same_account",
            )
        if amount <= 0:
            raise AppError("مبلغ انتقال باید بزرگ‌تر از صفر باشد", status_code=422, error_code="transfer_amount")

        period = await self.period_repo.get_current(tenant_id)
        if period is None:
            raise NotFoundError("دوره مالی جاری یافت نشد", error_code="period_not_found")

        number = await DocumentNumberService(self.session).allocate(tenant_id, "voucher")
        return await self._create_treasury_voucher(
            tenant_id,
            period.id,
            number,
            transaction_date or date.today(),
            to_account_id,
            from_account_id,
            amount,
            description or "انتقال بین حساب‌ها",
            actor_user_id,
            "treasury",
            post_immediately=await SourcePostingGuard(self.session).should_post_immediately(tenant_id),
            reference_number=reference,
        )

    async def post_receipt_payment_voucher(
        self,
        tenant_id: UUID,
        *,
        amount: Decimal,
        treasury_account_id: UUID,
        counter_account_id: UUID,
        is_receipt: bool,
        actor_user_id: str,
        description: str,
        transaction_date: date | None = None,
        reference: str | None = None,
        cash_box_id: UUID | None = None,
    ) -> Voucher:
        """Post GL for treasury receipt (Dr treasury / Cr counter) or payment (Dr counter / Cr treasury)."""
        from app.services.document_number_service import DocumentNumberService
        from app.services.source_posting_guard import SourcePostingGuard

        period = await self.period_repo.get_current(tenant_id)
        if period is None:
            raise NotFoundError("دوره مالی جاری یافت نشد", error_code="period_not_found")

        tx_date = transaction_date or date.today()
        number = await DocumentNumberService(self.session).allocate(tenant_id, "voucher")
        if is_receipt:
            debit_id, credit_id = treasury_account_id, counter_account_id
        else:
            debit_id, credit_id = counter_account_id, treasury_account_id
            if cash_box_id is not None:
                cash_box = await self.cash_box_repo.get(tenant_id, cash_box_id)
                if cash_box is not None and cash_box.current_balance < amount:
                    raise TreasuryError("موجودی صندوق کافی نیست")

        voucher = await self._create_treasury_voucher(
            tenant_id,
            period.id,
            number,
            tx_date,
            debit_id,
            credit_id,
            amount,
            description,
            actor_user_id,
            "treasury",
            post_immediately=await SourcePostingGuard(self.session).should_post_immediately(tenant_id),
            reference_number=reference,
        )

        if cash_box_id is not None:
            cash_box = await self.cash_box_repo.get(tenant_id, cash_box_id)
            if cash_box is not None:
                if is_receipt:
                    cash_box.current_balance += amount
                else:
                    cash_box.current_balance -= amount
        return voucher

    async def _create_treasury_voucher(
        self,
        tenant_id: UUID,
        period_id: UUID,
        voucher_number: str,
        voucher_date: date,
        debit_account_id: UUID,
        credit_account_id: UUID,
        amount: Decimal,
        description: str,
        actor_user_id: str,
        source_module: str,
        *,
        post_immediately: bool = True,
        reference_number: str | None = None,
    ) -> Voucher:
        voucher = Voucher(
            tenant_id=tenant_id,
            fiscal_period_id=period_id,
            voucher_number=voucher_number,
            voucher_date=voucher_date,
            status=VoucherStatus.DRAFT,
            description=description,
            source_module=source_module,
            reference_number=reference_number,
            created_by=actor_user_id,
        )
        await self.voucher_repo.add(voucher)

        self.session.add(VoucherLine(
            tenant_id=tenant_id, voucher_id=voucher.id, line_number=1,
            account_id=debit_account_id, debit=amount, credit=Decimal("0"),
            description=description,
        ))
        self.session.add(VoucherLine(
            tenant_id=tenant_id, voucher_id=voucher.id, line_number=2,
            account_id=credit_account_id, debit=Decimal("0"), credit=amount,
            description=description,
        ))
        await self.session.flush()
        if not post_immediately:
            return voucher
        return await self.posting_engine.post_voucher(
            tenant_id, voucher.id, actor_user_id=actor_user_id, source_module=source_module
        )
