<?php

namespace App\Models;

use CodeIgniter\Database\ConnectionInterface;
use CodeIgniter\Model;
use CodeIgniter\Validation\ValidationInterface;

class PaymentModel extends Model
{
    protected $table = 'payments';
    protected $primaryKey = 'id';
    protected $returnType = 'array';

    /**
     * Keep this list broad, then filter it in __construct() against the real DB schema.
     * This prevents CI from trying to insert/update columns that do not exist, such as `balance`.
     */
    protected $allowedFields = [
        'parent_id',
        'invoice_id',

        // Legacy invoice snapshot fields. Do not use these as source of truth.
        'total_amount',

        // Actual payment fields.
        'paid_amount',
        'balance',
        'balance_amount',
        'balance_after_payment',
        'number_of_installments',
        'installment_seq',
        'transaction_id',
        'check_file',
        'check_number',
        'payment_method',
        'payment_date',
        'school_year',
        'status',
        'updated_by',
    ];

    protected $useTimestamps = false;

    /**
     * Let DB defaults handle created_at / updated_at.
     * Keep validation strict for actual payment data, but do not require legacy snapshot columns.
     */
    protected $cleanValidationRules = true;

    protected $validationRules = [
        'parent_id' => 'required|integer',
        'invoice_id' => 'required|integer',

        // Legacy snapshot. Optional because invoice total belongs to invoices table.
        'total_amount' => 'permit_empty|decimal',

        'paid_amount' => 'required|decimal|greater_than[0]',

        // These are optional because not every live schema has a balance snapshot column.
        'balance' => 'permit_empty|decimal|greater_than_equal_to[0]',
        'balance_amount' => 'permit_empty|decimal|greater_than_equal_to[0]',
        'balance_after_payment' => 'permit_empty|decimal|greater_than_equal_to[0]',

        // Legacy/current sequence columns.
        'number_of_installments' => 'permit_empty|integer|greater_than[0]',
        'installment_seq' => 'permit_empty|integer|greater_than[0]',

        'transaction_id' => 'permit_empty|max_length[100]',
        'payment_method' => 'required|in_list[cash,check,card]',
        'payment_date' => 'required|valid_date',
        'school_year' => 'permit_empty|max_length[20]',
        'status' => 'required|max_length[50]',
        'check_file' => 'permit_empty|max_length[255]',
        'check_number' => 'permit_empty|max_length[100]',
        'updated_by' => 'permit_empty|integer',
    ];

    protected $validationMessages = [
        'parent_id' => [
            'required' => 'Parent ID is required.',
            'integer' => 'Parent ID must be an integer.',
        ],
        'invoice_id' => [
            'required' => 'Invoice ID is required.',
            'integer' => 'Invoice ID must be an integer.',
        ],
        'total_amount' => [
            'decimal' => 'Total amount must be a valid decimal.',
        ],
        'paid_amount' => [
            'required' => 'Paid amount is required.',
            'decimal' => 'Paid amount must be a valid decimal.',
            'greater_than' => 'Paid amount must be greater than zero.',
        ],
        'balance' => [
            'decimal' => 'Balance must be a valid decimal.',
            'greater_than_equal_to' => 'Balance must not be negative.',
        ],
        'balance_amount' => [
            'decimal' => 'Balance amount must be a valid decimal.',
            'greater_than_equal_to' => 'Balance amount must not be negative.',
        ],
        'balance_after_payment' => [
            'decimal' => 'Balance after payment must be a valid decimal.',
            'greater_than_equal_to' => 'Balance after payment must not be negative.',
        ],
        'payment_date' => [
            'required' => 'Payment date is required.',
            'valid_date' => 'Payment date must be a valid date.',
        ],
        'payment_method' => [
            'required' => 'Payment method is required.',
            'in_list' => 'Payment method must be cash, check, or card.',
        ],
        'status' => [
            'required' => 'Status is required.',
            'max_length' => 'Status must not exceed 50 characters.',
        ],
        'number_of_installments' => [
            'integer' => 'Number of installments must be an integer.',
            'greater_than' => 'Number of installments must be greater than zero.',
        ],
        'installment_seq' => [
            'integer' => 'Installment sequence must be an integer.',
            'greater_than' => 'Installment sequence must be greater than zero.',
        ],
        'check_number' => [
            'max_length' => 'Check number must not exceed 100 characters.',
        ],
    ];

    private array $paymentColumns = [];

    private array $excludedPaymentStatuses = [
        'void',
        'voided',
        'refunded',
        'failed',
        'chargeback',
        'declined',
        'reversed',
        'canceled',
        'cancelled',
    ];

    public function __construct(?ConnectionInterface $db = null, ?ValidationInterface $validation = null)
    {
        parent::__construct($db, $validation);

        $this->paymentColumns = $this->getTableColumns($this->table);

        // Remove fields that do not exist in the live DB.
        // This prevents errors like "Unknown column balance".
        $this->allowedFields = array_values(array_filter(
            $this->allowedFields,
            fn (string $field): bool => in_array($field, $this->paymentColumns, true)
        ));

        // Remove validation rules for columns that do not exist.
        foreach (array_keys($this->validationRules) as $field) {
            if (!in_array($field, $this->allowedFields, true)) {
                unset($this->validationRules[$field]);
                unset($this->validationMessages[$field]);
            }
        }
    }

    /**
     * Get payments by parent ID with invoice context.
     *
     * The source of truth for invoice total/status/balance is invoices, not payments.
     */
    public function getPaymentsByParentId(int $parentId, ?string $schoolYear = null): array
    {
        return $this->parentPaymentHistoryQuery($parentId, $schoolYear)->findAll();
    }

    /**
     * Build parent payment history query with invoice context.
     *
     * Kept public so controllers can paginate without duplicating select/join logic.
     */
    public function parentPaymentHistoryQuery(int $parentId, ?string $schoolYear = null): self
    {
        $paymentBalanceSelect = $this->getPaymentBalanceSelectExpression('payments');

        $invoiceBalanceSelect = $this->columnExists('invoices', 'balance')
            ? 'invoices.balance AS invoice_current_balance'
            : '0.00 AS invoice_current_balance';

        $select = [
            'payments.id',
            'payments.invoice_id',
            'invoices.invoice_number',
            'payments.transaction_id',
            'payments.paid_amount',
            'payments.payment_method',
            'payments.check_number',
            'payments.check_file',
            'payments.payment_date',
            'payments.installment_seq',
            'payments.number_of_installments',
            $paymentBalanceSelect,
            'payments.status AS payment_status',
            'invoices.total_amount AS invoice_total',
            $invoiceBalanceSelect,
            'invoices.status AS invoice_status',
            'invoices.school_year',
        ];

        $model = new self($this->db);
        $builder = $model->select(implode(', ', $select), false)
            ->join('invoices', 'invoices.id = payments.invoice_id', 'inner')
            ->where('payments.parent_id', $parentId);

        if ($schoolYear !== null && $schoolYear !== '') {
            $builder->where('invoices.school_year', $schoolYear);
        }

        return $builder
            ->orderBy('payments.payment_date', 'DESC')
            ->orderBy('payments.id', 'DESC');
    }

    /**
     * Calculate the total paid amount for a parent in a school year.
     *
     * Uses invoices.school_year so old poisoned payment term data does not corrupt totals.
     */
    public function getTotalPaidByParentId(int $parentId, string $schoolYear): float
    {
        $result = $this->db->table('payments p')
            ->selectSum('p.paid_amount', 'total_paid')
            ->join('invoices i', 'i.id = p.invoice_id', 'inner')
            ->where('p.parent_id', $parentId)
            ->where('i.school_year', $schoolYear)
            ->where($this->successfulPaymentWhereSql('p.status'), null, false)
            ->get()
            ->getRowArray();

        return $result && isset($result['total_paid'])
            ? (float) $result['total_paid']
            : 0.00;
    }

    /**
     * Legacy method.
     *
     * This should not be used for new payment recording.
     * New payments should be inserted as immutable payment rows and invoices recalculated by InvoiceLedgerService.
     */
    public function updateBalance(int $paymentId, float $amountPaid): bool
    {
        if ($amountPaid <= 0) {
            return false;
        }

        $payment = $this->find($paymentId);
        if (!$payment) {
            return false;
        }

        $updateData = [
            'paid_amount' => round((float) ($payment['paid_amount'] ?? 0) + $amountPaid, 2),
        ];

        $balanceField = $this->getPaymentBalanceField();
        if ($balanceField !== null && array_key_exists($balanceField, $payment)) {
            $updateData[$balanceField] = max(
                0.00,
                round((float) ($payment[$balanceField] ?? 0) - $amountPaid, 2)
            );
        }

        return $this->update($paymentId, $updateData);
    }

    /**
     * Get payments by school year.
     *
     * Uses invoice school year when possible because payment.school_year may have old bad data.
     */
    public function getPaymentsByYear(string $schoolYear): array
    {
        $paymentBalanceSelect = $this->getPaymentBalanceSelectExpression('payments');

        $select = [
            'payments.*',
            'invoices.invoice_number',
            'invoices.total_amount AS invoice_total',
            'invoices.status AS invoice_status',
            $paymentBalanceSelect,
        ];

        return $this->select(implode(', ', $select), false)
            ->join('invoices', 'invoices.id = payments.invoice_id', 'inner')
            ->where('invoices.school_year', $schoolYear)
            ->orderBy('payments.payment_date', 'DESC')
            ->orderBy('payments.id', 'DESC')
            ->findAll();
    }

    /**
     * Generate a transaction ID.
     *
     * This is okay for legacy/manual use, but a DB UNIQUE KEY on transaction_id is still required.
     */
    public function generateNewTransactionId(): string
    {
        $currentYear = date('Y');
        $prefix = $currentYear . '-';

        $latest = $this->like('transaction_id', $prefix, 'after')
            ->orderBy('transaction_id', 'DESC')
            ->first();

        if ($latest && isset($latest['transaction_id'])) {
            $lastNumber = (int) str_replace($prefix, '', (string) $latest['transaction_id']);
            $newNumber = $lastNumber + 1;
        } else {
            $newNumber = 1;
        }

        return $prefix . str_pad((string) $newNumber, 6, '0', STR_PAD_LEFT);
    }

    /**
     * Get latest successful payment per invoice.
     *
     * Uses latest payment ID instead of latest date only.
     * Date-only matching can return duplicates when several payments happen on the same day.
     */
    public function getPaymentsByInvoice($invoiceIds): array
    {
        if (!is_array($invoiceIds)) {
            $invoiceIds = [$invoiceIds];
        }

        $invoiceIds = array_values(array_filter(array_map('intval', $invoiceIds)));

        if (empty($invoiceIds)) {
            return [];
        }

        $latestSub = $this->db->table('payments')
            ->select('invoice_id, MAX(id) AS max_id')
            ->whereIn('invoice_id', $invoiceIds)
            ->where('paid_amount >', 0)
            ->where($this->successfulPaymentWhereSql('status'), null, false)
            ->groupBy('invoice_id')
            ->getCompiledSelect();

        return $this->db->table('payments p')
            ->select([
                'p.invoice_id',
                'p.id AS payment_id',
                'p.paid_amount AS last_paid_amount',
                'p.payment_date AS last_payment_date',
                'p.status AS last_payment_status',
                'p.transaction_id',
                'p.payment_method',
                'p.check_number',
            ])
            ->join(
                "($latestSub) latest",
                'latest.invoice_id = p.invoice_id AND latest.max_id = p.id',
                'inner',
                false
            )
            ->whereIn('p.invoice_id', $invoiceIds)
            ->get()
            ->getResultArray();
    }

    private function getPaymentBalanceField(): ?string
    {
        foreach (['balance_after_payment', 'balance', 'balance_amount'] as $field) {
            if (in_array($field, $this->paymentColumns, true)) {
                return $field;
            }
        }

        return null;
    }

    private function getPaymentBalanceSelectExpression(string $alias = 'payments'): string
    {
        $field = $this->getPaymentBalanceField();

        if ($field === null) {
            return '0.00 AS balance_after_payment';
        }

        return "{$alias}.{$field} AS balance_after_payment";
    }

    private function successfulPaymentWhereSql(string $column): string
    {
        $escapedStatuses = array_map(
            fn (string $status): string => $this->db->escape($status),
            $this->excludedPaymentStatuses
        );

        return 'LOWER(COALESCE(' . $column . ', \'\')) NOT IN (' . implode(', ', $escapedStatuses) . ')';
    }

    private function getTableColumns(string $table): array
    {
        try {
            return $this->db->getFieldNames($table);
        } catch (\Throwable $e) {
            log_message('error', '[PaymentModel] Could not read table columns for {table}: {error}', [
                'table' => $table,
                'error' => $e->getMessage(),
            ]);

            return [];
        }
    }

    private function columnExists(string $table, string $column): bool
    {
        try {
            return $this->db->fieldExists($column, $table);
        } catch (\Throwable $e) {
            return false;
        }
    }
}
