"""
IntegrationService - Automatic journal entry creation for loan and expense transactions.

This service ensures all loan operations are properly recorded in the accounting system
using double-entry bookkeeping principles.
"""

from decimal import Decimal
from typing import Optional
from django.db import transaction
from django.utils import timezone
from django.contrib.auth import get_user_model

from accounting.models import Account, JournalEntry, JournalEntryLine
from accounting.services.accounting_service import AccountingService

User = get_user_model()


class IntegrationService:
    """
    Service for automatic journal entry creation from loan and expense transactions.
    """

    def __init__(self):
        self.accounting_service = AccountingService()
        self._account_cache = {}

    def _get_account(self, code: str) -> Account:
        """
        Get account by code with caching.
        
        Args:
            code: Account code to retrieve
            
        Returns:
            Account instance
            
        Raises:
            Account.DoesNotExist: If account not found
        """
        if code not in self._account_cache:
            self._account_cache[code] = Account.objects.get(code=code, is_active=True)
        return self._account_cache[code]

    @transaction.atomic
    def create_loan_disbursement_entry(self, loan, user: Optional[User] = None) -> JournalEntry:
        """
        Create journal entry for loan disbursement.
        
        Entry:
            DR: Loan Portfolio (Asset)       Principal Amount
                CR: Cash/Bank (Asset)                Principal Amount
        
        Args:
            loan: Loan instance being disbursed
            user: User creating the entry (defaults to loan.approved_by)
            
        Returns:
            Created JournalEntry instance (in posted status)
        """
        if user is None:
            user = loan.approved_by or loan.loan_officer

        # Get accounts using organizational codes
        loan_portfolio_account = self._get_account('202001')  # Loan Portfolio
        
        # Determine disbursement account based on payment method
        # Default to Cash if no method specified
        cash_account = self._get_account('202003')  # Cash - Main Branch
        
        # Create journal entry
        journal_entry = JournalEntry.objects.create(
            reference_number=f'LOAN-DISB-{loan.loan_number}',
            transaction_date=loan.disbursement_date.date() if loan.disbursement_date else timezone.now().date(),
            description=f'Loan Disbursement - {loan.borrower.get_full_name()} - {loan.loan_number}',
            status='draft',
            branch=getattr(loan, 'branch', None) or getattr(loan.borrower, 'branch', None),
            loan=loan,
            created_by=user
        )

        # Create journal entry lines
        # Debit: Loan Portfolio
        JournalEntryLine.objects.create(
            journal_entry=journal_entry,
            account=loan_portfolio_account,
            description=f'Loan disbursed to {loan.borrower.get_full_name()}',
            debit_amount=loan.principal_amount,
            credit_amount=Decimal('0.00'),
            line_number=1
        )

        # Credit: Cash/Bank
        JournalEntryLine.objects.create(
            journal_entry=journal_entry,
            account=cash_account,
            description=f'Disbursement payment - {loan.loan_number}',
            debit_amount=Decimal('0.00'),
            credit_amount=loan.principal_amount,
            line_number=2
        )

        # Automatically post the entry
        self.accounting_service.post_journal_entry(journal_entry, user)

        return journal_entry

    @transaction.atomic
    def create_loan_repayment_entry(self, repayment, user: Optional[User] = None) -> JournalEntry:
        """
        Create journal entry for loan repayment.
        
        Entry splits repayment amount between principal, interest, and fees:
            DR: Cash/Bank (Asset)              Total Amount
                CR: Loan Portfolio (Asset)             Principal Portion
                CR: Interest Income (Income)           Interest Portion
                CR: Fee Income (Income)                Fee Portion
                CR: Penalty Income (Income)            Penalty Portion (if any)
        
        Args:
            repayment: Repayment instance being recorded
            user: User creating the entry (defaults to repayment.recorded_by)
            
        Returns:
            Created JournalEntry instance (in posted status)
        """
        if user is None:
            user = repayment.recorded_by or repayment.loan.loan_officer

        # Get accounts using organizational codes
        loan_portfolio_account = self._get_account('202001')  # Loan Portfolio
        interest_income_account = self._get_account('401001')  # Interest Income - Loans
        fee_income_account = self._get_account('401002')  # Fee Income
        
        # Note: Penalty income would need a separate account if tracked separately
        # For now, we'll use fee income for penalties too
        penalty_income_account = fee_income_account
        
        # Determine cash account based on payment source
        if repayment.payment_source == 'automatic':
            cash_account = self._get_account('202005')  # M-Pesa Account
        else:
            cash_account = self._get_account('202003')  # Cash - Main Branch
        
        # Calculate allocation (from repayment or loan)
        principal_portion = repayment.principal_portion or Decimal('0.00')
        interest_portion = repayment.interest_portion or Decimal('0.00')
        fee_portion = repayment.fee_portion or Decimal('0.00')
        penalty_portion = repayment.penalty_portion or Decimal('0.00')
        
        # Create journal entry
        payment_date = repayment.payment_date
        if hasattr(payment_date, 'date'):
            payment_date = payment_date.date()
        
        journal_entry = JournalEntry.objects.create(
            reference_number=f'LOAN-REPAY-{repayment.receipt_number}',
            transaction_date=payment_date,
            description=f'Loan Repayment - {repayment.loan.borrower.get_full_name()} - {repayment.loan.loan_number}',
            status='draft',
            branch=getattr(repayment.loan, 'branch', None) or getattr(repayment.loan.borrower, 'branch', None),
            loan=repayment.loan,
            created_by=user
        )

        line_num = 1

        # Debit: Cash/Bank (total amount received)
        JournalEntryLine.objects.create(
            journal_entry=journal_entry,
            account=cash_account,
            description=f'Payment received - {repayment.payment_method}',
            debit_amount=repayment.amount,
            credit_amount=Decimal('0.00'),
            line_number=line_num
        )
        line_num += 1

        # Credit: Loan Portfolio (principal reduction)
        if principal_portion > 0:
            JournalEntryLine.objects.create(
                journal_entry=journal_entry,
                account=loan_portfolio_account,
                description=f'Principal repayment - {repayment.loan.loan_number}',
                debit_amount=Decimal('0.00'),
                credit_amount=principal_portion,
                line_number=line_num
            )
            line_num += 1

        # Credit: Interest Income
        if interest_portion > 0:
            JournalEntryLine.objects.create(
                journal_entry=journal_entry,
                account=interest_income_account,
                description=f'Interest income - {repayment.loan.loan_number}',
                debit_amount=Decimal('0.00'),
                credit_amount=interest_portion,
                line_number=line_num
            )
            line_num += 1

        # Credit: Fee Income (includes fees and penalties)
        total_fee_income = fee_portion + penalty_portion
        if total_fee_income > 0:
            JournalEntryLine.objects.create(
                journal_entry=journal_entry,
                account=fee_income_account,
                description=f'Fee/Penalty income - {repayment.loan.loan_number}',
                debit_amount=Decimal('0.00'),
                credit_amount=total_fee_income,
                line_number=line_num
            )

        # Automatically post the entry
        self.accounting_service.post_journal_entry(journal_entry, user)

        return journal_entry

    @transaction.atomic
    def create_processing_fee_entry(self, loan, user: Optional[User] = None) -> Optional[JournalEntry]:
        """
        Create journal entry for processing fee collection (if collected separately).
        
        Entry:
            DR: Cash/Bank (Asset)              Fee Amount
                CR: Fee Income (Income)                Fee Amount
        
        Args:
            loan: Loan instance
            user: User creating the entry
            
        Returns:
            Created JournalEntry instance or None if no fee
        """
        if loan.processing_fee <= 0:
            return None

        if user is None:
            user = loan.approved_by or loan.loan_officer

        # Get accounts using organizational codes
        fee_income_account = self._get_account('401002')  # Fee Income
        cash_account = self._get_account('202003')  # Cash - Main Branch
        
        # Create journal entry
        journal_entry = JournalEntry.objects.create(
            reference_number=f'LOAN-FEE-{loan.loan_number}',
            transaction_date=loan.disbursement_date.date() if loan.disbursement_date else timezone.now().date(),
            description=f'Processing Fee - {loan.borrower.get_full_name()} - {loan.loan_number}',
            status='draft',
            branch=getattr(loan, 'branch', None) or getattr(loan.borrower, 'branch', None),
            loan=loan,
            created_by=user
        )

        # Debit: Cash/Bank
        JournalEntryLine.objects.create(
            journal_entry=journal_entry,
            account=cash_account,
            description=f'Processing fee collected - {loan.loan_number}',
            debit_amount=loan.processing_fee,
            credit_amount=Decimal('0.00'),
            line_number=1
        )

        # Credit: Fee Income
        JournalEntryLine.objects.create(
            journal_entry=journal_entry,
            account=fee_income_account,
            description=f'Processing fee income - {loan.loan_number}',
            debit_amount=Decimal('0.00'),
            credit_amount=loan.processing_fee,
            line_number=2
        )

        # Automatically post the entry
        self.accounting_service.post_journal_entry(journal_entry, user)

        return journal_entry

    @transaction.atomic
    def create_interest_accrual_entry(
        self, loan, interest_amount: Decimal, accrual_date, user: User
    ) -> JournalEntry:
        """
        Create journal entry for interest accrual (accrual basis accounting).
        
        Entry:
            DR: Accrued Interest Receivable (Asset)    Interest Amount
                CR: Interest Income (Income)                   Interest Amount
        
        Args:
            loan: Loan instance
            interest_amount: Amount of interest to accrue
            accrual_date: Date of accrual
            user: User creating the entry
            
        Returns:
            Created JournalEntry instance (in posted status)
        """
        # Get accounts using organizational codes
        accrued_interest_account = self._get_account('202002')  # Accrued Interest Receivable
        interest_income_account = self._get_account('401001')  # Interest Income - Loans
        
        # Create journal entry
        if hasattr(accrual_date, 'date'):
            accrual_date = accrual_date.date()
        
        journal_entry = JournalEntry.objects.create(
            reference_number=f'INT-ACCR-{loan.loan_number}-{accrual_date}',
            transaction_date=accrual_date,
            description=f'Interest Accrual - {loan.borrower.get_full_name()} - {loan.loan_number}',
            status='draft',
            branch=getattr(loan, 'branch', None) or getattr(loan.borrower, 'branch', None),
            loan=loan,
            created_by=user
        )

        # Debit: Accrued Interest Receivable
        JournalEntryLine.objects.create(
            journal_entry=journal_entry,
            account=accrued_interest_account,
            description=f'Interest accrued - {loan.loan_number}',
            debit_amount=interest_amount,
            credit_amount=Decimal('0.00'),
            line_number=1
        )

        # Credit: Interest Income
        JournalEntryLine.objects.create(
            journal_entry=journal_entry,
            account=interest_income_account,
            description=f'Interest income recognition - {loan.loan_number}',
            debit_amount=Decimal('0.00'),
            credit_amount=interest_amount,
            line_number=2
        )

        # Automatically post the entry
        self.accounting_service.post_journal_entry(journal_entry, user)

        return journal_entry

    @transaction.atomic
    def create_expense_entry(self, expense, user: Optional[User] = None) -> JournalEntry:
        """
        Create journal entry for approved expense.
        
        Entry:
            DR: Expense Account (Expense)      Expense Amount
                CR: Payment Account (Asset)            Expense Amount
        
        Args:
            expense: Expense instance being recorded
            user: User creating the entry (defaults to expense.approved_by)
            
        Returns:
            Created JournalEntry instance (in posted status)
        """
        if user is None:
            user = expense.approved_by

        # Map expense category to account
        expense_account = self._get_expense_account(expense.category)
        
        # Map payment method to account
        payment_account = self._get_payment_account(expense.payment_method)
        
        # Create journal entry
        expense_date = expense.expense_date
        if hasattr(expense_date, 'date'):
            expense_date = expense_date.date()
        
        journal_entry = JournalEntry.objects.create(
            reference_number=f'EXP-{expense.id}',
            transaction_date=expense_date,
            description=f'Expense - {expense.category} - {expense.description[:100]}',
            status='draft',
            branch=expense.branch,
            expense=expense,
            created_by=user
        )

        # Debit: Expense Account
        JournalEntryLine.objects.create(
            journal_entry=journal_entry,
            account=expense_account,
            description=expense.description[:200],
            debit_amount=expense.amount,
            credit_amount=Decimal('0.00'),
            line_number=1
        )

        # Credit: Payment Account
        JournalEntryLine.objects.create(
            journal_entry=journal_entry,
            account=payment_account,
            description=f'Payment via {expense.payment_method}',
            debit_amount=Decimal('0.00'),
            credit_amount=expense.amount,
            line_number=2
        )

        # Automatically post the entry
        self.accounting_service.post_journal_entry(journal_entry, user)

        return journal_entry

    def _get_expense_account(self, category: str) -> Account:
        """
        Map expense category to expense account.
        
        Args:
            category: Expense category string
            
        Returns:
            Account instance for the expense category
        """
        # Category to account code mapping (using organizational codes)
        category_mapping = {
            'salaries': '501002',     # Staff Salaries
            'rent': '501001',         # Operating Expenses
            'utilities': '501001',    # Operating Expenses
            'supplies': '501001',     # Operating Expenses
            'marketing': '501001',    # Operating Expenses
            'transport': '501001',    # Operating Expenses
            'maintenance': '501001',  # Operating Expenses
            'communication': '501001', # Operating Expenses
            'professional_fees': '501001',  # Operating Expenses
            'depreciation': '502001', # Depreciation Expense
            'loan_loss': '501003',    # Loan Loss Provision Expense
            'other': '501001',        # Operating Expenses
        }
        
        account_code = category_mapping.get(category.lower(), '501001')  # Default to Operating Expenses
        return self._get_account(account_code)

    def _get_payment_account(self, payment_method: str) -> Account:
        """
        Map payment method to asset account.
        
        Args:
            payment_method: Payment method string
            
        Returns:
            Account instance for the payment method
        """
        # Payment method to account code mapping (using organizational codes)
        payment_mapping = {
            'cash': '202003',          # Cash - Main Branch
            'bank_transfer': '202004', # Bank - Main Account
            'cheque': '202004',        # Bank - Main Account
            'mpesa': '202005',         # M-Pesa Account
            'card': '202004',          # Bank - Main Account
        }
        
        account_code = payment_mapping.get(payment_method.lower(), '202003')  # Default to Cash
        return self._get_account(account_code)
