<?php
namespace App\Services;

use App\Core\Audit;
use App\Core\Auth;
use App\Core\Database;
use App\Core\Dates;
use App\Core\Num;
use App\Core\Settings;
use App\Models\Payment;

/**
 * Pengelolaan transaksi pembayaran.
 *
 * Satu pembayaran dapat dialokasikan ke banyak invoice (payment_allocations),
 * dan satu invoice dapat menerima banyak pembayaran (DP, termin, pelunasan).
 * Seluruh operasi berjalan di dalam transaksi database dan selalu memicu
 * perhitungan ulang saldo invoice terkait.
 */
class PaymentService
{
    public const METHODS = [
        'TRANSFER' => 'Transfer Bank', 'TUNAI' => 'Tunai', 'CEK' => 'Cek',
        'BILYET_GIRO' => 'Bilyet Giro', 'VIRTUAL_ACCOUNT' => 'Virtual Account',
        'KARTU_KREDIT' => 'Kartu Kredit', 'KOMPENSASI' => 'Kompensasi', 'LAINNYA' => 'Lainnya',
    ];

    public const TYPES = [
        'PELUNASAN' => 'Pelunasan', 'SEBAGIAN' => 'Pembayaran Sebagian', 'DP' => 'Uang Muka (DP)',
        'RETENSI' => 'Pencairan Retensi', 'REFUND' => 'Pengembalian Dana (Refund)',
        'REVERSAL' => 'Pembalikan (Reversal)', 'PENYESUAIAN' => 'Penyesuaian',
    ];

    /**
     * Menyimpan pembayaran baru beserta alokasinya.
     *
     * @param array $data  header pembayaran
     * @param array $allocations [['invoice_id'=>int,'amount'=>float,'notes'=>string], ...]
     */
    public static function create(array $data, array $allocations): array
    {
        $db = Database::i();
        return $db->transaction(function () use ($db, $data, $allocations) {
            $pm = new Payment();
            $date = $data['payment_date'] ?? Dates::today();

            $alloc = [];
            $sum = 0.0;
            foreach ($allocations as $a) {
                $invId = (int)($a['invoice_id'] ?? 0);
                $amt = round((float)($a['amount'] ?? 0), 2);
                if ($invId <= 0 || abs($amt) < 0.005) continue;
                $alloc[] = ['invoice_id' => $invId, 'amount' => $amt, 'notes' => $a['notes'] ?? null];
                $sum += $amt;
            }
            if (!$alloc) throw new \RuntimeException('Pembayaran harus dialokasikan ke minimal satu invoice.');

            $amount = isset($data['amount']) && (float)$data['amount'] > 0 ? round((float)$data['amount'], 2) : $sum;
            if (abs($amount - $sum) > 0.5) {
                throw new \RuntimeException('Total alokasi (' . Num::rupiah($sum) . ') tidak sama dengan nilai pembayaran (' . Num::rupiah($amount) . ').');
            }

            // Validasi kelebihan bayar
            if (!Settings::bool('ar.izinkan_kelebihan_bayar', true)) {
                foreach ($alloc as $a) {
                    $inv = $db->one('SELECT invoice_no, outstanding_amount FROM invoices WHERE id = ?', [$a['invoice_id']]);
                    if ($inv && $a['amount'] > (float)$inv['outstanding_amount'] + 0.5) {
                        throw new \RuntimeException('Alokasi untuk invoice ' . $inv['invoice_no'] . ' (' . Num::rupiah($a['amount'])
                            . ') melebihi saldo ' . Num::rupiah((float)$inv['outstanding_amount']) . '.');
                    }
                }
            }

            $needApproval = Settings::bool('ar.wajib_approval_pembayaran', false)
                            && in_array($data['payment_type'] ?? 'PELUNASAN', ['REFUND', 'REVERSAL', 'PENYESUAIAN'], true);

            $paymentId = $db->insert('payments', [
                'payment_no'      => $data['payment_no'] ?? $pm->nextNo($date),
                'payment_date'    => $date,
                'customer_id'     => $data['customer_id'] ?? null,
                'bank_id'         => $data['bank_id'] ?? null,
                'method'          => $data['method'] ?? 'TRANSFER',
                'reference_no'    => $data['reference_no'] ?? null,
                'payment_type'    => $data['payment_type'] ?? 'PELUNASAN',
                'amount'          => $amount,
                'allocated_amount'=> $sum,
                'notes'           => $data['notes'] ?? null,
                'status'          => $needApproval ? 'draft' : 'posted',
                'approval_status' => $needApproval ? 'pending' : 'not_required',
                'created_by'      => Auth::id() ?: null,
            ]);

            foreach ($alloc as $a) {
                $db->insert('payment_allocations', [
                    'payment_id' => $paymentId,
                    'invoice_id' => $a['invoice_id'],
                    'amount'     => $a['amount'],
                    'notes'      => $a['notes'],
                ]);
                ArCalculator::recalculate($a['invoice_id']);
            }

            Audit::log('pembayaran', 'create', 'payment', $paymentId,
                'Pembayaran ' . Num::rupiah($amount) . ' untuk ' . count($alloc) . ' invoice', null,
                ['amount' => $amount, 'payment_date' => $date, 'alokasi' => count($alloc)]);

            if ($needApproval) {
                ApprovalService::request('pembayaran', 'adjust', 'payment', $paymentId,
                    'Persetujuan pembayaran ' . ($data['payment_type'] ?? '') . ' ' . Num::rupiah($amount), $amount);
            }

            return ['sukses' => true, 'payment_id' => $paymentId, 'perlu_persetujuan' => $needApproval,
                    'pesan' => $needApproval ? 'Pembayaran disimpan dan menunggu persetujuan.' : 'Pembayaran berhasil disimpan.'];
        });
    }

    /** Memperbarui pembayaran & alokasinya. */
    public static function update(int $paymentId, array $data, array $allocations): array
    {
        $db = Database::i();
        return $db->transaction(function () use ($db, $paymentId, $data, $allocations) {
            $before = $db->one('SELECT * FROM payments WHERE id = :id', ['id' => $paymentId]);
            if (!$before) throw new \RuntimeException('Pembayaran tidak ditemukan.');
            if ($before['status'] === 'reversed') throw new \RuntimeException('Pembayaran yang sudah dibalik tidak dapat diubah.');

            $affected = array_map('intval', $db->column(
                'SELECT invoice_id FROM payment_allocations WHERE payment_id = ? AND deleted_at IS NULL', [$paymentId]
            ));

            $db->exec('UPDATE payment_allocations SET deleted_at = NOW() WHERE payment_id = ? AND deleted_at IS NULL', [$paymentId]);

            $sum = 0.0;
            foreach ($allocations as $a) {
                $invId = (int)($a['invoice_id'] ?? 0);
                $amt = round((float)($a['amount'] ?? 0), 2);
                if ($invId <= 0 || abs($amt) < 0.005) continue;
                $db->insert('payment_allocations', ['payment_id' => $paymentId, 'invoice_id' => $invId, 'amount' => $amt, 'notes' => $a['notes'] ?? null]);
                $sum += $amt;
                $affected[] = $invId;
            }
            if ($sum <= 0) throw new \RuntimeException('Pembayaran harus dialokasikan ke minimal satu invoice.');

            $upd = array_intersect_key($data, array_flip([
                'payment_date', 'customer_id', 'bank_id', 'method', 'reference_no', 'payment_type', 'notes',
            ]));
            $upd['amount'] = round((float)($data['amount'] ?? $sum), 2);
            $upd['allocated_amount'] = $sum;
            $db->update('payments', $upd, 'id = :pk', ['pk' => $paymentId]);

            foreach (array_unique($affected) as $iid) ArCalculator::recalculate($iid);

            Audit::logDiff('pembayaran', 'update', 'payment', $paymentId, $before, $upd, 'Pembayaran diubah');
            return ['sukses' => true, 'pesan' => 'Pembayaran berhasil diperbarui.'];
        });
    }

    /**
     * Membalik pembayaran (reversal): membuat pembayaran negatif berpasangan
     * sehingga jejak audit tetap utuh dan saldo invoice kembali.
     */
    public static function reverse(int $paymentId, string $reason): array
    {
        $db = Database::i();
        return $db->transaction(function () use ($db, $paymentId, $reason) {
            $p = $db->one('SELECT * FROM payments WHERE id = :id AND deleted_at IS NULL', ['id' => $paymentId]);
            if (!$p) throw new \RuntimeException('Pembayaran tidak ditemukan.');
            if ($p['status'] === 'reversed') throw new \RuntimeException('Pembayaran ini sudah dibalik sebelumnya.');

            $allocs = $db->all('SELECT * FROM payment_allocations WHERE payment_id = ? AND deleted_at IS NULL', [$paymentId]);
            $pm = new Payment();
            $revId = $db->insert('payments', [
                'payment_no'      => $pm->nextNo(),
                'payment_date'    => Dates::today(),
                'customer_id'     => $p['customer_id'],
                'bank_id'         => $p['bank_id'],
                'method'          => $p['method'],
                'reference_no'    => $p['reference_no'],
                'payment_type'    => 'REVERSAL',
                'amount'          => -1 * (float)$p['amount'],
                'allocated_amount'=> -1 * (float)$p['allocated_amount'],
                'notes'           => 'Pembalikan atas ' . $p['payment_no'] . '. Alasan: ' . $reason,
                'status'          => 'posted',
                'approval_status' => 'approved',
                'reversal_of_payment_id' => $paymentId,
                'reversal_reason' => mb_substr($reason, 0, 400),
                'created_by'      => Auth::id() ?: null,
                'approved_by'     => Auth::id() ?: null,
                'approved_at'     => Dates::now(),
            ]);
            foreach ($allocs as $a) {
                $db->insert('payment_allocations', [
                    'payment_id' => $revId, 'invoice_id' => (int)$a['invoice_id'],
                    'amount' => -1 * (float)$a['amount'], 'notes' => 'Pembalikan',
                ]);
            }
            $db->update('payments', ['status' => 'reversed', 'reversal_reason' => mb_substr($reason, 0, 400)], 'id = :pk', ['pk' => $paymentId]);

            foreach ($allocs as $a) ArCalculator::recalculate((int)$a['invoice_id']);

            Audit::log('pembayaran', 'reversal', 'payment', $paymentId,
                'Pembayaran ' . $p['payment_no'] . ' dibalik. Alasan: ' . $reason, ['status' => $p['status']], ['status' => 'reversed']);

            return ['sukses' => true, 'reversal_id' => $revId, 'pesan' => 'Pembayaran berhasil dibalik.'];
        });
    }

    /** Hapus lunak pembayaran (hanya untuk koreksi input, bukan pembatalan finansial). */
    public static function delete(int $paymentId, string $reason = ''): array
    {
        $db = Database::i();
        return $db->transaction(function () use ($db, $paymentId, $reason) {
            $p = $db->one('SELECT * FROM payments WHERE id = :id AND deleted_at IS NULL', ['id' => $paymentId]);
            if (!$p) throw new \RuntimeException('Pembayaran tidak ditemukan.');
            $invIds = array_map('intval', $db->column('SELECT invoice_id FROM payment_allocations WHERE payment_id = ? AND deleted_at IS NULL', [$paymentId]));
            $db->exec('UPDATE payment_allocations SET deleted_at = NOW() WHERE payment_id = ?', [$paymentId]);
            $db->exec('UPDATE payments SET deleted_at = NOW() WHERE id = ?', [$paymentId]);
            foreach ($invIds as $iid) ArCalculator::recalculate($iid);
            Audit::log('pembayaran', 'delete', 'payment', $paymentId,
                'Pembayaran ' . $p['payment_no'] . ' dihapus. Alasan: ' . $reason, $p, null);
            return ['sukses' => true, 'pesan' => 'Pembayaran dihapus dan saldo invoice dihitung ulang.'];
        });
    }

    /** Menyetujui pembayaran yang tertunda. */
    public static function approve(int $paymentId): array
    {
        $db = Database::i();
        return $db->transaction(function () use ($db, $paymentId) {
            $p = $db->one('SELECT * FROM payments WHERE id = :id', ['id' => $paymentId]);
            if (!$p) throw new \RuntimeException('Pembayaran tidak ditemukan.');
            $db->update('payments', [
                'status' => 'posted', 'approval_status' => 'approved',
                'approved_by' => Auth::id() ?: null, 'approved_at' => Dates::now(),
            ], 'id = :pk', ['pk' => $paymentId]);
            foreach ($db->column('SELECT invoice_id FROM payment_allocations WHERE payment_id = ? AND deleted_at IS NULL', [$paymentId]) as $iid) {
                ArCalculator::recalculate((int)$iid);
            }
            Audit::log('pembayaran', 'approve', 'payment', $paymentId, 'Pembayaran ' . $p['payment_no'] . ' disetujui');
            return ['sukses' => true, 'pesan' => 'Pembayaran disetujui dan saldo invoice diperbarui.'];
        });
    }

    /**
     * Alokasi otomatis: membagi sebuah nilai pembayaran ke invoice-invoice
     * outstanding milik customer, mulai dari yang paling tua (FIFO).
     */
    public static function suggestAllocation(int $customerId, float $amount): array
    {
        $rows = Database::i()->all(
            "SELECT id, invoice_no, invoice_date, due_date, outstanding_amount
               FROM invoices
              WHERE customer_id = :c AND deleted_at IS NULL AND doc_status='active'
                AND outstanding_amount > 0
              ORDER BY due_date, invoice_date, id",
            ['c' => $customerId]
        );
        $out = [];
        $sisa = $amount;
        foreach ($rows as $r) {
            if ($sisa <= 0.005) break;
            $alloc = min($sisa, (float)$r['outstanding_amount']);
            $out[] = ['invoice_id' => (int)$r['id'], 'invoice_no' => $r['invoice_no'],
                      'invoice_date' => $r['invoice_date'], 'due_date' => $r['due_date'],
                      'outstanding' => (float)$r['outstanding_amount'], 'amount' => round($alloc, 2)];
            $sisa -= $alloc;
        }
        return ['alokasi' => $out, 'sisa' => round($sisa, 2)];
    }
}
