"""
GL Account Resolver Service

This service resolves the appropriate GL accounts for purchase items based on their item_type,
and handles the automatic creation of journal entries with the correct account routing.
"""
from decimal import Decimal
from typing import Dict, Optional, Tuple
from sqlalchemy.orm import Session

from app.models.accounting import AccountingCode, JournalEntry, AccountingEntry
from app.models.purchases import Purchase, PurchaseItem, Supplier
from app.models.item_type_mapping import ItemTypeEnum, GLAccountMapping, ItemTypeDefinition


class GLAccountResolver:
    """Resolves GL accounts for purchase items based on item type and configuration"""

    def __init__(self, db: Session):
        self.db = db

    def get_debit_account_for_item(self, purchase_item: PurchaseItem, supplier: Supplier) -> AccountingCode:
        """
        Get the appropriate debit (expense/asset) account for a purchase item.

        Logic:
        1. If item has explicit GL account override, use it
        2. If item_type_id is set, resolve from ItemTypeDefinition
        3. Fall back to product type classification
        4. Default to supplier's accounting code or General Expenses
        """

        # 1. Check for explicit GL account override on the item
        if hasattr(purchase_item, 'accounting_code_id') and purchase_item.accounting_code_id:
            account = self.db.query(AccountingCode).filter(
                AccountingCode.id == purchase_item.accounting_code_id
            ).first()
            if account:
                return account

        # 2. Check for asset-specific account (for capital asset purchases)
        if purchase_item.is_asset and hasattr(purchase_item, 'asset_accounting_code_id'):
            if purchase_item.asset_accounting_code_id:
                account = self.db.query(AccountingCode).filter(
                    AccountingCode.id == purchase_item.asset_accounting_code_id
                ).first()
                if account:
                    return account

        # 3. Resolve based on item type if available
        if hasattr(purchase_item, 'item_type_id') and purchase_item.item_type_id:
            item_type_def = self.db.query(ItemTypeDefinition).filter(
                ItemTypeDefinition.id == purchase_item.item_type_id
            ).first()
            if item_type_def and item_type_def.debit_account:
                return item_type_def.debit_account

        # 4. Use product's assigned GL account (PREFERRED for inventory items)
        if purchase_item.product:
            if purchase_item.product.accounting_code_id:
                # Use the product's assigned inventory account
                product_account = self.db.query(AccountingCode).filter(
                    AccountingCode.id == purchase_item.product.accounting_code_id
                ).first()
                if product_account:
                    return product_account
            
            # Fallback: if product is inventory but has no assigned account
            if purchase_item.is_inventory or (hasattr(purchase_item.product, 'product_type') and
                                              purchase_item.product.product_type == 'inventory_item'):
                # Inventory purchase - debit Merchandise Inventory account
                inventory_account = self._get_standard_account('Merchandise Inventory', 'Asset')
                if inventory_account:
                    return inventory_account

        # 5. Default to supplier's accounting code
        if supplier.accounting_code:
            return supplier.accounting_code

        # 6. Ultimate fallback to General Expenses
        return self._get_standard_account('General Expenses', 'Expense')

    def get_credit_account_for_payment(self, purchase: Purchase, payment_method: str = None) -> AccountingCode:
        """
        Get the appropriate credit account based on payment method.

        If payment is made immediately: Credit Cash account
        If payment is deferred (on credit): Credit Accounts Payable
        """
        payment_method = payment_method or getattr(purchase, 'payment_method', 'credit')

        if payment_method == 'cash':
            # Immediate cash payment - credit bank/cash account
            if hasattr(purchase, 'bank_account_id') and purchase.bank_account_id:
                return self.db.query(AccountingCode).filter(
                    AccountingCode.id == purchase.bank_account_id
                ).first()

            # Fall back to Cash in Hand
            return self._get_standard_account('Cash in Hand', 'Asset')
        else:
            # Deferred payment - credit Accounts Payable liability
            return self._get_standard_account('Accounts Payable', 'Liability')

    def get_vat_account_for_item(self, purchase_item: PurchaseItem) -> Optional[AccountingCode]:
        """
        Get the appropriate VAT account for a purchase item.

        - If item has specific VAT account override, use it
        - Otherwise use Input VAT (VAT Receivable) account
        """

        # 1. Check for item-level VAT account override
        if hasattr(purchase_item, 'vat_account_id') and purchase_item.vat_account_id:
            account = self.db.query(AccountingCode).filter(
                AccountingCode.id == purchase_item.vat_account_id
            ).first()
            if account:
                return account

        # 2. Use standard Input VAT account
        # Try code-based lookup first (1162 is typical for Input VAT detail account)
        vat_account = self.db.query(AccountingCode).filter(
            AccountingCode.code == '1162'
        ).first()

        if not vat_account:
            vat_account = self._get_standard_account('Input VAT', 'Asset')

        return vat_account

    def get_payable_account(self, supplier: Supplier) -> AccountingCode:
        """
        Get the supplier's specific Accounts Payable account for proper subsidiary ledger tracking.
        Falls back to general Accounts Payable if supplier doesn't have specific account.
        """
        if supplier.accounting_code:
            return supplier.accounting_code

        return self._get_standard_account('Accounts Payable', 'Liability')

    def create_purchase_journal_entries(self, purchase: Purchase, supplier: Supplier) -> Dict:
        """
        Create comprehensive journal entries for a purchase with proper GL account routing.

        Returns:
            Dictionary with:
            - 'entries': List of JournalEntry objects created
            - 'amounts': Summary of debit/credit amounts by account type
            - 'validated': Boolean indicating if journal is balanced
        """

        try:
            # Create accounting entry header
            accounting_entry = AccountingEntry(
                date_prepared=purchase.purchase_date,
                date_posted=purchase.purchase_date,
                particulars=f"Purchase from {supplier.name} - {purchase.reference or purchase.id}",
                book=f"PURCHASE-{purchase.id}",
                status='posted',
                branch_id=purchase.branch_id
            )
            self.db.add(accounting_entry)
            self.db.flush()

            entries = []
            totals_by_account = {}

            # Process each purchase item
            for item in purchase.purchase_items:
                # Get the appropriate debit account for this item
                debit_account = self.get_debit_account_for_item(item, supplier)

                # Create debit entry for the item amount
                debit_entry = JournalEntry(
                    accounting_entry_id=accounting_entry.id,
                    accounting_code_id=debit_account.id,
                    entry_type='debit',
                    debit_amount=item.total_cost,
                    credit_amount=Decimal('0'),
                    description=f"Purchase item: {item.description or item.product.name if item.product else 'Unknown'}",
                    date=purchase.purchase_date,
                    branch_id=purchase.branch_id
                )
                entries.append(debit_entry)

                # Track totals for summary
                key = f"{debit_account.code}:{debit_account.name}"
                if key not in totals_by_account:
                    totals_by_account[key] = {'debit': Decimal('0'), 'credit': Decimal('0')}
                totals_by_account[key]['debit'] += item.total_cost

                # Create VAT entry if item is taxable and has VAT
                if item.vat_amount and item.vat_amount > 0:
                    vat_account = self.get_vat_account_for_item(item)
                    if vat_account:
                        vat_entry = JournalEntry(
                            accounting_entry_id=accounting_entry.id,
                            accounting_code_id=vat_account.id,
                            entry_type='debit',
                            debit_amount=item.vat_amount,
                            credit_amount=Decimal('0'),
                            description=f"Input VAT for: {item.description or (item.product.name if item.product else 'Unknown')}",
                            date=purchase.purchase_date,
                            branch_id=purchase.branch_id
                        )
                        entries.append(vat_entry)

                        # Track VAT totals
                        key = f"{vat_account.code}:{vat_account.name}"
                        if key not in totals_by_account:
                            totals_by_account[key] = {'debit': Decimal('0'), 'credit': Decimal('0')}
                        totals_by_account[key]['debit'] += item.vat_amount

            # Get credit account based on payment method
            credit_account = self.get_credit_account_for_payment(purchase)

            # Create credit entry for total amount (including VAT)
            total_amount = purchase.total_amount + purchase.total_vat_amount
            credit_entry = JournalEntry(
                accounting_entry_id=accounting_entry.id,
                accounting_code_id=credit_account.id,
                entry_type='credit',
                debit_amount=Decimal('0'),
                credit_amount=total_amount,
                description=f"Accounts Payable / Cash for purchase from {supplier.name}",
                date=purchase.purchase_date,
                branch_id=purchase.branch_id
            )
            entries.append(credit_entry)

            # Track credit totals
            key = f"{credit_account.code}:{credit_account.name}"
            if key not in totals_by_account:
                totals_by_account[key] = {'debit': Decimal('0'), 'credit': Decimal('0')}
            totals_by_account[key]['credit'] += total_amount

            # Validate journal balance
            total_debits = sum(e.debit_amount for e in entries)
            total_credits = sum(e.credit_amount for e in entries)
            is_balanced = total_debits == total_credits

            if not is_balanced:
                raise ValueError(
                    f"Unbalanced journal for purchase {purchase.reference or purchase.id}: "
                    f"debits {total_debits} != credits {total_credits}"
                )

            # Add entries to database
            for entry in entries:
                self.db.add(entry)

            return {
                'entries': entries,
                'accounting_entry': accounting_entry,
                'amounts': totals_by_account,
                'total_debits': total_debits,
                'total_credits': total_credits,
                'validated': is_balanced
            }

        except Exception as e:
            raise ValueError(f"Error creating journal entries for purchase: {str(e)}")

    def _get_standard_account(self, name: str, account_type: str) -> Optional[AccountingCode]:
        """
        Get a standard accounting code by name and type.

        Tries exact match first, then common fallback names.
        """
        # Try exact match
        code = self.db.query(AccountingCode).filter(
            AccountingCode.name == name,
            AccountingCode.account_type == account_type
        ).first()

        if code:
            return code

        # Define fallback names for common accounts
        fallbacks = {
            ('Merchandise Inventory', 'Asset'): ['Inventory', 'Finished Goods', 'Stock'],
            ('Accounts Payable', 'Liability'): ['Trade Creditors', 'Creditors', 'Suppliers'],
            ('Cash in Hand', 'Asset'): ['Cash', 'Petty Cash'],
            ('Input VAT', 'Asset'): ['VAT Receivable', 'VAT Claimable', 'Input Tax'],
            ('General Expenses', 'Expense'): ['Operating Expenses', 'Admin Expenses'],
        }

        # Try fallbacks
        fallback_names = fallbacks.get((name, account_type), [])
        for fallback_name in fallback_names:
            code = self.db.query(AccountingCode).filter(
                AccountingCode.name == fallback_name,
                AccountingCode.account_type == account_type
            ).first()
            if code:
                return code

        return None
