from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from rest_framework.permissions import IsAuthenticated
from rest_framework.throttling import ScopedRateThrottle
from django.db.models import Q
from customers.models import Customer
from savings.models import SavingAccount
from loans.models import LoanApplication
from mmbanking.models import MobileBankingSubscription
from organisations.models import Organisation, OrganisationSetting
from savings.serializers import SavingAccountSerializer
from loans.serializers import LoanApplicationSerializer
from .models import USSDDepositInitiations,USSDDepositLiquidations,LiquidationTransactions, MMServiceProvider, MMServiceProviderTarrif
from ledgers.models import SystemTransactions, OrganisationSubAccount, OrganisationBranch, InterBranchTransactions
from django.contrib.auth import get_user_model
from ledgers.ledgers_helper import *
from savings.models import SavingAccountTransactions, SavingProductCharge, SchoolFeesPaymentTransactions, SchoolFeesIntegrations, TransferTransactions
from .helpers import is_airtel_number, get_branch_wallet_chart, get_member_wallet_chart, get_basic_auth_token, consume_yo_api, get_yo_wallet_chart
from savings.savings_helper import (
    assert_savings_account_can_credit,
    assert_savings_account_can_debit,
    get_account_balance,
    mark_savings_account_for_aml_deposit,
    save_reciever_transactions,
    save_sender_transactions,
    sync_savings_account_lifecycle,
    thread_multiple_booking_payments,
    update_savings_account_statuses,
)
from ledgers.ledgers_helper import generate_reference_no
from exservices.exservices_helper import send_customer_sms
from questbanker_api.utils import get_current_user
from .serializers import *
from decouple import config
from django.utils import timezone
from django.utils.dateparse import parse_datetime
import requests
import hashlib
import time
from rest_framework import viewsets
from rest_framework.permissions import IsAuthenticated
from rest_framework.throttling import ScopedRateThrottle
from notifications.notifications_helper import *
from mmcharges.mobile_money_helper import post_mobile_money_charge
from decimal import Decimal
from general.helper import logger_to_file

# Create your views here.
class USSDBankingMemberSavingsView(APIView):
    authentication_classes = []
    permission_classes = []

    def post(self, request, format=None):
        response_status = status.HTTP_404_NOT_FOUND
        account_balances = []
        message = {
            'customer_name':'',
            'member_number':'',
            'status': 'failed',
            'count':0,
            'accounts':[],
            'message': 'Invalid Details'
        }
        organisation_id = request.data.get('organisation_id', None)
        member_number   = request.data.get('member_number', None)

        customer = Customer.objects.filter(
            Q(member_number=member_number, is_deleted=False,customer_branch__branch_organisation__id=organisation_id) |
            Q(old_member_number=member_number, is_deleted=False,customer_branch__branch_organisation__id=organisation_id)).first()

        if not organisation_id:
            message['message'] = 'Organisation id not provided'
        if not member_number:
            message['message'] = 'Member number not provided'
        if not customer:
            message['message'] = 'Customer with Member number: '+str(member_number)+' not found'
        if customer:
            message['status'] = 'success'
            message['message'] = ''
            message['customer_name'] = customer.name
            message['member_number'] = customer.member_number
            savings_accounts = SavingAccount.objects.filter(account_customer=customer).all()
            if not savings_accounts:
                message['message'] = 'Not savings found'
            else:
                accounts_list_data =  SavingAccountSerializer(savings_accounts, many=True).data
                for account_data  in accounts_list_data:
                    account_balance = account_data['account_balance']
                    account_balances.append({
                        'id': account_data['id'],
                        'account_no': account_data['product_name'] + ' - ' + account_data['account_no'],
                        'account_name': account_data['product_name'],
                        'balance': account_balance['balance'],
                        'balance_raw': account_balance['balance_raw'],
                        'balance_actual': account_balance['balance_actual'],
                        'with_held': account_balance['with_held'],
                        'blocked_amount': account_balance['blocked_amount']
                    })
            message['count']   = len(account_balances)
            message['accounts'] = account_balances

            # Include organisation branch wallet (e-wallet) balance
            try:
                branch_id = customer.customer_branch.id if getattr(customer, 'customer_branch', None) else None
                if organisation_id and branch_id:
                    # Organisation branch wallet
                    wallet_chart = get_branch_wallet_chart(organisation_id, branch_id)
                    if wallet_chart:
                        wallet_balances = get_chart_of_account_balance_at(wallet_chart, branch_id)
                        message['organisation_wallet'] = {
                            'id': wallet_chart.id,
                            'account_code': wallet_chart.account_code,
                            'account_name': wallet_chart.account_name,
                            'balance': wallet_balances.get('balance', 0),
                            'balance_raw': wallet_balances.get('balance_raw', 0)
                        }

                    # Member wallet nested under branch wallet
                    member_wallet_chart = get_member_wallet_chart(organisation_id, branch_id, customer.id)
                    if member_wallet_chart:
                        member_wallet_balances = get_chart_of_account_balance_at(member_wallet_chart, branch_id)
                        message['member_wallet'] = {
                            'id': member_wallet_chart.id,
                            'account_code': member_wallet_chart.account_code,
                            'account_name': member_wallet_chart.account_name,
                            'balance': member_wallet_balances.get('balance', 0),
                            'balance_raw': member_wallet_balances.get('balance_raw', 0)
                        }
            except Exception:
                # If anything goes wrong, omit wallet info silently for USSD resilience
                message['organisation_wallet'] = message.get('organisation_wallet', {})
                message['member_wallet'] = message.get('member_wallet', {})
            response_status = status.HTTP_200_OK

        return Response(message, response_status)
    

class USSDBankingMemberLoansView(APIView):
    authentication_classes = []
    permission_classes = []

    def post(self, request, format=None):
        response_status = status.HTTP_404_NOT_FOUND
        loan_list = []
        message = {
            'customer_name':'',
            'member_number':'',
            'status': 'failed',
            'count':0,
            'loans':[],
            'message': 'Invalid Details'
        }
        organisation_id = request.data.get('organisation_id', None)
        member_number   = request.data.get('member_number', None)

        customer = Customer.objects.filter(
            Q(member_number=member_number, is_deleted=False,customer_branch__branch_organisation__id=organisation_id) |
            Q(old_member_number=member_number, is_deleted=False,customer_branch__branch_organisation__id=organisation_id)).first()

        if not organisation_id:
            message['message'] = 'Organisation id not provided'
        if not member_number:
            message['message'] = 'Member number not provided'
        if not customer:
            message['message'] = 'Customer with Member number: '+str(member_number)+' not found'
        if customer:
            message['status'] = 'success'
            message['message'] = ''
            message['customer_name'] = customer.name
            message['member_number'] = customer.member_number
            loans = LoanApplication.objects.filter(customer=customer).all()
            if not loans:
                message['message'] = 'Not loans found'
            else:
                loans_list_data =  LoanApplicationSerializer(loans, many=True).data
                for loans_data  in loans_list_data:
                    loan_balance = loans_data['loan_balance']
                    loan_due_balance = loans_data['loan_schedule_due_data']
                    loan_list.append({
                        'status': loans_data['status'],
                        'loan_amount':loans_data['loan_amount'],
                        'principal_bal': loan_balance['principal_bal'],
                        'interest_bal': loan_balance['interest_bal'],
                        'penalty_bal': loan_balance['penalty_bal'],
                        'total_with_penalty': loan_balance['total_with_penalty'],
                        'princ_due': loan_due_balance['princ_due'],
                        'interest_due': loan_due_balance['interest_due'],
                        'penalty_due': loan_due_balance['penalty_due'],
                        'total_due': loan_due_balance['total_due'],
                        'officer': loans_data['loan_officer_full_name'],
                        'product_name': loans_data['loan_application_product_data']['product_name']
                    })
            message['count']   = len(loan_list)
            message['loans'] = loan_list
            response_status = status.HTTP_200_OK
        return Response(message, response_status)
    
class USSDBankingVerifyNumberView(APIView):
    authentication_classes = []
    permission_classes = []

    def post(self, request, format=None):
        response_status = status.HTTP_404_NOT_FOUND
        message = {
            'status': 'failed'
        }
        number = request.data.get('number', None)
        if number:
            telephone_no = '0' + number[-9:]
            subscription = MobileBankingSubscription.objects.filter(Q(telephone_no=telephone_no) | Q(customer__member_number=number) | Q(customer__old_member_number=number)).first()
            if subscription:
                message = {
                    'status': 'success'
                }
                response_status = status.HTTP_200_OK

        return Response(message, response_status)
    

class UssdBankingProcessLiquidationViewSet(viewsets.ModelViewSet):
    serializer_class = USSDDepositLiquidationsSerializer

    def _get_approved_liquidation_transactions(self, organisation_id, branch=None, start_date=None, end_date=None):
        filter_query = {}

        if branch:
            e_wallet_chart = get_branch_wallet_chart(organisation_id, branch)
            if not e_wallet_chart:
                return SystemTransactions.objects.none()
            filter_query['credit_chart'] = e_wallet_chart
            filter_query['branch_id'] = branch
        else:
            parent_chart = OrganisationSubAccount.objects.filter(
                account_code='sys-11418',
                account_organisation_id=organisation_id
            ).first()
            if not parent_chart:
                return SystemTransactions.objects.none()
            filter_query['credit_chart__parent_id'] = parent_chart

        if start_date and end_date:
            filter_query['date_added__range'] = (start_date, end_date)
        elif start_date:
            filter_query['date_added__gte'] = start_date
        elif end_date:
            filter_query['date_added__lte'] = end_date

        return SystemTransactions.objects.filter(**filter_query).order_by('-id')

    def _get_pending_liquidation_requests(
        self,
        organisation_id,
        branch=None,
        start_date=None,
        end_date=None,
        status=None,
    ):
        filter_query = {'organisation_id': organisation_id}

        if branch:
            filter_query['request_added_by__user_organisation_branch__id'] = branch
        if start_date:
            filter_query['transaction_date__gte'] = start_date
        if end_date:
            filter_query['transaction_date__lte'] = end_date
        if status in ['pending', 'canceled']:
            filter_query['status'] = status
        else:
            filter_query['status__in'] = ['pending', 'canceled']

        return (
            USSDDepositLiquidations.objects.select_related(
                'request_added_by',
                'request_added_by__user_staff',
                'request_added_by__user_organisation_branch',
                'selected_account',
                'approved_by',
                'organisation',
            )
            .filter(**filter_query)
            .order_by('-id')
        )

    def _liquidation_sort_key(self, item):
        parsed_date = parse_datetime(item.get('transaction_date')) if item.get('transaction_date') else None
        if parsed_date is None:
            return 0
        if timezone.is_naive(parsed_date):
            parsed_date = timezone.make_aware(parsed_date, timezone.get_current_timezone())
        return parsed_date.timestamp()

    def get_queryset(self):
        filter_query = {}
        organisation_id = get_current_user(self.request, 'organisation_id', None)
        status = (self.request.query_params.get('status', '') or '').lower()
        branch = self.request.query_params.get('branch', None)
        start_date = self.request.query_params.get('start', None)
        end_date = self.request.query_params.get('end', None)

        if not organisation_id:
            return USSDDepositLiquidations.objects.none()

        if organisation_id:
            filter_query['organisation_id'] = organisation_id
        if branch:
            filter_query['request_added_by__user_organisation_branch__id'] = branch
        if start_date:
            filter_query['transaction_date__gte'] = start_date
        if end_date:
            filter_query['transaction_date__lte'] = end_date
        if status in ['pending', 'canceled', 'cancelled', 'approved']:
            if status == 'cancelled':
                status = 'canceled'
            filter_query['status'] = status
            
        requests = (
            USSDDepositLiquidations.objects.select_related(
                'request_added_by',
                'request_added_by__user_staff',
                'request_added_by__user_organisation_branch',
                'selected_account',
                'approved_by',
                'organisation',
            )
            .filter(**filter_query)
            .order_by('-id')
        )
        return requests

    def list(self, request, *args, **kwargs):
        organisation_id = get_current_user(request, 'organisation_id', None)
        if not organisation_id:
            return Response({'count': 0, 'data': [], 'total_approved_amount': 0})

        branch = request.query_params.get('branch', None)
        start_date = request.query_params.get('start', None)
        end_date = request.query_params.get('end', None)
        status = (request.query_params.get('status', '') or '').lower()
        if status == 'cancelled':
            status = 'canceled'

        approved_transactions = self._get_approved_liquidation_transactions(
            organisation_id,
            branch=branch,
            start_date=start_date,
            end_date=end_date,
        )
        approved_data = USSDLiquidationTransactionsSerializer(
            approved_transactions,
            many=True,
        ).data

        if status == 'approved':
            data = approved_data
        else:
            pending_requests = self._get_pending_liquidation_requests(
                organisation_id,
                branch=branch,
                start_date=start_date,
                end_date=end_date,
                status=status if status in ['pending', 'canceled'] else None,
            )
            pending_data = USSDDepositLiquidationsSerializer(
                pending_requests,
                many=True,
            ).data

            if status in ['pending', 'canceled']:
                data = pending_data
            else:
                data = approved_data + pending_data
                data.sort(key=self._liquidation_sort_key, reverse=True)

        total_approved_amount = sum(
            float(item.get('approved_amount') or 0)
            for item in approved_data
        )
        response_data = {
            'count': len(data),
            'data': data,
            'total_approved_amount': total_approved_amount
        }
        return Response(response_data)
    
    def perform_create(self, serializer):
        organisation_id = get_current_user(self.request, 'organisation_id', None)
        organisation = Organisation.objects.get(pk=organisation_id)
        serializer.save( organisation=organisation, request_added_by=self.request.user)

    def perform_update(self,serializer):
        organisation_id = get_current_user(self.request, 'organisation_id', None)
        status = self.request.data.get('status', None)
        updated_instance = serializer.save(over_draft_last_updated_by=self.request.user,approved_by=self.request.user)
        if status == 'approved':
            if updated_instance and updated_instance.status == 'approved':
                organisation = Organisation.objects.filter(pk=organisation_id).first()
                reference_no = generate_reference_no(updated_instance.selected_account.account_line, updated_instance.organisation.id)
                heading      = 'Liquidation Request for ' + updated_instance.request_added_by.user_organisation_branch.branch_organisation.name
                e_wallet_chart = get_branch_wallet_chart(organisation.id, updated_instance.request_added_by.user_organisation_branch_id)
                if e_wallet_chart:
                    saved_transaction = SystemTransactions.objects.create(
                        amount=updated_instance.approved_amount,
                        heading=heading, 
                        reference_no=reference_no,
                        payment_method=updated_instance.payment_method,
                        voucher_no='',
                        debit_chart=updated_instance.selected_account,
                        credit_chart=e_wallet_chart,
                        branch=updated_instance.request_added_by.user_organisation_branch,
                        added_by=updated_instance.request_added_by)
                    if saved_transaction:
                        LiquidationTransactions.objects.create(request=updated_instance,transaction=saved_transaction)
                        updated_instance.save()

    
class UssdBankingInitiateDepositView(APIView):
    authentication_classes = []
    permission_classes = []
    search_fields    = ['branch',]
   
    def get(self, request, format=None):
        organisation_id = request.query_params.get('org_id', None)
        branch = request.query_params.get('branch', None)
        start_date = request.query_params.get('start', None)
        end_date = request.query_params.get('end', None)

        if not organisation_id:
            return Response({'count': 0, 'results': [], 'total_deposits': 0})

        # Get e-wallet chart(s) - Deposits = money COMING INTO e-wallet (e-wallet is DEBITED)
        filter_query = {}
        
        if branch:
            e_wallet_chart = get_branch_wallet_chart(organisation_id, branch)
            if e_wallet_chart:
                filter_query['debit_chart'] = e_wallet_chart
            else:
                return Response({'count': 0, 'results': [], 'total_deposits': 0})
        else:
            # Get all e-wallet charts for the organisation (parent code sys-11418)
            parent_chart = OrganisationSubAccount.objects.filter(
                account_code='sys-11418',
                account_organisation_id=organisation_id
            ).first()
            if parent_chart:
                filter_query['debit_chart__parent_id'] = parent_chart
            else:
                return Response({'count': 0, 'results': [], 'total_deposits': 0})

        if start_date and end_date:
            filter_query['date_added__range'] = (start_date, end_date)

        # Get transactions that debit e-wallet (money coming in - credit from another account, debit to e-wallet means deposit)
        deposits = SystemTransactions.objects.filter(**filter_query).order_by('-id')
        total_deposits = sum(float(tx.amount) for tx in deposits)

        from .serializers import USSDDepositTransactionsSerializer
        serializer = USSDDepositTransactionsSerializer(deposits, many=True)
        response_data = {
            'count': len(deposits),
            'results': serializer.data,
            'total_deposits': total_deposits,
        }

        return Response(response_data)

    def post(self, request, format=None):

        response_status = status.HTTP_202_ACCEPTED
        message = {
            'status': 'failed'
        }
        
        account_id = request.data.get('id', None)
        amount = request.data.get('amount', None)
        telephone = request.data.get('telephone', None)
        member_number = request.data.get('member_number', None)
        organisation_id = request.data.get('organisation_id', None)
        telephone = telephone[-9:]

        mm_provider_obj = OrganisationSetting.objects.filter(org_setting__id=organisation_id,setting_key='mm_provider').first()
        mm_status_obj   = OrganisationSetting.objects.filter(org_setting__id=organisation_id,setting_key='mm_status',setting_value='active').first()
        
        if not mm_provider_obj:
            message = {'status':'failed','message':'No selected service provider'}
            return Response(message, response_status)

        if not mm_status_obj:
            message = {'status':'failed','message':'Mobile Banking services currently disabled.'}
            return Response(message, response_status)
        
        aggregator = MMServiceProvider.objects.filter(id=mm_provider_obj.setting_value,status='active',deposit='active').first()
        if not aggregator:
            message = {'status':'failed','message':'No selected service provider'}
            return Response(message, response_status)

        if float(amount) > 0 and len(telephone) > 8 and account_id and organisation_id and member_number:
            amount = float(amount)
            transaction_id = hashlib.md5((telephone + str(time.time())).encode()).hexdigest()
            
            #Select MM Service Provider
            initiation_status = 'not-supported'
            charge = 0
            if aggregator.unique_identifier == 'airtel' and is_airtel_number(telephone):
                charge = round(amount * (100 / 98)) - amount
                data = {
                    "payment": {
                        "payer_number": telephone,
                        "amount": amount + charge,
                        "callback_url": config('PAYMENT_GATEWAY_CALLBACK_URL'),
                        "source": "QuestBanker",
                        "payment_code": transaction_id
                    }
                }

                basic_token = get_basic_auth_token()
                response = requests.post(
                    config('PAYMENT_GATEWAY_URL') + 'akello-pay/token-auth',
                    json = {"grant_type": "client_credentials"},
                    headers = {
                        "Content-Type": "application/x-www-form-urlencoded",
                        "Authorization": f"Basic {basic_token}"
                    }
                )

                if response.status_code == 200:
                    response_data = response.json()
                    response = requests.post(
                        config('PAYMENT_GATEWAY_URL') + 'api/akello-pay/airtel/requesttopay/',
                        json = data,
                        headers = {
                            "Content-Type": "application/json",
                            "Authorization": f"Bearer {response_data['access_token']}"
                        }
                    )
                    if response.status_code == 200:
                        response_data = response.json()
                        initiation_status = 'pending' if response_data['data']['transaction']['status'] == 'Success.' else 'failed'
            
            elif aggregator.unique_identifier == 'flexi_pay':

                data = {
                    "payment": {
                        "payer_number": telephone,
                        "amount": int(amount),
                        "callback_url": config('PAYMENT_GATEWAY_CALLBACK_URL'),
                        "source": "QBCore",
                        "payment_code": transaction_id,
                        "source_system":"MM"
                    }
                }

                basic_token = get_basic_auth_token()
                response = requests.post(
                    config('PAYMENT_GATEWAY_URL') + 'akello-pay/token-auth',
                    json = {"grant_type": "client_credentials"},
                    headers = {
                        "Content-Type": "application/x-www-form-urlencoded",
                        "Authorization": f"Basic {basic_token}"
                    }
                )

                if response.status_code == 200:
                    response_data = response.json()
                    response = requests.post(
                        config('PAYMENT_GATEWAY_URL') + 'api/akello-pay/flexipay/request-to-pay/',
                        json = data,
                        headers = {
                            "Content-Type": "application/json",
                            "Authorization": f"Bearer {response_data['access_token']}"
                        }
                    )
                    if response.status_code == 200:
                        response_data = response.json()
                        initiation_status = 'pending' if response_data['response']['statusCode'] == '00' else 'failed'

            else:
                try:
                    charge = 0
                    # Get Aggregator Tarrif Mark Up
                    charge_obj = MMServiceProviderTarrif.objects.filter(provider=aggregator,transaction_type='deposit_charge',min_value__lte=float(amount),max_value__gte=float(amount)).first()
                    if charge_obj:
                        if charge_obj.charge_type == 'flat':
                            charge = charge_obj.charge
                        else:
                            charge = 0
                    data = {
                        "payment": {
                            "payer_number": telephone,
                            "amount": amount + charge,
                            "callback_url": config('PAYMENT_GATEWAY_CALLBACK_URL'),
                            "source": "QBCore",
                            "payment_code": transaction_id,
                            "narrative": "Savings Deposit"
                        }
                    }
                    response = requests.post(
                        config('PAYMENT_GATEWAY_URL') + 'api/akello-pay/yo-ug/requesttopay/',
                        json = data,
                        headers = {
                            "Content-Type": "application/json"
                        }
                    )
                    if response.status_code == 200:
                        # Checking response status
                        response_data = response.json()
                        initiation_status = 'pending' if response_data['Status'] == 'OK' else 'failed'
                except Exception as e:
                    content = {'status': str(e)}
                    print(e)
                    return Response(content, response_status)

            initiation = USSDDepositInitiations(amount=amount, charge=charge, phone=telephone, member_number=member_number, organisation_id=organisation_id, account_id=account_id,transaction_id=transaction_id, status=initiation_status)
            initiation.save()
        
            message = {
                'status': initiation_status
            }

            save_user_notification({
                "heading":  "Mobile Banking Deposit Initiation",
                "message": f"Mobile Banking Deposit of amount {initiation.amount} has been initiated for: {Organisation.objects.get(id=initiation.organisation_id)} by {initiation.member_number} as at {initiation.date_added.date()}",
                "branch":None,
                "branch_name":None,
                "added_by":initiation.member_number,
                "last_updated_by":initiation.member_number,
                "key":"ussd_banking"
            })
        return Response(message, response_status)
    
class UssdBankingProcessDepositView(APIView):
    authentication_classes = []
    permission_classes = []

    def post(self, request, format=None):
        response_status = status.HTTP_202_ACCEPTED
        message = {'status': 'failed'}

        transaction_status = request.data.get('status', None)
        payer_number = request.data.get('payer_number', None)
        transaction_id = request.data.get('payment_unique_number', None)
        payment_provider = request.data.get('payment_provider', None)
        payer_number = payer_number[-9:]

        if payment_provider == "YO_UG":
            transaction_status = 'processed' if transaction_status == 'success' else 'declined'
        else:
            transaction_status = 'processed' if (transaction_status in ['TS', '200']) else 'declined'

        transaction = USSDDepositInitiations.objects.filter(transaction_id=transaction_id, phone=payer_number).first()
        if transaction and transaction.status == 'pending':
            customer_account = transaction.account
            organisation = Organisation.objects.filter(id=transaction.organisation_id).first()
            organisation_branch_id = customer_account.customer_branch.id

            if customer_account and transaction_status == 'processed':
                assert_savings_account_can_credit(customer_account, 'deposits')
                reference_no = generate_reference_no(customer_account.account_product.accounts_chart.account_line, organisation.id, 'mm-dep')
                e_wallet_chart = get_branch_wallet_chart(organisation.id, organisation_branch_id)
                if e_wallet_chart:
                    deposit_data = {
                        "amount": transaction.amount,
                        "heading": f"MM Deposit: by {customer_account.account_customer.name} - {payer_number}",
                        "reference_no": reference_no,
                        "payment_method": "settlement",
                        "branch_id": organisation_branch_id,
                        "debit_chart": e_wallet_chart,
                        "credit_chart": customer_account.account_product.accounts_chart
                    }
                    system_transaction = SystemTransactions.objects.create(**deposit_data)
                    save_trans = SavingAccountTransactions.objects.create(
                        transaction_type='deposit',
                        customer_account=customer_account,
                        transaction=system_transaction
                    )
                    mark_savings_account_for_aml_deposit(
                        customer_account,
                        transaction.amount,
                        transaction=system_transaction,
                    )

                    # POST MOBILE MONEY CHARGE 
                    try:
                        charge_result = post_mobile_money_charge(
                            member=customer_account.account_customer,
                            amount=Decimal(transaction.amount),
                            transaction_type='deposit',  # or 'withdrawal' if needed
                            organization=organisation,
                            customer_account=customer_account,
                            created_by=self.request.user
                        )
                        if 'charge_transaction_id' in charge_result and charge_result['charge_transaction_id']:
                            SavingAccountTransactions.objects.create(
                            transaction_type='deposit_charge',
                            customer_account=customer_account,
                            transaction_id=charge_result['charge_transaction_id'],
                            parent_id=save_trans.id  # links to main deposit
                        )

                    except Exception as e:
                        # Log failure but don't block main deposit
                        print(f"Mobile money charge failed: {e}")

                    # Notify customer
                    send_customer_sms({
                        "sms_key": "cash_deposit_sms",
                        "customer_account": customer_account,
                        "user": self.request.user,
                        "branch_id": organisation_branch_id,
                        "save_trans": save_trans,
                        "customer": customer_account.account_customer
                    })
                    sync_savings_account_lifecycle(customer_account)

                    # Process account booking payments
                    thread_multiple_booking_payments(customer_account, organisation.id, customer_account.customer_branch.id, None)

            # Update transaction status
            transaction.status = transaction_status
            transaction.save()

        message = {'status': 'processed'}
        return Response(message, response_status)



class MMServiceProviderViewSet(viewsets.ModelViewSet):
    serializer_class = MMServiceProviderSerializer

    def get_queryset(self):
       return MMServiceProvider.objects.all().order_by('name')
    
class MMServiceProviderTarrifsViewSet(viewsets.ModelViewSet):
    serializer_class = MMServiceProviderTarrifSerializer
    filter_backends = ( DjangoFilterBackend, )
    filterset_fields = ['transaction_type']

    def get_queryset(self):
        provider_id = self.request.query_params.get('provider', None)
        if provider_id:
            return MMServiceProviderTarrif.objects.filter(provider__id=provider_id).order_by('transaction_type')
        return MMServiceProviderTarrif.objects.all().order_by('transaction_type')
    
    def perform_create(self, serializer):
        min_value   = self.request.data.get('min_value', None)
        max_value   = self.request.data.get('max_value', None)
        charge_name = f'{min_value} - {max_value}'
        serializer.save(tarrif_added_by=self.request.user,charge_name=charge_name)

    def perform_update(self,serializer):
        min_value   = self.request.data.get('min_value', None)
        max_value   = self.request.data.get('max_value', None)
        charge_name = f'{min_value} - {max_value}'
        serializer.save(tarrif_last_updated_by=self.request.user,charge_name=charge_name)


class SchoolFeesSearchStudentViewSet(APIView):
    authentication_classes = []
    permission_classes = []

    def post(self, request, format=None):
        results = []
        student_number    = request.data.get('student_number', None)
        organisation_id = request.data.get('organisation_id', None)
        member_number    = request.data.get('member_number', None)

        if student_number:
            data = {
                "accountNumber": student_number
            }

            school_code = student_number[:5]
            school = SchoolFeesIntegrations.objects.filter(school_code=school_code, school_organisation__id=organisation_id).first()
            if not school:
                return Response({"results":results, "count":len(results)})
            
            # saving group
            customer_account = SavingAccount.objects.filter(
                    account_customer=school.customer,  deleted=False,status='active' ).order_by('-id').first()
            if not customer_account:
                return Response({"results":results, "count":len(results)})

            basic_token = get_basic_auth_token()
            response = requests.post(
                config('PAYMENT_GATEWAY_URL') + 'akello-pay/token-auth',
                json = {"grant_type": "client_credentials"},
                headers = {
                    "Content-Type": "application/x-www-form-urlencoded",
                    "Authorization": f"Basic {basic_token}"
                }
            )

            if response.status_code == 200:
                response_data = response.json()
                response = requests.post(
                    config('PAYMENT_GATEWAY_URL') + 'api/akello-pay/sure-pay/request-to-validate-student/',
                    json = data,
                    headers = {
                        "Content-Type": "application/json",
                        "Authorization": f"Bearer {response_data['access_token']}"
                    }
                )
                if response.status_code == 200:
                    student = response.json()
                    results.append({ "accountNumber": student['accountNumber'], "accountName": student['accountName'], "accountProvider": student['accountProvider'],
                    "outstandingBalance": student['outstandingBalance'], "accountType": student['accountType'], "customer_id": school.customer.id,
                    "customer_name": school.customer.name, "saving_account_id": customer_account.id })

        return Response({ "results": results, "count":len(results) }, status=status.HTTP_200_OK)


class MakeFeesPaymentViewSet(APIView):
    ''' Make Payment '''

    def get(self, request, format=None):
        return  Response({"message":" Method Supported"})
    
    def post(self, request, format=None):
        results = {"statusDesc": 'Error Occurred', "statusCode":'57'}
        status_code = status.HTTP_500_INTERNAL_SERVER_ERROR

        current = timezone.now()
        record_date =  current.strftime('%Y-%m-%d')

        account    = request.data.get('id', None)
        amount    = request.data.get('amount', None)
        account_number    = request.data.get('accountNumber', None)
        account_name    = request.data.get('accountName', None)
        account_provider = request.data.get('accountProvider', None)
        saving_account_id    = request.data.get('saving_account_id', None)
        
        if not account:
            return Response({"statusDesc":"No account provided", "statusCode":'57'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
    
        if not record_date:
            return Response({"statusDesc":"No date provided", "statusCode":'57'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

        if not amount:
            return Response({"statusDesc":"No amount provided", "statusCode":'57'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

        if not account_number:
            return Response({"statusDesc":"No student acc", "statusCode":'57'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

        if not account_name:
            return Response({"statusDesc":"No student name", "statusCode":'57'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

        if not account_provider:
            return Response({"statusDesc":"No School name", "statusCode":'57'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

        if not saving_account_id:
            return Response({"statusDesc":"No School Saving Account", "statusCode":'57'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

        savings_account = None
        savings_account = SavingAccount.objects.get(pk=account)
        if not savings_account:
            return Response({"statusDesc":"No account provided", "statusCode":'57'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

        account_balance = get_account_balance(savings_account)['balance_raw']
        
        if account_balance < int(amount):
            return Response({"statusDesc":"Insufficient balance", "statusCode":'57'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

        # school account
        destination_saving_account = SavingAccount.objects.filter(id=saving_account_id).first()
        if not destination_saving_account:
            return Response({"statusDesc":"No School Saving Account", "statusCode":'57'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

        organisation_id = savings_account.customer_branch.branch_organisation.id
        branch_id = savings_account.customer_branch.id
        deposited_by = savings_account.account_customer.name

        # Make sure pay payment
        basic_token = get_basic_auth_token()
        response = requests.post(
            config('PAYMENT_GATEWAY_URL') + 'akello-pay/token-auth',
            json = {"grant_type": "client_credentials"},
            headers = {
                "Content-Type": "application/x-www-form-urlencoded",
                "Authorization": f"Basic {basic_token}"
            }
        )

        if response.status_code == 200:
            data = {
                "accountNumber": account_number,
                "accountName": account_name,
                "amount":amount,
                "record_date":record_date,
                "narration":"Fees Payment"
            }
            response_data = response.json()
            response = requests.post(
                config('PAYMENT_GATEWAY_URL') + 'api/akello-pay/sure-pay/request-to-pay/',
                json = data,
                headers = {
                    "Content-Type": "application/json",
                    "Authorization": f"Bearer {response_data['access_token']}"
                }
            )

            if response.status_code == 200:
                payment_obj = response.json()
                heading = f'Cash transfer: for {account_name} - {account_number} by {deposited_by}'

                sender_obj= {"charge": 0, "id": destination_saving_account.id, "amount": amount, "date": record_date}
                extra_data = { "sender_id": savings_account.id, "send_sms": True, "description": 'Fees Payment', "heading":heading, "voucher_no":'', 'source':'ussd' }

                user = get_user_model().objects.filter(user_organisation_branch__branch_organisation__id=organisation_id).order_by('id').first()
                # Save sender transactions details
                saved_transaction = save_sender_transactions(user, request, sender_obj, extra_data)
                if not saved_transaction:
                    return Response({"statusDesc":"Sender transaction not posted.", "statusCode":'57'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

                # Save reciever transactions details
                receiver_obj= {"charge": 0, "id": destination_saving_account.id, "amount": amount, "date": record_date}
                extra_data = { "sender_id": savings_account.id, "send_sms": True, "description": 'Fees Payment', "heading":heading, "voucher_no":'', 'source':'ussd' }
                saved_reciever = save_reciever_transactions(user, request, receiver_obj, extra_data)
                if not saved_reciever:
                    return Response({"statusDesc":"Reciever transaction not posted.", "statusCode":'57'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

                # Reconcile inter-branch transactions
                if savings_account.customer_branch.id != destination_saving_account.customer_branch.id:
                    inter_branch_trans_field = {
                        "source_transaction":saved_transaction.transaction,
                        "destination_transaction":saved_reciever.transaction,
                        "added_by":user
                    }
                    InterBranchTransactions.objects.create(**inter_branch_trans_field)
            
                # Save transfer transactions details mapping
                transfer_fields = {
                    "sender_transaction": saved_transaction,
                    "reciever_transaction": saved_reciever
                }
                transfer_details = TransferTransactions.objects.create(**transfer_fields)
                if not transfer_details:
                    return Response({"statusDesc":"Mapping transaction not posted.", "statusCode":'57'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

                # save school fees transaction
                payment_data = {"transaction": saved_transaction, "transaction_id": payment_obj['transactionId'], "student_number": account_number, "student_name":account_name,
                "third_party_school_name": account_provider, "customer_account":destination_saving_account, "branch": OrganisationBranch.objects.get(pk=branch_id) }
                SchoolFeesPaymentTransactions.objects.create(**payment_data)
                
                results = {"statusDesc": 'Payment Successfully', "statusCode":'00'}
                status_code =status.HTTP_200_OK
                thread_multiple_booking_payments(destination_saving_account, organisation_id, destination_saving_account.customer_branch.id, user.id)

        return  Response(results, status_code)
    

class UssdInitiateWithdrawViewSet(APIView):
    # first do a withdraw on qbcore first 
    # then do a withdraw on the aggregator
    # then update the qbcore withdraw status
    # if it fails update reverse the transaction from qbcore
    ''' Make Payment '''
    authentication_classes = []
    permission_classes = []

    def get(self, request, format=None):
        return  Response({"message":" Method Supported"})

    def post(self, request, format=None):
        # organisation_id = request.data.get('organisation_id', None)
        organisation_id = request.data.get('organisation_id', None)
        res = request.data
        mm_provider_obj = OrganisationSetting.objects.filter(org_setting__id=organisation_id,setting_key='mm_provider').first()
        mm_status_obj   = OrganisationSetting.objects.filter(org_setting__id=organisation_id,setting_key='mm_status',setting_value='active').first()
        response_status = status.HTTP_200_OK
        account = request.data.get('id', None)
        savings_account = SavingAccount.objects.get(pk=account)




        # return Response({'organisation_id': organisation_id, 'savings_account': savings_account if savings_account else 'Not There'}, status=response_status)

        if not savings_account:
            return Response({'mobileBanking': 'Transaction failed', 'response': { "Status": "ERROR",  "StatusCode": "-13", "StatusMessage": "No account provided"}}, status=status.HTTP_400_BAD_REQUEST)
            # return Response({"status":"failed","message":"No account provided"}, status=status.HTTP_404_NOT_FOUND)
        update_savings_account_statuses(customer_id=savings_account.account_customer_id)
        assert_savings_account_can_debit(savings_account, 'withdrawals')
        organisation_branch_id= savings_account.customer_branch_id
        amount = request.data.get('amount', None)
        telephone = request.data.get('telephone', None)
        telephone = request.data.get('telephone')
        if telephone and len(telephone) >= 9:
            telephone = '256' + telephone[-9:]
        else:
            telephone = None  # or raise an error, or handle appropriately

        # telephone = '256'+telephone[-9:]
        # message = {
        #     'status': 'failed'
        # }

        if telephone[-9:] != savings_account.account_customer.telephone[-9:]:
            return Response({'mobileBanking': 'Transaction failed', 'response': { "Status": "ERROR",  "StatusCode": "-13", "StatusMessage": "Telephone number does not match account holder"}}, status=status.HTTP_400_BAD_REQUEST)
            


        if not mm_provider_obj:
            return Response({'mobileBanking': 'Transaction failed', 'response': { "Status": "ERROR",  "StatusCode": "-13", "StatusMessage": "No selected service provider"}}, status=status.HTTP_400_BAD_REQUEST)
            # message = {'status':'failed','message':'No selected service provider'}
            # return Response(message, response_status)

        if not mm_status_obj:
            # message = {'status':'failed','message':'Mobile Banking services currently disabled.'}
            return Response({'mobileBanking': 'Transaction failed', 'response': { "Status": "ERROR",  "StatusCode": "-13", "StatusMessage": "Mobile Banking services currently disabled."}}, status=status.HTTP_400_BAD_REQUEST)
            # return Response(message, response_status)
    
        aggregator = MMServiceProvider.objects.filter(id=mm_provider_obj.setting_value,status='active',deposit='active').first()
        if not aggregator:
            # message = {'status':'failed','message':'No selected service provider'}
            return Response({'mobileBanking': 'Transaction failed', 'response': { "Status": "ERROR",  "StatusCode": "-13", "StatusMessage": "No selected service provider"}}, status=status.HTTP_400_BAD_REQUEST)
            # return Response(message, response_status)
        
        

        max_withdraw = savings_account.account_product.max_withdraws

        withdraw_charge = SavingProductCharge.objects.filter(saving_product=savings_account.account_product, charge_key='withdrawal_charge').first()
        charge = None

        if withdraw_charge :
            if savings_account.account_product.default_charge_type == 'flat':
                charge = withdraw_charge.charge
            else:
                charge= float(withdraw_charge.charge/100) * amount


        account_balance = get_account_balance(savings_account)
        if account_balance and account_balance['balance_raw'] < (float(amount) + float(charge)):
           return Response({'mobileBanking': 'Transaction failed', 'response': { "Status": "ERROR",  "StatusCode": "-13", "StatusMessage": "Insufficient balance amount "}}, status=status.HTTP_400_BAD_REQUEST)
        
        heading = "MM Withdraw: by {member_name} - {payer_number}".format(member_name=savings_account.account_customer.name, payer_number=savings_account.account_customer.member_number)
        customer_account = ''

        reference_no = generate_reference_no(savings_account.account_product.accounts_chart.account_line, organisation_id, 'mm-wit')

        e_wallet_chart = get_branch_wallet_chart(organisation_id, organisation_branch_id)

        if e_wallet_chart:

            # consume aggregator api
            # soap_url = "https://sandbox.yo.co.ug/services/yopaymentsdev/task.php"  # Replace with actual endpoint
            soap_url = config('YO_SOAP_API_URL')
            soap_username = config('YO_SOAP_API_USERNAME')   #90001735890
            soap_password = config('YO_SOAP_API_PASSWORD')   #3507841444

            headers = {
                "Content-Type": "text/xml; charset=utf-8"
            }
            transaction_id = hashlib.md5((telephone + str(time.time())).encode()).hexdigest()

                # SOAP payload to send
            payload = f"""<?xml version="1.0" encoding="UTF-8"?>
                <AutoCreate>
                <Request>
                <APIUsername>{soap_username}</APIUsername>
                <APIPassword>{soap_password}</APIPassword>
                <Method>acwithdrawfunds</Method>
                <NonBlocking></NonBlocking>
                <Amount>{amount}</Amount>
                <Account>{telephone}</Account>
                <AccountProviderCode></AccountProviderCode>
                <Narrative>{heading} </Narrative>
                <ExternalReference>{transaction_id}</ExternalReference>
                <ProviderReferenceText></ProviderReferenceText>
                </Request>
                </AutoCreate>"""

            response = consume_yo_api(soap_url, headers, payload)
            if response.get('Status') == 'ERROR':
                return Response({'mobileBanking': 'Transaction failed', 'response': response})

            data = {
                "amount": amount,
                "heading": heading,
                "reference_no": reference_no,
                "payment_method": "settlement",
                "branch_id": organisation_branch_id,
                "debit_chart":  savings_account.account_product.accounts_chart,
                "credit_chart": e_wallet_chart
            }

            system_transaction = SystemTransactions.objects.create(**data)
            if system_transaction:
                savings_transaction_fields = {
                            "transaction_type": 'withdrawal',
                            "customer_account": savings_account,
                            "transaction": system_transaction
                        }
                save_trans = SavingAccountTransactions.objects.create(**savings_transaction_fields)


                charge_fields = {
                    "heading": 'Saving withdrawal charge on A/C No: ' + savings_account.account_no,
                    "coment": 'Saving withdrawal charge on A/C No: ' + savings_account.account_no,
                    "amount": charge,
                    "credit_chart": withdraw_charge.accounts_chart,
                    "debit_chart": savings_account.account_product.accounts_chart,
                    "reference_no": reference_no,
                    "payment_method": "mobile_banking",
                    "branch_id": organisation_branch_id,
                }

                charge_transaction = SystemTransactions.objects.create(**charge_fields)
                if charge_transaction:
                    charge_transaction_fields = {
                        "transaction_type": 'withdrawal',
                        "customer_account": savings_account,
                        "transaction": charge_transaction
                    }
                    SavingAccountTransactions.objects.create(**charge_transaction_fields)
                sync_savings_account_lifecycle(savings_account)



                # response = system_transaction
                
        return Response({'mobileBanking':'Transaction successful', 'response': response}, response_status)        


# ------------------ Organisation and Member Wallet Operations ------------------ #

class OrgWalletInitiateDepositView(APIView):
    permission_classes = [IsAuthenticated]
    throttle_classes = [ScopedRateThrottle]
    throttle_scope = 'org_wallet'

    def post(self, request, format=None):
        organisation_id = request.data.get('organisation_id')
        branch_id = request.data.get('branch_id')
        amount = request.data.get('amount')
        source_chart_id = request.data.get('source_chart_id')
        narration = request.data.get('narration', 'Organisation Wallet Top-up')

        if not organisation_id or not branch_id or not amount or not source_chart_id:
            return Response({"status": "failed", "message": "organisation_id, branch_id, amount, source_chart_id are required"}, status=status.HTTP_400_BAD_REQUEST)

        # Authorize: user must belong to organisation and branch
        if getattr(request.user, 'user_organisation_branch_id', None) != int(branch_id):
            return Response({"status": "failed", "message": "Forbidden for this branch"}, status=status.HTTP_403_FORBIDDEN)
        if request.user.user_organisation_branch.branch_organisation_id != int(organisation_id):
            return Response({"status": "failed", "message": "Forbidden for this organisation"}, status=status.HTTP_403_FORBIDDEN)

        wallet_chart = get_branch_wallet_chart(organisation_id, branch_id)
        source_chart = OrganisationSubAccount.objects.filter(id=source_chart_id, account_organisation_id=organisation_id).first()
        if not source_chart:
            return Response({"status": "failed", "message": "Invalid source_chart_id"}, status=status.HTTP_400_BAD_REQUEST)

        reference_no = generate_reference_no(wallet_chart.account_line, organisation_id, 'org-dep')
        data = {
            "amount": float(amount),
            "heading": narration,
            "reference_no": reference_no,
            "payment_method": "internal",
            "branch_id": branch_id,
            "debit_chart": source_chart,
            "credit_chart": wallet_chart
        }
        system_transaction = SystemTransactions.objects.create(**data)
        return Response({"status": "success", "transaction_id": system_transaction.id}, status=status.HTTP_200_OK)


class OrgWalletWithdrawView(APIView):
    permission_classes = [IsAuthenticated]
    throttle_classes = [ScopedRateThrottle]
    throttle_scope = 'org_wallet'

    def post(self, request, format=None):
        organisation_id = request.data.get('organisation_id')
        branch_id = request.data.get('branch_id')
        amount = request.data.get('amount')
        destination_chart_id = request.data.get('destination_chart_id')
        telephone = request.data.get('telephone')  # optional to initiate YO cash-out
        narration = request.data.get('narration', 'Organisation Wallet Withdrawal')

        if not organisation_id or not branch_id or not amount:
            return Response({"status": "failed", "message": "organisation_id, branch_id, amount are required"}, status=status.HTTP_400_BAD_REQUEST)

        # Authorize: user must belong to organisation and branch
        if getattr(request.user, 'user_organisation_branch_id', None) != int(branch_id):
            return Response({"status": "failed", "message": "Forbidden for this branch"}, status=status.HTTP_403_FORBIDDEN)
        if request.user.user_organisation_branch.branch_organisation_id != int(organisation_id):
            return Response({"status": "failed", "message": "Forbidden for this organisation"}, status=status.HTTP_403_FORBIDDEN)

        wallet_chart = get_branch_wallet_chart(organisation_id, branch_id)
        destination_chart = None
        if destination_chart_id:
            destination_chart = OrganisationSubAccount.objects.filter(id=destination_chart_id, account_organisation_id=organisation_id).first()

        reference_no = generate_reference_no(wallet_chart.account_line, organisation_id, 'org-wit')
        data = {
            "amount": float(amount),
            "heading": narration,
            "reference_no": reference_no,
            "payment_method": "internal",
            "branch_id": branch_id,
            "debit_chart": wallet_chart,
            "credit_chart": destination_chart if destination_chart else wallet_chart
        }
        # If no destination chart is provided, still log a transaction by crediting back same wallet (acts as placeholder).
        system_transaction = SystemTransactions.objects.create(**data)

        response = {"status": "success", "transaction_id": system_transaction.id}

        # Optional: Initiate YO cash-out
        if telephone and len(telephone) >= 9:
            telephone = '256' + telephone[-9:]
            soap_url = config('YO_SOAP_API_URL')
            soap_username = config('YO_SOAP_API_USERNAME')
            soap_password = config('YO_SOAP_API_PASSWORD')
            headers = {"Content-Type": "text/xml; charset=utf-8"}
            ext_ref = hashlib.md5((telephone + str(time.time())).encode()).hexdigest()
            payload = f"""<?xml version="1.0" encoding="UTF-8"?>
                <AutoCreate>
                <Request>
                <APIUsername>{soap_username}</APIUsername>
                <APIPassword>{soap_password}</APIPassword>
                <Method>acwithdrawfunds</Method>
                <NonBlocking></NonBlocking>
                <Amount>{amount}</Amount>
                <Account>{telephone}</Account>
                <AccountProviderCode></AccountProviderCode>
                <Narrative>{narration}</Narrative>
                <ExternalReference>{ext_ref}</ExternalReference>
                <ProviderReferenceText></ProviderReferenceText>
                </Request>
                </AutoCreate>"""
            yo_response = consume_yo_api(soap_url, headers, payload)
            response['yo_response'] = yo_response

            # On success, book Yo wallet -> org wallet
            if yo_response.get('Status') == 'OK':
                yo_wallet = get_yo_wallet_chart(organisation_id, branch_id)
                reference_no = generate_reference_no(yo_wallet.account_line, organisation_id, 'yo-wit')
                SystemTransactions.objects.create(
                    amount=float(amount),
                    heading=f"YO Cash-out to {telephone} - {narration}",
                    reference_no=reference_no,
                    payment_method="yo",
                    branch_id=branch_id,
                    debit_chart=wallet_chart,
                    credit_chart=yo_wallet
                )

        return Response(response, status=status.HTTP_200_OK)


class OrgWalletDisburseView(APIView):
    permission_classes = [IsAuthenticated]
    throttle_classes = [ScopedRateThrottle]
    throttle_scope = 'org_wallet'

    def post(self, request, format=None):
        organisation_id = request.data.get('organisation_id')
        branch_id = request.data.get('branch_id')
        disbursements = request.data.get('disbursements', [])  # [{customer_id, amount, narration}]

        if not organisation_id or not branch_id or not disbursements:
            return Response({"status": "failed", "message": "organisation_id, branch_id, disbursements are required"}, status=status.HTTP_400_BAD_REQUEST)

        # Authorize: user must belong to organisation and branch
        if getattr(request.user, 'user_organisation_branch_id', None) != int(branch_id):
            return Response({"status": "failed", "message": "Forbidden for this branch"}, status=status.HTTP_403_FORBIDDEN)
        if request.user.user_organisation_branch.branch_organisation_id != int(organisation_id):
            return Response({"status": "failed", "message": "Forbidden for this organisation"}, status=status.HTTP_403_FORBIDDEN)

        wallet_chart = get_branch_wallet_chart(organisation_id, branch_id)

        results = []
        for item in disbursements:
            customer_id = item.get('customer_id')
            amount = float(item.get('amount', 0))
            narration = item.get('narration', f"Disbursement to member {customer_id}")
            if not customer_id or amount <= 0:
                continue

            member_wallet_chart = get_member_wallet_chart(organisation_id, branch_id, customer_id)
            reference_no = generate_reference_no(wallet_chart.account_line, organisation_id, 'org-dsb')
            tx = SystemTransactions.objects.create(
                amount=amount,
                heading=narration,
                reference_no=reference_no,
                payment_method="internal",
                branch_id=branch_id,
                debit_chart=wallet_chart,
                credit_chart=member_wallet_chart
            )
            results.append({"customer_id": customer_id, "transaction_id": tx.id})

        return Response({"status": "success", "results": results, "count": len(results)}, status=status.HTTP_200_OK)


class MemberWalletDepositView(APIView):
    permission_classes = [IsAuthenticated]
    throttle_classes = [ScopedRateThrottle]
    throttle_scope = 'member_wallet'

    def post(self, request, format=None):
        organisation_id = request.data.get('organisation_id')
        branch_id = request.data.get('branch_id')
        customer_id = request.data.get('customer_id')
        amount = request.data.get('amount')
        narration = request.data.get('narration', 'Member Wallet Top-up')

        if not organisation_id or not branch_id or not customer_id or not amount:
            return Response({"status": "failed", "message": "organisation_id, branch_id, customer_id, amount are required"}, status=status.HTTP_400_BAD_REQUEST)

        # Authorize: user must belong to organisation and branch
        if getattr(request.user, 'user_organisation_branch_id', None) != int(branch_id):
            return Response({"status": "failed", "message": "Forbidden for this branch"}, status=status.HTTP_403_FORBIDDEN)
        if request.user.user_organisation_branch.branch_organisation_id != int(organisation_id):
            return Response({"status": "failed", "message": "Forbidden for this organisation"}, status=status.HTTP_403_FORBIDDEN)

        wallet_chart = get_branch_wallet_chart(organisation_id, branch_id)
        member_wallet_chart = get_member_wallet_chart(organisation_id, branch_id, customer_id)

        reference_no = generate_reference_no(wallet_chart.account_line, organisation_id, 'mem-dep')
        tx = SystemTransactions.objects.create(
            amount=float(amount),
            heading=narration,
            reference_no=reference_no,
            payment_method="internal",
            branch_id=branch_id,
            debit_chart=wallet_chart,
            credit_chart=member_wallet_chart
        )

        return Response({"status": "success", "transaction_id": tx.id}, status=status.HTTP_200_OK)

class MemberWalletWithdrawView(APIView):
    permission_classes = [IsAuthenticated]
    throttle_classes = [ScopedRateThrottle]
    throttle_scope = 'member_wallet'

    def post(self, request, format=None):
        organisation_id = request.data.get('organisation_id')
        branch_id = request.data.get('branch_id')
        customer_id = request.data.get('customer_id')
        amount = request.data.get('amount')
        telephone = request.data.get('telephone')  # optional YO cash-out
        narration = request.data.get('narration', 'Member Wallet Withdrawal')

        if not organisation_id or not branch_id or not customer_id or not amount:
            return Response({"status": "failed", "message": "organisation_id, branch_id, customer_id, amount are required"}, status=status.HTTP_400_BAD_REQUEST)

        # Authorize: user must belong to organisation and branch
        if getattr(request.user, 'user_organisation_branch_id', None) != int(branch_id):
            return Response({"status": "failed", "message": "Forbidden for this branch"}, status=status.HTTP_403_FORBIDDEN)
        if request.user.user_organisation_branch.branch_organisation_id != int(organisation_id):
            return Response({"status": "failed", "message": "Forbidden for this organisation"}, status=status.HTTP_403_FORBIDDEN)

        member_wallet_chart = get_member_wallet_chart(organisation_id, branch_id, customer_id)
        wallet_chart = get_branch_wallet_chart(organisation_id, branch_id)

        reference_no = generate_reference_no(wallet_chart.account_line, organisation_id, 'mem-wit')
        tx = SystemTransactions.objects.create(
            amount=float(amount),
            heading=narration,
            reference_no=reference_no,
            payment_method="internal",
            branch_id=branch_id,
            debit_chart=member_wallet_chart,
            credit_chart=wallet_chart
        )

        response = {"status": "success", "transaction_id": tx.id}

        # Optional: YO cash-out
        if telephone and len(telephone) >= 9:
            telephone = '256' + telephone[-9:]
            soap_url = config('YO_SOAP_API_URL')
            soap_username = config('YO_SOAP_API_USERNAME')
            soap_password = config('YO_SOAP_API_PASSWORD')
            headers = {"Content-Type": "text/xml; charset=utf-8"}
            ext_ref = hashlib.md5((telephone + str(time.time())).encode()).hexdigest()
            payload = f"""<?xml version=\"1.0\" encoding=\"UTF-8\"?>
                <AutoCreate>
                <Request>
                <APIUsername>{soap_username}</APIUsername>
                <APIPassword>{soap_password}</APIPassword>
                <Method>acwithdrawfunds</Method>
                <NonBlocking></NonBlocking>
                <Amount>{amount}</Amount>
                <Account>{telephone}</Account>
                <AccountProviderCode></AccountProviderCode>
                <Narrative>{narration}</Narrative>
                <ExternalReference>{ext_ref}</ExternalReference>
                <ProviderReferenceText></ProviderReferenceText>
                </Request>
                </AutoCreate>"""
            yo_response = consume_yo_api(soap_url, headers, payload)
            response['yo_response'] = yo_response
            if yo_response.get('Status') == 'OK':
                yo_wallet = get_yo_wallet_chart(organisation_id, branch_id)
                reference_no = generate_reference_no(yo_wallet.account_line, organisation_id, 'yo-wit')
                SystemTransactions.objects.create(
                    amount=float(amount),
                    heading=f"YO Cash-out to {telephone} - {narration}",
                    reference_no=reference_no,
                    payment_method="yo",
                    branch_id=branch_id,
                    debit_chart=wallet_chart,
                    credit_chart=yo_wallet
                )

        return Response(response, status=status.HTTP_200_OK)

class QdfWalletsView(APIView):
    permission_classes = [IsAuthenticated]

    def get(self, request, format=None):
        org_ids = OrganisationSetting.objects.filter(
            setting_key="mm_status", setting_value="active"
        ).values_list("org_setting_id", flat=True)

        organisations = Organisation.objects.filter(id__in=org_ids)

        org_data = []
        for org in organisations:
            members = MobileBankingSubscription.objects.filter(
                customer__customer_branch__branch_organisation=org,
                active=True
            ).select_related("customer")

            setting = OrganisationSetting.objects.filter(
                org_setting=org, setting_key="mm_status", setting_value="active"
            ).first()

            org_data.append({
                "id": org.id,
                "organisation_id": org.id,
                "branch_id": None,
                "organisation_name": org.name,
                "created_at": org.date_added,
                "member_count": members.count(),
                "wallet_balance": 0,
                "transaction_count": 0,
            })

        response = {
            "organizations": org_data,
            "qdf_main_balance": 0
        }
        return Response(response, status=status.HTTP_200_OK)


class EwalletBalancesView(APIView):
    permission_classes = [IsAuthenticated]

    def get(self, request, format=None):
        from django.db.models import Sum, F
        from exservices.models import SmsPurchaseTransactions

        # Get all organisations that have mobile banking enabled
        org_ids = OrganisationSetting.objects.filter(
            setting_key="mm_status", setting_value="active"
        ).values_list("org_setting_id", flat=True)

        organisations = Organisation.objects.filter(id__in=org_ids).order_by('name')
        results = []

        for org in organisations:
            # Get the parent e-wallet chart for this org
            parent_chart = OrganisationSubAccount.objects.filter(
                account_code='sys-11418',
                account_organisation_id=org.id
            ).first()

            if not parent_chart:
                results.append({
                    'organisation_id': org.id,
                    'organisation_name': org.name,
                    'total_deposits': 0,
                    'total_withdrawals': 0,
                    'total_sms_purchases': 0,
                    'balance': 0,
                    'branches': []
                })
                continue

            # Get all branch wallet charts under this parent
            wallet_charts = OrganisationSubAccount.objects.filter(parent_id=parent_chart)
            wallet_chart_ids = list(wallet_charts.values_list('id', flat=True))

            # Total deposits (debit to e-wallet = money in)
            total_deposits = SystemTransactions.objects.filter(
                debit_chart_id__in=wallet_chart_ids,
                transaction_type='normal',
                deleted=False
            ).aggregate(total=Sum('amount'))['total'] or 0

            # Total credits (money out of e-wallet)
            total_credits = SystemTransactions.objects.filter(
                credit_chart_id__in=wallet_chart_ids,
                transaction_type='normal',
                deleted=False
            ).aggregate(total=Sum('amount'))['total'] or 0

            # SMS purchases (subset of credits)
            sms_transaction_ids = SmsPurchaseTransactions.objects.filter(
                transaction__credit_chart_id__in=wallet_chart_ids
            ).values_list('transaction_id', flat=True)

            total_sms_purchases = SystemTransactions.objects.filter(
                id__in=sms_transaction_ids,
                transaction_type='normal',
                deleted=False
            ).aggregate(total=Sum('amount'))['total'] or 0

            total_withdrawals = round(total_credits - total_sms_purchases, 2)
            balance = round(total_deposits - total_credits, 2)

            # Per-branch breakdown
            branch_data = []
            for chart in wallet_charts:
                setting = OrganisationSetting.objects.filter(
                    org_setting_id=org.id,
                    setting_key__endswith='_branch_wallet_chart',
                    setting_value=str(chart.id)
                ).first()
                branch_id = setting.setting_key.replace('_branch_wallet_chart', '') if setting else None
                branch = OrganisationBranch.objects.filter(id=branch_id).first() if branch_id else None

                branch_deposits = SystemTransactions.objects.filter(
                    debit_chart=chart, transaction_type='normal', deleted=False
                ).aggregate(total=Sum('amount'))['total'] or 0

                branch_credits = SystemTransactions.objects.filter(
                    credit_chart=chart, transaction_type='normal', deleted=False
                ).aggregate(total=Sum('amount'))['total'] or 0

                branch_sms_tx_ids = SmsPurchaseTransactions.objects.filter(
                    transaction__credit_chart=chart
                ).values_list('transaction_id', flat=True)

                branch_sms = SystemTransactions.objects.filter(
                    id__in=branch_sms_tx_ids, transaction_type='normal', deleted=False
                ).aggregate(total=Sum('amount'))['total'] or 0

                branch_data.append({
                    'branch_id': branch.id if branch else None,
                    'branch_name': branch.name if branch else chart.account_name,
                    'deposits': round(branch_deposits, 2),
                    'withdrawals': round(branch_credits - branch_sms, 2),
                    'sms_purchases': round(branch_sms, 2),
                    'balance': round(branch_deposits - branch_credits, 2),
                })

            results.append({
                'organisation_id': org.id,
                'organisation_name': org.name,
                'total_deposits': round(total_deposits, 2),
                'total_withdrawals': round(total_withdrawals, 2),
                'total_sms_purchases': round(total_sms_purchases, 2),
                'balance': balance,
                'branches': branch_data
            })

        return Response({'results': results, 'count': len(results)}, status=status.HTTP_200_OK)


class QdfMembersSummaryView(APIView):
    permission_classes = [IsAuthenticated]

    def get(self, request, format=None):
        org_ids = OrganisationSetting.objects.filter(
            setting_key="mm_status", setting_value="active"
        ).values_list("org_setting_id", flat=True)

        organisations = Organisation.objects.filter(id__in=org_ids)

        members = MobileBankingSubscription.objects.filter(
            customer__customer_branch__branch_organisation__in=organisations,
            active=True
        ).select_related("customer")

        response = {
            "total_members": members.count(),
            "total_transactions": 0
        }
        return Response(response, status=status.HTTP_200_OK)   

class OrgWalletTransactionsView(APIView):
    def get(self, request, organisation_id, format=None):
        tx_types = request.GET.get("transaction_types", "")
        filters = tx_types.split(",") if tx_types else []

        transactions = SystemTransactions.objects.filter(
            debit_chart__account_organisation_id=organisation_id
        ).order_by("-added_by")

        if filters:
            transactions = transactions.filter(type__in=filters)

        tx_list = []
        for tx in transactions[:50]:  # limit
            tx_list.append({
                "id": tx.id,
                "date": tx.created_at,
                "type": tx.type,
                "amount": float(tx.amount),
                "narration": tx.narration,
                "initiated_by": tx.initiated_by.username if tx.initiated_by else "System",
            })

        return Response({"transactions": tx_list}, status=status.HTTP_200_OK)


class YoCashInWebhookView(APIView):
    authentication_classes = []
    permission_classes = []

    def post(self, request, format=None):
        # Basic shared-secret validation
        webhook_token = request.headers.get('X-Webhook-Token')
        expected_token = config('YO_WEBHOOK_TOKEN', default='')
        if not expected_token or webhook_token != expected_token:
            return Response({"status": "failed", "message": "Unauthorized"}, status=status.HTTP_401_UNAUTHORIZED)

        organisation_id = request.data.get('organisation_id')
        branch_id = request.data.get('branch_id')
        amount = request.data.get('amount')
        target_type = request.data.get('target_type')  # 'org' | 'member'
        customer_id = request.data.get('customer_id')
        narration = request.data.get('narration', 'QDF Cash-in')
        status_flag = request.data.get('status')  # 'OK' | 'SUCCESS' etc

        if not organisation_id or not branch_id or not amount or target_type not in ['org', 'member']:
            return Response({"status": "failed", "message": "organisation_id, branch_id, amount, target_type required"}, status=status.HTTP_400_BAD_REQUEST)

        # Only process successful notifications
        if str(status_flag).upper() not in ['OK', 'SUCCESS', 'TS', '200']:
            return Response({"status": "ignored", "message": "Non-success status"}, status=status.HTTP_202_ACCEPTED)

        yo_wallet = get_yo_wallet_chart(organisation_id, branch_id)

        if target_type == 'org':
            wallet_chart = get_branch_wallet_chart(organisation_id, branch_id)
            reference_no = generate_reference_no(wallet_chart.account_line, organisation_id, 'qdf-in')
            SystemTransactions.objects.create(
                amount=float(amount),
                heading=narration,
                reference_no=reference_no,
                payment_method="yo",
                branch_id=branch_id,
                debit_chart=wallet_chart,
                credit_chart=yo_wallet
            )
        else:
            if not customer_id:
                return Response({"status": "failed", "message": "customer_id required for member target"}, status=status.HTTP_400_BAD_REQUEST)
            member_wallet_chart = get_member_wallet_chart(organisation_id, branch_id, customer_id)
            reference_no = generate_reference_no(member_wallet_chart.account_line, organisation_id, 'qdf-in')
            SystemTransactions.objects.create(
                amount=float(amount),
                heading=narration,
                reference_no=reference_no,
                payment_method="yo",
                branch_id=branch_id,
                debit_chart=member_wallet_chart,
                credit_chart=yo_wallet
            )

        return Response({"status": "success"}, status=status.HTTP_200_OK)

# ------------------ Organisation and Member Wallet Operations ------------------ #

class OrgWalletInitiateDepositView(APIView):
    permission_classes = [IsAuthenticated]
    throttle_classes = [ScopedRateThrottle]
    throttle_scope = 'org_wallet'

    def post(self, request, format=None):
        organisation_id = request.data.get('organisation_id')
        branch_id = request.data.get('branch_id')
        amount = request.data.get('amount')
        source_chart_id = request.data.get('source_chart_id')
        narration = request.data.get('narration', 'Organisation Wallet Top-up')

        if not organisation_id or not branch_id or not amount or not source_chart_id:
            return Response({"status": "failed", "message": "organisation_id, branch_id, amount, source_chart_id are required"}, status=status.HTTP_400_BAD_REQUEST)

        # Authorize: user must belong to organisation and branch
        if getattr(request.user, 'user_organisation_branch_id', None) != int(branch_id):
            return Response({"status": "failed", "message": "Forbidden for this branch"}, status=status.HTTP_403_FORBIDDEN)
        if request.user.user_organisation_branch.branch_organisation_id != int(organisation_id):
            return Response({"status": "failed", "message": "Forbidden for this organisation"}, status=status.HTTP_403_FORBIDDEN)

        wallet_chart = get_branch_wallet_chart(organisation_id, branch_id)
        source_chart = OrganisationSubAccount.objects.filter(id=source_chart_id, account_organisation_id=organisation_id).first()
        if not source_chart:
            return Response({"status": "failed", "message": "Invalid source_chart_id"}, status=status.HTTP_400_BAD_REQUEST)

        reference_no = generate_reference_no(wallet_chart.account_line, organisation_id, 'org-dep')
        data = {
            "amount": float(amount),
            "heading": narration,
            "reference_no": reference_no,
            "payment_method": "internal",
            "branch_id": branch_id,
            "debit_chart": source_chart,
            "credit_chart": wallet_chart
        }
        system_transaction = SystemTransactions.objects.create(**data)
        return Response({"status": "success", "transaction_id": system_transaction.id}, status=status.HTTP_200_OK)


class OrgWalletWithdrawView(APIView):
    permission_classes = [IsAuthenticated]
    throttle_classes = [ScopedRateThrottle]
    throttle_scope = 'org_wallet'

    def post(self, request, format=None):
        organisation_id = request.data.get('organisation_id')
        branch_id = request.data.get('branch_id')
        amount = request.data.get('amount')
        destination_chart_id = request.data.get('destination_chart_id')
        telephone = request.data.get('telephone')  # optional to initiate YO cash-out
        narration = request.data.get('narration', 'Organisation Wallet Withdrawal')

        if not organisation_id or not branch_id or not amount:
            return Response({"status": "failed", "message": "organisation_id, branch_id, amount are required"}, status=status.HTTP_400_BAD_REQUEST)

        # Authorize: user must belong to organisation and branch
        if getattr(request.user, 'user_organisation_branch_id', None) != int(branch_id):
            return Response({"status": "failed", "message": "Forbidden for this branch"}, status=status.HTTP_403_FORBIDDEN)
        if request.user.user_organisation_branch.branch_organisation_id != int(organisation_id):
            return Response({"status": "failed", "message": "Forbidden for this organisation"}, status=status.HTTP_403_FORBIDDEN)

        wallet_chart = get_branch_wallet_chart(organisation_id, branch_id)
        destination_chart = None
        if destination_chart_id:
            destination_chart = OrganisationSubAccount.objects.filter(id=destination_chart_id, account_organisation_id=organisation_id).first()

        reference_no = generate_reference_no(wallet_chart.account_line, organisation_id, 'org-wit')
        data = {
            "amount": float(amount),
            "heading": narration,
            "reference_no": reference_no,
            "payment_method": "internal",
            "branch_id": branch_id,
            "debit_chart": wallet_chart,
            "credit_chart": destination_chart if destination_chart else wallet_chart
        }
        # If no destination chart is provided, still log a transaction by crediting back same wallet (acts as placeholder).
        system_transaction = SystemTransactions.objects.create(**data)

        response = {"status": "success", "transaction_id": system_transaction.id}

        # Optional: Initiate QDF cash-out
        if telephone and len(telephone) >= 9:
            telephone = '256' + telephone[-9:]
            soap_url = config('YO_SOAP_API_URL')
            soap_username = config('YO_SOAP_API_USERNAME')
            soap_password = config('YO_SOAP_API_PASSWORD')
            headers = {"Content-Type": "text/xml; charset=utf-8"}
            ext_ref = hashlib.md5((telephone + str(time.time())).encode()).hexdigest()
            payload = f"""<?xml version="1.0" encoding="UTF-8"?>
                <AutoCreate>
                <Request>
                <APIUsername>{soap_username}</APIUsername>
                <APIPassword>{soap_password}</APIPassword>
                <Method>acwithdrawfunds</Method>
                <NonBlocking></NonBlocking>
                <Amount>{amount}</Amount>
                <Account>{telephone}</Account>
                <AccountProviderCode></AccountProviderCode>
                <Narrative>{narration}</Narrative>
                <ExternalReference>{ext_ref}</ExternalReference>
                <ProviderReferenceText></ProviderReferenceText>
                </Request>
                </AutoCreate>"""
            yo_response = consume_yo_api(soap_url, headers, payload)
            response['qdf_response'] = yo_response

            # On success, book QDF wallet -> org wallet
            if yo_response.get('Status') == 'OK':
                yo_wallet = get_yo_wallet_chart(organisation_id, branch_id)
                reference_no = generate_reference_no(yo_wallet.account_line, organisation_id, 'qdf-wit')
                SystemTransactions.objects.create(
                    amount=float(amount),
                    heading=f"QDF Cash-out to {telephone} - {narration}",
                    reference_no=reference_no,
                    payment_method="yo",
                    branch_id=branch_id,
                    debit_chart=wallet_chart,
                    credit_chart=yo_wallet
                )

        return Response(response, status=status.HTTP_200_OK)


class OrgWalletDisburseView(APIView):
    permission_classes = [IsAuthenticated]
    throttle_classes = [ScopedRateThrottle]
    throttle_scope = 'org_wallet'

    def post(self, request, format=None):
        organisation_id = request.data.get('organisation_id')
        branch_id = request.data.get('branch_id')
        disbursements = request.data.get('disbursements', [])  # [{customer_id, amount, narration}]

        if not organisation_id or not branch_id or not disbursements:
            return Response({"status": "failed", "message": "organisation_id, branch_id, disbursements are required"}, status=status.HTTP_400_BAD_REQUEST)

        # Authorize: user must belong to organisation and branch
        if getattr(request.user, 'user_organisation_branch_id', None) != int(branch_id):
            return Response({"status": "failed", "message": "Forbidden for this branch"}, status=status.HTTP_403_FORBIDDEN)
        if request.user.user_organisation_branch.branch_organisation_id != int(organisation_id):
            return Response({"status": "failed", "message": "Forbidden for this organisation"}, status=status.HTTP_403_FORBIDDEN)

        wallet_chart = get_branch_wallet_chart(organisation_id, branch_id)

        results = []
        for item in disbursements:
            customer_id = item.get('customer_id')
            amount = float(item.get('amount', 0))
            narration = item.get('narration', f"Disbursement to member {customer_id}")
            if not customer_id or amount <= 0:
                continue

            member_wallet_chart = get_member_wallet_chart(organisation_id, branch_id, customer_id)
            reference_no = generate_reference_no(wallet_chart.account_line, organisation_id, 'org-dsb')
            tx = SystemTransactions.objects.create(
                amount=amount,
                heading=narration,
                reference_no=reference_no,
                payment_method="internal",
                branch_id=branch_id,
                debit_chart=wallet_chart,
                credit_chart=member_wallet_chart
            )
            results.append({"customer_id": customer_id, "transaction_id": tx.id})

        return Response({"status": "success", "results": results, "count": len(results)}, status=status.HTTP_200_OK)


class MemberWalletDepositView(APIView):
    permission_classes = [IsAuthenticated]
    throttle_classes = [ScopedRateThrottle]
    throttle_scope = 'member_wallet'

    def post(self, request, format=None):
        organisation_id = request.data.get('organisation_id')
        branch_id = request.data.get('branch_id')
        customer_id = request.data.get('customer_id')
        amount = request.data.get('amount')
        narration = request.data.get('narration', 'Member Wallet Top-up')

        if not organisation_id or not branch_id or not customer_id or not amount:
            return Response({"status": "failed", "message": "organisation_id, branch_id, customer_id, amount are required"}, status=status.HTTP_400_BAD_REQUEST)

        # Authorize: user must belong to organisation and branch
        if getattr(request.user, 'user_organisation_branch_id', None) != int(branch_id):
            return Response({"status": "failed", "message": "Forbidden for this branch"}, status=status.HTTP_403_FORBIDDEN)
        if request.user.user_organisation_branch.branch_organisation_id != int(organisation_id):
            return Response({"status": "failed", "message": "Forbidden for this organisation"}, status=status.HTTP_403_FORBIDDEN)

        wallet_chart = get_branch_wallet_chart(organisation_id, branch_id)
        member_wallet_chart = get_member_wallet_chart(organisation_id, branch_id, customer_id)

        reference_no = generate_reference_no(wallet_chart.account_line, organisation_id, 'mem-dep')
        tx = SystemTransactions.objects.create(
            amount=float(amount),
            heading=narration,
            reference_no=reference_no,
            payment_method="internal",
            branch_id=branch_id,
            debit_chart=wallet_chart,
            credit_chart=member_wallet_chart
        )

        return Response({"status": "success", "transaction_id": tx.id}, status=status.HTTP_200_OK)

class MemberWalletWithdrawView(APIView):
    permission_classes = [IsAuthenticated]
    throttle_classes = [ScopedRateThrottle]
    throttle_scope = 'member_wallet'

    def post(self, request, format=None):
        organisation_id = request.data.get('organisation_id')
        branch_id = request.data.get('branch_id')
        customer_id = request.data.get('customer_id')
        amount = request.data.get('amount')
        telephone = request.data.get('telephone')  # optional YO cash-out
        narration = request.data.get('narration', 'Member Wallet Withdrawal')

        if not organisation_id or not branch_id or not customer_id or not amount:
            return Response({"status": "failed", "message": "organisation_id, branch_id, customer_id, amount are required"}, status=status.HTTP_400_BAD_REQUEST)

        # Authorize: user must belong to organisation and branch
        if getattr(request.user, 'user_organisation_branch_id', None) != int(branch_id):
            return Response({"status": "failed", "message": "Forbidden for this branch"}, status=status.HTTP_403_FORBIDDEN)
        if request.user.user_organisation_branch.branch_organisation_id != int(organisation_id):
            return Response({"status": "failed", "message": "Forbidden for this organisation"}, status=status.HTTP_403_FORBIDDEN)

        member_wallet_chart = get_member_wallet_chart(organisation_id, branch_id, customer_id)
        wallet_chart = get_branch_wallet_chart(organisation_id, branch_id)

        reference_no = generate_reference_no(wallet_chart.account_line, organisation_id, 'mem-wit')
        tx = SystemTransactions.objects.create(
            amount=float(amount),
            heading=narration,
            reference_no=reference_no,
            payment_method="internal",
            branch_id=branch_id,
            debit_chart=member_wallet_chart,
            credit_chart=wallet_chart
        )

        response = {"status": "success", "transaction_id": tx.id}

        # Optional: QDF cash-out
        if telephone and len(telephone) >= 9:
            telephone = '256' + telephone[-9:]
            soap_url = config('YO_SOAP_API_URL')
            soap_username = config('YO_SOAP_API_USERNAME')
            soap_password = config('YO_SOAP_API_PASSWORD')
            headers = {"Content-Type": "text/xml; charset=utf-8"}
            ext_ref = hashlib.md5((telephone + str(time.time())).encode()).hexdigest()
            payload = f"""<?xml version=\"1.0\" encoding=\"UTF-8\"?>
                <AutoCreate>
                <Request>
                <APIUsername>{soap_username}</APIUsername>
                <APIPassword>{soap_password}</APIPassword>
                <Method>acwithdrawfunds</Method>
                <NonBlocking></NonBlocking>
                <Amount>{amount}</Amount>
                <Account>{telephone}</Account>
                <AccountProviderCode></AccountProviderCode>
                <Narrative>{narration}</Narrative>
                <ExternalReference>{ext_ref}</ExternalReference>
                <ProviderReferenceText></ProviderReferenceText>
                </Request>
                </AutoCreate>"""
            yo_response = consume_yo_api(soap_url, headers, payload)
            response['qdf_response'] = yo_response
            if yo_response.get('Status') == 'OK':
                yo_wallet = get_yo_wallet_chart(organisation_id, branch_id)
                reference_no = generate_reference_no(yo_wallet.account_line, organisation_id, 'qdf-wit')
                SystemTransactions.objects.create(
                    amount=float(amount),
                    heading=f"QDF Cash-out to {telephone} - {narration}",
                    reference_no=reference_no,
                    payment_method="yo",
                    branch_id=branch_id,
                    debit_chart=wallet_chart,
                    credit_chart=yo_wallet
                )

        return Response(response, status=status.HTTP_200_OK)


class YoCashInWebhookView(APIView):
    authentication_classes = []
    permission_classes = []

    def post(self, request, format=None):
        # Basic shared-secret validation
        webhook_token = request.headers.get('X-Webhook-Token')
        expected_token = config('YO_WEBHOOK_TOKEN', default='')
        if not expected_token or webhook_token != expected_token:
            return Response({"status": "failed", "message": "Unauthorized"}, status=status.HTTP_401_UNAUTHORIZED)

        organisation_id = request.data.get('organisation_id')
        branch_id = request.data.get('branch_id')
        amount = request.data.get('amount')
        target_type = request.data.get('target_type')  # 'org' | 'member'
        customer_id = request.data.get('customer_id')
        narration = request.data.get('narration', 'QDF Cash-in')
        status_flag = request.data.get('status')  # 'OK' | 'SUCCESS' etc

        if not organisation_id or not branch_id or not amount or target_type not in ['org', 'member']:
            return Response({"status": "failed", "message": "organisation_id, branch_id, amount, target_type required"}, status=status.HTTP_400_BAD_REQUEST)

        # Only process successful notifications
        if str(status_flag).upper() not in ['OK', 'SUCCESS', 'TS', '200']:
            return Response({"status": "ignored", "message": "Non-success status"}, status=status.HTTP_202_ACCEPTED)

        yo_wallet = get_yo_wallet_chart(organisation_id, branch_id)

        if target_type == 'org':
            wallet_chart = get_branch_wallet_chart(organisation_id, branch_id)
            reference_no = generate_reference_no(wallet_chart.account_line, organisation_id, 'qdf-in')
            SystemTransactions.objects.create(
                amount=float(amount),
                heading=narration,
                reference_no=reference_no,
                payment_method="yo",
                branch_id=branch_id,
                debit_chart=wallet_chart,
                credit_chart=yo_wallet
            )
        else:
            if not customer_id:
                return Response({"status": "failed", "message": "customer_id required for member target"}, status=status.HTTP_400_BAD_REQUEST)
            member_wallet_chart = get_member_wallet_chart(organisation_id, branch_id, customer_id)
            reference_no = generate_reference_no(member_wallet_chart.account_line, organisation_id, 'qdf-in')
            SystemTransactions.objects.create(
                amount=float(amount),
                heading=narration,
                reference_no=reference_no,
                payment_method="yo",
                branch_id=branch_id,
                debit_chart=member_wallet_chart,
                credit_chart=yo_wallet
            )

        return Response({"status": "success"}, status=status.HTTP_200_OK)

# ------------------ Organisation and Member Wallet Operations ------------------ #

class OrgWalletInitiateDepositView(APIView):
    permission_classes = [IsAuthenticated]
    throttle_classes = [ScopedRateThrottle]
    throttle_scope = 'org_wallet'

    def post(self, request, format=None):
        organisation_id = request.data.get('organisation_id')
        branch_id = request.data.get('branch_id')
        amount = request.data.get('amount')
        source_chart_id = request.data.get('source_chart_id')
        narration = request.data.get('narration', 'Organisation Wallet Top-up')

        if not organisation_id or not branch_id or not amount or not source_chart_id:
            return Response({"status": "failed", "message": "organisation_id, branch_id, amount, source_chart_id are required"}, status=status.HTTP_400_BAD_REQUEST)

        # Authorize: user must belong to organisation and branch
        if getattr(request.user, 'user_organisation_branch_id', None) != int(branch_id):
            return Response({"status": "failed", "message": "Forbidden for this branch"}, status=status.HTTP_403_FORBIDDEN)
        if request.user.user_organisation_branch.branch_organisation_id != int(organisation_id):
            return Response({"status": "failed", "message": "Forbidden for this organisation"}, status=status.HTTP_403_FORBIDDEN)

        wallet_chart = get_branch_wallet_chart(organisation_id, branch_id)
        source_chart = OrganisationSubAccount.objects.filter(id=source_chart_id, account_organisation_id=organisation_id).first()
        if not source_chart:
            return Response({"status": "failed", "message": "Invalid source_chart_id"}, status=status.HTTP_400_BAD_REQUEST)

        reference_no = generate_reference_no(wallet_chart.account_line, organisation_id, 'org-dep')
        data = {
            "amount": float(amount),
            "heading": narration,
            "reference_no": reference_no,
            "payment_method": "internal",
            "branch_id": branch_id,
            "debit_chart": source_chart,
            "credit_chart": wallet_chart
        }
        system_transaction = SystemTransactions.objects.create(**data)
        return Response({"status": "success", "transaction_id": system_transaction.id}, status=status.HTTP_200_OK)


class OrgWalletWithdrawView(APIView):
    permission_classes = [IsAuthenticated]
    throttle_classes = [ScopedRateThrottle]
    throttle_scope = 'org_wallet'

    def post(self, request, format=None):
        organisation_id = request.data.get('organisation_id')
        branch_id = request.data.get('branch_id')
        amount = request.data.get('amount')
        destination_chart_id = request.data.get('destination_chart_id')
        telephone = request.data.get('telephone')  # optional to initiate YO cash-out
        narration = request.data.get('narration', 'Organisation Wallet Withdrawal')

        if not organisation_id or not branch_id or not amount:
            return Response({"status": "failed", "message": "organisation_id, branch_id, amount are required"}, status=status.HTTP_400_BAD_REQUEST)

        # Authorize: user must belong to organisation and branch
        if getattr(request.user, 'user_organisation_branch_id', None) != int(branch_id):
            return Response({"status": "failed", "message": "Forbidden for this branch"}, status=status.HTTP_403_FORBIDDEN)
        if request.user.user_organisation_branch.branch_organisation_id != int(organisation_id):
            return Response({"status": "failed", "message": "Forbidden for this organisation"}, status=status.HTTP_403_FORBIDDEN)

        wallet_chart = get_branch_wallet_chart(organisation_id, branch_id)
        destination_chart = None
        if destination_chart_id:
            destination_chart = OrganisationSubAccount.objects.filter(id=destination_chart_id, account_organisation_id=organisation_id).first()

        reference_no = generate_reference_no(wallet_chart.account_line, organisation_id, 'org-wit')
        data = {
            "amount": float(amount),
            "heading": narration,
            "reference_no": reference_no,
            "payment_method": "internal",
            "branch_id": branch_id,
            "debit_chart": wallet_chart,
            "credit_chart": destination_chart if destination_chart else wallet_chart
        }
        # If no destination chart is provided, still log a transaction by crediting back same wallet (acts as placeholder).
        system_transaction = SystemTransactions.objects.create(**data)

        response = {"status": "success", "transaction_id": system_transaction.id}

        # Optional: Initiate QDF cash-out
        if telephone and len(telephone) >= 9:
            telephone = '256' + telephone[-9:]
            soap_url = config('YO_SOAP_API_URL')
            soap_username = config('YO_SOAP_API_USERNAME')
            soap_password = config('YO_SOAP_API_PASSWORD')
            headers = {"Content-Type": "text/xml; charset=utf-8"}
            ext_ref = hashlib.md5((telephone + str(time.time())).encode()).hexdigest()
            payload = f"""<?xml version="1.0" encoding="UTF-8"?>
                <AutoCreate>
                <Request>
                <APIUsername>{soap_username}</APIUsername>
                <APIPassword>{soap_password}</APIPassword>
                <Method>acwithdrawfunds</Method>
                <NonBlocking></NonBlocking>
                <Amount>{amount}</Amount>
                <Account>{telephone}</Account>
                <AccountProviderCode></AccountProviderCode>
                <Narrative>{narration}</Narrative>
                <ExternalReference>{ext_ref}</ExternalReference>
                <ProviderReferenceText></ProviderReferenceText>
                </Request>
                </AutoCreate>"""
            yo_response = consume_yo_api(soap_url, headers, payload)
            response['qdf_response'] = yo_response

            # On success, book QDF wallet -> org wallet
            if yo_response.get('Status') == 'OK':
                yo_wallet = get_yo_wallet_chart(organisation_id, branch_id)
                reference_no = generate_reference_no(yo_wallet.account_line, organisation_id, 'qdf-wit')
                SystemTransactions.objects.create(
                    amount=float(amount),
                    heading=f"QDF Cash-out to {telephone} - {narration}",
                    reference_no=reference_no,
                    payment_method="yo",
                    branch_id=branch_id,
                    debit_chart=wallet_chart,
                    credit_chart=yo_wallet
                )

        return Response(response, status=status.HTTP_200_OK)


class OrgWalletDisburseView(APIView):
    permission_classes = [IsAuthenticated]
    throttle_classes = [ScopedRateThrottle]
    throttle_scope = 'org_wallet'

    def post(self, request, format=None):
        organisation_id = request.data.get('organisation_id')
        branch_id = request.data.get('branch_id')
        disbursements = request.data.get('disbursements', [])  # [{customer_id, amount, narration}]

        if not organisation_id or not branch_id or not disbursements:
            return Response({"status": "failed", "message": "organisation_id, branch_id, disbursements are required"}, status=status.HTTP_400_BAD_REQUEST)

        # Authorize: user must belong to organisation and branch
        if getattr(request.user, 'user_organisation_branch_id', None) != int(branch_id):
            return Response({"status": "failed", "message": "Forbidden for this branch"}, status=status.HTTP_403_FORBIDDEN)
        if request.user.user_organisation_branch.branch_organisation_id != int(organisation_id):
            return Response({"status": "failed", "message": "Forbidden for this organisation"}, status=status.HTTP_403_FORBIDDEN)

        wallet_chart = get_branch_wallet_chart(organisation_id, branch_id)

        results = []
        for item in disbursements:
            customer_id = item.get('customer_id')
            amount = float(item.get('amount', 0))
            narration = item.get('narration', f"Disbursement to member {customer_id}")
            if not customer_id or amount <= 0:
                continue

            member_wallet_chart = get_member_wallet_chart(organisation_id, branch_id, customer_id)
            reference_no = generate_reference_no(wallet_chart.account_line, organisation_id, 'org-dsb')
            tx = SystemTransactions.objects.create(
                amount=amount,
                heading=narration,
                reference_no=reference_no,
                payment_method="internal",
                branch_id=branch_id,
                debit_chart=wallet_chart,
                credit_chart=member_wallet_chart
            )
            results.append({"customer_id": customer_id, "transaction_id": tx.id})

        return Response({"status": "success", "results": results, "count": len(results)}, status=status.HTTP_200_OK)


class MemberWalletDepositView(APIView):
    permission_classes = [IsAuthenticated]
    throttle_classes = [ScopedRateThrottle]
    throttle_scope = 'member_wallet'

    def post(self, request, format=None):
        organisation_id = request.data.get('organisation_id')
        branch_id = request.data.get('branch_id')
        customer_id = request.data.get('customer_id')
        amount = request.data.get('amount')
        narration = request.data.get('narration', 'Member Wallet Top-up')

        if not organisation_id or not branch_id or not customer_id or not amount:
            return Response({"status": "failed", "message": "organisation_id, branch_id, customer_id, amount are required"}, status=status.HTTP_400_BAD_REQUEST)

        # Authorize: user must belong to organisation and branch
        if getattr(request.user, 'user_organisation_branch_id', None) != int(branch_id):
            return Response({"status": "failed", "message": "Forbidden for this branch"}, status=status.HTTP_403_FORBIDDEN)
        if request.user.user_organisation_branch.branch_organisation_id != int(organisation_id):
            return Response({"status": "failed", "message": "Forbidden for this organisation"}, status=status.HTTP_403_FORBIDDEN)

        wallet_chart = get_branch_wallet_chart(organisation_id, branch_id)
        member_wallet_chart = get_member_wallet_chart(organisation_id, branch_id, customer_id)

        reference_no = generate_reference_no(wallet_chart.account_line, organisation_id, 'mem-dep')
        tx = SystemTransactions.objects.create(
            amount=float(amount),
            heading=narration,
            reference_no=reference_no,
            payment_method="internal",
            branch_id=branch_id,
            debit_chart=wallet_chart,
            credit_chart=member_wallet_chart
        )

        return Response({"status": "success", "transaction_id": tx.id}, status=status.HTTP_200_OK)

class MemberWalletWithdrawView(APIView):
    permission_classes = [IsAuthenticated]
    throttle_classes = [ScopedRateThrottle]
    throttle_scope = 'member_wallet'

    def post(self, request, format=None):
        organisation_id = request.data.get('organisation_id')
        branch_id = request.data.get('branch_id')
        customer_id = request.data.get('customer_id')
        amount = request.data.get('amount')
        telephone = request.data.get('telephone')  # optional YO cash-out
        narration = request.data.get('narration', 'Member Wallet Withdrawal')

        if not organisation_id or not branch_id or not customer_id or not amount:
            return Response({"status": "failed", "message": "organisation_id, branch_id, customer_id, amount are required"}, status=status.HTTP_400_BAD_REQUEST)

        # Authorize: user must belong to organisation and branch
        if getattr(request.user, 'user_organisation_branch_id', None) != int(branch_id):
            return Response({"status": "failed", "message": "Forbidden for this branch"}, status=status.HTTP_403_FORBIDDEN)
        if request.user.user_organisation_branch.branch_organisation_id != int(organisation_id):
            return Response({"status": "failed", "message": "Forbidden for this organisation"}, status=status.HTTP_403_FORBIDDEN)

        member_wallet_chart = get_member_wallet_chart(organisation_id, branch_id, customer_id)
        wallet_chart = get_branch_wallet_chart(organisation_id, branch_id)

        reference_no = generate_reference_no(wallet_chart.account_line, organisation_id, 'mem-wit')
        tx = SystemTransactions.objects.create(
            amount=float(amount),
            heading=narration,
            reference_no=reference_no,
            payment_method="internal",
            branch_id=branch_id,
            debit_chart=member_wallet_chart,
            credit_chart=wallet_chart
        )

        response = {"status": "success", "transaction_id": tx.id}

        # Optional: QDF cash-out
        if telephone and len(telephone) >= 9:
            telephone = '256' + telephone[-9:]
            soap_url = config('YO_SOAP_API_URL')
            soap_username = config('YO_SOAP_API_USERNAME')
            soap_password = config('YO_SOAP_API_PASSWORD')
            headers = {"Content-Type": "text/xml; charset=utf-8"}
            ext_ref = hashlib.md5((telephone + str(time.time())).encode()).hexdigest()
            payload = f"""<?xml version=\"1.0\" encoding=\"UTF-8\"?>
                <AutoCreate>
                <Request>
                <APIUsername>{soap_username}</APIUsername>
                <APIPassword>{soap_password}</APIPassword>
                <Method>acwithdrawfunds</Method>
                <NonBlocking></NonBlocking>
                <Amount>{amount}</Amount>
                <Account>{telephone}</Account>
                <AccountProviderCode></AccountProviderCode>
                <Narrative>{narration}</Narrative>
                <ExternalReference>{ext_ref}</ExternalReference>
                <ProviderReferenceText></ProviderReferenceText>
                </Request>
                </AutoCreate>"""
            yo_response = consume_yo_api(soap_url, headers, payload)
            response['qdf_response'] = yo_response
            if yo_response.get('Status') == 'OK':
                yo_wallet = get_yo_wallet_chart(organisation_id, branch_id)
                reference_no = generate_reference_no(yo_wallet.account_line, organisation_id, 'qdf-wit')
                SystemTransactions.objects.create(
                    amount=float(amount),
                    heading=f"QDF Cash-out to {telephone} - {narration}",
                    reference_no=reference_no,
                    payment_method="yo",
                    branch_id=branch_id,
                    debit_chart=wallet_chart,
                    credit_chart=yo_wallet
                )

        return Response(response, status=status.HTTP_200_OK)


class YoCashInWebhookView(APIView):
    authentication_classes = []
    permission_classes = []

    def post(self, request, format=None):
        # Basic shared-secret validation
        webhook_token = request.headers.get('X-Webhook-Token')
        expected_token = config('YO_WEBHOOK_TOKEN', default='')
        if not expected_token or webhook_token != expected_token:
            return Response({"status": "failed", "message": "Unauthorized"}, status=status.HTTP_401_UNAUTHORIZED)

        organisation_id = request.data.get('organisation_id')
        branch_id = request.data.get('branch_id')
        amount = request.data.get('amount')
        target_type = request.data.get('target_type')  # 'org' | 'member'
        customer_id = request.data.get('customer_id')
        narration = request.data.get('narration', 'YO Cash-in')
        status_flag = request.data.get('status')  # 'OK' | 'SUCCESS' etc

        if not organisation_id or not branch_id or not amount or target_type not in ['org', 'member']:
            return Response({"status": "failed", "message": "organisation_id, branch_id, amount, target_type required"}, status=status.HTTP_400_BAD_REQUEST)

        # Only process successful notifications
        if str(status_flag).upper() not in ['OK', 'SUCCESS', 'TS', '200']:
            return Response({"status": "ignored", "message": "Non-success status"}, status=status.HTTP_202_ACCEPTED)

        yo_wallet = get_yo_wallet_chart(organisation_id, branch_id)

        if target_type == 'org':
            wallet_chart = get_branch_wallet_chart(organisation_id, branch_id)
            reference_no = generate_reference_no(wallet_chart.account_line, organisation_id, 'yo-in')
            SystemTransactions.objects.create(
                amount=float(amount),
                heading=narration,
                reference_no=reference_no,
                payment_method="yo",
                branch_id=branch_id,
                debit_chart=wallet_chart,
                credit_chart=yo_wallet
            )
        else:
            if not customer_id:
                return Response({"status": "failed", "message": "customer_id required for member target"}, status=status.HTTP_400_BAD_REQUEST)
            member_wallet_chart = get_member_wallet_chart(organisation_id, branch_id, customer_id)
            reference_no = generate_reference_no(member_wallet_chart.account_line, organisation_id, 'yo-in')
            SystemTransactions.objects.create(
                amount=float(amount),
                heading=narration,
                reference_no=reference_no,
                payment_method="yo",
                branch_id=branch_id,
                debit_chart=member_wallet_chart,
                credit_chart=yo_wallet
            )

        return Response({"status": "success"}, status=status.HTTP_200_OK)
