<?php

declare(strict_types=1);

namespace App\Repositories;

use DateTimeImmutable;
use PDO;
use RuntimeException;
use Throwable;

final class EncaissementRepository extends BaseRepository
{
    public function all(): array
    {
        if (!$this->tableExists('encaissements')) {
            return [];
        }

        return $this->db->query("\n            SELECT e.*, f.numero AS facture_numero, c.raison_sociale AS client_nom\n            FROM encaissements e\n            INNER JOIN factures f ON f.id = e.facture_id\n            INNER JOIN clients c ON c.id = e.client_id\n            WHERE e.actif = 1\n            ORDER BY e.date_encaissement DESC, e.id DESC\n        ")->fetchAll(PDO::FETCH_ASSOC);
    }

    public function journal(array $filters = []): array
    {
        if (!$this->tableExists('encaissements')) {
            return [];
        }

        $sql = "\n            SELECT e.*, f.numero AS facture_numero, c.raison_sociale AS client_nom\n            FROM encaissements e\n            INNER JOIN factures f ON f.id = e.facture_id\n            INNER JOIN clients c ON c.id = e.client_id\n            WHERE 1 = 1\n        ";
        $params = [];
        if (!empty($filters['date_from'])) {
            $sql .= ' AND e.date_encaissement >= :date_from';
            $params['date_from'] = $filters['date_from'];
        }
        if (!empty($filters['date_to'])) {
            $sql .= ' AND e.date_encaissement <= :date_to';
            $params['date_to'] = $filters['date_to'];
        }
        if (!empty($filters['client_id'])) {
            $sql .= ' AND e.client_id = :client_id';
            $params['client_id'] = (int)$filters['client_id'];
        }
        if (!empty($filters['facture'])) {
            $sql .= ' AND f.numero LIKE :facture';
            $params['facture'] = '%' . $filters['facture'] . '%';
        }
        if (!empty($filters['mode'])) {
            $sql .= ' AND e.mode_reglement = :mode';
            $params['mode'] = $filters['mode'];
        }
        if (($filters['statut'] ?? 'tous') === 'actifs') {
            $sql .= ' AND e.actif = 1';
        } elseif (($filters['statut'] ?? 'tous') === 'annules') {
            $sql .= ' AND e.actif = 0';
        }
        $sql .= ' ORDER BY e.date_encaissement DESC, e.id DESC';

        $stmt = $this->db->prepare($sql);
        $stmt->execute($params);
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }

    public function paymentClients(): array
    {
        if (!$this->tableExists('encaissements')) {
            return [];
        }
        return $this->db->query("SELECT DISTINCT c.id, c.raison_sociale, c.code_client FROM clients c INNER JOIN encaissements e ON e.client_id = c.id ORDER BY c.raison_sociale")->fetchAll(PDO::FETCH_ASSOC);
    }

    public function modeTotals(array $rows): array
    {
        $totals = [];
        foreach ($rows as $row) {
            if ((int)$row['actif'] !== 1) continue;
            $mode = (string)$row['mode_reglement'];
            $totals[$mode] = ($totals[$mode] ?? 0.0) + (float)$row['montant'];
        }
        ksort($totals);
        return $totals;
    }

    public function clientStatement(int $clientId, array $filters = []): array
    {
        $filters['client_id'] = $clientId;
        return $this->journal($filters);
    }

    public function clientSummary(int $clientId): ?array
    {
        $stmt = $this->db->prepare("\n            SELECT c.id, c.code_client, c.raison_sociale, c.adresse, c.code_postal, c.ville, c.email, c.telephone,\n                   COALESCE(SUM(CASE WHEN f.actif = 1 THEN f.total_ttc ELSE 0 END), 0) AS total_facture,\n                   COALESCE(SUM(CASE WHEN f.actif = 1 THEN f.montant_regle ELSE 0 END), 0) AS total_regle,\n                   COALESCE(SUM(CASE WHEN f.actif = 1 THEN f.reste_du ELSE 0 END), 0) AS reste_du\n            FROM clients c LEFT JOIN factures f ON f.client_id = c.id\n            WHERE c.id = :id\n            GROUP BY c.id, c.code_client, c.raison_sociale, c.adresse, c.code_postal, c.ville, c.email, c.telephone\n        ");
        $stmt->execute(['id' => $clientId]);
        $row = $stmt->fetch(PDO::FETCH_ASSOC);
        return $row ?: null;
    }

    public function forFacture(int $factureId): array
    {
        if (!$this->tableExists('encaissements')) {
            return [];
        }

        $stmt = $this->db->prepare("\n            SELECT e.*, f.numero AS facture_numero, c.raison_sociale AS client_nom\n            FROM encaissements e\n            INNER JOIN factures f ON f.id = e.facture_id\n            INNER JOIN clients c ON c.id = e.client_id\n            WHERE e.facture_id = :facture_id\n            ORDER BY e.date_encaissement DESC, e.id DESC\n        ");
        $stmt->execute(['facture_id' => $factureId]);
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }

    public function find(int $id): ?array
    {
        if ($id <= 0 || !$this->tableExists('encaissements')) {
            return null;
        }

        $stmt = $this->db->prepare("\n            SELECT e.*, f.numero AS facture_numero, f.total_ttc, f.reste_du, c.raison_sociale AS client_nom\n            FROM encaissements e\n            INNER JOIN factures f ON f.id = e.facture_id\n            INNER JOIN clients c ON c.id = e.client_id\n            WHERE e.id = :id\n            LIMIT 1\n        ");
        $stmt->execute(['id' => $id]);
        $row = $stmt->fetch(PDO::FETCH_ASSOC);
        return $row ?: null;
    }

    public function store(array $data): int
    {
        [$factureId, $montant, $date, $mode] = $this->validateInput($data);

        $this->beginTransaction();
        try {
            $facture = $this->lockInvoice($factureId);
            if (!$facture) {
                throw new RuntimeException('Facture introuvable.');
            }

            if (!$this->isActivePaymentMethod($mode)) {
                throw new RuntimeException('Ce mode de règlement est inexistant ou désactivé.');
            }

            $totalTtc = round((float)($facture['total_ttc'] ?? 0), 2);
            $totalAvant = $this->totalPaidForFacture($factureId);
            $resteReel = max(0.0, round($totalTtc - $totalAvant, 2));

            if ($resteReel <= 0) {
                throw new RuntimeException('Cette facture est déjà entièrement réglée.');
            }
            if ($montant > $resteReel + 0.001) {
                throw new RuntimeException('Le montant encaissé ne peut pas être supérieur au reste dû de ' . number_format($resteReel, 2, ',', ' ') . ' MAD.');
            }

            $stmt = $this->db->prepare("\n                INSERT INTO encaissements (\n                    client_id, facture_id, date_encaissement, mode_reglement, reference_reglement, montant, observations\n                ) VALUES (\n                    :client_id, :facture_id, :date_encaissement, :mode_reglement, :reference_reglement, :montant, :observations\n                )\n            ");
            $stmt->execute([
                'client_id' => (int)$facture['client_id'],
                'facture_id' => $factureId,
                'date_encaissement' => $date,
                'mode_reglement' => $mode,
                'reference_reglement' => trim((string)($data['reference_reglement'] ?? '')) ?: null,
                'montant' => $montant,
                'observations' => trim((string)($data['observations'] ?? '')) ?: null,
            ]);
            $id = $this->lastInsertId();

            $this->recalculateInvoice($factureId, $totalTtc);
            $this->commit();
            return $id;
        } catch (Throwable $exception) {
            $this->rollBack();
            throw $exception;
        }
    }

    public function update(int $id, array $data): int
    {
        if ($id <= 0) {
            throw new RuntimeException('Encaissement invalide.');
        }

        [, $montant, $date, $mode] = $this->validateInput($data, false);

        $this->beginTransaction();
        try {
            $encaissement = $this->lockPayment($id);
            if (!$encaissement) {
                throw new RuntimeException('Encaissement introuvable.');
            }
            if ((int)$encaissement['actif'] !== 1) {
                throw new RuntimeException('Un encaissement annulé ne peut plus être modifié.');
            }

            $factureId = (int)$encaissement['facture_id'];
            $facture = $this->lockInvoice($factureId);
            if (!$facture) {
                throw new RuntimeException('Facture introuvable.');
            }
            if (!$this->isActivePaymentMethod($mode)) {
                throw new RuntimeException('Ce mode de règlement est inexistant ou désactivé.');
            }

            $totalTtc = round((float)($facture['total_ttc'] ?? 0), 2);
            $totalAutres = $this->totalPaidForFacture($factureId, $id);
            $maximum = max(0.0, round($totalTtc - $totalAutres, 2));
            if ($montant > $maximum + 0.001) {
                throw new RuntimeException('Le montant ne peut pas être supérieur au maximum autorisé de ' . number_format($maximum, 2, ',', ' ') . ' MAD.');
            }

            $stmt = $this->db->prepare("\n                UPDATE encaissements\n                SET date_encaissement = :date_encaissement,\n                    mode_reglement = :mode_reglement,\n                    reference_reglement = :reference_reglement,\n                    montant = :montant,\n                    observations = :observations\n                WHERE id = :id AND actif = 1\n            ");
            $stmt->execute([
                'date_encaissement' => $date,
                'mode_reglement' => $mode,
                'reference_reglement' => trim((string)($data['reference_reglement'] ?? '')) ?: null,
                'montant' => $montant,
                'observations' => trim((string)($data['observations'] ?? '')) ?: null,
                'id' => $id,
            ]);

            $this->recalculateInvoice($factureId, $totalTtc);
            $this->commit();
            return $factureId;
        } catch (Throwable $exception) {
            $this->rollBack();
            throw $exception;
        }
    }

    public function cancel(int $id): int
    {
        if ($id <= 0) {
            throw new RuntimeException('Encaissement invalide.');
        }

        $this->beginTransaction();
        try {
            $encaissement = $this->lockPayment($id);
            if (!$encaissement) {
                throw new RuntimeException('Encaissement introuvable.');
            }
            if ((int)$encaissement['actif'] !== 1) {
                throw new RuntimeException('Cet encaissement est déjà annulé.');
            }

            $factureId = (int)$encaissement['facture_id'];
            $facture = $this->lockInvoice($factureId);
            if (!$facture) {
                throw new RuntimeException('Facture introuvable.');
            }

            $stmt = $this->db->prepare('UPDATE encaissements SET actif = 0 WHERE id = :id AND actif = 1');
            $stmt->execute(['id' => $id]);

            $this->recalculateInvoice($factureId, round((float)($facture['total_ttc'] ?? 0), 2));
            $this->commit();
            return $factureId;
        } catch (Throwable $exception) {
            $this->rollBack();
            throw $exception;
        }
    }

    public function totalPaidForFacture(int $factureId, int $excludePaymentId = 0): float
    {
        if (!$this->tableExists('encaissements')) {
            return 0.0;
        }

        $sql = "\n            SELECT COALESCE(SUM(montant), 0)\n            FROM encaissements\n            WHERE facture_id = :facture_id AND actif = 1\n        ";
        $params = ['facture_id' => $factureId];
        if ($excludePaymentId > 0) {
            $sql .= ' AND id <> :exclude_id';
            $params['exclude_id'] = $excludePaymentId;
        }

        $stmt = $this->db->prepare($sql);
        $stmt->execute($params);
        return round((float)$stmt->fetchColumn(), 2);
    }

    public function unpaidInvoices(): array
    {
        if (!$this->tableExists('factures')) {
            return [];
        }

        return $this->db->query("\n            SELECT f.*, c.raison_sociale AS client_nom\n            FROM factures f\n            INNER JOIN clients c ON c.id = f.client_id\n            WHERE f.actif = 1 AND COALESCE(f.reste_du, f.total_ttc) > 0\n            ORDER BY f.date_facture DESC, f.numero DESC\n        ")->fetchAll(PDO::FETCH_ASSOC);
    }

    private function validateInput(array $data, bool $requireInvoice = true): array
    {
        if (!$this->tableExists('encaissements')) {
            throw new RuntimeException('La table encaissements n’est pas installée.');
        }

        $factureId = (int)($data['facture_id'] ?? 0);
        if ($requireInvoice && $factureId <= 0) {
            throw new RuntimeException('Veuillez sélectionner une facture.');
        }

        $montant = round((float)($data['montant'] ?? 0), 2);
        if ($montant <= 0) {
            throw new RuntimeException('Le montant encaissé doit être supérieur à zéro.');
        }

        $date = trim((string)($data['date_encaissement'] ?? '')) ?: date('Y-m-d');
        $parsedDate = DateTimeImmutable::createFromFormat('!Y-m-d', $date);
        if (!$parsedDate || $parsedDate->format('Y-m-d') !== $date) {
            throw new RuntimeException('La date d’encaissement est invalide.');
        }

        $mode = trim((string)($data['mode_reglement'] ?? ''));
        if ($mode === '') {
            throw new RuntimeException('Veuillez sélectionner un mode de règlement.');
        }

        return [$factureId, $montant, $date, $mode];
    }

    private function lockPayment(int $id): ?array
    {
        $stmt = $this->db->prepare('SELECT * FROM encaissements WHERE id = :id FOR UPDATE');
        $stmt->execute(['id' => $id]);
        $row = $stmt->fetch(PDO::FETCH_ASSOC);
        return $row ?: null;
    }

    private function lockInvoice(int $factureId): ?array
    {
        $stmt = $this->db->prepare('SELECT * FROM factures WHERE id = :id AND actif = 1 FOR UPDATE');
        $stmt->execute(['id' => $factureId]);
        $row = $stmt->fetch(PDO::FETCH_ASSOC);
        return $row ?: null;
    }

    private function isActivePaymentMethod(string $label): bool
    {
        if (!$this->tableExists('modes_reglement')) {
            return in_array($label, ['Carte bancaire', 'Espèces', 'Chèque', 'Virement'], true);
        }

        $stmt = $this->db->prepare('SELECT COUNT(*) FROM modes_reglement WHERE libelle = :libelle AND actif = 1');
        $stmt->execute(['libelle' => $label]);
        return (int)$stmt->fetchColumn() > 0;
    }

    private function recalculateInvoice(int $factureId, float $totalTtc): void
    {
        $this->updateInvoicePaymentStatus($factureId, $totalTtc, $this->totalPaidForFacture($factureId));
    }

    private function updateInvoicePaymentStatus(int $factureId, float $totalTtc, float $montantRegle): void
    {
        $montantRegle = max(0.0, min($totalTtc, round($montantRegle, 2)));
        $resteDu = max(0.0, round($totalTtc - $montantRegle, 2));
        $statut = $resteDu <= 0.001 ? 'réglée' : ($montantRegle > 0 ? 'partiellement réglée' : 'émise');

        $stmt = $this->db->prepare("\n            UPDATE factures\n            SET montant_regle = :montant_regle, reste_du = :reste_du, statut = :statut\n            WHERE id = :id\n        ");
        $stmt->execute([
            'montant_regle' => $montantRegle,
            'reste_du' => $resteDu,
            'statut' => $statut,
            'id' => $factureId,
        ]);
    }

    private function tableExists(string $table): bool
    {
        $stmt = $this->db->prepare('SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = :table');
        $stmt->execute(['table' => $table]);
        return (int)$stmt->fetchColumn() > 0;
    }
}
