"""Phase 5.10 — Financial Report Engine."""
from __future__ import annotations

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

from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession

from app.models.foundation import Account
from app.models.posting import JournalEntry
from app.models.reporting import FinancialReport, ReportExport
from app.models.types import (
    AccountCategory,
    AccountType,
    JournalStatus,
    ReportExportFormat,
    ReportStatus,
    ReportType,
)
from app.repositories.base import TenantBaseRepository
from app.repositories.ledger import GeneralLedgerRepository, LedgerBalanceRepository
from app.services.balance_engine import BalanceEngine
from shared.exceptions import NotFoundError

_ZERO = Decimal("0")
_CASH_TOKENS = ("نقد", "صندوق", "بانک", "bank", "cash", "treasury", "تنخواه")


class FinancialReportRepo(TenantBaseRepository[FinancialReport]):
    model = FinancialReport


class ReportExportRepo(TenantBaseRepository[ReportExport]):
    model = ReportExport


class FinancialReportEngine:
    """Generates financial reports from posted accounting data only."""

    def __init__(self, session: AsyncSession) -> None:
        self.session = session
        self.report_repo = FinancialReportRepo(session)
        self.balance_repo = LedgerBalanceRepository(session)
        self.gl_repo = GeneralLedgerRepository(session)
        self.balance_engine = BalanceEngine(session)
        self.export_repo = ReportExportRepo(session)

    async def _list_accounts(self, tenant_id: UUID) -> list[Account]:
        stmt = (
            select(Account)
            .where(Account.tenant_id == tenant_id)
            .order_by(Account.code)
        )
        result = await self.session.execute(stmt)
        return list(result.scalars().all())

    @staticmethod
    def _filter_accounts_by_mode(accounts: list[Account], mode: str) -> list[Account]:
        """Iranian COA: level 3 = معین, level >= 4 = تفصیلی; with safe fallbacks."""
        if mode == "subsidiary":
            hit = [a for a in accounts if a.level == 3]
            if hit:
                return hit
            hit = [
                a
                for a in accounts
                if a.is_postable and not a.is_group and a.level <= 3
            ]
            if hit:
                return hit
            return [a for a in accounts if a.is_postable and not a.is_group]
        if mode == "detail":
            hit = [a for a in accounts if a.level >= 4]
            if hit:
                return hit
            return [a for a in accounts if a.is_postable and not a.is_group]
        return [a for a in accounts if a.is_postable and not a.is_group]

    @staticmethod
    def _empty_balance_row(account: Account, fiscal_period_id: UUID) -> dict:
        return {
            "account_id": str(account.id),
            "account_code": account.code,
            "account_name": account.name,
            "account_type": account.account_type.value
            if hasattr(account.account_type, "value")
            else str(account.account_type),
            "account_category": account.account_category.value
            if hasattr(account.account_category, "value")
            else str(account.account_category),
            "level": account.level,
            "fiscal_period_id": str(fiscal_period_id),
            "opening_balance": "0",
            "debit_total": "0",
            "credit_total": "0",
            "closing_balance": "0",
            "current_balance": "0",
        }

    async def _balance_rows_for_accounts(
        self,
        tenant_id: UUID,
        fiscal_period_id: UUID,
        accounts: list[Account],
        *,
        include_zero: bool = True,
    ) -> list[dict]:
        balances = await self.balance_repo.list_by_period(tenant_id, fiscal_period_id)
        by_account = {b.account_id: b for b in balances}
        rows: list[dict] = []
        for account in accounts:
            bal = by_account.get(account.id)
            if bal is None:
                if not include_zero:
                    continue
                rows.append(self._empty_balance_row(account, fiscal_period_id))
                continue
            if (
                not include_zero
                and bal.opening_balance == _ZERO
                and bal.debit_total == _ZERO
                and bal.credit_total == _ZERO
                and bal.closing_balance == _ZERO
            ):
                continue
            rows.append(
                {
                    "account_id": str(account.id),
                    "account_code": account.code,
                    "account_name": account.name,
                    "account_type": account.account_type.value
                    if hasattr(account.account_type, "value")
                    else str(account.account_type),
                    "account_category": account.account_category.value
                    if hasattr(account.account_category, "value")
                    else str(account.account_category),
                    "level": account.level,
                    "fiscal_period_id": str(fiscal_period_id),
                    "opening_balance": str(bal.opening_balance),
                    "debit_total": str(bal.debit_total),
                    "credit_total": str(bal.credit_total),
                    "closing_balance": str(bal.closing_balance),
                    "current_balance": str(bal.current_balance),
                }
            )
        return rows

    async def _statement_lines(
        self, tenant_id: UUID, fiscal_period_id: UUID, account_id: UUID
    ) -> list[dict]:
        gl_lines = await self.gl_repo.list_by_account_period(
            tenant_id, account_id, fiscal_period_id
        )
        if gl_lines:
            running = _ZERO
            out: list[dict] = []
            for line in gl_lines:
                running = line.running_balance
                out.append(
                    {
                        "id": str(line.id),
                        "entry_date": line.entry_date.isoformat(),
                        "voucher_id": str(line.voucher_id),
                        "journal_entry_id": str(line.journal_entry_id),
                        "description": line.description,
                        "debit": str(line.debit),
                        "credit": str(line.credit),
                        "running_balance": str(running),
                    }
                )
            return out

        stmt = (
            select(JournalEntry)
            .where(
                JournalEntry.tenant_id == tenant_id,
                JournalEntry.fiscal_period_id == fiscal_period_id,
                JournalEntry.account_id == account_id,
                JournalEntry.status == JournalStatus.POSTED,
            )
            .order_by(JournalEntry.entry_date, JournalEntry.entry_number)
        )
        result = await self.session.execute(stmt)
        entries = list(result.scalars().all())
        running = _ZERO
        out = []
        for e in entries:
            running = running + e.debit - e.credit
            out.append(
                {
                    "id": str(e.id),
                    "entry_date": e.entry_date.isoformat(),
                    "voucher_id": str(e.voucher_id),
                    "entry_number": e.entry_number,
                    "description": e.description,
                    "debit": str(e.debit),
                    "credit": str(e.credit),
                    "running_balance": str(running),
                }
            )
        return out

    @staticmethod
    def _sum_rows(rows: list[dict]) -> dict:
        opening = sum(Decimal(r["opening_balance"]) for r in rows)
        debit = sum(Decimal(r["debit_total"]) for r in rows)
        credit = sum(Decimal(r["credit_total"]) for r in rows)
        closing = sum(Decimal(r["closing_balance"]) for r in rows)
        return {
            "opening_balance": str(opening),
            "debit_total": str(debit),
            "credit_total": str(credit),
            "closing_balance": str(closing),
            "row_count": len(rows),
        }

    async def _persist(
        self,
        *,
        tenant_id: UUID,
        report_type: ReportType,
        name: str,
        fiscal_period_id: UUID,
        data: dict,
        generated_by: str,
    ) -> FinancialReport:
        report = FinancialReport(
            tenant_id=tenant_id,
            report_type=report_type,
            name=name,
            fiscal_period_id=fiscal_period_id,
            generated_at=datetime.now(timezone.utc),
            status=ReportStatus.GENERATED,
            report_data=json.dumps(data, ensure_ascii=False),
            generated_by=generated_by,
        )
        return await self.report_repo.add(report)

    async def generate_level_ledger_report(
        self,
        tenant_id: UUID,
        fiscal_period_id: UUID,
        *,
        mode: str,
        account_id: UUID | None,
        generated_by: str,
    ) -> FinancialReport:
        accounts = await self._list_accounts(tenant_id)
        account_map = {a.id: a for a in accounts}
        selected = account_map.get(account_id) if account_id else None
        if account_id and selected is None:
            raise NotFoundError("حساب یافت نشد", error_code="account_not_found")

        if selected is not None:
            rows = await self._balance_rows_for_accounts(
                tenant_id, fiscal_period_id, [selected], include_zero=True
            )
            lines = await self._statement_lines(tenant_id, fiscal_period_id, selected.id)
            data = {
                "mode": "statement",
                "level_mode": mode,
                "account": {
                    "id": str(selected.id),
                    "code": selected.code,
                    "name": selected.name,
                    "level": selected.level,
                },
                "rows": rows,
                "lines": lines,
                "totals": self._sum_rows(rows),
            }
        else:
            filtered = self._filter_accounts_by_mode(accounts, mode)
            rows = await self._balance_rows_for_accounts(
                tenant_id, fiscal_period_id, filtered, include_zero=False
            )
            # If no movements yet, still show the chart accounts so UI is useful
            if not rows and filtered:
                rows = await self._balance_rows_for_accounts(
                    tenant_id, fiscal_period_id, filtered, include_zero=True
                )
            data = {
                "mode": "summary",
                "level_mode": mode,
                "rows": rows,
                "lines": [],
                "totals": self._sum_rows(rows),
            }

        label = "معین" if mode == "subsidiary" else "تفصیلی"
        return await self._persist(
            tenant_id=tenant_id,
            report_type=ReportType.GENERAL_LEDGER,
            name=f"گزارش {label} {fiscal_period_id}",
            fiscal_period_id=fiscal_period_id,
            data=data,
            generated_by=generated_by,
        )

    async def generate_analytical_report(
        self,
        tenant_id: UUID,
        fiscal_period_id: UUID,
        *,
        generated_by: str,
    ) -> FinancialReport:
        accounts = await self._list_accounts(tenant_id)
        postable = [a for a in accounts if a.is_postable and not a.is_group]
        rows = await self._balance_rows_for_accounts(
            tenant_id, fiscal_period_id, postable, include_zero=False
        )
        by_type: dict[str, dict] = {}
        by_category: dict[str, dict] = {}
        for row in rows:
            for bucket, key in (
                (by_type, row["account_type"]),
                (by_category, row["account_category"]),
            ):
                agg = bucket.setdefault(
                    key,
                    {
                        "key": key,
                        "opening_balance": _ZERO,
                        "debit_total": _ZERO,
                        "credit_total": _ZERO,
                        "closing_balance": _ZERO,
                        "account_count": 0,
                    },
                )
                agg["opening_balance"] += Decimal(row["opening_balance"])
                agg["debit_total"] += Decimal(row["debit_total"])
                agg["credit_total"] += Decimal(row["credit_total"])
                agg["closing_balance"] += Decimal(row["closing_balance"])
                agg["account_count"] += 1

        def _serialize(bucket: dict[str, dict]) -> list[dict]:
            out = []
            for item in bucket.values():
                out.append(
                    {
                        "key": item["key"],
                        "opening_balance": str(item["opening_balance"]),
                        "debit_total": str(item["debit_total"]),
                        "credit_total": str(item["credit_total"]),
                        "closing_balance": str(item["closing_balance"]),
                        "account_count": item["account_count"],
                    }
                )
            return sorted(out, key=lambda x: x["key"])

        data = {
            "mode": "analytical",
            "by_type": _serialize(by_type),
            "by_category": _serialize(by_category),
            "rows": rows,
            "totals": self._sum_rows(rows),
        }
        return await self._persist(
            tenant_id=tenant_id,
            report_type=ReportType.CUSTOM,
            name=f"گزارش تحلیلی {fiscal_period_id}",
            fiscal_period_id=fiscal_period_id,
            data=data,
            generated_by=generated_by,
        )

    @staticmethod
    def _is_cash_like(account: Account) -> bool:
        blob = f"{account.code} {account.name}".lower()
        return any(token in blob for token in _CASH_TOKENS)

    async def generate_cash_flow_report(
        self,
        tenant_id: UUID,
        fiscal_period_id: UUID,
        *,
        generated_by: str,
    ) -> FinancialReport:
        accounts = await self._list_accounts(tenant_id)
        cash_accounts = [
            a
            for a in accounts
            if a.is_postable
            and not a.is_group
            and a.account_type == AccountType.ASSET
            and self._is_cash_like(a)
        ]
        if not cash_accounts:
            cash_accounts = [
                a
                for a in accounts
                if a.is_postable
                and not a.is_group
                and a.account_category == AccountCategory.CURRENT_ASSET
            ]
        rows = await self._balance_rows_for_accounts(
            tenant_id, fiscal_period_id, cash_accounts, include_zero=True
        )
        totals = self._sum_rows(rows)
        data = {
            "mode": "cash_flow",
            "method": "cash_accounts_movement",
            "rows": rows,
            "totals": totals,
            "net_cash_change": str(
                Decimal(totals["closing_balance"]) - Decimal(totals["opening_balance"])
            ),
            "inflows": totals["debit_total"],
            "outflows": totals["credit_total"],
        }
        return await self._persist(
            tenant_id=tenant_id,
            report_type=ReportType.CASH_FLOW,
            name=f"جریان وجوه نقد {fiscal_period_id}",
            fiscal_period_id=fiscal_period_id,
            data=data,
            generated_by=generated_by,
        )

    async def generate_equity_statement(
        self,
        tenant_id: UUID,
        fiscal_period_id: UUID,
        *,
        generated_by: str,
    ) -> FinancialReport:
        accounts = await self._list_accounts(tenant_id)
        equity_accounts = [
            a
            for a in accounts
            if a.is_postable
            and not a.is_group
            and a.account_type == AccountType.EQUITY
        ]
        rows = await self._balance_rows_for_accounts(
            tenant_id, fiscal_period_id, equity_accounts, include_zero=True
        )
        data = {
            "mode": "equity_statement",
            "rows": rows,
            "totals": self._sum_rows(rows),
        }
        return await self._persist(
            tenant_id=tenant_id,
            report_type=ReportType.EQUITY_STATEMENT,
            name=f"تغییرات حقوق صاحبان سهام {fiscal_period_id}",
            fiscal_period_id=fiscal_period_id,
            data=data,
            generated_by=generated_by,
        )

    async def generate_trial_balance_report(
        self, tenant_id: UUID, fiscal_period_id: UUID, *, generated_by: str
    ) -> FinancialReport:
        snapshot = await self.balance_engine.generate_trial_balance(tenant_id, fiscal_period_id)
        report = FinancialReport(
            tenant_id=tenant_id,
            report_type=ReportType.TRIAL_BALANCE,
            name=f"Trial Balance {fiscal_period_id}",
            fiscal_period_id=fiscal_period_id,
            generated_at=datetime.now(timezone.utc),
            status=ReportStatus.GENERATED,
            report_data=json.dumps({
                "total_debit": str(snapshot.total_debit),
                "total_credit": str(snapshot.total_credit),
                "difference": str(snapshot.difference),
                "is_balanced": snapshot.is_balanced,
            }),
            generated_by=generated_by,
        )
        return await self.report_repo.add(report)

    async def generate_balance_sheet(
        self, tenant_id: UUID, fiscal_period_id: UUID, *, generated_by: str
    ) -> FinancialReport:
        balances = await self.balance_repo.list_by_period(tenant_id, fiscal_period_id)
        total_assets = sum(b.closing_balance for b in balances if b.closing_balance > 0)
        report = FinancialReport(
            tenant_id=tenant_id,
            report_type=ReportType.BALANCE_SHEET,
            name=f"Balance Sheet {fiscal_period_id}",
            fiscal_period_id=fiscal_period_id,
            generated_at=datetime.now(timezone.utc),
            status=ReportStatus.GENERATED,
            report_data=json.dumps({"total_assets": str(total_assets), "account_count": len(balances)}),
            generated_by=generated_by,
        )
        return await self.report_repo.add(report)

    async def generate_income_statement(
        self, tenant_id: UUID, fiscal_period_id: UUID, *, generated_by: str
    ) -> FinancialReport:
        balances = await self.balance_repo.list_by_period(tenant_id, fiscal_period_id)
        total_debit = sum(b.debit_total for b in balances)
        total_credit = sum(b.credit_total for b in balances)
        report = FinancialReport(
            tenant_id=tenant_id,
            report_type=ReportType.INCOME_STATEMENT,
            name=f"Income Statement {fiscal_period_id}",
            fiscal_period_id=fiscal_period_id,
            generated_at=datetime.now(timezone.utc),
            status=ReportStatus.GENERATED,
            report_data=json.dumps({
                "total_revenue": str(total_credit),
                "total_expense": str(total_debit),
                "net": str(total_credit - total_debit),
            }),
            generated_by=generated_by,
        )
        return await self.report_repo.add(report)

    async def export_report(
        self,
        tenant_id: UUID,
        report_id: UUID,
        export_format: ReportExportFormat,
        *,
        exported_by: str,
    ) -> ReportExport:
        report = await self.report_repo.get(tenant_id, report_id)
        if report is None:
            raise NotFoundError("گزارش یافت نشد", error_code="report_not_found")

        export = ReportExport(
            tenant_id=tenant_id,
            report_id=report_id,
            export_format=export_format,
            file_path=f"/exports/{report_id}.{export_format.value}",
            exported_at=datetime.now(timezone.utc),
            exported_by=exported_by,
        )
        return await self.export_repo.add(export)
