<?php
// Custom Exceptions
class InvalidLoanTypeException extends Exception {}
class InvalidLoanAmountException extends Exception {}
class InvalidLoanPeriodException extends Exception {}
class InvalidChargeMethodException extends Exception {}
class InvalidChargeAmountException extends Exception {}
class InvalidChargeTypeException extends Exception {}
class InvalidAmortizationTypeException extends Exception {}
class InsufficientGuarantorBalanceException extends Exception {}

// GuarantorHandler Class
class GuarantorHandler
{
    private $pdo;

    public function __construct(PDO $pdo)
    {
        $this->pdo = $pdo;
    }

    /**
     * Validate guarantors' balances for a loan application.
     *
     * @param array $guarantorIds List of guarantor member IDs.
     * @param float $guaranteeRequired The amount that needs to be guaranteed.
     * @param string $basisSavingsAccountType The account type used for guaranteeing.
     * @return bool True if guarantors' balances are sufficient.
     * @throws InsufficientGuarantorBalanceException If guarantors' balances are insufficient.
     */
    public function validateGuarantors(array $guarantorIds, float $guaranteeRequired, string $basisSavingsAccountType): bool
    {
        // Fetch total available balance of all guarantors
        $totalGuarantorBalance = $this->getTotalGuarantorBalance($guarantorIds, $basisSavingsAccountType);

        // Check if the total balance meets the required guarantee
        if ($totalGuarantorBalance < $guaranteeRequired) {
            throw new InsufficientGuarantorBalanceException("Guarantors' balances are insufficient. Required: {$guaranteeRequired}, Available: {$totalGuarantorBalance}.");
        }

        return true;
    }

    /**
     * Calculate the total available balance of all guarantors.
     *
     * @param array $guarantorIds List of guarantor member IDs.
     * @param string $basisSavingsAccountType The account type used for guaranteeing.
     * @return float The total available balance of all guarantors.
     */
    private function getTotalGuarantorBalance(array $guarantorIds, string $basisSavingsAccountType): float
    {
        $placeholders = implode(',', array_fill(0, count($guarantorIds), '?'));
        $sql = "SELECT SUM(available_balance) AS total_balance
                FROM member_accounts
                WHERE member_id IN ($placeholders)
                  AND account_type = ?";
        $stmt = $this->pdo->prepare($sql);

        // Bind guarantor IDs and account type
        $params = array_merge($guarantorIds, [$basisSavingsAccountType]);
        $stmt->execute($params);

        $result = $stmt->fetch(PDO::FETCH_ASSOC);
        return (float) $result['total_balance'];
    }
}

// LoanHandler Class
class LoanHandler
{
    private $pdo;
    private $guarantorHandler;

    public function __construct(PDO $pdo, GuarantorHandler $guarantorHandler)
    {
        $this->pdo = $pdo;
        $this->guarantorHandler = $guarantorHandler;
    }

    /**
     * Fetch loan conditions by type code.
     *
     * @param string $typeCode The loan type code.
     * @return array The loan conditions.
     * @throws InvalidLoanTypeException If the loan type is invalid.
     */
    public function getLoanConditions(int $accountTypeId): array
    {
        $sql = "SELECT * FROM account_types WHERE id = :account_type_id";
        $stmt = $this->pdo->prepare($sql);
        $stmt->execute([':account_type_id' => $accountTypeId]);
        $conditions = $stmt->fetch(PDO::FETCH_ASSOC);

        if (!$conditions) {
            throw new InvalidLoanTypeException("Invalid loan type.");
        }

        return $conditions;
    }

    /**
     * Calculate maximum loan eligibility and check if guarantors are required.
     *
     * @param int $memberId The member ID of the applicant.
     * @param float $requestedLoanAmount The loan amount requested by the applicant.
     * @param string $typeCode The loan type code.
     * @return array An array containing the maximum loan eligibility, guarantee required, and guarantor validation status.
     * @throws InvalidLoanTypeException If the loan type is invalid.
     * @throws InsufficientGuarantorBalanceException If guarantors' balances are insufficient.
     */
    public function calculateLoanEligibility(int $memberId, float $requestedLoanAmount, string $typeCode): array
    {
        // Fetch loan conditions
        $conditions = $this->getLoanConditions($typeCode);
        $basisSavingsAccountType = $conditions['basis_savings_account_type'];
        $maxLoanFactor = $conditions['max_loan_factor'];

        // Fetch member's available balance
        $availableBalance = $this->getMemberAvailableBalance($memberId, $basisSavingsAccountType);

        // Calculate maximum loan eligibility
        $maxLoanEligibility = $availableBalance * $maxLoanFactor;

        // Determine if guarantors are required
        $guaranteeRequired = max(0, $requestedLoanAmount - $maxLoanEligibility);

        // If guarantors are required, validate their balances
        $guarantorValidation = false;
        if ($guaranteeRequired > 0) {
            // Example: Fetch guarantor IDs (this can be dynamic based on user input)
            $guarantorIds = [2, 3, 4]; // Replace with actual guarantor IDs
            $guarantorValidation = $this->guarantorHandler->validateGuarantors($guarantorIds, $guaranteeRequired, $basisSavingsAccountType);
        }

        return [
            'max_loan_eligibility' => $maxLoanEligibility,
            'guarantee_required' => $guaranteeRequired,
            'guarantor_validation' => $guarantorValidation,
        ];
    }

    /**
     * Fetch member's available balance from the member_accounts table.
     *
     * @param int $memberId The member ID.
     * @param string $accountType The account type.
     * @return float The available balance.
     */
    private function getMemberAvailableBalance(int $memberId, string $accountType): float
    {
        $sql = "SELECT available_balance
                FROM member_accounts
                WHERE member_id = :member_id
                  AND account_type = :account_type";
        $stmt = $this->pdo->prepare($sql);
        $stmt->execute([':member_id' => $memberId, ':account_type' => $accountType]);
        $result = $stmt->fetch(PDO::FETCH_ASSOC);

        return (float) $result['available_balance'];
    }

    /**
     * Validate loan application.
     *
     * @param float $loanAmount The loan amount.
     * @param int $loanPeriod The loan period in months.
     * @param string $typeCode The loan type code.
     * @return bool True if the loan application is valid.
     * @throws InvalidLoanAmountException If the loan amount is invalid.
     * @throws InvalidLoanPeriodException If the loan period is invalid.
     */
    public function validateLoanApplication(float $loanAmount, int $loanPeriod, string $typeCode): bool
    {
        $conditions = $this->getLoanConditions($typeCode);

        // Validate loan amount
        if ($loanAmount < $conditions['min_loan_amount'] || $loanAmount > $conditions['max_loan_amount']) {
            throw new InvalidLoanAmountException("Loan amount must be between {$conditions['min_loan_amount']} and {$conditions['max_loan_amount']}.");
        }

        // Validate loan period
        if ($loanPeriod > $conditions['max_period']) {
            throw new InvalidLoanPeriodException("Loan period cannot exceed {$conditions['max_period']} months.");
        }

        return true;
    }

    /**
     * Calculate loan charges and adjust loan amount.
     *
     * @param float $loanAmount The loan amount.
     * @param string $typeCode The loan type code.
     * @return array An array containing the charges and adjusted loan amount.
     * @throws InvalidLoanTypeException If the loan type is invalid.
     * @throws InvalidChargeMethodException If the charge method is invalid.
     * @throws InvalidChargeAmountException If the charge amount is invalid.
     * @throws InvalidChargeTypeException If the charge type is invalid.
     */
    public function calculateCharges(float $loanAmount, string $typeCode): array
    {
        $conditions = $this->getLoanConditions($typeCode);

        // List of charges
        $chargeTypes = ['loan_processing_fee', 'loan_insurance', 'service_charge', 'bridge_charge'];
        $charges = [];
        $adjustedLoanAmount = $loanAmount;

        foreach ($chargeTypes as $chargeType) {
            // Fetch charge amount & type, default to 0
            $chargeAmount = $conditions[$chargeType] ?? 0;
            $chargeTypeMethod = $conditions[$chargeType . '_type'] ?? 'fixed';

            // Calculate charge
            $calculatedCharge = $this->calculateCharge($loanAmount, $chargeAmount, $chargeTypeMethod);
            $charges[$chargeType] = $calculatedCharge;

            // Fetch charge method (default: topped_up)
            $chargeMethod = strtolower($conditions[$chargeType . '_method'] ?? 'topped_up');

            // Deduct or top up loan amount
            if ($chargeMethod === 'deducted') {
                $adjustedLoanAmount -= $calculatedCharge;
            } elseif ($chargeMethod === 'topped_up') {
                $adjustedLoanAmount += $calculatedCharge;
            } else {
                throw new InvalidChargeMethodException("Invalid charge method: {$chargeMethod} for {$chargeType}.");
            }
        }

        // Ensure adjusted loan amount is not negative
        if ($adjustedLoanAmount < 0) {
            throw new InvalidLoanAmountException("Adjusted loan amount cannot be negative.");
        }

        // Calculate total charges
        $charges['total_charges'] = array_sum($charges);

        return [
            'charges' => $charges,
            'adjusted_loan_amount' => $adjustedLoanAmount,
        ];
    }

    /**
     * Calculate charge based on type (fixed or percentage).
     *
     * @param float $loanAmount The loan amount.
     * @param float $chargeAmount The charge amount.
     * @param string $chargeType The charge type ('fixed' or 'percentage').
     * @return float The calculated charge.
     * @throws InvalidChargeAmountException If the charge amount is invalid.
     * @throws InvalidChargeTypeException If the charge type is invalid.
     */
    private function calculateCharge(float $loanAmount, float $chargeAmount, string $chargeType): float
    {
        if (!is_numeric($chargeAmount) || $chargeAmount < 0) {
            throw new InvalidChargeAmountException("Invalid charge amount: {$chargeAmount}.");
        }

        if ($chargeType === 'fixed' || $chargeType === 'flat') {
            return $chargeAmount;
        } elseif ($chargeType === 'percentage') {
            return $loanAmount * ($chargeAmount / 100);
        } else {
            throw new InvalidChargeTypeException("Invalid charge type: {$chargeType}.");
        }
    }

    /**
     * Calculate monthly repayment and total interest.
     *
     * @param float $loanAmount The loan amount.
     * @param int $loanPeriod The loan period in months.
     * @param string $typeCode The loan type code.
     * @return array An array containing the monthly installment and total repayment.
     * @throws InvalidLoanTypeException If the loan type is invalid.
     * @throws InvalidAmortizationTypeException If the amortization type is invalid.
     */
    public function calculateLoanRepayment(float $loanAmount, int $loanPeriod, string $typeCode): array
    {
        $conditions = $this->getLoanConditions($typeCode);

        $interestRate = $conditions['interest_rate'] ?? 0;
        $amortizationType = strtolower($conditions['amortization_type'] ?? 'reducing');

        // Convert annual rate to monthly rate
        $monthlyRate = $interestRate / 100 / 12;

        // Monthly installment calculation based on amortization type
        switch ($amortizationType) {
            case 'reducing':
                // Reducing balance amortization
                if ($monthlyRate > 0) {
                    $monthlyInstallment = ($loanAmount * $monthlyRate) / (1 - pow(1 + $monthlyRate, -$loanPeriod));
                } else {
                    $monthlyInstallment = $loanAmount / $loanPeriod;
                }
                break;

            case 'flat':
                // Flat rate amortization
                $totalInterest = ($loanAmount * $interestRate / 100) * ($loanPeriod / 12);
                $totalRepayment = $loanAmount + $totalInterest;
                $monthlyInstallment = $totalRepayment / $loanPeriod;
                break;

            case 'constant':
                // Constant amortization (equal principal payments + interest)
                $principalPayment = $loanAmount / $loanPeriod;
                $monthlyInstallment = $principalPayment + ($loanAmount * $monthlyRate);
                break;

            default:
                throw new InvalidAmortizationTypeException("Invalid amortization type: {$amortizationType}.");
        }

        return [
            'monthly_installment' => round($monthlyInstallment, 2),
            'total_repayment' => round($monthlyInstallment * $loanPeriod, 2),
        ];
    }

    /**
     * Generate repayment schedule.
     *
     * @param float $loanAmount The loan amount.
     * @param int $loanPeriod The loan period in months.
     * @param string $typeCode The loan type code.
     * @return array The repayment schedule.
     * @throws InvalidLoanTypeException If the loan type is invalid.
     * @throws InvalidAmortizationTypeException If the amortization type is invalid.
     */
    public function generateRepaymentSchedule(float $loanAmount, int $loanPeriod, string $typeCode): array
    {
        $conditions = $this->getLoanConditions($typeCode);

        $interestRate = $conditions['interest_rate'] ?? 0;
        $amortizationType = strtolower($conditions['amortization_type'] ?? 'reducing');

        // Convert annual rate to monthly rate
        $monthlyRate = $interestRate / 100 / 12;

        $schedule = [];
        $remainingBalance = $loanAmount;

        for ($month = 1; $month <= $loanPeriod; $month++) {
            $interest = $remainingBalance * $monthlyRate;

            switch ($amortizationType) {
                case 'reducing':
                    $principal = ($loanAmount * $monthlyRate) / (1 - pow(1 + $monthlyRate, -$loanPeriod)) - $interest;
                    break;

                case 'flat':
                    $principal = $loanAmount / $loanPeriod;
                    break;

                case 'constant':
                    $principal = $loanAmount / $loanPeriod;
                    break;

                default:
                    throw new InvalidAmortizationTypeException("Invalid amortization type: {$amortizationType}.");
            }

            $totalPayment = $principal + $interest;
            $remainingBalance -= $principal;

            $schedule[] = [
                'month' => $month,
                'principal' => round($principal, 2),
                'interest' => round($interest, 2),
                'total_payment' => round($totalPayment, 2),
                'remaining_balance' => round($remainingBalance, 2),
            ];
        }

        return $schedule;
    }
}