<?php

declare(strict_types=1);

namespace App\Controllers;

require_once __DIR__ . '/../Services/LogService.php';
require_once __DIR__ . '/../Services/EventLogService.php';
require_once __DIR__ . '/../Services/ApiService.php';
require_once __DIR__ . '/../Services/ReceiptService.php';
require_once __DIR__ . '/../Services/SectorService.php';

use PDO;
use PDOException;
use Database;
use AppConfig;
use Request;
use Response;
use Session;
use App\Services\LogService;
use App\Services\EventLogService;
use App\Services\ApiService;
use App\Services\SectorService;
use Exception;
use DateTime;

class PassageController {
    private PDO $db;
    private AppConfig $appConfig;

    public function __construct() {
        $this->db = Database::getInstance();
        $this->appConfig = AppConfig::getInstance();
    }

    /**
     * Récupère l'entité de l'utilisateur connecté
     * 
     * @param int $userId ID de l'utilisateur
     * @return int|null ID de l'entité ou null si non trouvé
     */
    private function getUserEntiteId(int $userId): ?int {
        try {
            $stmt = $this->db->prepare('SELECT fk_entite FROM users WHERE id = ?');
            $stmt->execute([$userId]);
            $user = $stmt->fetch(PDO::FETCH_ASSOC);

            return $user ? (int)$user['fk_entite'] : null;
        } catch (Exception $e) {
            LogService::log('Erreur lors de la récupération de l\'entité utilisateur', [
                'level' => 'error',
                'error' => $e->getMessage(),
                'userId' => $userId
            ]);
            return null;
        }
    }

    /**
     * Vérifie si l'utilisateur a accès à l'opération
     * 
     * @param int $userId ID de l'utilisateur
     * @param int $operationId ID de l'opération
     * @return bool True si l'utilisateur a accès
     */
    private function hasAccessToOperation(int $userId, int $operationId): bool {
        try {
            $entiteId = $this->getUserEntiteId($userId);
            if (!$entiteId) {
                return false;
            }

            $stmt = $this->db->prepare('
                SELECT COUNT(*) as count 
                FROM operations 
                WHERE id = ? AND fk_entite = ? AND chk_active = 1
            ');
            $stmt->execute([$operationId, $entiteId]);
            $result = $stmt->fetch(PDO::FETCH_ASSOC);

            return $result && $result['count'] > 0;
        } catch (Exception $e) {
            LogService::log('Erreur lors de la vérification d\'accès à l\'opération', [
                'level' => 'error',
                'error' => $e->getMessage(),
                'userId' => $userId,
                'operationId' => $operationId
            ]);
            return false;
        }
    }

    /**
     * Valide les données d'un passage
     * 
     * @param array $data Données à valider
     * @param int|null $passageId ID du passage (pour update)
     * @return array|null Erreurs de validation ou null
     */
    private function validatePassageData(array $data, ?int $passageId = null): ?array {
        $errors = [];

        // Validation de l'opération
        if (!isset($data['fk_operation']) || empty($data['fk_operation'])) {
            $errors[] = 'L\'ID de l\'opération est obligatoire';
        }

        // Validation de l'utilisateur
        if (!isset($data['fk_user']) || empty($data['fk_user'])) {
            $errors[] = 'L\'ID de l\'utilisateur est obligatoire';
        }

        // Validation de l'adresse
        if (!isset($data['numero']) || empty(trim($data['numero']))) {
            $errors[] = 'Le numéro de rue est obligatoire';
        }

        if (!isset($data['rue']) || empty(trim($data['rue']))) {
            $errors[] = 'Le nom de rue est obligatoire';
        }

        if (!isset($data['ville']) || empty(trim($data['ville']))) {
            $errors[] = 'La ville est obligatoire';
        }

        // Validation du nom (chiffré) - obligatoire seulement si (type=1 Effectué ou 5 Lot) ET email présent
        $fk_type = isset($data['fk_type']) ? (int)$data['fk_type'] : 0;
        $hasEmail = (isset($data['email']) && !empty(trim($data['email']))) ||
                    (isset($data['encrypted_email']) && !empty($data['encrypted_email']));

        if (($fk_type === 1 || $fk_type === 5) && $hasEmail) {
            if (!isset($data['encrypted_name']) && !isset($data['name'])) {
                $errors[] = 'Le nom est obligatoire pour ce type de passage avec email';
            } elseif (isset($data['name']) && empty(trim($data['name']))) {
                $errors[] = 'Le nom ne peut pas être vide pour ce type de passage avec email';
            }
        }

        // Validation du montant
        if (isset($data['montant'])) {
            $montant = (float)$data['montant'];
            if ($montant < 0) {
                $errors[] = 'Le montant ne peut pas être négatif';
            }
            if ($montant > 999999.99) {
                $errors[] = 'Le montant ne peut pas dépasser 999999.99';
            }
        }

        // Validation de l'email si fourni
        if (isset($data['email']) && !empty($data['email'])) {
            if (!filter_var($data['email'], FILTER_VALIDATE_EMAIL)) {
                $errors[] = 'Format d\'email invalide';
            }
        }

        // Validation des coordonnées GPS si fournies
        if (isset($data['gps_lat']) && !empty($data['gps_lat'])) {
            $lat = (float)$data['gps_lat'];
            if ($lat < -90 || $lat > 90) {
                $errors[] = 'Latitude invalide (doit être entre -90 et 90)';
            }
        }

        if (isset($data['gps_lng']) && !empty($data['gps_lng'])) {
            $lng = (float)$data['gps_lng'];
            if ($lng < -180 || $lng > 180) {
                $errors[] = 'Longitude invalide (doit être entre -180 et 180)';
            }
        }

        // Validation de l'ID Stripe si fourni
        if (isset($data['stripe_payment_id']) && !empty($data['stripe_payment_id'])) {
            $stripeId = trim($data['stripe_payment_id']);
            // L'ID PaymentIntent Stripe doit commencer par 'pi_'
            if (!preg_match('/^pi_[a-zA-Z0-9]{24,}$/', $stripeId)) {
                $errors[] = 'Format d\'ID de paiement Stripe invalide';
            }
        }

        return empty($errors) ? null : $errors;
    }

    /**
     * Récupère tous les passages de l'entité de l'utilisateur
     */
    public function getPassages(): void {
        try {
            $userId = Session::getUserId();
            if (!$userId) {
                Response::json([
                    'status' => 'error',
                    'message' => 'Vous devez être connecté pour effectuer cette action'
                ], 401);
                return;
            }

            $entiteId = $this->getUserEntiteId($userId);
            if (!$entiteId) {
                Response::json([
                    'status' => 'error',
                    'message' => 'Entité non trouvée pour cet utilisateur'
                ], 404);
                return;
            }

            // Paramètres de pagination
            $page = isset($_GET['page']) ? max(1, (int)$_GET['page']) : 1;
            $limit = isset($_GET['limit']) ? min(100, max(1, (int)$_GET['limit'])) : 20;
            $offset = ($page - 1) * $limit;

            // Filtres optionnels
            $operationId = isset($_GET['operation_id']) ? (int)$_GET['operation_id'] : null;
            $userId_filter = isset($_GET['user_id']) ? (int)$_GET['user_id'] : null;

            // Construction de la requête
            $whereConditions = ['o.fk_entite = ?'];
            $params = [$entiteId];

            if ($operationId) {
                $whereConditions[] = 'p.fk_operation = ?';
                $params[] = $operationId;
            }

            if ($userId_filter) {
                $whereConditions[] = 'p.fk_user = ?';
                $params[] = $userId_filter;
            }

            $whereClause = implode(' AND ', $whereConditions);

            // Requête principale avec jointures
            $stmt = $this->db->prepare("
                SELECT
                    p.id, p.fk_operation, p.fk_sector, p.fk_user, p.fk_adresse,
                    p.passed_at, p.fk_type, p.numero, p.rue, p.rue_bis, p.ville,
                    p.fk_habitat, p.appt, p.niveau, p.residence, p.gps_lat, p.gps_lng,
                    p.encrypted_name, p.montant, p.fk_type_reglement, p.remarque,
                    p.encrypted_email, p.encrypted_phone, p.nom_recu, p.date_recu,
                    p.chk_email_sent, p.stripe_payment_id, p.stripe_payment_link_id, p.docremis, p.date_repasser, p.nb_passages,
                    p.chk_mobile, p.anomalie, p.created_at, p.updated_at, p.chk_active,
                    o.libelle as operation_libelle,
                    u.encrypted_name as user_name, ou.first_name as user_first_name
                FROM ope_pass p
                INNER JOIN operations o ON p.fk_operation = o.id
                INNER JOIN ope_users ou ON p.fk_user = ou.id
                INNER JOIN users u ON ou.fk_user = u.id
                WHERE $whereClause AND p.chk_active = 1
                ORDER BY p.created_at DESC
                LIMIT ? OFFSET ?
            ");

            $params[] = $limit;
            $params[] = $offset;
            $stmt->execute($params);
            $passages = $stmt->fetchAll(PDO::FETCH_ASSOC);

            // Déchiffrement des données sensibles
            foreach ($passages as &$passage) {
                $passage['name'] = ApiService::decryptData($passage['encrypted_name']);
                $passage['email'] = !empty($passage['encrypted_email']) ?
                    ApiService::decryptSearchableData($passage['encrypted_email']) : '';
                $passage['phone'] = !empty($passage['encrypted_phone']) ?
                    ApiService::decryptData($passage['encrypted_phone']) : '';
                $passage['user_name'] = ApiService::decryptData($passage['user_name']);

                // Suppression des champs chiffrés
                unset($passage['encrypted_name'], $passage['encrypted_email'], $passage['encrypted_phone']);
            }

            // Compter le total pour la pagination
            $countStmt = $this->db->prepare("
                SELECT COUNT(*) as total
                FROM ope_pass p
                INNER JOIN operations o ON p.fk_operation = o.id
                WHERE $whereClause AND p.chk_active = 1
            ");
            $countStmt->execute(array_slice($params, 0, -2)); // Enlever limit et offset
            $totalResult = $countStmt->fetch(PDO::FETCH_ASSOC);
            $total = $totalResult['total'];

            Response::json([
                'status' => 'success',
                'passages' => $passages,
                'pagination' => [
                    'page' => $page,
                    'limit' => $limit,
                    'total' => $total,
                    'pages' => ceil($total / $limit)
                ]
            ], 200);
        } catch (Exception $e) {
            LogService::log('Erreur lors de la récupération des passages', [
                'level' => 'error',
                'error' => $e->getMessage(),
                'userId' => $userId ?? null
            ]);

            Response::json([
                'status' => 'error',
                'message' => 'Erreur lors de la récupération des passages'
            ], 500);
        }
    }

    /**
     * Récupère un passage spécifique par son ID
     */
    public function getPassageById(string $id): void {
        try {
            $userId = Session::getUserId();
            if (!$userId) {
                Response::json([
                    'status' => 'error',
                    'message' => 'Vous devez être connecté pour effectuer cette action'
                ], 401);
                return;
            }

            $entiteId = $this->getUserEntiteId($userId);
            if (!$entiteId) {
                Response::json([
                    'status' => 'error',
                    'message' => 'Entité non trouvée pour cet utilisateur'
                ], 404);
                return;
            }

            $passageId = (int)$id;

            $stmt = $this->db->prepare('
                SELECT
                    p.*,
                    o.libelle as operation_libelle,
                    u.encrypted_name as user_name, ou.first_name as user_first_name
                FROM ope_pass p
                INNER JOIN operations o ON p.fk_operation = o.id
                INNER JOIN ope_users ou ON p.fk_user = ou.id
                INNER JOIN users u ON ou.fk_user = u.id
                WHERE p.id = ? AND o.fk_entite = ? AND p.chk_active = 1
            ');

            $stmt->execute([$passageId, $entiteId]);
            $passage = $stmt->fetch(PDO::FETCH_ASSOC);

            if (!$passage) {
                Response::json([
                    'status' => 'error',
                    'message' => 'Passage non trouvé'
                ], 404);
                return;
            }

            // Déchiffrement des données sensibles
            $passage['name'] = ApiService::decryptData($passage['encrypted_name']);
            $passage['email'] = !empty($passage['encrypted_email']) ?
                ApiService::decryptSearchableData($passage['encrypted_email']) : '';
            $passage['phone'] = !empty($passage['encrypted_phone']) ?
                ApiService::decryptData($passage['encrypted_phone']) : '';
            $passage['user_name'] = ApiService::decryptData($passage['user_name']);

            // Suppression des champs chiffrés
            unset($passage['encrypted_name'], $passage['encrypted_email'], $passage['encrypted_phone']);

            Response::json([
                'status' => 'success',
                'passage' => $passage
            ], 200);
        } catch (Exception $e) {
            LogService::log('Erreur lors de la récupération du passage', [
                'level' => 'error',
                'error' => $e->getMessage(),
                'passageId' => $id,
                'userId' => $userId ?? null
            ]);

            Response::json([
                'status' => 'error',
                'message' => 'Erreur lors de la récupération du passage'
            ], 500);
        }
    }

    /**
     * Récupère tous les passages d'une opération spécifique
     */
    public function getPassagesByOperation(string $operation_id): void {
        try {
            $userId = Session::getUserId();
            if (!$userId) {
                Response::json([
                    'status' => 'error',
                    'message' => 'Vous devez être connecté pour effectuer cette action'
                ], 401);
                return;
            }

            $operationId = (int)$operation_id;

            // Vérifier l'accès à l'opération
            if (!$this->hasAccessToOperation($userId, $operationId)) {
                Response::json([
                    'status' => 'error',
                    'message' => 'Vous n\'avez pas accès à cette opération'
                ], 403);
                return;
            }

            // Paramètres de pagination
            $page = isset($_GET['page']) ? max(1, (int)$_GET['page']) : 1;
            $limit = isset($_GET['limit']) ? min(100, max(1, (int)$_GET['limit'])) : 50;
            $offset = ($page - 1) * $limit;

            $stmt = $this->db->prepare('
                SELECT
                    p.id, p.fk_operation, p.fk_sector, p.fk_user, p.passed_at,
                    p.numero, p.rue, p.rue_bis, p.ville, p.gps_lat, p.gps_lng,
                    p.encrypted_name, p.montant, p.fk_type_reglement, p.remarque,
                    p.encrypted_email, p.encrypted_phone, p.stripe_payment_id, p.stripe_payment_link_id, p.chk_email_sent,
                    p.docremis, p.date_repasser, p.nb_passages, p.chk_mobile,
                    p.anomalie, p.created_at, p.updated_at,
                    u.encrypted_name as user_name, ou.first_name as user_first_name
                FROM ope_pass p
                INNER JOIN ope_users ou ON p.fk_user = ou.id
                INNER JOIN users u ON ou.fk_user = u.id
                WHERE p.fk_operation = ? AND p.chk_active = 1
                ORDER BY p.created_at DESC
                LIMIT ? OFFSET ?
            ');

            $stmt->execute([$operationId, $limit, $offset]);
            $passages = $stmt->fetchAll(PDO::FETCH_ASSOC);

            // Déchiffrement des données sensibles
            foreach ($passages as &$passage) {
                $passage['name'] = ApiService::decryptData($passage['encrypted_name']);
                $passage['email'] = !empty($passage['encrypted_email']) ?
                    ApiService::decryptSearchableData($passage['encrypted_email']) : '';
                $passage['phone'] = !empty($passage['encrypted_phone']) ?
                    ApiService::decryptData($passage['encrypted_phone']) : '';
                $passage['user_name'] = ApiService::decryptData($passage['user_name']);

                // Suppression des champs chiffrés
                unset($passage['encrypted_name'], $passage['encrypted_email'], $passage['encrypted_phone']);
            }

            // Compter le total
            $countStmt = $this->db->prepare('
                SELECT COUNT(*) as total
                FROM ope_pass 
                WHERE fk_operation = ? AND chk_active = 1
            ');
            $countStmt->execute([$operationId]);
            $totalResult = $countStmt->fetch(PDO::FETCH_ASSOC);
            $total = $totalResult['total'];

            Response::json([
                'status' => 'success',
                'passages' => $passages,
                'pagination' => [
                    'page' => $page,
                    'limit' => $limit,
                    'total' => $total,
                    'pages' => ceil($total / $limit)
                ]
            ], 200);
        } catch (Exception $e) {
            LogService::log('Erreur lors de la récupération des passages par opération', [
                'level' => 'error',
                'error' => $e->getMessage(),
                'operationId' => $operation_id,
                'userId' => $userId ?? null
            ]);

            Response::json([
                'status' => 'error',
                'message' => 'Erreur lors de la récupération des passages'
            ], 500);
        }
    }

    /**
     * Crée un nouveau passage
     */
    public function createPassage(): void {
        try {
            $userId = Session::getUserId();
            if (!$userId) {
                Response::json([
                    'status' => 'error',
                    'message' => 'Vous devez être connecté pour effectuer cette action'
                ], 401);
                return;
            }

            $data = Request::getJson();

            // Validation des données
            $errors = $this->validatePassageData($data);
            if ($errors) {
                Response::json([
                    'status' => 'error',
                    'message' => 'Erreurs de validation',
                    'errors' => $errors
                ], 400);
                return;
            }

            $operationId = (int)$data['fk_operation'];

            // Vérifier l'accès à l'opération
            if (!$this->hasAccessToOperation($userId, $operationId)) {
                Response::json([
                    'status' => 'error',
                    'message' => 'Vous n\'avez pas accès à cette opération'
                ], 403);
                return;
            }

            // Récupérer ope_users.id pour l'utilisateur du passage
            // $data['fk_user'] peut contenir soit users.id soit ope_users.id
            $passageUserId = (int)$data['fk_user'];

            // Vérifier d'abord si c'est déjà un ope_users.id valide
            $stmtCheckOpeUser = $this->db->prepare('
                SELECT id FROM ope_users
                WHERE id = ? AND fk_operation = ?
            ');
            $stmtCheckOpeUser->execute([$passageUserId, $operationId]);
            $opeUserId = $stmtCheckOpeUser->fetchColumn();

            if (!$opeUserId) {
                // Ce n'est pas un ope_users.id, essayer de le convertir depuis users.id
                $stmtOpeUser = $this->db->prepare('
                    SELECT id FROM ope_users
                    WHERE fk_user = ? AND fk_operation = ?
                ');
                $stmtOpeUser->execute([$passageUserId, $operationId]);
                $opeUserId = $stmtOpeUser->fetchColumn();
            }

            if (!$opeUserId) {
                Response::json([
                    'status' => 'error',
                    'message' => 'Utilisateur non trouvé dans cette opération'
                ], 404);
                return;
            }

            // Détermination automatique du secteur
            $sectorId = null;
            $gpsLat = isset($data['gps_lat']) && $data['gps_lat'] !== '' ? (float)$data['gps_lat'] : null;
            $gpsLng = isset($data['gps_lng']) && $data['gps_lng'] !== '' ? (float)$data['gps_lng'] : null;

            // 1. Si fk_sector > 0 fourni → l'utiliser directement
            if (isset($data['fk_sector']) && (int)$data['fk_sector'] > 0) {
                $sectorId = (int)$data['fk_sector'];
                LogService::info('[PassageController] Secteur fourni par le client', [
                    'sector_id' => $sectorId
                ]);
            }

            // 2. Si pas de secteur et GPS valide (différent de 0.0) → recherche par GPS
            if ($sectorId === null && $gpsLat !== null && $gpsLng !== null && ($gpsLat != 0.0 || $gpsLng != 0.0)) {
                $sectorService = new SectorService();
                $sectorId = $sectorService->findSectorByGps($operationId, $gpsLat, $gpsLng);
                LogService::info('[PassageController] Recherche secteur par GPS', [
                    'operation_id' => $operationId,
                    'lat' => $gpsLat,
                    'lng' => $gpsLng,
                    'sector_found' => $sectorId
                ]);
            }

            // 3. Si toujours pas de secteur et adresse fournie → géocodage + recherche
            if ($sectorId === null && !empty($data['numero']) && !empty($data['rue']) && !empty($data['ville'])) {
                // Récupérer le code postal de l'entité pour la vérification du département
                $stmtEntite = $this->db->prepare('
                    SELECT e.code_postal FROM entites e
                    INNER JOIN operations o ON o.fk_entite = e.id
                    WHERE o.id = ?
                ');
                $stmtEntite->execute([$operationId]);
                $entiteCp = $stmtEntite->fetchColumn() ?: '';

                $sectorService = new SectorService();
                $result = $sectorService->findSectorByAddress(
                    $operationId,
                    trim($data['numero']),
                    $data['rue_bis'] ?? '',
                    trim($data['rue']),
                    trim($data['ville']),
                    $entiteCp
                );

                if ($result) {
                    $sectorId = $result['sector_id'];
                    // Mettre à jour les coordonnées GPS si le géocodage les a trouvées
                    if ($result['gps_lat'] && $result['gps_lng']) {
                        $gpsLat = $result['gps_lat'];
                        $gpsLng = $result['gps_lng'];
                    }
                    LogService::info('[PassageController] Recherche secteur par adresse', [
                        'operation_id' => $operationId,
                        'adresse' => $data['numero'] . ' ' . $data['rue'] . ' ' . $data['ville'],
                        'sector_found' => $sectorId,
                        'gps_geocoded' => ($result['gps_lat'] && $result['gps_lng'])
                    ]);
                }
            }

            // 4. Fallback : si toujours pas de secteur, prendre le 1er secteur de l'opération
            if ($sectorId === null) {
                $stmtFirstSector = $this->db->prepare('
                    SELECT id FROM ope_sectors
                    WHERE fk_operation = ? AND chk_active = 1
                    ORDER BY id ASC
                    LIMIT 1
                ');
                $stmtFirstSector->execute([$operationId]);
                $firstSectorId = $stmtFirstSector->fetchColumn();

                if ($firstSectorId) {
                    $sectorId = (int)$firstSectorId;
                    LogService::info('[PassageController] Fallback: premier secteur de l\'opération', [
                        'operation_id' => $operationId,
                        'sector_id' => $sectorId
                    ]);
                }
            }

            // Chiffrement des données sensibles
            $encryptedName = '';
            if (isset($data['name']) && !empty(trim($data['name']))) {
                $encryptedName = ApiService::encryptData($data['name']);
            } elseif (isset($data['encrypted_name']) && !empty($data['encrypted_name'])) {
                $encryptedName = $data['encrypted_name'];
            }
            // Le nom peut rester vide si les conditions ne l'exigent pas
            $encryptedEmail = isset($data['email']) && !empty($data['email']) ?
                ApiService::encryptSearchableData($data['email']) : '';
            $encryptedPhone = isset($data['phone']) && !empty($data['phone']) ?
                ApiService::encryptData($data['phone']) : '';

            // Préparation des données pour l'insertion
            $insertData = [
                'fk_operation' => $operationId,
                'fk_sector' => $sectorId, // Peut être NULL si aucun secteur trouvé
                'fk_user' => $opeUserId,
                'fk_adresse' => $data['fk_adresse'] ?? '',
                'passed_at' => isset($data['passed_at']) ? $data['passed_at'] : null,
                'fk_type' => isset($data['fk_type']) ? (int)$data['fk_type'] : 0,
                'numero' => trim($data['numero']),
                'rue' => trim($data['rue']),
                'rue_bis' => $data['rue_bis'] ?? '',
                'ville' => trim($data['ville']),
                'fk_habitat' => isset($data['fk_habitat']) ? (int)$data['fk_habitat'] : 1,
                'appt' => $data['appt'] ?? '',
                'niveau' => $data['niveau'] ?? '',
                'residence' => $data['residence'] ?? '',
                'gps_lat' => $gpsLat ?? '',
                'gps_lng' => $gpsLng ?? '',
                'encrypted_name' => $encryptedName,
                'montant' => isset($data['montant']) ? (float)$data['montant'] : 0.00,
                'fk_type_reglement' => isset($data['fk_type_reglement']) ? (int)$data['fk_type_reglement'] : 1,
                'remarque' => $data['remarque'] ?? '',
                'encrypted_email' => $encryptedEmail,
                'encrypted_phone' => $encryptedPhone,
                'stripe_payment_id' => isset($data['stripe_payment_id']) ? trim($data['stripe_payment_id']) : null,
                'nom_recu' => $data['nom_recu'] ?? null,
                'date_recu' => isset($data['date_recu']) ? $data['date_recu'] : null,
                'docremis' => isset($data['docremis']) ? (int)$data['docremis'] : 0,
                'date_repasser' => isset($data['date_repasser']) ? $data['date_repasser'] : null,
                'nb_passages' => isset($data['nb_passages']) ? (int)$data['nb_passages'] : 1,
                'chk_mobile' => isset($data['chk_mobile']) ? (int)$data['chk_mobile'] : 0,
                'anomalie' => isset($data['anomalie']) ? (int)$data['anomalie'] : 0,
                'fk_user_creat' => $userId
            ];

            // Construction de la requête d'insertion
            $fields = array_keys($insertData);
            $placeholders = array_fill(0, count($fields), '?');

            $sql = 'INSERT INTO ope_pass (' . implode(', ', $fields) . ') VALUES (' . implode(', ', $placeholders) . ')';

            $stmt = $this->db->prepare($sql);
            $stmt->execute(array_values($insertData));

            $passageId = $this->db->lastInsertId();

            // Log de création du passage
            EventLogService::logPassageCreated(
                (int)$passageId,
                $insertData['fk_operation'],
                $insertData['fk_sector'] ?? 0, // 0 si secteur non trouvé
                $insertData['montant'],
                (string)$insertData['fk_type_reglement']
            );

            // Enregistrer la génération du reçu dans shutdown_function pour garantir son exécution
            // Même si le worker FPM est tué après fastcgi_finish_request()
            $fkType = isset($data['fk_type']) ? (int)$data['fk_type'] : 0;
            if ($fkType === 1 || $fkType === 5) {
                // Vérifier si un email a été fourni
                $hasEmail = false;
                if (!empty($data['email'])) {
                    $hasEmail = filter_var($data['email'], FILTER_VALIDATE_EMAIL) !== false;
                } elseif (!empty($encryptedEmail)) {
                    $hasEmail = true;
                }

                if ($hasEmail) {
                    $capturedPassageId = $passageId; // Capturer pour la closure

                    register_shutdown_function(function() use ($capturedPassageId) {
                        try {
                            $receiptService = new \App\Services\ReceiptService();
                            $receiptGenerated = $receiptService->generateReceiptForPassage($capturedPassageId);

                            if ($receiptGenerated) {
                                LogService::log('Reçu généré automatiquement pour le passage', [
                                    'level' => 'info',
                                    'passageId' => $capturedPassageId
                                ]);
                            }
                        } catch (Exception $e) {
                            LogService::log('Erreur lors de la génération automatique du reçu', [
                                'level' => 'warning',
                                'error' => $e->getMessage(),
                                'passageId' => $capturedPassageId
                            ]);
                        }
                    });
                }
            }

            // Envoyer la réponse immédiatement pour éviter les timeouts
            Response::json([
                'status' => 'success',
                'message' => 'Passage créé avec succès',
                'passage_id' => $passageId,
                'receipt_generated' => false  // On va générer le reçu en arrière-plan
            ], 201);

            // Flush la sortie pour s'assurer que la réponse est envoyée
            if (ob_get_level()) {
                ob_end_flush();
            }
            flush();

            // Fermer la connexion HTTP mais continuer le traitement
            if (function_exists('fastcgi_finish_request')) {
                fastcgi_finish_request();
            }

            return; // Fin de la méthode
        } catch (Exception $e) {
            LogService::log('Erreur lors de la création du passage', [
                'level' => 'error',
                'error' => $e->getMessage(),
                'userId' => $userId ?? null
            ]);

            Response::json([
                'status' => 'error',
                'message' => 'Erreur lors de la création du passage'
            ], 500);
        }
    }

    /**
     * Met à jour un passage existant
     */
    public function updatePassage(string $id): void {
        try {
            $userId = Session::getUserId();
            if (!$userId) {
                Response::json([
                    'status' => 'error',
                    'message' => 'Vous devez être connecté pour effectuer cette action'
                ], 401);
                return;
            }

            $passageId = (int)$id;
            $data = Request::getJson();

            // Vérifier que le passage existe et appartient à l'entité de l'utilisateur
            $entiteId = $this->getUserEntiteId($userId);
            if (!$entiteId) {
                Response::json([
                    'status' => 'error',
                    'message' => 'Entité non trouvée pour cet utilisateur'
                ], 404);
                return;
            }

            $stmt = $this->db->prepare('
                SELECT p.id, p.fk_operation, p.fk_type, p.fk_user
                FROM ope_pass p
                INNER JOIN operations o ON p.fk_operation = o.id
                WHERE p.id = ? AND o.fk_entite = ? AND p.chk_active = 1
            ');
            $stmt->execute([$passageId, $entiteId]);
            $passage = $stmt->fetch(PDO::FETCH_ASSOC);

            if (!$passage) {
                Response::json([
                    'status' => 'error',
                    'message' => 'Passage non trouvé'
                ], 404);
                return;
            }

            // Validation des données
            $errors = $this->validatePassageData($data, $passageId);
            if ($errors) {
                Response::json([
                    'status' => 'error',
                    'message' => 'Erreurs de validation',
                    'errors' => $errors
                ], 400);
                return;
            }

            // Récupérer ope_users.id pour l'utilisateur connecté
            $operationId = $passage['fk_operation'];
            $stmtCurrentOpeUser = $this->db->prepare('
                SELECT id FROM ope_users
                WHERE fk_user = ? AND fk_operation = ?
            ');
            $stmtCurrentOpeUser->execute([$userId, $operationId]);
            $currentOpeUserId = $stmtCurrentOpeUser->fetchColumn();

            if (!$currentOpeUserId) {
                Response::json([
                    'status' => 'error',
                    'message' => 'Utilisateur connecté non trouvé dans cette opération'
                ], 404);
                return;
            }

            // Si le passage était de type 2 et que l'utilisateur actuel est différent du créateur
            // On force l'attribution du passage à l'utilisateur actuel
            if ((int)$passage['fk_type'] === 2 && (int)$passage['fk_user'] !== $currentOpeUserId) {
                $data['fk_user'] = $currentOpeUserId;

                LogService::log('Attribution automatique d\'un passage type 2 à l\'utilisateur', [
                    'level' => 'info',
                    'passageId' => $passageId,
                    'ancien_user' => $passage['fk_user'],
                    'nouveau_user' => $currentOpeUserId
                ]);
            }

            // Construction de la requête de mise à jour dynamique
            $updateFields = [];
            $params = [];

            // Champs pouvant être mis à jour
            $updatableFields = [
                'fk_sector',
                // Note: fk_user est traité séparément pour conversion users.id -> ope_users.id
                'fk_adresse',
                'passed_at',
                'fk_type',
                'numero',
                'rue',
                'rue_bis',
                'ville',
                'fk_habitat',
                'appt',
                'niveau',
                'residence',
                'gps_lat',
                'gps_lng',
                'montant',
                'fk_type_reglement',
                'remarque',
                'stripe_payment_id',
                'stripe_payment_link_id',
                'nom_recu',
                'date_recu',
                'docremis',
                'date_repasser',
                'nb_passages',
                'chk_mobile',
                'anomalie'
            ];

            foreach ($updatableFields as $field) {
                if (isset($data[$field])) {
                    $updateFields[] = "$field = ?";
                    $params[] = $data[$field];
                }
            }

            // Traitement spécial pour fk_user : conversion users.id -> ope_users.id
            if (isset($data['fk_user'])) {
                // Si $data['fk_user'] vient de l'attribution automatique, c'est déjà ope_users.id
                // Sinon, on doit convertir users.id en ope_users.id
                $providedUserId = (int)$data['fk_user'];

                // Vérifier si c'est déjà un ope_users.id valide
                $stmtCheckOpeUser = $this->db->prepare('
                    SELECT id FROM ope_users
                    WHERE id = ? AND fk_operation = ?
                ');
                $stmtCheckOpeUser->execute([$providedUserId, $operationId]);
                $isOpeUserId = $stmtCheckOpeUser->fetchColumn();

                if ($isOpeUserId) {
                    // C'est déjà un ope_users.id valide
                    $updateFields[] = "fk_user = ?";
                    $params[] = $providedUserId;
                } else {
                    // C'est probablement un users.id, on le convertit
                    $stmtGetOpeUser = $this->db->prepare('
                        SELECT id FROM ope_users
                        WHERE fk_user = ? AND fk_operation = ?
                    ');
                    $stmtGetOpeUser->execute([$providedUserId, $operationId]);
                    $convertedOpeUserId = $stmtGetOpeUser->fetchColumn();

                    if ($convertedOpeUserId) {
                        $updateFields[] = "fk_user = ?";
                        $params[] = $convertedOpeUserId;
                    } else {
                        // Utilisateur non trouvé, on ignore cette mise à jour
                        LogService::log('Tentative de mise à jour avec un utilisateur invalide', [
                            'level' => 'warning',
                            'passageId' => $passageId,
                            'provided_user_id' => $providedUserId,
                            'operation_id' => $operationId
                        ]);
                    }
                }
            }

            // Gestion des champs chiffrés
            if (array_key_exists('name', $data)) {
                $updateFields[] = "encrypted_name = ?";
                // Permettre de vider le nom si les conditions le permettent
                $params[] = !empty(trim($data['name'])) ? ApiService::encryptData($data['name']) : '';
            }

            if (isset($data['email'])) {
                $updateFields[] = "encrypted_email = ?";
                $params[] = !empty($data['email']) ? ApiService::encryptSearchableData($data['email']) : '';
            }

            if (isset($data['phone'])) {
                $updateFields[] = "encrypted_phone = ?";
                $params[] = !empty($data['phone']) ? ApiService::encryptData($data['phone']) : '';
            }

            if (empty($updateFields)) {
                Response::json([
                    'status' => 'error',
                    'message' => 'Aucune donnée à mettre à jour'
                ], 400);
                return;
            }

            // Ajout des champs de mise à jour
            $updateFields[] = "updated_at = NOW()";
            $updateFields[] = "fk_user_modif = ?";
            $params[] = $userId;
            $params[] = $passageId;

            $sql = 'UPDATE ope_pass SET ' . implode(', ', $updateFields) . ' WHERE id = ?';
            $stmt = $this->db->prepare($sql);
            $stmt->execute($params);

            // Log de mise à jour du passage (changements simplifiés)
            $changes = [];
            foreach ($data as $key => $value) {
                // Ne logger que les champs non sensibles
                if (!in_array($key, ['name', 'email', 'phone', 'encrypted_name', 'encrypted_email', 'encrypted_phone'])) {
                    $changes[$key] = ['new' => $value];
                } else {
                    // Indiquer qu'un champ chiffré a été modifié
                    $changes[$key] = true;
                }
            }

            if (!empty($changes)) {
                EventLogService::logPassageUpdated((int)$passageId, $changes);
            }

            // Enregistrer la génération du reçu dans shutdown_function pour garantir son exécution
            // Même si le worker FPM est tué après fastcgi_finish_request()
            $capturedPassageId = $passageId;
            $capturedDb = $this->db;

            register_shutdown_function(function() use ($capturedPassageId, $capturedDb) {
                try {
                    // Récupérer les données actualisées du passage
                    $stmt = $capturedDb->prepare('SELECT fk_type, encrypted_email, nom_recu FROM ope_pass WHERE id = ?');
                    $stmt->execute([$capturedPassageId]);
                    $updatedPassage = $stmt->fetch(PDO::FETCH_ASSOC);

                    if ($updatedPassage) {
                        // Générer un reçu si :
                        // - C'est un don (fk_type = 1) ou un lot (fk_type = 5)
                        // - Il y a un email valide
                        // - Il n'y a pas encore de reçu (nom_recu est vide ou null)
                        $fkType = (int)$updatedPassage['fk_type'];
                        if (($fkType === 1 || $fkType === 5) &&
                            !empty($updatedPassage['encrypted_email']) &&
                            empty($updatedPassage['nom_recu'])) {

                            // Vérifier que l'email est valide en le déchiffrant
                            $email = ApiService::decryptSearchableData($updatedPassage['encrypted_email']);

                            if (!empty($email) && filter_var($email, FILTER_VALIDATE_EMAIL)) {
                                $receiptService = new \App\Services\ReceiptService();
                                $receiptGenerated = $receiptService->generateReceiptForPassage($capturedPassageId);

                                if ($receiptGenerated) {
                                    LogService::log('Reçu généré automatiquement après mise à jour du passage', [
                                        'level' => 'info',
                                        'passageId' => $capturedPassageId
                                    ]);
                                }
                            }
                        }
                    }
                } catch (Exception $e) {
                    LogService::log('Erreur lors de la génération automatique du reçu après mise à jour', [
                        'level' => 'warning',
                        'error' => $e->getMessage(),
                        'passageId' => $capturedPassageId
                    ]);
                }
            });

            // Envoyer la réponse immédiatement pour éviter les timeouts
            Response::json([
                'status' => 'success',
                'message' => 'Passage mis à jour avec succès',
                'receipt_generated' => false  // On va générer le reçu en arrière-plan
            ], 200);

            // Flush la sortie pour s'assurer que la réponse est envoyée
            if (ob_get_level()) {
                ob_end_flush();
            }
            flush();

            // Fermer la connexion HTTP mais continuer le traitement
            if (function_exists('fastcgi_finish_request')) {
                fastcgi_finish_request();
            }

            return; // Fin de la méthode
        } catch (Exception $e) {
            LogService::log('Erreur lors de la mise à jour du passage', [
                'level' => 'error',
                'error' => $e->getMessage(),
                'passageId' => $id,
                'userId' => $userId ?? null
            ]);

            Response::json([
                'status' => 'error',
                'message' => 'Erreur lors de la mise à jour du passage'
            ], 500);
        }
    }

    /**
     * Supprime (désactive) un passage
     */
    public function deletePassage(string $id): void {
        try {
            $userId = Session::getUserId();
            if (!$userId) {
                Response::json([
                    'status' => 'error',
                    'message' => 'Vous devez être connecté pour effectuer cette action'
                ], 401);
                return;
            }

            $passageId = (int)$id;

            // Récupérer le rôle de l'utilisateur
            $stmt = $this->db->prepare('SELECT fk_role, fk_entite FROM users WHERE id = ?');
            $stmt->execute([$userId]);
            $user = $stmt->fetch(PDO::FETCH_ASSOC);
            
            if (!$user) {
                Response::json([
                    'status' => 'error',
                    'message' => 'Utilisateur non trouvé'
                ], 404);
                return;
            }
            
            $userRole = (int)$user['fk_role'];
            $entiteId = (int)$user['fk_entite'];
            
            // Si l'utilisateur est un membre (fk_role = 1), vérifier les permissions de l'entité
            if ($userRole === 1) {
                $stmt = $this->db->prepare('SELECT chk_user_delete_pass FROM entites WHERE id = ?');
                $stmt->execute([$entiteId]);
                $entite = $stmt->fetch(PDO::FETCH_ASSOC);
                
                if (!$entite || (int)$entite['chk_user_delete_pass'] !== 1) {
                    LogService::log('Tentative de suppression de passage non autorisée', [
                        'level' => 'warning',
                        'userId' => $userId,
                        'userRole' => $userRole,
                        'entiteId' => $entiteId,
                        'passageId' => $passageId,
                        'chk_user_delete_pass' => $entite ? $entite['chk_user_delete_pass'] : null
                    ]);
                    
                    Response::json([
                        'status' => 'error',
                        'message' => 'Vous n\'avez pas l\'autorisation de supprimer des passages'
                    ], 403);
                    return;
                }
            }
            
            // Vérifier que le passage existe et appartient à l'entité de l'utilisateur
            if (!$entiteId) {
                Response::json([
                    'status' => 'error',
                    'message' => 'Entité non trouvée pour cet utilisateur'
                ], 404);
                return;
            }

            $stmt = $this->db->prepare('
                SELECT p.id, p.fk_operation
                FROM ope_pass p
                INNER JOIN operations o ON p.fk_operation = o.id
                WHERE p.id = ? AND o.fk_entite = ? AND p.chk_active = 1
            ');
            $stmt->execute([$passageId, $entiteId]);
            $passage = $stmt->fetch(PDO::FETCH_ASSOC);

            if (!$passage) {
                Response::json([
                    'status' => 'error',
                    'message' => 'Passage non trouvé'
                ], 404);
                return;
            }

            // Désactiver le passage (soft delete)
            $stmt = $this->db->prepare('
                UPDATE ope_pass
                SET chk_active = 0, updated_at = NOW(), fk_user_modif = ?
                WHERE id = ?
            ');

            $stmt->execute([$userId, $passageId]);

            // Log de suppression du passage
            EventLogService::logPassageDeleted(
                $passageId,
                (int)$passage['fk_operation'],
                true // soft delete
            );

            Response::json([
                'status' => 'success',
                'message' => 'Passage supprimé avec succès'
            ], 200);
        } catch (Exception $e) {
            LogService::log('Erreur lors de la suppression du passage', [
                'level' => 'error',
                'error' => $e->getMessage(),
                'passageId' => $id,
                'userId' => $userId ?? null
            ]);

            Response::json([
                'status' => 'error',
                'message' => 'Erreur lors de la suppression du passage'
            ], 500);
        }
    }
    
    /**
     * Récupère le reçu PDF d'un passage
     * 
     * @param string $id ID du passage
     * @return void
     */
    public function getReceipt(string $id): void {
        try {
            $userId = Session::getUserId();
            if (!$userId) {
                Response::json([
                    'status' => 'error',
                    'message' => 'Vous devez être connecté pour effectuer cette action'
                ], 401);
                return;
            }
            
            $passageId = (int)$id;
            
            // Vérifier que le passage existe et que l'utilisateur y a accès
            $entiteId = $this->getUserEntiteId($userId);
            if (!$entiteId) {
                Response::json([
                    'status' => 'error',
                    'message' => 'Entité non trouvée pour cet utilisateur'
                ], 404);
                return;
            }
            
            // Récupérer les informations du passage et du reçu
            $stmt = $this->db->prepare('
                SELECT p.id, p.nom_recu, p.date_creat_recu, p.fk_operation, o.fk_entite
                FROM ope_pass p
                INNER JOIN operations o ON p.fk_operation = o.id
                WHERE p.id = ? AND o.fk_entite = ? AND p.chk_active = 1
            ');
            $stmt->execute([$passageId, $entiteId]);
            $passage = $stmt->fetch(PDO::FETCH_ASSOC);
            
            if (!$passage) {
                Response::json([
                    'status' => 'error',
                    'message' => 'Passage non trouvé ou accès non autorisé'
                ], 404);
                return;
            }
            
            if (empty($passage['nom_recu'])) {
                Response::json([
                    'status' => 'error',
                    'message' => 'Aucun reçu disponible pour ce passage'
                ], 404);
                return;
            }
            
            // Récupérer le fichier depuis la table medias
            $stmt = $this->db->prepare('
                SELECT file_path, mime_type, file_size, fichier
                FROM medias
                WHERE support = ? AND support_id = ? AND file_category = ?
                ORDER BY created_at DESC
                LIMIT 1
            ');
            $stmt->execute(['passage', $passageId, 'recu']);
            $media = $stmt->fetch(PDO::FETCH_ASSOC);
            
            if (!$media) {
                // Si pas trouvé dans medias, essayer de construire le chemin
                $filePath = __DIR__ . '/../../uploads/entites/' . $passage['fk_entite'] . 
                           '/recus/' . $passage['fk_operation'] . '/' . $passage['nom_recu'];
                
                if (!file_exists($filePath)) {
                    Response::json([
                        'status' => 'error',
                        'message' => 'Fichier reçu introuvable'
                    ], 404);
                    return;
                }
                
                $media = [
                    'file_path' => $filePath,
                    'mime_type' => 'application/pdf',
                    'fichier' => $passage['nom_recu'],
                    'file_size' => filesize($filePath)
                ];
            }
            
            // Vérifier que le fichier existe
            if (!file_exists($media['file_path'])) {
                Response::json([
                    'status' => 'error',
                    'message' => 'Fichier reçu introuvable sur le serveur'
                ], 404);
                return;
            }
            
            // Lire le contenu du fichier
            $pdfContent = file_get_contents($media['file_path']);
            if ($pdfContent === false) {
                Response::json([
                    'status' => 'error',
                    'message' => 'Impossible de lire le fichier reçu'
                ], 500);
                return;
            }
            
            // Option 1: Retourner le PDF directement (pour téléchargement)
            if (isset($_GET['download']) && $_GET['download'] === 'true') {
                header('Content-Type: ' . $media['mime_type']);
                header('Content-Disposition: attachment; filename="' . $media['fichier'] . '"');
                header('Content-Length: ' . $media['file_size']);
                header('Cache-Control: no-cache, must-revalidate');
                echo $pdfContent;
                exit;
            }
            
            // Option 2: Retourner le PDF en base64 dans JSON (pour Flutter)
            $base64 = base64_encode($pdfContent);
            
            Response::json([
                'status' => 'success',
                'receipt' => [
                    'passage_id' => $passageId,
                    'file_name' => $media['fichier'],
                    'mime_type' => $media['mime_type'],
                    'file_size' => $media['file_size'],
                    'created_at' => $passage['date_creat_recu'],
                    'data_base64' => $base64
                ]
            ], 200);
            
        } catch (Exception $e) {
            LogService::log('Erreur lors de la récupération du reçu', [
                'level' => 'error',
                'error' => $e->getMessage(),
                'passageId' => $id,
                'userId' => $userId ?? null
            ]);
            
            Response::json([
                'status' => 'error',
                'message' => 'Erreur lors de la récupération du reçu'
            ], 500);
        }
    }
}
