import requests
import base64
import json
from datetime import datetime, timedelta
from django.utils import timezone
from django.conf import settings
from .models import MpesaConfiguration, MpesaAccessToken
import logging


class MpesaService:
    """
    Service class for M-Pesa API interactions
    """
    
    def __init__(self, configuration=None):
        self.config = configuration or MpesaConfiguration.get_active_config()
        if not self.config:
            raise ValueError("No active M-Pesa configuration found")
        
        # Set base URLs based on environment
        if self.config.environment == 'production':
            self.base_url = 'https://api.safaricom.co.ke'
        else:
            self.base_url = 'https://sandbox.safaricom.co.ke'
    
    def get_access_token(self):
        """
        Generate M-Pesa access token
        """
        url = f"{self.base_url}/oauth/v1/generate?grant_type=client_credentials"
        
        # Create basic auth header
        credentials = f"{self.config.consumer_key}:{self.config.consumer_secret}"
        encoded_credentials = base64.b64encode(credentials.encode()).decode()
        
        headers = {
            'Authorization': f'Basic {encoded_credentials}',
            'Content-Type': 'application/json'
        }
        
        try:
            response = requests.get(url, headers=headers, timeout=30)
            response.raise_for_status()
            
            data = response.json()
            access_token = data['access_token']
            expires_in = int(data['expires_in'])  # seconds
            
            # Store token in database
            expires_at = timezone.now() + timedelta(seconds=expires_in - 60)  # 1 minute buffer
            
            # Delete old tokens for this configuration
            MpesaAccessToken.objects.filter(configuration=self.config).delete()
            
            # Create new token
            MpesaAccessToken.objects.create(
                configuration=self.config,
                access_token=access_token,
                expires_at=expires_at
            )
            
            return access_token
            
        except requests.exceptions.RequestException as e:
            raise Exception(f"Failed to get M-Pesa access token: {str(e)}")
        except KeyError as e:
            raise Exception(f"Invalid response from M-Pesa API: {str(e)}")
    
    def get_valid_access_token(self):
        """
        Get a valid access token, generating one if needed
        """
        return MpesaAccessToken.get_valid_token(self.config)
    
    def register_urls(self, validation_url=None, confirmation_url=None):
        """
        Register validation and confirmation URLs with M-Pesa
        Note: Production uses v2, sandbox uses v1
        """
        if self.config.environment == 'production':
            url = f"{self.base_url}/mpesa/c2b/v2/registerurl"
        else:
            url = f"{self.base_url}/mpesa/c2b/v1/registerurl"
        
        access_token = self.get_valid_access_token()
        
        headers = {
            'Authorization': f'Bearer {access_token}',
            'Content-Type': 'application/json'
        }
        
        payload = {
            'ShortCode': self.config.business_short_code,
            'ResponseType': self.config.response_type,
            'ConfirmationURL': confirmation_url or self.config.confirmation_url,
            'ValidationURL': validation_url or self.config.validation_url
        }
        
        try:
            response = requests.post(url, json=payload, headers=headers, timeout=30)
            response.raise_for_status()
            
            return response.json()
            
        except requests.exceptions.RequestException as e:
            raise Exception(f"Failed to register M-Pesa URLs: {str(e)}")
    
    def simulate_c2b_payment(self, phone_number, amount, bill_ref_number=None):
        """
        Simulate C2B payment (for testing in sandbox)
        """
        if self.config.environment != 'sandbox':
            raise ValueError("Payment simulation is only available in sandbox environment")
        
        url = f"{self.base_url}/mpesa/c2b/v1/simulate"
        
        access_token = self.get_valid_access_token()
        
        headers = {
            'Authorization': f'Bearer {access_token}',
            'Content-Type': 'application/json'
        }
        
        # Format phone number
        if phone_number.startswith('0'):
            phone_number = '254' + phone_number[1:]
        elif phone_number.startswith('+254'):
            phone_number = phone_number[1:]
        elif not phone_number.startswith('254'):
            phone_number = '254' + phone_number
        
        payload = {
            'ShortCode': self.config.business_short_code,
            'CommandID': 'CustomerPayBillOnline',
            'Amount': str(amount),
            'Msisdn': phone_number,
            'BillRefNumber': bill_ref_number or 'TEST001'
        }
        
        try:
            response = requests.post(url, json=payload, headers=headers, timeout=30)
            response.raise_for_status()
            
            return response.json()
            
        except requests.exceptions.RequestException as e:
            raise Exception(f"Failed to simulate M-Pesa payment: {str(e)}")
    
    def query_transaction_status(self, transaction_id):
        """
        Query the status of a transaction
        """
        # This would require additional M-Pesa API endpoints
        # Implementation depends on specific requirements
        pass
    
    def get_account_balance(self):
        """
        Get M-Pesa account balance
        """
        # This would require additional M-Pesa API endpoints
        # Implementation depends on specific requirements
        pass


class PaymentProcessor:
    """
    Service for processing M-Pesa payments and matching them to loans
    """
    
    @staticmethod
    def process_validation_callback(callback_data):
        """
        Process M-Pesa validation callback
        """
        from .models import MpesaCallback
        from loans.models import MpesaTransaction
        
        try:
            # Create callback log
            callback = MpesaCallback.objects.create(
                callback_type='validation',
                raw_data=callback_data
            )
            
            # Extract transaction data
            trans_id = callback_data.get('TransID')
            
            # Check if transaction already exists
            transaction, created = MpesaTransaction.objects.get_or_create(
                trans_id=trans_id,
                defaults={
                    'transaction_type': callback_data.get('TransactionType', 'Pay Bill'),
                    'amount': callback_data.get('TransAmount', 0),
                    'phone_number': callback_data.get('MSISDN', ''),
                    'trans_time': callback_data.get('TransTime'),
                    'business_short_code': callback_data.get('BusinessShortCode'),
                    'bill_ref_number': callback_data.get('BillRefNumber'),
                    'invoice_number': callback_data.get('InvoiceNumber'),
                    'org_account_balance': callback_data.get('OrgAccountBalance'),
                    'third_party_trans_id': callback_data.get('ThirdPartyTransID'),
                    'msisdn': callback_data.get('MSISDN'),
                    'first_name': callback_data.get('FirstName'),
                    'middle_name': callback_data.get('MiddleName'),
                    'last_name': callback_data.get('LastName'),
                    'raw_validation_data': callback_data,
                    'status': 'pending'
                }
            )
            
            callback.transaction = transaction
            callback.save()
            
            # Try to match borrower
            borrower = transaction.match_borrower()
            
            if borrower:
                # Try to match loan
                loan = transaction.match_loan()
                
                if loan:
                    transaction.status = 'validated'
                    transaction.save()
                    
                    response = {
                        'ResultCode': '0',
                        'ResultDesc': 'Accepted'
                    }
                else:
                    # No matching loan found
                    response = {
                        'ResultCode': 'C2B00012',
                        'ResultDesc': 'No active loan found for this account'
                    }
            else:
                # No matching borrower found
                response = {
                    'ResultCode': 'C2B00011',
                    'ResultDesc': 'Invalid phone number - not registered'
                }
            
            callback.response_sent = response
            callback.processed = True
            callback.save()
            
            return response
            
        except Exception as e:
            # Log error and reject transaction
            return {
                'ResultCode': 'C2B00016',
                'ResultDesc': f'System error: {str(e)}'
            }
    
    @staticmethod
    def process_confirmation_callback(callback_data):
        """
        Process M-Pesa confirmation callback
        """
        from .models import MpesaCallback
        from loans.models import MpesaTransaction
        from decimal import Decimal
        import logging
        
        logger = logging.getLogger(__name__)
        
        try:
            # Extract transaction data first
            trans_id = callback_data.get('TransID') or callback_data.get('TransId')
            if not trans_id:
                logger.error("No TransID in callback data")
                # Still create callback for logging
                callback = MpesaCallback.objects.create(
                    callback_type='confirmation',
                    raw_data=callback_data,
                    processed=True
                )
                return {
                    'ResultCode': '0',
                    'ResultDesc': 'No TransID provided'
                }
            
            # Check if transaction already exists and is fully processed
            existing_transaction = MpesaTransaction.objects.filter(trans_id=trans_id).first()
            if existing_transaction and existing_transaction.repayment:
                # Transaction already processed successfully
                logger.info(f"Transaction {trans_id} already processed with repayment {existing_transaction.repayment.id}")
                return {
                    'ResultCode': '0',
                    'ResultDesc': 'Already processed'
                }
            
            # Check for existing callback by looking for callbacks linked to this transaction
            callback = None
            if existing_transaction:
                # Use the reverse relation (default is mpesacallback_set)
                callback = existing_transaction.mpesacallback_set.filter(callback_type='confirmation').first()
            
            # Create callback log if it doesn't exist
            if not callback:
                callback = MpesaCallback.objects.create(
                    callback_type='confirmation',
                    raw_data=callback_data,
                    processed=False
                )
            elif callback.processed and existing_transaction and not existing_transaction.repayment:
                # Callback was marked processed but payment wasn't actually processed
                # Reset it so we can retry
                logger.warning(f"Callback was marked processed but no repayment exists. Resetting for retry.")
                callback.processed = False
                callback.save()
            elif callback.processed:
                logger.info(f"Callback already processed for TransID: {trans_id}")
                return {
                    'ResultCode': '0',
                    'ResultDesc': 'Already processed'
                }
            
            # Handle phone number - truncate if hashed/too long
            msisdn = callback_data.get('MSISDN', '')
            phone_truncated = msisdn[:17] if msisdn and len(msisdn) > 17 else msisdn
            
            # Convert amount to Decimal
            trans_amount = callback_data.get('TransAmount', 0)
            if isinstance(trans_amount, (int, float)):
                trans_amount = Decimal(str(trans_amount))
            elif isinstance(trans_amount, str):
                trans_amount = Decimal(trans_amount)
            
            # Get or create transaction
            transaction, transaction_created = MpesaTransaction.objects.get_or_create(
                trans_id=trans_id,
                defaults={
                    'transaction_type': callback_data.get('TransactionType', 'Pay Bill'),
                    'amount': trans_amount,
                    'phone_number': phone_truncated,
                    'msisdn': phone_truncated,
                    'trans_time': callback_data.get('TransTime'),
                    'business_short_code': callback_data.get('BusinessShortCode'),
                    'bill_ref_number': callback_data.get('BillRefNumber'),
                    'invoice_number': callback_data.get('InvoiceNumber'),
                    'org_account_balance': callback_data.get('OrgAccountBalance'),
                    'third_party_trans_id': callback_data.get('ThirdPartyTransID'),
                    'first_name': callback_data.get('FirstName'),
                    'middle_name': callback_data.get('MiddleName'),
                    'last_name': callback_data.get('LastName'),
                    'raw_confirmation_data': callback_data,
                    'status': 'confirmed',
                    'is_automatic': True,
                    'payment_source': 'automatic'
                }
            )
            
            # Update existing transaction if needed
            if not transaction_created:
                transaction.raw_confirmation_data = callback_data
                if callback_data.get('OrgAccountBalance'):
                    transaction.org_account_balance = callback_data.get('OrgAccountBalance')
                if transaction.status != 'processed':
                    transaction.status = 'confirmed'
                transaction.save()
            
            # Link callback to transaction
            callback.transaction = transaction
            callback.save()
            
            # Process the payment (always attempt to process)
            payment_processed = False
            try:
                logger.info(f"Attempting to process payment for transaction {trans_id}")
                logger.info(f"  Bill Ref (ID): {transaction.bill_ref_number}")
                logger.info(f"  Amount: {transaction.amount}")
                logger.info(f"  Phone: {transaction.msisdn or transaction.phone_number}")
                
                success = transaction.process_payment()
                transaction.refresh_from_db()
                
                if success:
                    # Verify repayment exists
                    try:
                        repayment = transaction.repayment
                        if repayment:
                            logger.info(f"Payment processed successfully for transaction {trans_id}, repayment: {repayment.id} ({repayment.receipt_number})")
                            payment_processed = True
                        else:
                            logger.warning(f"Payment processing returned True but no repayment linked for transaction {trans_id}")
                            logger.warning(f"Transaction status: {transaction.status}")
                            logger.warning(f"Processing notes: {transaction.processing_notes}")
                    except Exception as e:
                        logger.error(f"Error accessing repayment for transaction {trans_id}: {str(e)}")
                        logger.warning(f"Payment processing returned True but repayment does not exist")
                else:
                    logger.warning(f"Payment processing returned False for transaction {trans_id}")
                    logger.warning(f"Transaction status: {transaction.status}")
                    if transaction.processing_notes:
                        logger.warning(f"Processing notes: {transaction.processing_notes}")
            except Exception as e:
                logger.error(f"Error processing payment for transaction {trans_id}: {str(e)}", exc_info=True)
                # Don't mark as processed if there was an exception
                transaction.refresh_from_db()
                if not transaction.processing_notes:
                    transaction.processing_notes = f"Exception during processing: {str(e)}"
                    transaction.save()
            
            # Always acknowledge the callback to M-Pesa (they require this)
            response = {
                'ResultCode': '0',
                'ResultDesc': 'Success' if payment_processed else 'Received'
            }
            
            callback.response_sent = response
            
            # Only mark callback as processed if payment was actually successfully processed
            if payment_processed:
                callback.processed = True
                logger.info(f"Callback marked as processed for transaction {trans_id}")
            else:
                # Don't mark as processed - allows retry on next callback or manual processing
                logger.warning(f"Callback NOT marked as processed for transaction {trans_id} - payment not successfully processed")
                callback.processed = False
            
            callback.save()
            
            return response
            
        except Exception as e:
            logger.error(f"Error processing M-Pesa confirmation callback: {str(e)}", exc_info=True)
            # Always acknowledge receipt even on error
            return {
                'ResultCode': '0',
                'ResultDesc': 'Received with processing error'
            }
    
    @staticmethod
    def allocate_payment_to_multiple_loans(transaction, loans):
        """
        Allocate a single payment across multiple loans
        """
        from .models import PaymentAllocation
        from loans.models import Repayment
        from decimal import Decimal
        
        remaining_amount = transaction.amount
        allocations = []
        
        # Sort loans by priority (due date, then outstanding amount)
        sorted_loans = sorted(loans, key=lambda l: (l.due_date, -l.outstanding_amount))
        
        for loan in sorted_loans:
            if remaining_amount <= Decimal('0'):
                break
            
            outstanding = loan.outstanding_amount
            allocation_amount = min(remaining_amount, outstanding)
            
            # Create repayment for this allocation
            repayment = Repayment.objects.create(
                loan=loan,
                amount=allocation_amount,
                payment_method='mpesa',
                mpesa_transaction_id=transaction.trans_id or transaction.mpesa_transaction_id,
                mpesa_phone_number=transaction.msisdn or transaction.phone_number,
                payment_date=transaction.get_transaction_datetime()
            )
            
            # Create allocation record
            allocation = PaymentAllocation.objects.create(
                mpesa_transaction=transaction,
                loan=loan,
                allocated_amount=allocation_amount,
                repayment=repayment
            )
            
            allocations.append(allocation)
            remaining_amount -= allocation_amount
        
        return allocations