"""
AccountingService - Core accounting service for transaction management.

This service ensures double-entry integrity and maintains a complete audit trail.
"""

from decimal import Decimal
from typing import Optional
from django.db import transaction
from django.core.exceptions import ValidationError
from django.utils import timezone
from django.contrib.auth import get_user_model

from accounting.models import (
    Account,
    JournalEntry,
    JournalEntryLine,
    GeneralLedger,
    FiscalPeriod,
    AccountBalance,
)

User = get_user_model()


class AccountingService:
    """
    Core accounting service for transaction management.
    Ensures double-entry integrity and maintains audit trail.
    """

    def post_journal_entry(self, journal_entry: JournalEntry, user: User) -> bool:
        """
        Post a journal entry to general ledger.

        This method validates the journal entry, creates general ledger postings,
        calculates running balances, and updates the journal entry status.

        Validates:
        - Total debits == total credits
        - Period is open for the transaction date
        - All accounts are active

        Args:
            journal_entry: The JournalEntry instance to post
            user: The User posting the journal entry

        Returns:
            bool: True if posting was successful

        Raises:
            ValidationError: If validation fails or posting cannot be completed
        """
        with transaction.atomic():
            # 1. Validate debits == credits
            total_debits = sum(
                line.debit_amount for line in journal_entry.lines.all()
            )
            total_credits = sum(
                line.credit_amount for line in journal_entry.lines.all()
            )

            if total_debits != total_credits:
                raise ValidationError(
                    f"Debits must equal credits. "
                    f"Debits: {total_debits}, Credits: {total_credits}"
                )

            # 2. Check period status
            period = FiscalPeriod.objects.filter(
                start_date__lte=journal_entry.transaction_date,
                end_date__gte=journal_entry.transaction_date,
                status='open'
            ).first()

            if not period:
                raise ValidationError(
                    f"Period is closed for transaction date "
                    f"{journal_entry.transaction_date}. "
                    f"Cannot post journal entry."
                )

            # 3. Validate all accounts are active
            for line in journal_entry.lines.all():
                if not line.account.is_active:
                    raise ValidationError(
                        f"Account {line.account.code} - {line.account.name} "
                        f"is inactive. Cannot post journal entry."
                    )

            # 4. Create general ledger entries with running balance calculation
            for line in journal_entry.lines.all():
                # Get previous balance for this account and branch
                last_entry = GeneralLedger.objects.filter(
                    account=line.account,
                    branch=journal_entry.branch
                ).order_by('-posted_at', '-id').first()

                previous_balance = (
                    last_entry.balance if last_entry else Decimal('0.00')
                )

                # Calculate new balance based on account type
                # Assets/Expenses: increase with debits, decrease with credits
                # Liabilities/Equity/Income: increase with credits, decrease with debits
                if line.account.account_type in ['asset', 'expense']:
                    new_balance = (
                        previous_balance + line.debit_amount - line.credit_amount
                    )
                else:  # liability, equity, income
                    new_balance = (
                        previous_balance + line.credit_amount - line.debit_amount
                    )

                # Create ledger entry
                GeneralLedger.objects.create(
                    account=line.account,
                    journal_entry=journal_entry,
                    journal_entry_line=line,
                    transaction_date=journal_entry.transaction_date,
                    description=line.description,
                    reference_number=journal_entry.reference_number,
                    debit_amount=line.debit_amount,
                    credit_amount=line.credit_amount,
                    balance=new_balance,
                    branch=journal_entry.branch,
                    posted_at=timezone.now(),
                    posted_by=user
                )

            # 5. Update journal entry status to 'posted'
            journal_entry.status = 'posted'
            journal_entry.posted_by = user
            journal_entry.posted_at = timezone.now()
            journal_entry.save()

            # 6. Invalidate cached balances
            self._invalidate_balance_cache(journal_entry.transaction_date)

            return True

    def reverse_journal_entry(
        self, journal_entry: JournalEntry, user: User, reversal_date, reason: str
    ) -> JournalEntry:
        """
        Create a reversing journal entry.

        This method creates a new journal entry that reverses the original entry
        by swapping debits and credits. The reversal is automatically posted and
        linked to the original entry.

        Args:
            journal_entry: The JournalEntry instance to reverse
            user: The User creating the reversal
            reversal_date: The date for the reversal entry
            reason: The reason for the reversal

        Returns:
            JournalEntry: The newly created reversal entry

        Raises:
            ValidationError: If the entry cannot be reversed
        """
        with transaction.atomic():
            # 1. Validate that the entry can be reversed
            if journal_entry.status != 'posted':
                raise ValidationError(
                    f"Only posted journal entries can be reversed. "
                    f"Current status: {journal_entry.status}"
                )

            if hasattr(journal_entry, 'reversed_by') and journal_entry.reversed_by:
                raise ValidationError(
                    f"Journal entry {journal_entry.reference_number} "
                    f"has already been reversed."
                )

            # 2. Create the reversal journal entry
            reversal_entry = JournalEntry.objects.create(
                reference_number=f"{journal_entry.reference_number}-REV",
                transaction_date=reversal_date,
                description=f"REVERSAL: {reason} (Original: {journal_entry.reference_number})",
                branch=journal_entry.branch,
                loan=journal_entry.loan,
                expense=journal_entry.expense,
                reverses=journal_entry,
                created_by=user,
                status='draft'
            )

            # 3. Create reversed journal entry lines (swap debits and credits)
            for original_line in journal_entry.lines.all():
                JournalEntryLine.objects.create(
                    journal_entry=reversal_entry,
                    account=original_line.account,
                    description=f"Reversal: {original_line.description}",
                    debit_amount=original_line.credit_amount,  # Swap credit to debit
                    credit_amount=original_line.debit_amount,  # Swap debit to credit
                    line_number=original_line.line_number
                )

            # 4. Auto-post the reversal entry
            self.post_journal_entry(reversal_entry, user)

            # 5. Update original entry status to 'reversed'
            journal_entry.status = 'reversed'
            journal_entry.save()

            return reversal_entry

    def calculate_account_balance(
        self, account: Account, as_of_date, branch: Optional = None
    ) -> Decimal:
        """
        Calculate account balance as of a specific date.

        This method checks cached balances first for performance, then calculates
        from general ledger if needed.

        Args:
            account: The Account instance
            as_of_date: The date to calculate balance as of
            branch: Optional Branch to filter by

        Returns:
            Decimal: The account balance
        """
        from django.core.cache import cache
        
        # Try memory cache first with key format "balance_{code}_{date}_{branch}"
        cache_key = f"balance_{account.code}_{as_of_date}_{branch.id if branch else 'all'}"
        cached_balance = cache.get(cache_key)
        if cached_balance is not None:
            return cached_balance
        
        # Check for cached balance in database
        cache_filter = {
            'account': account,
            'as_of_date': as_of_date
        }
        if branch:
            cache_filter['branch'] = branch

        cached_balance_obj = AccountBalance.objects.filter(**cache_filter).first()
        if cached_balance_obj:
            # Store in memory cache for faster subsequent access
            cache.set(cache_key, cached_balance_obj.net_balance, 3600)  # 1 hour
            return cached_balance_obj.net_balance

        # Calculate from general ledger
        ledger_filter = {
            'account': account,
            'transaction_date__lte': as_of_date
        }
        if branch:
            ledger_filter['branch'] = branch

        # Get the last ledger entry up to the date
        last_entry = GeneralLedger.objects.filter(
            **ledger_filter
        ).order_by('-transaction_date', '-posted_at', '-id').first()

        balance = last_entry.balance if last_entry else Decimal('0.00')
        
        # Store in memory cache
        cache.set(cache_key, balance, 3600)  # Cache for 1 hour
        
        return balance

    def get_trial_balance(self, as_of_date, branch: Optional = None) -> dict:
        """
        Generate trial balance report data.

        This method calculates balances for all active accounts and groups them
        by account type.

        Args:
            as_of_date: The date to generate trial balance as of
            branch: Optional Branch to filter by

        Returns:
            dict: Trial balance data with accounts, totals, balanced flag, and by_type breakdown
        """
        from django.core.cache import cache

        # Create cache key
        cache_key = f"trial_balance_{as_of_date}_{branch.id if branch else 'all'}"
        cached = cache.get(cache_key)
        if cached:
            return cached

        # Get all active accounts using only() to limit fields
        accounts = Account.objects.filter(is_active=True).only(
            'id', 'code', 'name', 'account_type'
        ).order_by('code')

        report_data = {
            'report_date': as_of_date,
            'branch': branch.name if branch else 'All Branches',
            'accounts': [],
            'totals': {
                'total_debits': Decimal('0.00'),
                'total_credits': Decimal('0.00'),
                'balanced': True
            },
            'by_type': {
                'asset': {'debit': Decimal('0.00'), 'credit': Decimal('0.00')},
                'liability': {'debit': Decimal('0.00'), 'credit': Decimal('0.00')},
                'equity': {'debit': Decimal('0.00'), 'credit': Decimal('0.00')},
                'income': {'debit': Decimal('0.00'), 'credit': Decimal('0.00')},
                'expense': {'debit': Decimal('0.00'), 'credit': Decimal('0.00')},
            }
        }

        for account in accounts:
            balance = self.calculate_account_balance(account, as_of_date, branch)

            if balance != Decimal('0.00'):
                # Determine debit or credit balance based on account type and balance sign
                # For asset/expense accounts: positive balance = debit
                # For liability/equity/income accounts: positive balance = credit
                if account.account_type in ['asset', 'expense']:
                    debit_bal = balance if balance > 0 else Decimal('0.00')
                    credit_bal = abs(balance) if balance < 0 else Decimal('0.00')
                else:  # liability, equity, income
                    debit_bal = abs(balance) if balance < 0 else Decimal('0.00')
                    credit_bal = balance if balance > 0 else Decimal('0.00')

                account_data = {
                    'code': account.code,
                    'name': account.name,
                    'type': account.account_type,
                    'debit_balance': debit_bal,
                    'credit_balance': credit_bal
                }
                report_data['accounts'].append(account_data)

                # Update totals
                report_data['totals']['total_debits'] += debit_bal
                report_data['totals']['total_credits'] += credit_bal

                # Update by_type breakdown
                report_data['by_type'][account.account_type]['debit'] += debit_bal
                report_data['by_type'][account.account_type]['credit'] += credit_bal

        # Check if balanced
        report_data['totals']['balanced'] = (
            report_data['totals']['total_debits'] ==
            report_data['totals']['total_credits']
        )

        # Cache for 1 hour
        cache.set(cache_key, report_data, 3600)

        return report_data

    def get_chart_of_accounts(self, include_inactive: bool = False) -> dict:
        """
        Get Chart of Accounts structure with caching.
        Implements 24-hour cache as per Requirement 20.4.
        
        Args:
            include_inactive: Whether to include inactive accounts
        
        Returns:
            dict: Hierarchical chart of accounts with account details
        """
        from django.core.cache import cache
        
        # Create cache key based on inclusion of inactive accounts
        cache_key = f"chart_of_accounts_{'all' if include_inactive else 'active'}"
        cached = cache.get(cache_key)
        if cached:
            return cached
        
        # Build query with optimization
        accounts_qs = Account.objects.select_related('parent_account').only(
            'id', 'code', 'name', 'account_type', 'subtype', 
            'description', 'is_active', 'is_system_account',
            'parent_account__code', 'parent_account__name'
        )
        
        if not include_inactive:
            accounts_qs = accounts_qs.filter(is_active=True)
        
        accounts_qs = accounts_qs.order_by('code')
        
        # Build hierarchical structure
        chart_data = {
            'by_type': {
                'asset': [],
                'liability': [],
                'equity': [],
                'income': [],
                'expense': []
            },
            'total_accounts': 0,
            'active_accounts': 0,
            'inactive_accounts': 0
        }
        
        accounts_list = list(accounts_qs)
        chart_data['total_accounts'] = len(accounts_list)
        
        for account in accounts_list:
            if account.is_active:
                chart_data['active_accounts'] += 1
            else:
                chart_data['inactive_accounts'] += 1
            
            account_data = {
                'code': account.code,
                'name': account.name,
                'type': account.account_type,
                'subtype': account.subtype,
                'description': account.description,
                'is_active': account.is_active,
                'is_system': account.is_system_account,
                'parent_code': account.parent_account.code if account.parent_account else None
            }
            
            chart_data['by_type'][account.account_type].append(account_data)
        
        # Cache for 24 hours (86400 seconds) as per Requirement 20.4
        cache.set(cache_key, chart_data, 86400)
        
        return chart_data

    def invalidate_chart_cache(self):
        """
        Invalidate Chart of Accounts cache when accounts are modified.
        Should be called when accounts are created, updated, or deleted.
        """
        from django.core.cache import cache
        
        cache.delete('chart_of_accounts_all')
        cache.delete('chart_of_accounts_active')

    def _invalidate_balance_cache(self, transaction_date):
        """
        Invalidate cached account balances from the transaction date forward.

        Args:
            transaction_date: The date from which to invalidate cache
        """
        from django.core.cache import cache
        
        # Delete cached balances from this date forward
        AccountBalance.objects.filter(
            as_of_date__gte=transaction_date
        ).delete()
        
        # Clear related memory caches
        # Note: Pattern-based deletion not available in all cache backends
        try:
            # Try pattern-based deletion if available (Redis cache)
            if hasattr(cache, 'delete_pattern'):
                cache.delete_pattern('trial_balance_*')
                cache.delete_pattern('balance_*')
                cache.delete_pattern('income_statement_*')
                cache.delete_pattern('balance_sheet_*')
                cache.delete_pattern('cash_flow_*')
                cache.delete_pattern('chart_of_accounts_*')
        except (AttributeError, Exception):
            # Fallback: clear entire cache or skip
            # In production, consider using cache versioning instead
            pass
