"""
SasaPay Payment Service
Mirrors the old PHP integration in:
  - application/include/sapapaystkpush.php        (C2B STK push)
  - application/include/sasapayfundsdisbursement.php (B2C disbursement)
  - SasaPayIPN.php                                (IPN callback handler)

Credentials from old system:
  client_id:     e63utew7Vk3RmwKxjuMY3jocycFb5eoMZkzyKrPQ
  client_secret: 6ysF7fWBESaCNYfsh4aJOWxijTFx4JFIu5LqGFWnMx7TJxKiwaysaiNwe14Wgj9FupB6fhtRFZUas2geZZLjHwbHiShbX8w5bvWjy061Bq1SHX9EQtsBCtnuTLgaOmDV
  merchant_code: 1122
  network_code:  63902  (M-Pesa)
"""

import requests
import base64
import logging
from datetime import datetime, timedelta
from django.conf import settings
from django.utils import timezone

logger = logging.getLogger(__name__)

SASAPAY_BASE_URL = 'https://api.sasapay.app/api/v1'
TOKEN_URL = f'{SASAPAY_BASE_URL}/auth/token/?grant_type=client_credentials'
STK_PUSH_URL = f'{SASAPAY_BASE_URL}/payments/request-payment/'
B2C_URL = f'{SASAPAY_BASE_URL}/payments/b2c/'


def _get_credentials():
    return {
        'client_id': getattr(settings, 'SASAPAY_CLIENT_ID',
                             'e63utew7Vk3RmwKxjuMY3jocycFb5eoMZkzyKrPQ'),
        'client_secret': getattr(settings, 'SASAPAY_CLIENT_SECRET',
                                 '6ysF7fWBESaCNYfsh4aJOWxijTFx4JFIu5LqGFWnMx7TJxKiwaysaiNwe14Wgj9FupB6fhtRFZUas2geZZLjHwbHiShbX8w5bvWjy061Bq1SHX9EQtsBCtnuTLgaOmDV'),
        'merchant_code': getattr(settings, 'SASAPAY_MERCHANT_CODE', '1122'),
        'network_code': getattr(settings, 'SASAPAY_NETWORK_CODE', '63902'),
    }


def _get_base_url():
    """Return the base callback URL for this deployment."""
    return getattr(settings, 'SITE_URL', 'https://uzuriapps.xyz')


# ---------------------------------------------------------------------------
# Token management (simple in-memory cache; good enough for single-process)
# ---------------------------------------------------------------------------
_token_cache = {'token': None, 'expires_at': None}


def get_access_token(force_refresh=False):
    """
    Obtain a SasaPay OAuth2 access token using client credentials.
    Caches the token until it expires.
    """
    now = datetime.utcnow()
    if (not force_refresh
            and _token_cache['token']
            and _token_cache['expires_at']
            and now < _token_cache['expires_at']):
        return _token_cache['token']

    creds = _get_credentials()
    auth_header = base64.b64encode(
        f"{creds['client_id']}:{creds['client_secret']}".encode()
    ).decode()

    headers = {'Authorization': f'Basic {auth_header}'}
    try:
        resp = requests.get(TOKEN_URL, headers=headers, timeout=30)
        resp.raise_for_status()
        data = resp.json()
        token = data.get('access_token')
        if not token:
            raise ValueError(f"No access_token in response: {data}")

        # SasaPay tokens typically expire in 3600 s; cache with 60 s buffer
        expires_in = int(data.get('expires_in', 3600))
        _token_cache['token'] = token
        _token_cache['expires_at'] = now + timedelta(seconds=expires_in - 60)
        logger.info("SasaPay access token obtained successfully.")
        return token
    except Exception as exc:
        logger.error(f"Failed to get SasaPay access token: {exc}")
        raise


# ---------------------------------------------------------------------------
# C2B STK Push  (request payment from customer)
# ---------------------------------------------------------------------------

def initiate_stk_push(phone_number, amount, account_reference,
                       transaction_desc='Loan Payment', callback_url=None):
    """
    Send an STK push to a customer's phone via SasaPay.

    Args:
        phone_number (str): Customer phone in 2547xxxxxxxx format.
        amount (float|Decimal): Amount to collect.
        account_reference (str): Loan account number / reference.
        transaction_desc (str): Short description shown on the phone.
        callback_url (str): Override the default callback URL.

    Returns:
        dict: SasaPay API response.
    """
    creds = _get_credentials()
    token = get_access_token()

    if callback_url is None:
        callback_url = f"{_get_base_url()}/payments/sasapay/stk-callback/"

    # Normalise phone: 07xx → 2547xx
    phone = str(phone_number).strip()
    if phone.startswith('+'):
        phone = phone[1:]
    if phone.startswith('0'):
        phone = '254' + phone[1:]

    payload = {
        'MerchantCode': creds['merchant_code'],
        'NetworkCode': creds['network_code'],
        'TransactionFee': '0',
        'Currency': 'KES',
        'Amount': str(amount),
        'CallBackURL': callback_url,
        'PhoneNumber': phone,
        'TransactionDesc': transaction_desc,
        'AccountReference': account_reference,
    }

    headers = {
        'Authorization': f'Bearer {token}',
        'Content-Type': 'application/json',
    }

    try:
        resp = requests.post(STK_PUSH_URL, json=payload, headers=headers, timeout=30)
        resp.raise_for_status()
        data = resp.json()
        logger.info(f"STK push initiated for {phone}, ref={account_reference}: {data}")
        return data
    except Exception as exc:
        logger.error(f"STK push failed for {phone}: {exc}")
        raise


# ---------------------------------------------------------------------------
# B2C Disbursement  (send money to customer)
# ---------------------------------------------------------------------------

def disburse_loan(phone_number, amount, loan_reference, callback_url=None):
    """
    Disburse loan funds to a borrower via SasaPay B2C.

    Args:
        phone_number (str): Recipient phone in 2547xxxxxxxx format.
        amount (float|Decimal): Amount to disburse.
        loan_reference (str): Loan account number used as merchant transaction ref.
        callback_url (str): Override the default callback URL.

    Returns:
        dict: SasaPay API response.
    """
    creds = _get_credentials()
    token = get_access_token()

    if callback_url is None:
        callback_url = f"{_get_base_url()}/payments/sasapay/disbursement-callback/"

    # Normalise phone
    phone = str(phone_number).strip()
    if phone.startswith('+'):
        phone = phone[1:]
    if phone.startswith('0'):
        phone = '254' + phone[1:]

    payload = {
        'MerchantCode': creds['merchant_code'],
        'MerchantTransactionReference': loan_reference,
        'Amount': str(amount),
        'Currency': 'KES',
        'ReceiverNumber': phone,
        'Channel': creds['network_code'],
        'Reason': 'Loan disbursement',
        'CallBackURL': callback_url,
    }

    headers = {
        'Authorization': f'Bearer {token}',
        'Content-Type': 'application/json',
    }

    try:
        resp = requests.post(B2C_URL, json=payload, headers=headers, timeout=30)
        resp.raise_for_status()
        data = resp.json()
        logger.info(f"B2C disbursement initiated for {phone}, ref={loan_reference}: {data}")
        return data
    except Exception as exc:
        logger.error(f"B2C disbursement failed for {phone}: {exc}")
        raise


# ---------------------------------------------------------------------------
# IPN / Callback processors
# ---------------------------------------------------------------------------

def process_ipn_callback(data):
    """
    Process an incoming SasaPay IPN (C2B payment notification).
    Mirrors SasaPayIPN.php logic.

    Expected fields in `data`:
        MerchantCode, BusinessShortCode, InvoiceNumber, PaymentMethod,
        TransID, ThirdPartyTransID (M-Pesa ref), FullName, FirstName,
        MiddleName, LastName, TransactionType, MSISDN, OrgAccountBalance,
        TransAmount, BillRefNumber

    Returns:
        dict: {'success': bool, 'message': str, ...}
    """
    from decimal import Decimal

    trans_id = data.get('TransID', '')
    mpesa_ref = data.get('ThirdPartyTransID', '')
    full_name = data.get('FullName', '')
    first_name = data.get('FirstName', '')
    last_name = data.get('LastName', '')
    msisdn = data.get('MSISDN', '')
    trans_amount = Decimal(str(data.get('TransAmount', '0')))
    bill_ref = data.get('BillRefNumber', '')
    trans_time = datetime.now()

    logger.info(
        f"SasaPay IPN received: TransID={trans_id}, Amount={trans_amount}, "
        f"BillRef={bill_ref}, MSISDN={msisdn}"
    )

    # Save raw IPN log
    _save_sasapay_ipn_log(data)

    # Try to match borrower by phone (BillRefNumber = customer phone in old system)
    from users.models import CustomUser
    borrower = None

    # 1. Try BillRefNumber as a loan account number first (most precise)
    from loans.models import Loan
    try:
        loan_by_ref = Loan.active_objects.get(loan_number=bill_ref, status='active')
        borrower = loan_by_ref.borrower
        # Use this specific loan directly — skip the "find active loan" step below
        _matched_loan = loan_by_ref
    except (Loan.DoesNotExist, Loan.MultipleObjectsReturned):
        _matched_loan = None

    # 2. Try BillRefNumber as a phone number
    if borrower is None:
        phone_variants = _phone_variants(bill_ref)
        for variant in phone_variants:
            try:
                borrower = CustomUser.objects.get(phone_number=variant, role='borrower')
                _matched_loan = None  # will find loan below
                break
            except CustomUser.DoesNotExist:
                continue

    # 3. Try MSISDN (the paying phone) as a fallback
    if borrower is None:
        for variant in _phone_variants(msisdn):
            try:
                borrower = CustomUser.objects.get(phone_number=variant, role='borrower')
                _matched_loan = None
                break
            except CustomUser.DoesNotExist:
                continue

    if borrower is None:
        # Unknown payment — log it and notify admins
        logger.warning(
            f"SasaPay IPN: No borrower found for BillRef={bill_ref}. "
            f"Saving as unknown payment."
        )
        _save_unknown_sasapay_payment(
            amount=trans_amount, paid_by=full_name,
            msisdn=msisdn, bill_ref=bill_ref, reference=mpesa_ref,
        )
        # Send SMS notification
        try:
            from .sms_service import send_unknown_payment_sms
            customer_phone = _normalise_phone(msisdn)
            send_unknown_payment_sms(full_name, bill_ref, trans_amount, mpesa_ref, customer_phone)
        except Exception as sms_exc:
            logger.error(f"SMS failed for unknown payment: {sms_exc}")
        return {'success': False, 'message': 'Unknown payment — no matching borrower'}

    # Find the borrower's active loan
    from loans.models import Loan, Repayment
    if _matched_loan:
        loan = _matched_loan
    else:
        try:
            loan = Loan.active_objects.filter(
                borrower=borrower, status='active'
            ).order_by('-created_at').first()
        except Exception:
            loan = None

    if loan is None:
        logger.warning(f"SasaPay IPN: Borrower {borrower} has no active loan.")
        _save_unknown_sasapay_payment(
            amount=trans_amount, paid_by=full_name,
            msisdn=msisdn, bill_ref=bill_ref, reference=mpesa_ref,
        )
        return {'success': False, 'message': 'No active loan for borrower'}

    # Create a repayment record
    try:
        repayment = Repayment.objects.create(
            loan=loan,
            amount=trans_amount,
            payment_method='mpesa',       # 'mpesa' is the valid choice; SasaPay routes via M-Pesa
            payment_source='automatic',
            mpesa_transaction_id=trans_id,
            mpesa_phone_number=_normalise_phone(msisdn)[:17],
            payment_date=trans_time,
        )
        logger.info(f"Repayment {repayment.receipt_number} created for loan {loan.loan_number}")
    except Exception as exc:
        logger.error(f"Failed to create repayment: {exc}")
        return {'success': False, 'message': str(exc)}

    # Send payment confirmation SMS
    try:
        from .sms_service import send_payment_confirmation_sms
        # Use outstanding_amount (the correct property on the Loan model)
        try:
            new_balance = float(loan.outstanding_amount)
        except Exception:
            new_balance = 0.0
        customer_phone = _normalise_phone(msisdn)
        borrower_name = f"{first_name} {last_name}".strip() or full_name
        send_payment_confirmation_sms(
            borrower_name=borrower_name,
            amount=float(trans_amount),
            reference=mpesa_ref or trans_id,
            loan_number=loan.loan_number,
            new_balance=new_balance,
            customer_phone=customer_phone,
        )
    except Exception as sms_exc:
        logger.error(f"SMS failed after SasaPay IPN: {sms_exc}")

    return {
        'success': True,
        'message': 'Payment processed',
        'receipt_number': repayment.receipt_number,
        'loan_number': loan.loan_number,
    }


def process_stk_callback(data):
    """
    Process the STK push callback from SasaPay.
    Mirrors sasapaystkpushprocessor.php.

    Expected fields:
        CheckoutRequestID, ResultCode, ResultDesc, SourceChannel,
        TransAmount, TransactionDate
    """
    checkout_id = data.get('CheckoutRequestID', '')
    result_code = data.get('ResultCode', '')
    result_desc = data.get('ResultDesc', '')
    trans_amount = data.get('TransAmount', 0)

    logger.info(
        f"SasaPay STK callback: CheckoutID={checkout_id}, "
        f"ResultCode={result_code}, Desc={result_desc}"
    )
    _save_stk_result(checkout_id, result_code, result_desc, trans_amount)
    return {'success': result_code == '0', 'result_desc': result_desc}


def process_disbursement_callback(data):
    """
    Process the B2C disbursement callback from SasaPay.
    Mirrors sasapayfundsdisbursementprocessor.php.

    Expected fields:
        SasaPayTransactionID, ResultCode, ResultDesc, SourceChannel,
        ThirdPartyTransactionCode, TransactionAmount, MerchantAccountBalance,
        MerchantTransactionReference, TransactionDate
    """
    sasapay_tx_id = data.get('SasaPayTransactionID', '')
    result_code = data.get('ResultCode', '')
    result_desc = data.get('ResultDesc', '')
    mpesa_ref = data.get('ThirdPartyTransactionCode', '')
    amount = data.get('TransactionAmount', 0)
    balance = data.get('MerchantAccountBalance', 0)
    loan_ref = data.get('MerchantTransactionReference', '')

    logger.info(
        f"SasaPay disbursement callback: TxID={sasapay_tx_id}, "
        f"ResultCode={result_code}, LoanRef={loan_ref}"
    )
    _save_disbursement_result(
        loan_ref, sasapay_tx_id, mpesa_ref,
        result_code, result_desc, amount, balance,
    )

    if result_code == '0':
        # Update loan disbursement status
        try:
            from loans.models import Loan
            loan = Loan.active_objects.filter(loan_number=loan_ref).first()
            if loan and loan.status == 'approved':
                loan.status = 'active'
                loan.save(update_fields=['status'])
                logger.info(f"Loan {loan_ref} activated after disbursement.")

                # Send disbursement SMS
                try:
                    from .sms_service import send_loan_disbursement_sms
                    phone = _normalise_phone(loan.borrower.phone_number)
                    send_loan_disbursement_sms(
                        borrower_name=loan.borrower.get_full_name(),
                        amount=float(amount),
                        loan_number=loan_ref,
                        customer_phone=phone,
                    )
                except Exception as sms_exc:
                    logger.error(f"Disbursement SMS failed: {sms_exc}")
        except Exception as exc:
            logger.error(f"Failed to update loan after disbursement: {exc}")

    return {'success': result_code == '0', 'result_desc': result_desc}


# ---------------------------------------------------------------------------
# Internal helpers
# ---------------------------------------------------------------------------

def _normalise_phone(phone):
    """Convert any Kenyan phone format to +2547xxxxxxxx."""
    phone = str(phone).strip()
    if phone.startswith('+'):
        return phone
    if phone.startswith('0'):
        return '+254' + phone[1:]
    if phone.startswith('254'):
        return '+' + phone
    return phone


def _phone_variants(phone):
    """Return multiple format variants of a phone number for DB matching."""
    phone = str(phone).strip()
    variants = [phone]
    if phone.startswith('0'):
        variants += ['+254' + phone[1:], '254' + phone[1:]]
    elif phone.startswith('+254'):
        variants += [phone[1:], '0' + phone[4:]]
    elif phone.startswith('254'):
        variants += ['+' + phone, '0' + phone[3:]]
    return variants


def _save_sasapay_ipn_log(data):
    """Persist the raw IPN payload for audit purposes."""
    try:
        SasaPayIPNLog.objects.create(raw_data=data)
    except Exception as exc:
        logger.warning(f"Could not save IPN log: {exc}")


def _save_unknown_sasapay_payment(amount, paid_by, msisdn, bill_ref, reference):
    """Save an unrecognised SasaPay payment for admin review."""
    try:
        SasaPayUnknownPayment.objects.create(
            amount=amount,
            paid_by=paid_by,
            msisdn=msisdn,
            bill_ref=bill_ref,
            reference=reference,
        )
    except Exception as exc:
        logger.warning(f"Could not save unknown payment: {exc}")


def _save_stk_result(checkout_id, result_code, result_desc, trans_amount):
    """Persist STK push result."""
    try:
        SasaPaySTKResult.objects.create(
            checkout_request_id=checkout_id,
            result_code=result_code,
            result_desc=result_desc,
            trans_amount=trans_amount,
        )
    except Exception as exc:
        logger.warning(f"Could not save STK result: {exc}")


def _save_disbursement_result(loan_ref, sasapay_tx_id, mpesa_ref,
                               result_code, result_desc, amount, balance):
    """Persist B2C disbursement result."""
    try:
        SasaPayDisbursementResult.objects.create(
            loan_reference=loan_ref,
            sasapay_transaction_id=sasapay_tx_id,
            mpesa_reference=mpesa_ref,
            result_code=result_code,
            result_desc=result_desc,
            trans_amount=amount,
            merchant_balance=balance,
        )
    except Exception as exc:
        logger.warning(f"Could not save disbursement result: {exc}")


# Lazy imports to avoid circular dependency at module load time
from payments.sasapay_models import (  # noqa: E402
    SasaPayIPNLog,
    SasaPayUnknownPayment,
    SasaPaySTKResult,
    SasaPayDisbursementResult,
)
