<?php

declare(strict_types=1);

namespace App\Services;

use App\Core\SchemaRuntime;
use App\Repositories\SupplierPaymentRepository;
use App\Shared\Schema\PartySchemaSupport;
use App\Shared\Schema\SchemaSupport;
use InvalidArgumentException;
use PDO;
use Throwable;

final class SupplierPaymentService
{
    public function __construct(private PDO $pdo, private SupplierPaymentRepository $repo)
    {
    }

    public function ensureSchemaOrThrow(bool $needsAlloc = false, bool $needsLedger = false): void
    {
        SchemaRuntime::ensure($this->pdo, 'parties_schema', static function (): void {
            PartySchemaSupport::ensurePartiesSchema($this->pdo);
        }, 'sql/migrate_v1_18_parties.sql');
        SchemaRuntime::ensure($this->pdo, 'parties_migration_bridge', static function (): void {
            PartySchemaSupport::ensurePartiesMigrateFromCustomersSuppliers($this->pdo);
        }, 'sql/migrate_v1_18_parties.sql');
        if ($needsLedger) {
            SchemaRuntime::ensure($this->pdo, 'party_ledger_schema', static function (): void {
                PartySchemaSupport::ensurePartyLedgerSchema($this->pdo);
            }, 'sql/migrate_v1_20_party_ledger.sql');
        }
        if (!SchemaSupport::tableExists($this->pdo, 'supplier_payments')) {
            throw new InvalidArgumentException('مطلوب تشغيل ملف التحديث: sql/migrate_v1_29_supplier_payments.sql');
        }
        if ($needsAlloc && !SchemaSupport::tableExists($this->pdo, 'supplier_payment_allocations')) {
            throw new InvalidArgumentException('مطلوب تشغيل ملف التحديث: sql/migrate_v1_30_supplier_payment_allocations.sql');
        }
    }

    public function loadForm(int $id): array
    {
        $isEdit = $id > 0;
        if ($isEdit) {
            $payment = $this->repo->findById($id);
            if (!$payment) throw new InvalidArgumentException('Not found');
            if (strtoupper((string)($payment['status'] ?? '')) === 'POSTED') {
                throw new InvalidArgumentException('لا يمكن تعديل سند مُرحّل.');
            }
            $payment['party_id'] = (int)($payment['party_id'] ?? $payment['supplier_id'] ?? 0);
        } else {
            $payment = [
                'pay_date' => date('Y-m-d'),
                'amount' => 0,
                'method' => 'CASH',
                'ref_no' => '',
                'notes' => '',
                'party_id' => 0,
            ];
        }

        $party = null;
        if ((int)($payment['party_id'] ?? 0) > 0) {
            $party = $this->repo->fetchParty((int)$payment['party_id']) ?: null;
        }
        return compact('payment', 'party', 'isEdit', 'id');
    }

    public function save(int $id, array $input, int $userId): int
    {
        $isEdit = $id > 0;
        $saveAction = (string)($input['save_action'] ?? 'draft');
        if (!in_array($saveAction, ['draft', 'post'], true)) $saveAction = 'draft';

        $data = [
            'party_id' => (int)($input['party_id'] ?? $input['supplier_id'] ?? 0),
            'pay_date' => trim((string)($input['pay_date'] ?? '')),
            'amount' => (float)($input['amount'] ?? 0),
            'method' => strtoupper(trim((string)($input['method'] ?? 'CASH'))),
            'ref_no' => trim((string)($input['ref_no'] ?? '')),
            'notes' => trim((string)($input['notes'] ?? '')),
        ];
        if ($data['party_id'] <= 0) throw new InvalidArgumentException('اختر الطرف.');
        if ($data['pay_date'] === '') $data['pay_date'] = date('Y-m-d');
        if ($data['amount'] <= 0) throw new InvalidArgumentException('قيمة السداد يجب أن تكون أكبر من صفر.');
        if (!in_array($data['method'], ['CASH', 'TRANSFER', 'CHEQUE'], true)) $data['method'] = 'CASH';

        $allocIds = $input['alloc_invoice_id'] ?? [];
        $allocAmounts = $input['alloc_amount'] ?? [];
        $wantsAlloc = (int)($input['alloc_payload'] ?? 0) === 1;

        $this->pdo->beginTransaction();
        try {
            if ($isEdit) {
                $current = $this->repo->findById($id);
                if (!$current) throw new InvalidArgumentException('Not found');
                if (strtoupper((string)($current['status'] ?? '')) === 'POSTED') throw new InvalidArgumentException('لا يمكن تعديل سند مُرحّل.');
                $this->repo->update($id, $data);
            } else {
                $id = $this->repo->create($data, $userId);
                $this->repo->updatePayNo($id, 'SP' . date('ym') . '-' . str_pad((string)$id, 6, '0', STR_PAD_LEFT));
            }

            if ($wantsAlloc) {
                $pairs = $this->normalizeAllocations($allocIds, $allocAmounts);
                $this->validateAllocations($id, $data['party_id'], (float)$data['amount'], $pairs);
                $this->repo->deleteAllocations($id);
                foreach ($pairs as $invId => $amt) {
                    $this->repo->insertAllocation($id, $invId, $amt);
                }
            }

            if ($saveAction === 'post') {
                $this->postPayment($id, $data['party_id'], $userId);
            } else {
                audit_log($isEdit ? 'UPDATE_SUPPLIER_PAYMENT' : 'CREATE_SUPPLIER_PAYMENT', 'supplier_payments', $id, ['amount' => $data['amount'], 'party_id' => $data['party_id']]);
            }
            $this->pdo->commit();
            return $id;
        } catch (Throwable $e) {
            if ($this->pdo->inTransaction()) $this->pdo->rollBack();
            throw $e;
        }
    }

    public function postExisting(int $id, int $userId): void
    {
        $payment = $this->repo->findById($id);
        if (!$payment) throw new InvalidArgumentException('Not found');
        if (strtoupper((string)($payment['status'] ?? '')) === 'POSTED') return;

        $this->pdo->beginTransaction();
        try {
            $this->postPayment($id, (int)($payment['party_id'] ?? $payment['supplier_id'] ?? 0), $userId);
            $this->pdo->commit();
        } catch (Throwable $e) {
            if ($this->pdo->inTransaction()) $this->pdo->rollBack();
            throw $e;
        }
    }

    public function loadView(int $id): array
    {
        $payment = $this->repo->findViewById($id);
        if (!$payment) throw new InvalidArgumentException('Not found');
        $allocations = $this->repo->fetchAllocations($id);
        $allocTotal = 0.0;
        foreach ($allocations as $a) $allocTotal += (float)$a['amount'];
        $unallocated = max(0, (float)($payment['amount'] ?? 0) - $allocTotal);
        return compact('payment', 'allocations', 'allocTotal', 'unallocated');
    }

    public function loadSupplierStatement(int $partyId, string $from, string $to): array
    {
        SchemaRuntime::ensure($this->pdo, 'party_ledger_schema', static function (): void {
            PartySchemaSupport::ensurePartyLedgerSchema($this->pdo);
        }, 'sql/migrate_v1_20_party_ledger.sql');
        $hasEventAt = column_exists($this->pdo, 'party_ledger', 'event_at');
        $party = null;
        $rows = [];
        $opening = 0.0;
        $closing = null;

        if ($from === '') $from = date('Y-m-01');
        if ($to === '') $to = date('Y-m-d');

        if ($partyId > 0) {
            $party = $this->repo->fetchSupplierById($partyId) ?: null;
            if ($party) {
                $fromDt = date('Y-m-d', strtotime($from));
                $toDt = date('Y-m-d', strtotime($to));
                $opening = (float)($party['opening_balance'] ?? 0) + party_balance($this->pdo, $partyId, $fromDt);
                $rows = $this->repo->fetchStatementRows($partyId, $fromDt, $toDt, $hasEventAt);
                $closing = $opening;
                foreach ($rows as $r) {
                    $closing += (float)($r['debit'] ?? 0) - (float)($r['credit'] ?? 0);
                }
            }
        }

        return compact('partyId', 'party', 'from', 'to', 'opening', 'closing', 'rows');
    }

    public function loadSupplierAging(int $partyId, string $asOf): array
    {
        if ($asOf === '') $asOf = date('Y-m-d');
        $buckets = [
            ['label' => '0-30', 'min' => 0, 'max' => 30],
            ['label' => '31-60', 'min' => 31, 'max' => 60],
            ['label' => '61-90', 'min' => 61, 'max' => 90],
            ['label' => '+90', 'min' => 91, 'max' => 100000],
        ];

        $suppliers = $partyId > 0 ? array_values(array_filter([$this->repo->fetchSupplierById($partyId)], fn($x) => (bool)$x)) : $this->repo->fetchSuppliers();
        $rows = [];
        $totals = array_fill_keys(array_column($buckets, 'label'), 0.0);
        $totals['total'] = 0.0;

        $invoices = $this->repo->fetchSupplierAgingInvoices($partyId, $asOf);
        $retMap = $this->repo->fetchAgingReturnMap($asOf);
        $payMap = table_exists($this->pdo, 'supplier_payment_allocations') && table_exists($this->pdo, 'supplier_payments')
            ? $this->repo->fetchAgingPaymentMap($asOf)
            : [];

        $bySupplier = [];
        foreach ($invoices as $inv) {
            $invId = (int)$inv['id'];
            $supplierId = (int)$inv['party_id'];
            $outstanding = (float)$inv['total'] - (float)($retMap[$invId] ?? 0) - (float)($payMap[$invId] ?? 0);
            if ($outstanding <= 0.00001) continue;
            $days = (int)floor((strtotime($asOf) - strtotime(substr((string)$inv['inv_date'], 0, 10))) / 86400);
            if ($days < 0) $days = 0;
            $bucketLabel = '+90';
            foreach ($buckets as $b) {
                if ($days >= $b['min'] && $days <= $b['max']) { $bucketLabel = $b['label']; break; }
            }
            if (!isset($bySupplier[$supplierId])) {
                $bySupplier[$supplierId] = array_fill_keys(array_column($buckets, 'label'), 0.0);
                $bySupplier[$supplierId]['total'] = 0.0;
            }
            $bySupplier[$supplierId][$bucketLabel] += $outstanding;
            $bySupplier[$supplierId]['total'] += $outstanding;
        }

        foreach ($suppliers as $s) {
            $sid = (int)$s['id'];
            $r = $bySupplier[$sid] ?? (array_fill_keys(array_column($buckets, 'label'), 0.0) + ['total' => 0.0]);
            if (($r['total'] ?? 0) <= 0.00001) continue;
            $rows[] = ['supplier' => $s, 'buckets' => $r];
            foreach (array_column($buckets, 'label') as $lbl) $totals[$lbl] += (float)$r[$lbl];
            $totals['total'] += (float)$r['total'];
        }

        $selectedSupplier = ($partyId > 0 && $suppliers) ? $suppliers[0] : null;
        return compact('asOf', 'partyId', 'selectedSupplier', 'buckets', 'rows', 'totals');
    }

    private function normalizeAllocations(mixed $ids, mixed $amounts): array
    {
        $pairs = [];
        if (is_array($ids) && is_array($amounts)) {
            $n = min(count($ids), count($amounts));
            for ($i = 0; $i < $n; $i++) {
                $invId = (int)$ids[$i];
                $amt = (float)($amounts[$i] ?? 0);
                if ($invId > 0 && $amt > 0) {
                    $pairs[$invId] = ($pairs[$invId] ?? 0) + $amt;
                }
            }
        }
        return $pairs;
    }

    private function validateAllocations(int $paymentId, int $partyId, float $amount, array $pairs): void
    {
        if (!table_exists($this->pdo, 'supplier_payment_allocations')) {
            if ($pairs) throw new InvalidArgumentException('مطلوب تشغيل ملف التحديث: sql/migrate_v1_30_supplier_payment_allocations.sql');
            return;
        }
        $allocTotal = 0.0;
        foreach ($pairs as $amt) $allocTotal += (float)$amt;
        if ($allocTotal - $amount > 0.01) throw new InvalidArgumentException('إجمالي التوزيع أكبر من قيمة السداد.');
        if (!$pairs) return;

        $invoiceIds = array_keys($pairs);
        $postedMap = $this->repo->fetchPostedAllocMap($invoiceIds);
        $curMap = $this->repo->fetchCurrentAllocMap($paymentId, $invoiceIds);
        $invoiceMap = $this->repo->fetchPostedInvoicesMap($invoiceIds, $partyId);
        $retMap = $this->repo->fetchReturnMap($invoiceIds);

        foreach ($pairs as $invId => $amt) {
            if (!isset($invoiceMap[$invId])) throw new InvalidArgumentException('فاتورة غير صالحة للتوزيع.');
            $totalInv = (float)$invoiceMap[$invId] - (float)($retMap[$invId] ?? 0);
            $available = $totalInv - (float)($postedMap[$invId] ?? 0) + (float)($curMap[$invId] ?? 0);
            if ($amt - $available > 0.01) throw new InvalidArgumentException('قيمة التوزيع تتجاوز المتاح للفاتورة ' . $invId);
        }
    }

    private function postPayment(int $id, int $partyId, int $userId): void
    {
        $payment = $this->repo->findById($id);
        if (!$payment) throw new InvalidArgumentException('Not found');
        $this->repo->deleteLedger($id);
        party_add_ledger(
            $this->pdo,
            $partyId,
            'PAYMENT',
            $id,
            (string)($payment['pay_no'] ?? ''),
            (string)($payment['pay_date'] ?? date('Y-m-d')),
            (float)($payment['amount'] ?? 0),
            0.0,
            'سداد طرف'
        );
        $this->repo->markPosted($id, $userId);
        audit_log('POST_SUPPLIER_PAYMENT', 'supplier_payments', $id, ['amount' => (float)($payment['amount'] ?? 0), 'party_id' => $partyId]);
    }
}
