"""
ReconciliationService - Cross-statement reconciliation and integrity verification.

Encapsulates Trial Balance, IS–SFP, and opening-balance-continuity checks so
the same logic can be shared between the report generation pipeline and the
DataIntegrityChecker management command.
"""

from dataclasses import dataclass
from decimal import Decimal
from typing import Optional
from datetime import date

from accounting.models import Account
from accounting.services.accounting_service import AccountingService
from accounting.services.report_service import ReportService


@dataclass
class ReconciliationResult:
    """Result of an IS–SFP cross-statement reconciliation check."""
    reconciled: bool
    is_net_income: Decimal
    equity_opening: Decimal
    equity_closing: Decimal
    equity_change: Decimal
    difference: Decimal


@dataclass
class TBBalanceResult:
    """Result of a Trial Balance debit/credit balance check."""
    balanced: bool
    total_debits: Decimal
    total_credits: Decimal
    difference: Decimal


class ReconciliationService:
    """
    Service for verifying cross-statement reconciliation and accounting integrity.

    Provides three verification methods:
    - verify_tb_balance: TB total debits == total credits
    - verify_is_sfp_reconciliation: IS net income == change in equity
    - verify_opening_balance_continuity: opening balance(d) == closing balance(d-1)
    """

    def __init__(self):
        self.accounting_service = AccountingService()
        self.report_service = ReportService()

    def verify_tb_balance(
        self,
        as_of_date: date,
        branch: Optional = None,
    ) -> TBBalanceResult:
        """
        Verify that Trial Balance total debits equal total credits.

        Args:
            as_of_date: The date to check the trial balance as of.
            branch: Optional branch to filter by.

        Returns:
            TBBalanceResult with balanced flag, totals, and difference.

        Requirements: 7.2, 10.2
        """
        tb = self.accounting_service.get_trial_balance(as_of_date, branch)
        total_debits = tb['totals']['total_debits']
        total_credits = tb['totals']['total_credits']
        difference = abs(total_debits - total_credits)
        return TBBalanceResult(
            balanced=difference < Decimal('0.01'),
            total_debits=total_debits,
            total_credits=total_credits,
            difference=difference,
        )

    def verify_is_sfp_reconciliation(
        self,
        start_date: date,
        end_date: date,
        branch: Optional = None,
        raise_on_failure: bool = False,
    ) -> ReconciliationResult:
        """
        Verify that IS net income equals the change in equity for the period.

        IS net_income should equal equity_closing − equity_opening for the
        same period, within ±0.01 KES.

        Args:
            start_date: Start of the period.
            end_date: End of the period.
            branch: Optional branch to filter by.
            raise_on_failure: If True and reconciliation fails, raise
                              ReconciliationError instead of returning a result.

        Returns:
            ReconciliationResult with all fields populated.

        Requirements: 6.1, 6.2, 6.3
        """
        from datetime import timedelta
        from accounting.exceptions import ReconciliationError

        # Step 1: Get IS net income from the income statement
        is_report = self.report_service.generate_income_statement(
            start_date, end_date, branch
        )
        is_net_income: Decimal = is_report['totals']['net_income']

        # Step 2: Sum equity account balances at closing and opening dates
        equity_accounts = Account.objects.filter(
            account_type='equity',
            is_active=True,
        )

        opening_date = start_date - timedelta(days=1)

        equity_closing = sum(
            (
                self.accounting_service.calculate_account_balance(account, end_date, branch)
                for account in equity_accounts
            ),
            Decimal('0.00'),
        )

        equity_opening = sum(
            (
                self.accounting_service.calculate_account_balance(account, opening_date, branch)
                for account in equity_accounts
            ),
            Decimal('0.00'),
        )

        # Step 3: Compute change and difference
        equity_change = equity_closing - equity_opening
        difference = abs(is_net_income - equity_change)
        reconciled = difference < Decimal('0.01')

        # Step 4: Raise if requested and reconciliation failed
        if raise_on_failure and not reconciled:
            raise ReconciliationError(is_net_income, equity_change)

        return ReconciliationResult(
            reconciled=reconciled,
            is_net_income=is_net_income,
            equity_opening=equity_opening,
            equity_closing=equity_closing,
            equity_change=equity_change,
            difference=difference,
        )

    def verify_opening_balance_continuity(
        self,
        account: Account,
        period_start: date,
        branch: Optional = None,
    ) -> bool:
        """
        Verify that the opening balance at period_start equals the closing
        balance at period_start − 1 day.

        The GL is the authoritative source. The closing balance of the prior day
        (period_start - 1) must equal the opening balance for the new period
        (also computed as the balance at period_start - 1, since no transactions
        on period_start should have affected it yet). This detects any discontinuity
        or phantom entries inserted at the boundary.

        Args:
            account: The account to check.
            period_start: The first day of the period.
            branch: Optional branch to filter by.

        Returns:
            True if values match within ±0.01 KES, False otherwise.

        Requirements: 8.1, 8.2
        """
        from datetime import timedelta

        prior_day = period_start - timedelta(days=1)

        # Closing balance of the day before period starts — this is the
        # authoritative GL balance at the end of the prior period.
        closing_balance_prior_day = self.accounting_service.calculate_account_balance(
            account, prior_day, branch
        )

        # Opening balance for period_start: in a continuous ledger this must
        # equal the closing balance of the prior day. We verify by computing
        # the balance at period_start using only entries up to (and not
        # including) period_start — which is exactly calculate_account_balance
        # at prior_day. To detect any phantom/backdated entries injected exactly
        # on period_start that shift the apparent opening, we also fetch the
        # balance at period_start itself and compare.
        opening_balance_period_start = self.accounting_service.calculate_account_balance(
            account, period_start, branch
        )

        difference = abs(closing_balance_prior_day - opening_balance_period_start)
        return difference < Decimal('0.01')
