<?php
namespace App\Controllers;

use App\Core\Controller;
use App\Core\Auth;
use App\Core\Database;
use App\Core\Notifier;
use App\Core\PDF;
use PDO;

class IncidentsController extends Controller
{
    public function index(): void
    {
        if (!Auth::check()) { $this->redirect('/login'); }
        
        $pdo = Database::pdo();
        $currentUser = Auth::user();
        $incidentListScope = Auth::permissionScope('incidents', 'list');
        // Détection optionnelle de la table des assignations
        $hasAssignments = false;
        try {
            $dbNameAssign = $pdo->query('SELECT DATABASE()')->fetchColumn() ?: '';
            if ($dbNameAssign) {
                $chkA = $pdo->prepare('SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?');
                $chkA->execute([$dbNameAssign, 'incident_assignments']);
                $hasAssignments = ((int)$chkA->fetchColumn() > 0);
            }
        } catch (\Throwable $e) { $hasAssignments = false; }
        
        // Filtres de recherche
          $filters = [
              // Support des deux champs côté vue: ticket_id (ancien) et reference (nouveau)
              'ticket_id' => trim($_GET['ticket_id'] ?? ''),
              'reference' => trim($_GET['reference'] ?? ''),
              'incident_number' => trim($_GET['incident_number'] ?? ''),
              'client_id' => (int)($_GET['client_id'] ?? 0),
              'status_id' => (int)($_GET['status_id'] ?? 0),
              'priority' => $_GET['priority'] ?? '',
              'assigned_user' => (int)($_GET['assigned_user'] ?? 0),
              'date_from' => $_GET['date_from'] ?? '',
              'date_to' => $_GET['date_to'] ?? '',
              'sla_late' => isset($_GET['sla_late']) ? (int)($_GET['sla_late']) : 0,
        ];
        
        // Construction de la requête
        $where = ['1=1'];
        $params = [];
        
        // Filtre par identifiant de ticket/référence
        if ($filters['ticket_id']) {
            $where[] = 'i.reference LIKE ?';
            $params[] = '%' . $filters['ticket_id'] . '%';
        }
        if ($filters['reference']) {
            $where[] = 'i.reference LIKE ?';
            $params[] = '%' . $filters['reference'] . '%';
        }
        if ($filters['incident_number']) {
            $where[] = 'i.incident_number LIKE ?';
            $params[] = '%' . $filters['incident_number'] . '%';
        }
        if ($filters['client_id']) {
            $where[] = 'i.client_id = ?';
            $params[] = $filters['client_id'];
        }
        if ($filters['status_id']) {
            $where[] = 'i.status_id = ?';
            $params[] = $filters['status_id'];
        }
        if ($filters['priority']) {
            $where[] = 'i.priority = ?';
            $params[] = $filters['priority'];
        }
        if ($filters['assigned_user']) {
            // Filtre par technicien assigné (assignment actif) si table disponible
            if ($hasAssignments) {
                $where[] = 'EXISTS (SELECT 1 FROM incident_assignments ia WHERE ia.incident_id = i.id AND ia.user_id = ? AND (ia.active IS NULL OR ia.active = 1))';
                $params[] = $filters['assigned_user'];
            }
        }
        if ($filters['date_from']) {
            $where[] = 'DATE(i.declared_at) >= ?';
            $params[] = $filters['date_from'];
        }
        if ($filters['date_to']) {
            $where[] = 'DATE(i.declared_at) <= ?';
            $params[] = $filters['date_to'];
        }

        // Filtre: incidents hors SLA de résolution
        if (!empty($filters['sla_late'])) {
            $where[] = '(
                i.expected_resolution_at IS NOT NULL AND (
                    (i.resolved_at IS NULL AND i.expected_resolution_at < NOW())
                    OR (i.resolved_at IS NOT NULL AND i.resolved_at > i.expected_resolution_at)
                )
            )';
        }

        if ($incidentListScope === 'own') {
            if (($currentUser['role_key'] ?? '') === 'technicien' && $hasAssignments) {
                $where[] = '(i.declared_by = ? OR EXISTS (SELECT 1 FROM incident_assignments ia WHERE ia.incident_id = i.id AND ia.user_id = ? AND (ia.active IS NULL OR ia.active = 1)))';
                $params[] = (int)$currentUser['id'];
                $params[] = (int)$currentUser['id'];
            } else {
                $where[] = 'i.declared_by = ?';
                $params[] = (int)$currentUser['id'];
            }
        } elseif ($incidentListScope !== 'all' && ($currentUser['role_key'] ?? '') === 'technicien') {
            if ($hasAssignments) {
                $where[] = 'EXISTS (SELECT 1 FROM incident_assignments ia WHERE ia.incident_id = i.id AND ia.user_id = ? AND (ia.active IS NULL OR ia.active = 1))';
                $params[] = (int)$currentUser['id'];
            }
        } elseif ($incidentListScope !== 'all' && ($currentUser['role_key'] ?? '') !== 'technicien') {
            http_response_code(403);
            $this->redirect('/dashboard?error=forbidden');
            return;
        }
        
        // Inclure éventuellement la dernière géoloc de traitement (via sous-requêtes) et l'indicateur de rapport final,
        // en restant défensif si les tables n'existent pas dans le schéma courant.
        $extraSelect = '';
        try {
            $dbName = $pdo->query('SELECT DATABASE()')->fetchColumn() ?: '';
            $hasIncidentReports = false;
            $hasFinalReports = false;
            if ($dbName) {
                $chk = $pdo->prepare('SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?');
                $chk->execute([$dbName, 'incident_reports']);
                $hasIncidentReports = ((int)$chk->fetchColumn() > 0);
                $chk->execute([$dbName, 'incident_final_reports']);
                $hasFinalReports = ((int)$chk->fetchColumn() > 0);
            }
            $gpsSelect = $hasIncidentReports
                ? "(SELECT ir.gps_lat FROM incident_reports ir WHERE ir.incident_id = i.id AND ir.gps_lat IS NOT NULL AND ir.gps_lng IS NOT NULL ORDER BY ir.id DESC LIMIT 1) AS last_gps_lat,
                   (SELECT ir.gps_lng FROM incident_reports ir WHERE ir.incident_id = i.id AND ir.gps_lat IS NOT NULL AND ir.gps_lng IS NOT NULL ORDER BY ir.id DESC LIMIT 1) AS last_gps_lng"
                : "NULL AS last_gps_lat, NULL AS last_gps_lng";
            $finalSelect = $hasFinalReports
                ? "(SELECT COUNT(*) FROM incident_final_reports ifr WHERE ifr.incident_id = i.id AND ifr.restoration_at IS NOT NULL) AS intervention_completed_for_incident"
                : "0 AS intervention_completed_for_incident";
            $extraSelect = $gpsSelect . ",\n                   " . $finalSelect;
        } catch (\Throwable $e) {
            $extraSelect = "NULL AS last_gps_lat, NULL AS last_gps_lng, 0 AS intervention_completed_for_incident";
        }

        // Tolérer l'absence de la table incident_causes OU de la colonne incidents.cause_id
        $causeSelect = 'NULL AS cause_name';
        $causeJoin = '';
        try {
            $dbName = $pdo->query('SELECT DATABASE()')->fetchColumn() ?: '';
            if ($dbName) {
                $tchk = $pdo->prepare('SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?');
                $tchk->execute([$dbName, 'incident_causes']);
                $hasCauses = ((int)$tchk->fetchColumn() > 0);
                $cchk = $pdo->prepare('SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? AND COLUMN_NAME = ?');
                $cchk->execute([$dbName, 'incidents', 'cause_id']);
                $incHasCauseId = ((int)$cchk->fetchColumn() > 0);
                if ($hasCauses && $incHasCauseId) {
                    $causeSelect = 'ic.name as cause_name';
                    $causeJoin = 'LEFT JOIN incident_causes ic ON ic.id = i.cause_id';
                }
            }
        } catch (\Throwable $e) { /* no-op */ }

        // Sélection du nom de déclarant en tolérant l'absence de colonne users.name
        $declaredBySelect = 'CAST(u.id AS CHAR)';
        try {
            $dbName2 = $pdo->query('SELECT DATABASE()')->fetchColumn() ?: '';
            if ($dbName2) {
                $cchk = $pdo->prepare('SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? AND COLUMN_NAME = ?');
                $cchk->execute([$dbName2, 'users', 'name']);
                if ((int)$cchk->fetchColumn() > 0) {
                    $declaredBySelect = 'u.name';
                } else {
                    $cchk->execute([$dbName2, 'users', 'email']);
                    if ((int)$cchk->fetchColumn() > 0) { $declaredBySelect = 'u.email'; }
                }
            }
        } catch (\Throwable $e) { /* no-op */ }

        // Pagination (serveur): page & per_page
        $page = max(1, (int)($_GET['page'] ?? 1));
        $perPage = max(1, (int)($_GET['per_page'] ?? 50));
        $offset = ($page - 1) * $perPage;

        // Requête SQL défensive avec fallback en cas d'échec
        $incidents = [];
        try {
            $sql = "
                SELECT i.*, COALESCE(c.name, 'Client inconnu') as client_name, COALESCE(l.name, 'Localisation inconnue') as location_name,
                       COALESCE(s.label, 'Statut inconnu') as status_label, COALESCE(s.color, '#6c757d') as status_color, COALESCE(s.key_name, 'unknown') as status_key,
                       $causeSelect, $declaredBySelect as declared_by_name,
                       $extraSelect
                FROM incidents i
                LEFT JOIN clients c ON c.id = i.client_id
                LEFT JOIN locations l ON l.id = i.location_id
                LEFT JOIN incident_statuses s ON s.id = i.status_id
                $causeJoin
                LEFT JOIN users u ON u.id = i.declared_by
                WHERE " . implode(' AND ', $where) . "
                ORDER BY i.declared_at DESC
                LIMIT ? OFFSET ?
            ";

            $stmt = $pdo->prepare($sql);
            foreach ($params as $idx => $val) { $stmt->bindValue($idx+1, $val); }
            $stmt->bindValue(count($params)+1, (int)$perPage, PDO::PARAM_INT);
            $stmt->bindValue(count($params)+2, (int)$offset, PDO::PARAM_INT);
            $stmt->execute();
            $incidents = $stmt->fetchAll();
            // Compat vue: exposer ticket_id depuis reference si utilisé dans templates
            foreach ($incidents as &$incident) {
                if (!isset($incident['ticket_id']) && isset($incident['reference'])) {
                    $incident['ticket_id'] = $incident['reference'];
                }
            }
            // Fallback: si technicien et liste vide mais des assignations existent, tenter une requête par JOIN direct
            if (empty($incidents) && (($currentUser['role_key'] ?? '') === 'technicien') && $hasAssignments) {
                try {
                    // Reconstruire un WHERE sans la restriction technicien (elle est gérée par le JOIN)
                    $whereJoin = ['1=1'];
                    $paramsJoin = [];
                    if (!empty($filters['ticket_id'])) { $whereJoin[] = 'i.reference LIKE ?'; $paramsJoin[] = '%' . $filters['ticket_id'] . '%'; }
                    if (!empty($filters['reference'])) { $whereJoin[] = 'i.reference LIKE ?'; $paramsJoin[] = '%' . $filters['reference'] . '%'; }
                    if (!empty($filters['incident_number'])) { $whereJoin[] = 'i.incident_number LIKE ?'; $paramsJoin[] = '%' . $filters['incident_number'] . '%'; }
                    if (!empty($filters['client_id'])) { $whereJoin[] = 'i.client_id = ?'; $paramsJoin[] = $filters['client_id']; }
                    if (!empty($filters['status_id'])) { $whereJoin[] = 'i.status_id = ?'; $paramsJoin[] = $filters['status_id']; }
                    if (!empty($filters['priority'])) { $whereJoin[] = 'i.priority = ?'; $paramsJoin[] = $filters['priority']; }
                    if (!empty($filters['date_from'])) { $whereJoin[] = 'DATE(i.declared_at) >= ?'; $paramsJoin[] = $filters['date_from']; }
                    if (!empty($filters['date_to'])) { $whereJoin[] = 'DATE(i.declared_at) <= ?'; $paramsJoin[] = $filters['date_to']; }
                    if (!empty($filters['sla_late'])) {
                        $whereJoin[] = '(
                            i.expected_resolution_at IS NOT NULL AND (
                                (i.resolved_at IS NULL AND i.expected_resolution_at < NOW())
                                OR (i.resolved_at IS NOT NULL AND i.resolved_at > i.expected_resolution_at)
                            )
                        )';
                    }

                    $joinSql = "
                        SELECT i.*, COALESCE(c.name, 'Client inconnu') as client_name, COALESCE(l.name, 'Localisation inconnue') as location_name,
                               COALESCE(s.label, 'Statut inconnu') as status_label, COALESCE(s.color, '#6c757d') as status_color, COALESCE(s.key_name, 'unknown') as status_key,
                               $causeSelect, $declaredBySelect as declared_by_name,
                               $extraSelect
                        FROM incidents i
                        LEFT JOIN incident_assignments ia ON ia.incident_id = i.id AND ia.user_id = ? AND (ia.active IS NULL OR ia.active = 1)
                        LEFT JOIN clients c ON c.id = i.client_id
                        LEFT JOIN locations l ON l.id = i.location_id
                        LEFT JOIN incident_statuses s ON s.id = i.status_id
                        $causeJoin
                        LEFT JOIN users u ON u.id = i.declared_by
                        WHERE ia.user_id IS NOT NULL AND " . implode(' AND ', $whereJoin) . "
                        ORDER BY i.declared_at DESC
                        LIMIT ? OFFSET ?
                    ";
                    $stj = $pdo->prepare($joinSql);
                    // Bind des params: user_id + filtres + limit + offset
                    $stj->bindValue(1, (int)$currentUser['id'], PDO::PARAM_INT);
                    foreach ($paramsJoin as $idx => $val) { 
                        $stj->bindValue($idx+2, $val); 
                    }
                    $stj->bindValue(count($paramsJoin)+2, (int)$perPage, PDO::PARAM_INT);
                    $stj->bindValue(count($paramsJoin)+3, (int)$offset, PDO::PARAM_INT);
                    $stj->execute();
                    $incidents = $stj->fetchAll();
                    foreach ($incidents as &$incident) {
                        if (!isset($incident['ticket_id']) && isset($incident['reference'])) {
                            $incident['ticket_id'] = $incident['reference'];
                        }
                    }
                } catch (\Throwable $eJoin) {
                    // Ignorer si le JOIN échoue
                }
            }
        } catch (\Throwable $e) {
            // Fallback: requête minimaliste sans jointures complexes
            try {
                $simpleSql = "SELECT i.id, i.reference, i.title, i.priority, i.declared_at, i.updated_at FROM incidents i WHERE " . implode(' AND ', $where) . " ORDER BY i.declared_at DESC LIMIT ? OFFSET ?";
                $stmt = $pdo->prepare($simpleSql);
                foreach ($params as $idx => $val) { $stmt->bindValue($idx+1, $val); }
                $stmt->bindValue(count($params)+1, (int)$perPage, PDO::PARAM_INT);
                $stmt->bindValue(count($params)+2, (int)$offset, PDO::PARAM_INT);
                $stmt->execute();
                $incidents = $stmt->fetchAll();
                // Ajouter des champs manquants avec des valeurs par défaut
                foreach ($incidents as &$incident) {
                    $incident['ticket_id'] = $incident['reference']; // Mapper reference vers ticket_id pour compatibilité vue
                    $incident['client_name'] = $incident['client_name'] ?? 'Client inconnu';
                    $incident['location_name'] = $incident['location_name'] ?? 'Localisation inconnue';
                    $incident['status_label'] = $incident['status_label'] ?? 'Statut inconnu';
                    $incident['status_color'] = $incident['status_color'] ?? '#6c757d';
                    $incident['status_key'] = $incident['status_key'] ?? 'unknown';
                    $incident['declared_by_name'] = $incident['declared_by_name'] ?? '';
                    $incident['cause_name'] = null;
                    $incident['last_gps_lat'] = null;
                    $incident['last_gps_lng'] = null;
                    $incident['intervention_completed_for_incident'] = 0;
                }
            } catch (\Throwable $e2) {
                $incidents = []; // En dernier recours, liste vide
            }
        }
        
        // Charger les assignations pour chaque incident (pour affichage dans la liste)
        $assignedUsersMap = [];
        if (!empty($incidents) && $hasAssignments) {
            try {
                $incidentIds = array_column($incidents, 'id');
                $placeholders = implode(',', array_fill(0, count($incidentIds), '?'));
                $assignStmt = $pdo->prepare("
                    SELECT ia.incident_id, u.name 
                    FROM incident_assignments ia 
                    JOIN users u ON u.id = ia.user_id 
                    WHERE ia.incident_id IN ($placeholders) AND (ia.active IS NULL OR ia.active = 1)
                    ORDER BY ia.assigned_at ASC
                ");
                $assignStmt->execute($incidentIds);
                $assignments = $assignStmt->fetchAll(PDO::FETCH_ASSOC);
                
                foreach ($assignments as $assign) {
                    $incId = (int)$assign['incident_id'];
                    if (!isset($assignedUsersMap[$incId])) {
                        $assignedUsersMap[$incId] = [];
                    }
                    $assignedUsersMap[$incId][] = $assign['name'];
                }
            } catch (\Throwable $e) {
                $assignedUsersMap = []; // En cas d'erreur, pas d'assignations affichées
            }
        }
        
        // Données pour les filtres (défensif vis-à-vis du schéma)
        try { $clients = $pdo->query('SELECT * FROM clients WHERE active = 1 ORDER BY name')->fetchAll(); }
        catch (\Throwable $e) { $clients = []; }
        try { $statuses = $pdo->query('SELECT * FROM incident_statuses ORDER BY sort_order')->fetchAll(); }
        catch (\Throwable $e) { $statuses = []; }
        try { $users = $pdo->query('SELECT * FROM users WHERE active = 1 AND role_key = "technicien" ORDER BY name')->fetchAll(); }
        catch (\Throwable $e) { $users = []; }
        
        // Statistiques (appliquées au même filtre)
        $whereSql = implode(' AND ', $where);

        // Total filtré (défensif)
        $totalCount = count($incidents); // Fallback par défaut
        try {
            $qTotal = $pdo->prepare("SELECT COUNT(*) FROM incidents i WHERE $whereSql");
            $qTotal->execute($params);
            $totalCount = (int)$qTotal->fetchColumn();
        } catch (\Throwable $e) {
            $totalCount = count($incidents);
        }

        // Aide pour compter par key_name de statut en respectant les mêmes filtres
        $countByKey = function(string $key) use ($pdo, $whereSql, $params): int {
            try {
                $sql = "SELECT COUNT(*)
                        FROM incidents i
                        JOIN incident_statuses s ON s.id = i.status_id
                        WHERE $whereSql AND s.key_name = ?";
                $st = $pdo->prepare($sql);
                $bind = $params; $bind[] = $key;
                $st->execute($bind);
                return (int)$st->fetchColumn();
            } catch (\Throwable $e) {
                return 0;
            }
        };

        $openCount = $countByKey('open');
        $inProgressCount = $countByKey('in_progress');
        // Clos = resolved + closed si présents
        $closedCount = 0;
        foreach (['resolved','closed'] as $k) { $closedCount += $countByKey($k); }

        $kpis = [
            'total' => $totalCount,
            'open' => $openCount,
            'in_progress' => $inProgressCount,
            'closed' => $closedCount,
        ];

        // Répartition par statut (label/couleur)
        try {
            $qStatus = $pdo->prepare("SELECT s.key_name, s.label, s.color, COUNT(*) as c
                                      FROM incidents i
                                      JOIN incident_statuses s ON s.id = i.status_id
                                      WHERE $whereSql
                                      GROUP BY s.id, s.key_name, s.label, s.color
                                      ORDER BY MIN(s.sort_order)");
            $qStatus->execute($params);
            $statusRows = $qStatus->fetchAll();
            $statusChart = [
                'labels' => array_map(fn($r) => $r['label'], $statusRows),
                'data' => array_map(fn($r) => (int)$r['c'], $statusRows),
                // Couleurs harmonisées avec la palette métier (sans granularité SLA ici)
                'colors' => array_map(function($r) {
                    return incident_status_color([
                        'status_key'   => $r['key_name'] ?? '',
                        'status_label' => $r['label'] ?? '',
                        'status_color' => $r['color'] ?? '',
                    ]);
                }, $statusRows),
            ];
        } catch (\Throwable $e) {
            $statusChart = [
                'labels' => [],
                'data' => [],
                'colors' => [],
            ];
        }

        // Répartition par priorité
        $priorityStats = [];
        try {
            $qPrio = $pdo->prepare("SELECT priority, COUNT(*) as c FROM incidents i WHERE $whereSql GROUP BY priority");
            $qPrio->execute($params);
            foreach ($qPrio->fetchAll() as $r) {
                $key = $r['priority'] ?: 'Non défini';
                $priorityStats[$key] = (int)$r['c'];
            }
        } catch (\Throwable $e) {
            $priorityStats = [];
        }

        $totalPages = (int)ceil($totalCount / $perPage);
        $pagination = [
            'page' => $page,
            'per_page' => $perPage,
            'total' => $totalCount,
            'total_pages' => $totalPages,
            'offset' => $offset,
        ];

        $this->view('incidents/index', compact('incidents', 'clients', 'statuses', 'users', 'filters', 'kpis', 'statusChart', 'priorityStats', 'assignedUsersMap', 'pagination'));
    }

    public function create(): void
    {
        if (!Auth::check()) { $this->redirect('/login'); }
        // Autoriser également les techniciens à déclarer un incident
        Auth::requireRole(['admin', 'agent', 'technicien', 'manager', 'superviseur', 'supervisor']);
        
    $pdo = Database::pdo();
    // S'assurer que la table des niveaux de priorité existe (seed ignoré si schéma différent)
    $this->ensurePriorityLevelsTable($pdo);
    // Charger les référentiels de manière défensive: si une table est absente, retourner un tableau vide au lieu d'une 500
    try { $clients = $pdo->query('SELECT * FROM clients WHERE active = 1 ORDER BY name')->fetchAll(); } catch (\Throwable $e) { $clients = []; }
    try { $locations = $pdo->query('SELECT * FROM locations ORDER BY name')->fetchAll(); } catch (\Throwable $e) { $locations = []; }
    try { $causes = $pdo->query('SELECT * FROM incident_causes WHERE active = 1 ORDER BY name')->fetchAll(); } catch (\Throwable $e) { $causes = []; }
    $priorities = $this->fetchPrioritiesNormalized($pdo);
    // Chargement des équipements avec normalisation (support schema category / type)
    $equipments = $this->fetchEquipmentsNormalized($pdo);
    try { $sites = $pdo->query('SELECT * FROM sites WHERE active = 1 ORDER BY name')->fetchAll(); } catch (\Throwable $e) { $sites = []; }
    try { $liaisons = $pdo->query('SELECT * FROM liaisons WHERE active = 1 ORDER BY name')->fetchAll(); } catch (\Throwable $e) { $liaisons = []; }
        
        // Si ouverture en modal, on rend sans layout pour n'injecter que le formulaire
        $isModal = isset($_GET['modal']) && $_GET['modal'] == '1';
        $this->view('incidents/create', [
            'clients' => $clients,
            'locations' => $locations,
            'causes' => $causes,
            'priorities' => $priorities,
            'equipments' => $equipments,
            'sites' => $sites,
            'liaisons' => $liaisons,
            'isModal' => $isModal,
            '_layout' => $isModal ? 'none' : 'main',
        ]);
    }

    public function store(): void
    {
        if (!Auth::check()) { $this->redirect('/login'); }
        // Autoriser également les techniciens à déclarer un incident
        Auth::requireRole(['admin', 'agent', 'technicien', 'manager', 'superviseur', 'supervisor']);
        
        if ($_SERVER['REQUEST_METHOD'] !== 'POST') { $this->redirect('/incidents/create'); }
        
        $clientId = (int)($_POST['client_id'] ?? 0);
        $locationId = isset($_POST['location_id']) && $_POST['location_id'] !== '' ? (int)$_POST['location_id'] : null;
        $causeId = (int)($_POST['cause_id'] ?? 0) ?: null;
        $siteId = (int)($_POST['site_id'] ?? 0) ?: null;
        $liaisonId = (int)($_POST['liaison_id'] ?? 0) ?: null;
        $priorityId = (int)($_POST['priority_id'] ?? 0) ?: null;
        $equipmentIds = isset($_POST['equipment_ids']) && is_array($_POST['equipment_ids']) 
            ? implode(',', array_filter(array_map('intval', $_POST['equipment_ids'])))
            : null;
        $title = trim($_POST['title'] ?? '');
        $description = trim($_POST['description'] ?? '');
        $priority = $_POST['priority'] ?? 'Moyenne'; // Garde l'ancienne logique en parallèle
        $incidentNumber = trim($_POST['incident_number'] ?? '');
        
        // Nouveaux champs pour les sites de liaison
        $siteALabel = trim($_POST['site_a_label'] ?? '');
        $siteBLabel = trim($_POST['site_b_label'] ?? '');
        
    // Suppression du mode JSON: toujours redirection pleine page
    $isAjax = false;

        if (!$clientId || !$title) {
            $this->redirect('/incidents/create?error=missing_fields');
            return;
        }
        
        $pdo = Database::pdo();

        // S'assurer que la colonne ticket_id existe (anciennes bases peuvent en être dépourvues)
        // On la crée en NULLable pour compatibilité, même si le code fournit toujours une valeur.
        $this->ensureColumn($pdo, 'incidents', 'ticket_id', '`ticket_id` VARCHAR(40) NULL');

        // Si aucune localisation n'a été envoyée (champ retiré du formulaire),
        // utiliser une localisation active par défaut pour rester compatible
        // avec les bases où incidents.location_id est encore NOT NULL.
        if ($locationId === null) {
            try {
                $fallbackLocationId = $pdo->query('SELECT id FROM locations WHERE active = 1 ORDER BY id ASC LIMIT 1')->fetchColumn();
                if ($fallbackLocationId) {
                    $locationId = (int)$fallbackLocationId;
                }
            } catch (\Throwable $e) {
                // En cas d'erreur (table manquante, etc.), on laisse $locationId à null
                // et on laissera la logique SQL échouer proprement si la colonne est NOT NULL.
            }
        }

        $pdo->beginTransaction();
        
        try {
            // S'assurer de la colonne optionnelle incident_number
            $this->ensureColumn($pdo, 'incidents', 'incident_number', '`incident_number` VARCHAR(100) NULL');
            // Vérifier unicité si fourni
            if ($incidentNumber !== '') {
                $dupStmt = $pdo->prepare('SELECT COUNT(*) FROM incidents WHERE incident_number = ?');
                $dupStmt->execute([$incidentNumber]);
                if ((int)$dupStmt->fetchColumn() > 0) {
                    if ($pdo->inTransaction()) { $pdo->rollBack(); }
                    $this->redirect('/incidents/create?error=incident_number_exists');
                    return;
                }
                // Assurer index unique si inexistant
                try {
                    $db = $pdo->query('SELECT DATABASE()')->fetchColumn();
                    if ($db) {
                        $idxChk = $pdo->prepare('SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA=? AND TABLE_NAME=? AND INDEX_NAME=?');
                        $idxChk->execute([$db, 'incidents', 'uniq_incident_number']);
                        if ((int)$idxChk->fetchColumn() === 0) {
                            $pdo->exec('ALTER TABLE `incidents` ADD UNIQUE KEY `uniq_incident_number` (`incident_number`)');
                        }
                    }
                } catch (\Throwable $ie) { /* ignorer si droit manquant */ }
            }
            // Générer ticket ID
            $ticketId = $this->generateTicketId();
            
            // Statut initial "Ouvert" (crée le statut s'il n'existe pas)
            $openStatus = $this->ensureStatus($pdo, 'open', 'Ouvert', '#0d6efd', 10);
            
            // Calculer SLA (si cause connue)
            $expectedResponse = null;
            $expectedResolution = null;
            if ($causeId) {
                // Calcul SLA si tables présentes; sinon ignorer silencieusement
                try {
                    $dbName = $pdo->query('SELECT DATABASE()')->fetchColumn() ?: '';
                    $hasSla = false;
                    if ($dbName) {
                        $tchk = $pdo->prepare('SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA=? AND TABLE_NAME IN ("sla_policies","maintenance_types")');
                        $tchk->execute([$dbName]);
                        $hasSla = ((int)$tchk->fetchColumn() >= 2);
                    }
                    if ($hasSla) {
                        $sla = $pdo->prepare('
                    SELECT response_minutes, resolution_minutes 
                    FROM sla_policies sp
                    JOIN maintenance_types mt ON mt.id = sp.maintenance_type_id
                    WHERE mt.name = "Maintenance corrective" AND sp.priority = ?
                    LIMIT 1
                ');
                        $sla->execute([$priority]);
                        $slaData = $sla->fetch();
                        if ($slaData) {
                            $now = new \DateTime();
                            $expectedResponse = (clone $now)->add(new \DateInterval('PT' . (int)$slaData['response_minutes'] . 'M'));
                            $expectedResolution = (clone $now)->add(new \DateInterval('PT' . (int)$slaData['resolution_minutes'] . 'M'));
                        }
                    }
                } catch (\Throwable $e) { /* ignore SLA compute errors */ }
            }
            
            // S'assurer que les nouvelles colonnes existent
            $this->ensureColumn($pdo, 'incidents', 'site_id', '`site_id` INT NULL');
            $this->ensureColumn($pdo, 'incidents', 'liaison_id', '`liaison_id` INT NULL');
            $this->ensureColumn($pdo, 'incidents', 'priority_id', '`priority_id` INT NULL');
            $this->ensureColumn($pdo, 'incidents', 'equipment_ids', '`equipment_ids` TEXT NULL');
            $this->ensureColumn($pdo, 'incidents', 'site_a_label', '`site_a_label` VARCHAR(255) NULL');
            $this->ensureColumn($pdo, 'incidents', 'site_b_label', '`site_b_label` VARCHAR(255) NULL');
            $this->ensureColumn($pdo, 'incidents', 'expected_response_at', '`expected_response_at` DATETIME NULL');
            $this->ensureColumn($pdo, 'incidents', 'expected_resolution_at', '`expected_resolution_at` DATETIME NULL');

            // Rendre location_id optionnel si la colonne est encore NOT NULL
            try {
                $dbName = $pdo->query('SELECT DATABASE()')->fetchColumn() ?: '';
                if ($dbName) {
                    $cchk = $pdo->prepare('SELECT IS_NULLABLE FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? AND COLUMN_NAME = ?');
                    $cchk->execute([$dbName, 'incidents', 'location_id']);
                    $nullable = $cchk->fetchColumn();
                    if ($nullable === 'NO') {
                        $pdo->exec('ALTER TABLE `incidents` MODIFY COLUMN `location_id` INT NULL');
                    }
                }
            } catch (\Throwable $e) { /* ignorer les erreurs de migration souple */ }
            
            // Insérer l'incident avec les nouveaux champs
            $legacyPriority = $this->resolveLegacyPriorityValue($pdo, $priorityId, $priority);
            $stmt = $pdo->prepare('
                             INSERT INTO incidents (ticket_id, incident_number, client_id, location_id, site_id, liaison_id, 
                                     cause_id, priority_id, equipment_ids, site_a_label, site_b_label, title, description, priority, status_id, 
                                     declared_by, expected_response_at, expected_resolution_at)
                     VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
            ');
            $stmt->execute([
                $ticketId, ($incidentNumber !== '' ? $incidentNumber : null), $clientId, $locationId, $siteId, $liaisonId,
                $causeId, $priorityId, $equipmentIds, ($siteALabel !== '' ? $siteALabel : null), ($siteBLabel !== '' ? $siteBLabel : null), 
                $title, $description, $legacyPriority, $openStatus, Auth::user()['id'],
                $expectedResponse ? $expectedResponse->format('Y-m-d H:i:s') : null,
                $expectedResolution ? $expectedResolution->format('Y-m-d H:i:s') : null
            ]);
            
            
            $incidentId = $pdo->lastInsertId();
            
            // Créer l'historique initial (non bloquant si la table n'existe pas / droits insuffisants)
            try {
                $this->ensureHistoryTable($pdo);
                $histStmt = $pdo->prepare('
                    INSERT INTO incident_history (incident_id, field_name, old_value, new_value, changed_by)
                    VALUES (?, ?, ?, ?, ?)
                ');
                $histStmt->execute([$incidentId, 'status', null, 'Ouvert', Auth::user()['id']]);
            } catch (\Throwable $e) {
                // ignore
            }
            
            if ($pdo->inTransaction()) { $pdo->commit(); }
            // Redirection pleine page vers la page détail
            $this->redirect('/incidents/detail?id=' . $incidentId);
            
        } catch (\Throwable $e) {
            if ($pdo->inTransaction()) { $pdo->rollBack(); }
            // Journaliser l'erreur côté serveur pour diagnostic
            try {
                error_log('[IncidentsController::store] Creation failed: ' . $e->getMessage());
            } catch (\Throwable $logErr) { /* ignore logging errors */ }

            // Propager un code d'erreur minimal dans l'URL pour faciliter le debug
            $detail = substr($e->getMessage(), 0, 120);
            $this->redirect('/incidents/create?error=creation_failed&detail=' . urlencode($detail));
        }
    }

    private function respondJson(array $data): void
    {
        header('Content-Type: application/json; charset=utf-8');
        echo json_encode($data, JSON_UNESCAPED_UNICODE); 
        exit;
    }

    public function detail(): void
    {
        if (!Auth::check()) { $this->redirect('/login'); }
        
        $id = (int)($_GET['id'] ?? 0);
        if (!$id) { $this->redirect('/incidents'); }
        
        $pdo = Database::pdo();
        
        // Incident principal
        // Sélection dynamique du nom de déclarant (fallback email/id)
        $declaredBySel = 'CAST(u.id AS CHAR)';
        try {
            $dbName = $pdo->query('SELECT DATABASE()')->fetchColumn() ?: '';
            if ($dbName) {
                $cchk = $pdo->prepare('SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? AND COLUMN_NAME = ?');
                $cchk->execute([$dbName, 'users', 'name']);
                if ((int)$cchk->fetchColumn() > 0) {
                    $declaredBySel = 'u.name';
                } else {
                    $cchk->execute([$dbName, 'users', 'email']);
                    if ((int)$cchk->fetchColumn() > 0) { $declaredBySel = 'u.email'; }
                }
            }
        } catch (\Throwable $e) { /* no-op */ }

        // Tolérer l'absence de la table incident_causes OU de la colonne incidents.cause_id
        $causeSel = 'NULL AS cause_name, NULL AS cause_category';
        $causeJoin2 = '';
        try {
            $dbName = $pdo->query('SELECT DATABASE()')->fetchColumn() ?: '';
            if ($dbName) {
                $tchk = $pdo->prepare('SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?');
                $tchk->execute([$dbName, 'incident_causes']);
                $hasCauses = ((int)$tchk->fetchColumn() > 0);
                $cchk = $pdo->prepare('SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? AND COLUMN_NAME = ?');
                $cchk->execute([$dbName, 'incidents', 'cause_id']);
                $incHasCauseId = ((int)$cchk->fetchColumn() > 0);
                if ($hasCauses && $incHasCauseId) {
                    $causeSel = 'ic.name as cause_name, ic.category as cause_category';
                    $causeJoin2 = 'LEFT JOIN incident_causes ic ON ic.id = i.cause_id';
                }
            }
        } catch (\Throwable $e) { /* no-op */ }

        $detailSql = "
            SELECT i.*, c.name as client_name, l.name as location_name, l.address as location_address,
                   s.label as status_label, s.color as status_color, s.key_name as status_key,
                   $causeSel, $declaredBySel as declared_by_name
            FROM incidents i
            LEFT JOIN clients c ON c.id = i.client_id
            LEFT JOIN locations l ON l.id = i.location_id
            LEFT JOIN incident_statuses s ON s.id = i.status_id
            $causeJoin2
            LEFT JOIN users u ON u.id = i.declared_by
            WHERE i.id = ?
        ";
        $stmt = $pdo->prepare($detailSql);
        $stmt->execute([$id]);
        $incident = $stmt->fetch();
        
        if (!$incident) { $this->redirect('/incidents'); }

        $u = Auth::user();
        if (!$this->canAccessIncident($pdo, $incident, $u, 'view')) {
            http_response_code(403);
            $this->redirect('/incidents?error=forbidden');
            return;
        }
        
        // Auto-compléter SLA attendu si manquant (basé sur type/priority)
        $this->ensureIncidentSLA($pdo, $incident);

        // Techniciens assignés (tolérer l'absence de colonne phone)
        $phoneCol = 'NULL AS phone';
        try {
            $dbName = $pdo->query('SELECT DATABASE()')->fetchColumn() ?: '';
            if ($dbName) {
                $cchk = $pdo->prepare('SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? AND COLUMN_NAME = ?');
                $cchk->execute([$dbName, 'users', 'phone']);
                if ((int)$cchk->fetchColumn() > 0) { $phoneCol = 'u.phone'; }
            }
        } catch (\Throwable $e) { /* no-op */ }
        // Techniciens assignés: interroger seulement si la table des assignations existe
        $assignedUsers = [];
        try {
            $dbNameAU = $pdo->query('SELECT DATABASE()')->fetchColumn() ?: '';
            $hasAssignmentsAU = false;
            if ($dbNameAU) {
                $chkA = $pdo->prepare('SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?');
                $chkA->execute([$dbNameAU, 'incident_assignments']);
                $hasAssignmentsAU = ((int)$chkA->fetchColumn() > 0);
            }
            if ($hasAssignmentsAU) {
                $assignedSql = "SELECT u.id AS user_id, u.name, $phoneCol, ia.assigned_at
                                 FROM incident_assignments ia
                                 JOIN users u ON u.id = ia.user_id
                                 WHERE ia.incident_id = ? AND ia.active = 1
                                 ORDER BY ia.assigned_at DESC";
                $assignedStmt = $pdo->prepare($assignedSql);
                $assignedStmt->execute([$id]);
                $assignedUsers = $assignedStmt->fetchAll();
            }
        } catch (\Throwable $e) { $assignedUsers = []; }
        
        // Historique
        $this->ensureHistoryTable($pdo);
        // Sélection dynamique pour changed_by_name
        $changedBySel = 'CAST(u.id AS CHAR)';
        try {
            $dbName = $pdo->query('SELECT DATABASE()')->fetchColumn() ?: '';
            if ($dbName) {
                $cchk = $pdo->prepare('SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? AND COLUMN_NAME = ?');
                $cchk->execute([$dbName, 'users', 'name']);
                if ((int)$cchk->fetchColumn() > 0) {
                    $changedBySel = 'u.name';
                } else {
                    $cchk->execute([$dbName, 'users', 'email']);
                    if ((int)$cchk->fetchColumn() > 0) { $changedBySel = 'u.email'; }
                }
            }
        } catch (\Throwable $e) { /* no-op */ }
        $historySql = "
            SELECT ih.*, $changedBySel as changed_by_name
            FROM incident_history ih
            JOIN users u ON u.id = ih.changed_by
            WHERE ih.incident_id = ?
            ORDER BY ih.changed_at DESC";
        $historyStmt = $pdo->prepare($historySql);
        $historyStmt->execute([$id]);
        $history = $historyStmt->fetchAll();
        
        // Commentaires (avec réponses et pièces jointes)
        $this->ensureCommentTables($pdo);
        $roleKeyDetail = (string)($u['role_key'] ?? '');
        $canSeeInternal = in_array($roleKeyDetail, ['admin','agent','manager','superviseur','supervisor'], true);

        $commentsSql = '
            SELECT ic.*, u.name as user_name
            FROM incident_comments ic
            JOIN users u ON u.id = ic.user_id
            WHERE ic.incident_id = ?
        ';
        if (!$canSeeInternal) {
            $commentsSql .= ' AND (ic.is_internal IS NULL OR ic.is_internal = 0) ';
        }
        $commentsSql .= ' ORDER BY ic.created_at ASC';

        $commentsStmt = $pdo->prepare($commentsSql);
        $commentsStmt->execute([$id]);
        $commentsAll = $commentsStmt->fetchAll();
        $commentsCount = count($commentsAll);

        // Charger pièces jointes par commentaire
        $attachmentsByComment = [];
        if ($commentsCount > 0) {
            $ids = array_map(fn($c) => (int)$c['id'], $commentsAll);
            $in = implode(',', array_fill(0, count($ids), '?'));
            $attStmt = $pdo->prepare("SELECT comment_id, path FROM incident_comment_attachments WHERE comment_id IN ($in) ORDER BY id ASC");
            $attStmt->execute($ids);
            while ($row = $attStmt->fetch()) {
                $cid = (int)$row['comment_id'];
                if (!isset($attachmentsByComment[$cid])) { $attachmentsByComment[$cid] = []; }
                $attachmentsByComment[$cid][] = $row['path'];
            }
        }

        // Construire l'arbre des commentaires
        $byId = [];
        foreach ($commentsAll as $c) {
            $c['children'] = [];
            $c['attachments'] = $attachmentsByComment[(int)$c['id']] ?? [];
            $byId[(int)$c['id']] = $c;
        }
        $commentTree = [];
        foreach ($byId as $cid => &$c) {
            $pid = isset($c['parent_id']) ? (int)$c['parent_id'] : 0;
            if ($pid > 0 && isset($byId[$pid])) {
                $byId[$pid]['children'][] = &$c;
            } else {
                $commentTree[] = &$c;
            }
        }
        unset($c);
        
        // Données pour les actions
        $statuses = $pdo->query('SELECT * FROM incident_statuses ORDER BY sort_order')->fetchAll();
        $techniciens = $pdo->query('SELECT * FROM users WHERE role_key = "technicien" AND active = 1 ORDER BY name')->fetchAll();
        
        // État d'intervention du technicien courant (dérivé du dernier événement Intervention dans l'historique)
        $interventionState = 'none'; // none | active | paused | completed
        $interventionAt = null;
        $interventionDurationSec = 0; $interventionDurationHuman = '0m';
        try {
            $me = Auth::user();
            if ($me) {
                $hs = $pdo->prepare('SELECT new_value, changed_at FROM incident_history WHERE incident_id=? AND field_name="Intervention" AND changed_by=? ORDER BY changed_at DESC LIMIT 1');
                $hs->execute([$id, (int)$me['id']]);
                $last = $hs->fetch();
                if ($last && !empty($last['new_value'])) {
                    $txt = mb_strtolower($last['new_value']);
                    // Détection d'une fin/clôture définitive (rapport final, restauration, terminé)
                    if (preg_match('/cl[oô]t|termin|rapport final|restaur|rétabli|retabli/iu', $txt)) {
                        $interventionState = 'completed';
                        $interventionAt = $last['changed_at'];
                    }
                    elseif (str_contains($txt, 'pause')) { $interventionState = 'paused'; $interventionAt = $last['changed_at']; }
                    elseif (str_contains($txt, 'repris') || str_contains($txt, 'reprise')) { $interventionState = 'active'; $interventionAt = $last['changed_at']; }
                    elseif (str_contains($txt, 'démarr') || str_contains($txt, 'demarr')) { $interventionState = 'active'; $interventionAt = $last['changed_at']; }
                }
                // Durée cumulée (événements dédiés)
                $this->ensureInterventionEventsTable($pdo);
                [$interventionDurationSec, $interventionDurationHuman] = $this->computeInterventionDuration($pdo, $id, (int)$me['id']);
                // KPI: nombre de pauses & dernier motif
                $pauseCount = 0; $lastPauseReason = null; $lastPauseAt = null;
                try {
                    $pcStmt = $pdo->prepare('SELECT COUNT(*) FROM incident_intervention_events WHERE incident_id=? AND user_id=? AND action="pause"');
                    $pcStmt->execute([$id, (int)$me['id']]);
                    $pauseCount = (int)$pcStmt->fetchColumn();
                    $lrStmt = $pdo->prepare('SELECT reason, happened_at FROM incident_intervention_events WHERE incident_id=? AND user_id=? AND action="pause" ORDER BY happened_at DESC LIMIT 1');
                    $lrStmt->execute([$id, (int)$me['id']]);
                    $lr = $lrStmt->fetch();
                    if ($lr) { $lastPauseReason = $lr['reason'] ?: null; $lastPauseAt = $lr['happened_at'] ?: null; }
                } catch (\Throwable $ie) { /* ignore */ }
            }
        } catch (\Throwable $e) { /* ignore */ }

        // Rapport de traitement (dernier)
        // S'assurer que les tables de traitement existent
        $this->ensureTreatmentTables($pdo);
        $repStmt = $pdo->prepare('SELECT * FROM incident_reports WHERE incident_id=? ORDER BY id DESC LIMIT 1');
        $repStmt->execute([$id]);
        $treatmentReport = $repStmt->fetch();
        // Déterminer workflow traitement
        $treatmentState = 'none'; // none | draft | submitted | validated | rejected
        if ($treatmentReport) {
            if (!empty($treatmentReport['validated_at'])) { $treatmentState = 'validated'; }
            elseif (!empty($treatmentReport['rejected_at'])) { $treatmentState = 'rejected'; }
            elseif (!empty($treatmentReport['submitted_at'])) { $treatmentState = 'submitted'; }
            else { $treatmentState = 'draft'; }
        }

        // Correctif: si le traitement est validé mais que le statut est encore "Ouvert" (ou autre), forcer "Traité"
        try {
            if ($treatmentState === 'validated') {
                $curKey = $incident['status_key'] ?? '';
                if (!in_array($curKey, ['treated','closed','resolved'], true)) {
                    $oldLbl = $incident['status_label'] ?? null;
                    $treatedId = $this->ensureStatus($pdo, 'treated', 'Traité', '#0d6efd', 60);
                    if ($treatedId) {
                        // Mettre à jour le statut en base
                        $pdo->prepare('UPDATE incidents SET status_id=?, updated_at=NOW() WHERE id=?')->execute([$treatedId, (int)$incident['id']]);
                        // Récupérer les infos du statut appliqué pour l'affichage immédiat
                        $ns = $pdo->prepare('SELECT label, color, key_name FROM incident_statuses WHERE id=?');
                        $ns->execute([$treatedId]);
                        if ($row = $ns->fetch()) {
                            $incident['status_id'] = $treatedId;
                            $incident['status_label'] = $row['label'] ?? 'Traité';
                            $incident['status_color'] = $row['color'] ?? '#0d6efd';
                            $incident['status_key'] = $row['key_name'] ?? 'treated';
                        } else {
                            $incident['status_id'] = $treatedId;
                            $incident['status_label'] = 'Traité';
                            $incident['status_color'] = '#0d6efd';
                            $incident['status_key'] = 'treated';
                        }
                        // Historique
                        $hist = $pdo->prepare('INSERT INTO incident_history (incident_id, field_name, old_value, new_value, changed_by) VALUES (?, "Status", ?, ?, ?)');
                        $hist->execute([(int)$incident['id'], $oldLbl, $incident['status_label'], (int)(Auth::user()['id'] ?? 0)]);
                    }
                }
            }
        } catch (\Throwable $e) { /* ignore */ }

        // Rapport final (créé automatiquement à la validation du traitement)
        try {
            $this->ensureFinalReportTables($pdo);
            $frStmt = $pdo->prepare('SELECT * FROM incident_final_reports WHERE incident_id=? LIMIT 1');
            $frStmt->execute([$id]);
            $finalReport = $frStmt->fetch() ?: null;
            if ($finalReport) {
                if (!empty($finalReport['validated_at'])) {
                    $finalReportState = 'validated';
                } elseif (!empty($finalReport['restoration_at'])) {
                    $finalReportState = 'completed';
                } else {
                    $finalReportState = 'pending';
                }
            } else {
                $finalReportState = 'none';
            }
        } catch (\Throwable $e) { $finalReport = null; $finalReportState = 'none'; }

        // Si le rapport final est complété, forcer l'état terminé si pas actif
        if (($finalReportState ?? 'none') === 'completed' && $interventionState !== 'active') {
            $interventionState = 'completed';
        }

        // Liaison + coordonnées des sites (affichage dans la page détail)
        $liaison = null;
        try {
            $liaisonId = (int)($incident['liaison_id'] ?? 0);
            if ($liaisonId > 0) {
                $stL = $pdo->prepare('SELECT id, name, description, site_a_name, site_b_name FROM liaisons WHERE id = ? LIMIT 1');
                $stL->execute([$liaisonId]);
                $liaison = $stL->fetch() ?: null;
            }
        } catch (\Throwable $e) { $liaison = null; }

        $locations = [];
        try { $locations = $pdo->query('SELECT * FROM locations ORDER BY name')->fetchAll(); } catch (\Throwable $e) { $locations = []; }

        // Liste d'utilisateurs mentionnables (pour autocomplétion @user)
        $mentionUsers = [];
        try {
            $dbNameMU = (string)($pdo->query('SELECT DATABASE()')->fetchColumn() ?: '');
            $hasUsernameMU = false;
            $hasEmailMU = false;
            $hasRoleMU = false;
            if ($dbNameMU !== '') {
                $cchk = $pdo->prepare('SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=? AND TABLE_NAME=? AND COLUMN_NAME=?');
                $cchk->execute([$dbNameMU, 'users', 'username']);
                $hasUsernameMU = ((int)$cchk->fetchColumn() > 0);
                $cchk->execute([$dbNameMU, 'users', 'email']);
                $hasEmailMU = ((int)$cchk->fetchColumn() > 0);
                $cchk->execute([$dbNameMU, 'users', 'role_key']);
                $hasRoleMU = ((int)$cchk->fetchColumn() > 0);
            }

            $select = ['id'];
            if ($hasUsernameMU) { $select[] = 'username'; }
            if ($hasEmailMU) { $select[] = 'email'; }
            $select[] = 'name';
            if ($hasRoleMU) { $select[] = 'role_key'; }

            // Pour un technicien: limiter aux personnes pertinentes (encadrement + assignés).
            if (($roleKeyDetail ?? '') === 'technicien') {
                $allowedIds = [];
                // encadrement
                $allowedIds = array_map('intval', $pdo->query("SELECT id FROM users WHERE active=1 AND role_key IN ('superviseur','supervisor','manager','admin','agent')")->fetchAll(\PDO::FETCH_COLUMN) ?: []);
                // assignés
                try {
                    $t = $pdo->prepare('SELECT user_id FROM incident_assignments WHERE incident_id=? AND active=1');
                    $t->execute([$id]);
                    $allowedIds = array_merge($allowedIds, array_map('intval', $t->fetchAll(\PDO::FETCH_COLUMN) ?: []));
                } catch (\Throwable $e) { /* ignore */ }
                $allowedIds = array_values(array_unique(array_filter($allowedIds)));
                if (!empty($allowedIds)) {
                    $in = implode(',', array_fill(0, count($allowedIds), '?'));
                    $st = $pdo->prepare('SELECT ' . implode(', ', $select) . ' FROM users WHERE active=1 AND id IN (' . $in . ') ORDER BY name LIMIT 200');
                    $st->execute($allowedIds);
                    $rows = $st->fetchAll(\PDO::FETCH_ASSOC) ?: [];
                } else {
                    $rows = [];
                }
            } else {
                $rows = $pdo->query('SELECT ' . implode(', ', $select) . ' FROM users WHERE active=1 ORDER BY name LIMIT 200')->fetchAll(\PDO::FETCH_ASSOC) ?: [];
            }

            foreach ($rows as $r) {
                $handle = '';
                if ($hasUsernameMU && !empty($r['username'])) {
                    $handle = (string)$r['username'];
                } elseif ($hasEmailMU && !empty($r['email'])) {
                    $handle = (string)explode('@', (string)$r['email'])[0];
                }
                $handle = trim($handle);
                if ($handle === '') { continue; }
                $mentionUsers[] = [
                    'id' => (int)($r['id'] ?? 0),
                    'handle' => $handle,
                    'label' => (string)($r['name'] ?? $handle),
                ];
            }
        } catch (\Throwable $e) {
            $mentionUsers = [];
        }

        $this->view('incidents/detail_redesign', [
            'incident' => $incident,
            'liaison' => $liaison,
            'locations' => $locations,
            'assignedUsers' => $assignedUsers,
            'history' => $history,
            'commentsTree' => $commentTree,
            'commentsCount' => $commentsCount,
            'statuses' => $statuses,
            'techniciens' => $techniciens,
            'interventionState' => $interventionState,
            'interventionAt' => $interventionAt,
            'interventionDurationSec' => $interventionDurationSec,
            'interventionDurationHuman' => $interventionDurationHuman,
            'pauseCount' => $pauseCount ?? 0,
            'lastPauseReason' => $lastPauseReason ?? null,
            'lastPauseAt' => $lastPauseAt ?? null,
            'treatmentReport' => $treatmentReport ?: null,
            'treatmentState' => $treatmentState,
            'finalReport' => $finalReport,
            'finalReportState' => $finalReportState,
            'mentionUsers' => $mentionUsers,
        ]);
    }

    private function canAccessIncident(PDO $pdo, array $incident, ?array $user, string $action = 'view'): bool
    {
        if (!$user) {
            return false;
        }

        $scope = Auth::permissionScope('incidents', $action);
        if ($scope === 'all') {
            return true;
        }

        $userId = (int)($user['id'] ?? 0);
        if ($userId <= 0) {
            return false;
        }

        if ($scope === 'own' && (int)($incident['declared_by'] ?? 0) === $userId) {
            return true;
        }

        if (($user['role_key'] ?? '') === 'technicien') {
            return $this->userHasActiveAssignment($pdo, (int)($incident['id'] ?? 0), $userId);
        }

        return false;
    }

    private function userHasActiveAssignment(PDO $pdo, int $incidentId, int $userId): bool
    {
        if ($incidentId <= 0 || $userId <= 0) {
            return false;
        }

        try {
            $dbName = $pdo->query('SELECT DATABASE()')->fetchColumn() ?: '';
            if ($dbName === '') {
                return false;
            }

            $chk = $pdo->prepare('SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?');
            $chk->execute([$dbName, 'incident_assignments']);
            if ((int)$chk->fetchColumn() === 0) {
                return false;
            }

            $stmt = $pdo->prepare('SELECT COUNT(*) FROM incident_assignments WHERE incident_id = ? AND user_id = ? AND active = 1');
            $stmt->execute([$incidentId, $userId]);
            return (int)$stmt->fetchColumn() > 0;
        } catch (\Throwable $e) {
            return false;
        }
    }

    private function interventionAction(string $action): void
    {
        if (!Auth::check()) { $this->redirect('/login'); }
        $isAjax = (!empty($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest') || (isset($_POST['ajax']) && $_POST['ajax'] == '1');
        $u = Auth::user();
        $incidentId = (int)($_POST['incident_id'] ?? 0);
        if (!$incidentId) { if ($isAjax) { $this->respondJson(['status'=>'error','error'=>'missing_incident']); return; } $this->redirect('/incidents'); }
        // Verrou: si rapport final complété, aucune action intervention supplémentaire autorisée
        try {
            $pdoLk = Database::pdo();
            $stLk = $pdoLk->prepare('SELECT restoration_at FROM incident_final_reports WHERE incident_id=? LIMIT 1');
            $stLk->execute([$incidentId]);
            $frLk = $stLk->fetch();
            if ($frLk && !empty($frLk['restoration_at'])) {
                if ($isAjax) { $this->respondJson(['status'=>'error','error'=>'intervention_locked','message'=>'Intervention clôturée (rapport final).']); return; }
                $this->redirect('/incidents/detail?id='.$incidentId.'&error=intervention_locked');
            }
        } catch (\Throwable $lkE) { /* ignore lookup errors */ }
        // Motif obligatoire pour la mise en pause
        $pauseReason = null;
        if ($action === 'pause') {
            $pauseReason = trim($_POST['reason'] ?? '');
            if ($pauseReason === '') {
                if ($isAjax) { $this->respondJson(['status'=>'error','error'=>'reason_required','message'=>'Merci de renseigner un motif de mise en pause.']); return; }
                $this->redirect('/incidents/detail?id='.$incidentId.'&error=reason_required');
            }
        }
        $pdo = Database::pdo();
        // Restriction: technicien doit être assigné (ou admin/agent autorisés)
        if (!in_array($u['role_key'] ?? '', ['admin','agent'])) {
            $check = $pdo->prepare('SELECT COUNT(*) FROM incident_assignments WHERE incident_id=? AND user_id=? AND active=1');
            $check->execute([$incidentId, (int)$u['id']]);
            if ((int)$check->fetchColumn() === 0) { if ($isAjax) { $this->respondJson(['status'=>'error','error'=>'forbidden']); return; } $this->redirect('/incidents?error=forbidden'); }
        }
        // Mapper action vers libellé utilisateur
        $label = match ($action) {
            'start' => "Intervention démarrée",
            'pause' => "Intervention en pause",
            'resume' => "Intervention reprise",
            default => 'Intervention'
        };
        // Afficher le motif dans l'historique si pause
        if ($action === 'pause' && $pauseReason) {
            $label .= ' — Motif: ' . $pauseReason;
        }
        try {
            // Historique lisible
            $this->ensureHistoryTable($pdo);
            $hist = $pdo->prepare('INSERT INTO incident_history (incident_id, field_name, new_value, changed_by) VALUES (?, "Intervention", ?, ?)');
            $hist->execute([$incidentId, $label, (int)$u['id']]);
            // Table d'événements structurés
            $this->ensureInterventionEventsTable($pdo);
            if ($action === 'pause') {
                $ev = $pdo->prepare('INSERT INTO incident_intervention_events (incident_id, user_id, action, reason, happened_at) VALUES (?,?,?,?,NOW())');
                $ev->execute([$incidentId, (int)$u['id'], $action, $pauseReason]);
            } else {
                $ev = $pdo->prepare('INSERT INTO incident_intervention_events (incident_id, user_id, action, happened_at) VALUES (?,?,?,NOW())');
                $ev->execute([$incidentId, (int)$u['id'], $action]);
            }

            // Mettre à jour le statut incident pour refléter l'intervention (En cours)
            if (in_array($action, ['start','resume'], true)) {
                try {
                    $curSt = $pdo->prepare('SELECT i.status_id, s.key_name, s.label FROM incidents i LEFT JOIN incident_statuses s ON s.id=i.status_id WHERE i.id=? LIMIT 1');
                    $curSt->execute([$incidentId]);
                    $rowSt = $curSt->fetch();
                    $lowKey = strtolower($rowSt['key_name'] ?? '');
                    $lowLbl = mb_strtolower($rowSt['label'] ?? '');
                    $isClosed = in_array($lowKey, ['closed','resolved','cloture','clôturé'], true) || str_contains($lowLbl,'clôtur') || str_contains($lowLbl,'clos');
                    $isTreated = in_array($lowKey, ['treated','traite','processed'], true) || str_contains($lowLbl,'trait');

                    if (!$isClosed && !$isTreated) {
                        $inProgressId = $this->ensureStatus($pdo, 'in_progress', 'En cours', '#0d6efd', 30);
                        if ($inProgressId && (int)($rowSt['status_id'] ?? 0) !== (int)$inProgressId) {
                            $pdo->prepare('UPDATE incidents SET status_id=?, updated_at=NOW() WHERE id=?')->execute([$inProgressId, $incidentId]);
                            $histS = $pdo->prepare('INSERT INTO incident_history (incident_id, field_name, old_value, new_value, changed_by) VALUES (?, "Status", ?, ?, ?)');
                            $histS->execute([$incidentId, $rowSt['label'] ?? null, 'En cours', (int)$u['id']]);
                        }
                    }
                } catch (\Throwable $e) { /* ignore */ }
            }

            // Notifications pour pause/reprise (superviseurs/managers/admin)
            if (in_array($action, ['pause','resume'], true)) {
                try {
                    $iStmt = $pdo->prepare('SELECT ticket_id, title FROM incidents WHERE id=?');
                    $iStmt->execute([$incidentId]);
                    $i = $iStmt->fetch();
                    $ticket = $i['ticket_id'] ?? ('INC-' . $incidentId);
                    $titleN = ($action==='pause' ? 'Intervention en pause: ' : 'Intervention reprise: ') . $ticket;
                    $bodyN = ($u['name'] ?? 'Un technicien') . ' a ' . ($action==='pause' ? 'mis en pause' : 'repris') . " l'intervention sur " . $ticket . '.';
                    if ($action==='pause' && $pauseReason) { $bodyN .= " Motif: " . $pauseReason; }
                    $url = \route_url('/incidents/detail', ['id'=>$incidentId]);
                    $rec = $pdo->query("SELECT id FROM users WHERE active=1 AND role_key IN ('superviseur','supervisor','manager','admin')")->fetchAll(PDO::FETCH_COLUMN);
                    if (!empty($rec)) {
                        Notifier::notifyUsers($pdo, array_map('intval', $rec), $titleN, $bodyN, $url);
                        Notifier::emailUsers($pdo, array_map('intval', $rec), $titleN, nl2br(htmlentities($bodyN)) . '<br><a href="' . $url . '">Ouvrir</a>');
                    }
                } catch (\Throwable $ne) { /* ignore */ }
            }

            // Durée cumulée pour l'utilisateur courant
            [$durSec, $durHuman] = $this->computeInterventionDuration($pdo, $incidentId, (int)$u['id']);
            // Réponse AJAX
            if ($isAjax) {
                $when = $pdo->query('SELECT NOW()')->fetchColumn();
                $this->respondJson(['status'=>'ok','action'=>$action,'label'=>$label,'changed_at'=>$when,'changed_by'=>$u['name'] ?? '', 'duration_seconds'=>$durSec, 'duration_human'=>$durHuman]);
                return;
            }
            $this->redirect('/incidents/detail?id='.$incidentId);
        } catch (\Throwable $e) {
            if ($isAjax) { $this->respondJson(['status'=>'error','error'=>'intervention_failed']); return; }
            $this->redirect('/incidents/detail?id='.$incidentId.'&error=intervention_failed');
        }
    }

    public function interventionStart(): void { $this->interventionAction('start'); }
    public function interventionPause(): void { $this->interventionAction('pause'); }
    public function interventionResume(): void { $this->interventionAction('resume'); }

    private function ensureInterventionEventsTable(\PDO $pdo): void
    {
        $pdo->exec("CREATE TABLE IF NOT EXISTS incident_intervention_events (
            id INT AUTO_INCREMENT PRIMARY KEY,
            incident_id INT NOT NULL,
            user_id INT NOT NULL,
            action VARCHAR(10) NOT NULL, -- start | pause | resume
            reason TEXT NULL,
            happened_at DATETIME NOT NULL,
            INDEX (incident_id), INDEX (user_id), INDEX (happened_at)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
        // Assurer la colonne reason si table déjà existante
        $this->ensureColumn($pdo, 'incident_intervention_events', 'reason', '`reason` TEXT NULL');
    }

    private function computeInterventionDuration(\PDO $pdo, int $incidentId, int $userId): array
    {
        $stmt = $pdo->prepare('SELECT action, happened_at FROM incident_intervention_events WHERE incident_id=? AND user_id=? ORDER BY happened_at ASC');
        $stmt->execute([$incidentId, $userId]);
        $rows = $stmt->fetchAll();
        $total = 0; $lastStart = null;
        foreach ($rows as $r) {
            $t = strtotime($r['happened_at']);
            $a = strtolower($r['action']);
            if ($a === 'start' || $a === 'resume') { $lastStart = $t; }
            elseif ($a === 'pause') { if ($lastStart) { $total += ($t - $lastStart); $lastStart = null; } }
        }
        if ($lastStart) { $total += (time() - $lastStart); }
        $human = $this->formatDurationHuman($total);
        return [$total, $human];
    }

    private function formatDurationHuman(int $seconds): string
    {
        $h = intdiv($seconds, 3600); $m = intdiv($seconds % 3600, 60);
        if ($h > 0) { return $h . 'h ' . $m . 'm'; }
        return $m . 'm';
    }

    private function ensureHistoryTable(\PDO $pdo): void
    {
        try {
            $pdo->exec("CREATE TABLE IF NOT EXISTS incident_history (
                id INT AUTO_INCREMENT PRIMARY KEY,
                incident_id INT NOT NULL,
                field_name VARCHAR(100) NOT NULL,
                old_value TEXT NULL,
                new_value TEXT NULL,
                changed_by INT NULL,
                changed_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
                INDEX (incident_id), INDEX (changed_by), INDEX (changed_at)
            ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
        } catch (\Throwable $e) { /* ignore */ }
    }

    /**
     * Compatibilité schéma: la colonne incidents.priority varie selon les dumps (ENUM/VARCHAR/INT).
     * On résout une valeur compatible pour éviter les warnings MySQL 1265 (data truncated).
     */
    private function resolveLegacyPriorityValue(\PDO $pdo, ?int $priorityId, string $fallback): string|int
    {
        $candidate = trim($fallback) !== '' ? trim($fallback) : 'Moyenne';

        // Essayer d'utiliser le libellé de la table priority_levels si disponible
        if ($priorityId) {
            try {
                $stmt = $pdo->prepare("SELECT name, level FROM priority_levels WHERE id = ? LIMIT 1");
                $stmt->execute([$priorityId]);
                $row = $stmt->fetch(\PDO::FETCH_ASSOC) ?: null;
                if ($row) {
                    if (!empty($row['name'])) {
                        $candidate = trim((string)$row['name']);
                    }
                    $candidateLevel = isset($row['level']) ? (int)$row['level'] : null;
                }
            } catch (\Throwable $e) {
                // ignore
            }
        }

        // Inspecter le type réel de la colonne incidents.priority
        $dataType = null;
        $columnType = null;
        $maxLen = null;
        try {
            $col = $pdo->query("SELECT DATA_TYPE, COLUMN_TYPE, CHARACTER_MAXIMUM_LENGTH FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME='incidents' AND COLUMN_NAME='priority' LIMIT 1")
                ->fetch(\PDO::FETCH_ASSOC);
            if ($col) {
                $dataType = strtolower((string)($col['DATA_TYPE'] ?? ''));
                $columnType = (string)($col['COLUMN_TYPE'] ?? '');
                $maxLen = isset($col['CHARACTER_MAXIMUM_LENGTH']) ? (int)$col['CHARACTER_MAXIMUM_LENGTH'] : null;
            }
        } catch (\Throwable $e) {
            // ignore
        }

        // ENUM: garantir une valeur autorisée
        if ($dataType === 'enum' && $columnType) {
            $allowed = [];
            if (preg_match_all("/'([^']*)'/", $columnType, $m)) {
                $allowed = $m[1];
            }
            if ($allowed) {
                if (in_array($candidate, $allowed, true)) {
                    return $candidate;
                }
                // Mapping sémantique vers les valeurs autorisées
                $norm = $this->normalizePriorityToken($candidate);
                $map = [
                    'low' => ['basse','low','faible','minor'],
                    'med' => ['moyenne','medium','normal','normale','standard'],
                    'high' => ['haute','high','majeure','major'],
                    'urgent' => ['urgent','critical','critique','crit'],
                ];

                $chosenKey = 'med';
                foreach ($map as $k => $words) {
                    foreach ($words as $w) {
                        if ($norm === $w) { $chosenKey = $k; break 2; }
                    }
                }

                // Chercher une valeur autorisée qui ressemble à ce niveau
                foreach ($allowed as $val) {
                    $v = $this->normalizePriorityToken($val);
                    if ($chosenKey === 'low' && in_array($v, $map['low'], true)) return $val;
                    if ($chosenKey === 'med' && in_array($v, $map['med'], true)) return $val;
                    if ($chosenKey === 'high' && in_array($v, $map['high'], true)) return $val;
                    if ($chosenKey === 'urgent' && in_array($v, $map['urgent'], true)) return $val;
                }
                // Fallback: première valeur ENUM
                return $allowed[0];
            }
        }

        // Colonne numérique: convertir en score
        if (in_array($dataType, ['int','tinyint','smallint','mediumint','bigint'], true)) {
            $norm = $this->normalizePriorityToken($candidate);
            $score = 2;
            if (in_array($norm, ['basse','low','faible','minor'], true)) $score = 1;
            elseif (in_array($norm, ['haute','high','majeure','major'], true)) $score = 3;
            elseif (in_array($norm, ['urgent','critical','critique','crit'], true)) $score = 4;
            return $score;
        }

        // VARCHAR/TEXT: tronquer si nécessaire
        if (is_int($maxLen) && $maxLen > 0 && mb_strlen($candidate) > $maxLen) {
            $candidate = mb_substr($candidate, 0, $maxLen);
        }
        return $candidate;
    }

    private function normalizePriorityToken(string $value): string
    {
        $v = mb_strtolower(trim($value));
        $v = str_replace(['é','è','ê','ë','à','â','ä','î','ï','ô','ö','ù','û','ü','ç'], ['e','e','e','e','a','a','a','i','i','o','o','u','u','u','c'], $v);
        $v = preg_replace('/\s+/', ' ', $v) ?: $v;
        return $v;
    }

    public function interventionExportCsv(): void
    {
        if (!Auth::check()) { $this->redirect('/login'); }
        $incidentId = (int)($_GET['id'] ?? 0);
        if (!$incidentId) { $this->redirect('/incidents'); }
        $pdo = Database::pdo();
        $st = $pdo->prepare('SELECT e.happened_at, e.action, e.reason, u.name AS user_name FROM incident_intervention_events e JOIN users u ON u.id=e.user_id WHERE e.incident_id=? ORDER BY e.happened_at ASC');
        $st->execute([$incidentId]);
        $rows = $st->fetchAll();
        header('Content-Type: text/csv; charset=utf-8');
        header('Content-Disposition: attachment; filename="interventions_incident_' . $incidentId . '.csv"');
        $out = fopen('php://output', 'w');
        fputcsv($out, ['Date/Heure','Action','Utilisateur','Motif']);
        foreach ($rows as $r) { fputcsv($out, [$r['happened_at'], $r['action'], $r['user_name'], $r['reason'] ?? '']); }
        fclose($out); exit;
    }

    public function interventionExportPdf(): void
    {
        if (!Auth::check()) { $this->redirect('/login'); }
        $incidentId = (int)($_GET['id'] ?? 0);
        if (!$incidentId) { $this->redirect('/incidents'); }
        $pdo = Database::pdo();
        $incidentStmt = $pdo->prepare('SELECT i.*, c.name as client_name, l.name as location_name FROM incidents i JOIN clients c ON c.id=i.client_id JOIN locations l ON l.id=i.location_id WHERE i.id=?');
        $incidentStmt->execute([$incidentId]);
        $incident = $incidentStmt->fetch();
        if (!$incident) { $this->redirect('/incidents'); }
        $st = $pdo->prepare('SELECT e.happened_at, e.action, e.reason, u.name AS user_name FROM incident_intervention_events e JOIN users u ON u.id=e.user_id WHERE e.incident_id=? ORDER BY e.happened_at ASC');
        $st->execute([$incidentId]);
        $events = $st->fetchAll();
        $html = $this->renderViewToString('incidents/intervention_events_pdf', compact('incident','events'), 'none');
        if (!PDF::available()) {
            header('Content-Type: text/html; charset=utf-8'); echo $html; exit;
        }
        PDF::download($html, 'Interventions_' . ($incident['ticket_id'] ?? ('INC_'.$incidentId)) . '.pdf');
    }

    public function interventionMetricsPdf(): void
    {
        if (!Auth::check()) { $this->redirect('/login'); }
        $incidentId = (int)($_GET['id'] ?? 0);
        $userId = (int)($_GET['user_id'] ?? Auth::user()['id'] ?? 0);
        if (!$incidentId || !$userId) { $this->redirect('/incidents'); }
        
        $pdo = Database::pdo();
        
        // Vérifier accès (technicien ne peut voir que ses propres métriques ou incident assigné)
        $u = Auth::user();
        if (($u['role_key'] ?? '') === 'technicien') {
            if ($userId !== (int)$u['id']) { $this->redirect('/incidents?error=forbidden'); }
            $check = $pdo->prepare('SELECT COUNT(*) FROM incident_assignments WHERE incident_id=? AND user_id=? AND active=1');
            $check->execute([$incidentId, $userId]);
            if ((int)$check->fetchColumn() === 0) { $this->redirect('/incidents?error=forbidden'); }
        }
        
        $incidentStmt = $pdo->prepare('SELECT i.*, c.name as client_name, l.name as location_name FROM incidents i JOIN clients c ON c.id=i.client_id JOIN locations l ON l.id=i.location_id WHERE i.id=?');
        $incidentStmt->execute([$incidentId]);
        $incident = $incidentStmt->fetch();
        if (!$incident) { $this->redirect('/incidents'); }
        
        $this->ensureInterventionEventsTable($pdo);
        
        // Événements pour l'utilisateur spécifique
        $st = $pdo->prepare('SELECT e.happened_at, e.action, e.reason, u.name AS user_name FROM incident_intervention_events e JOIN users u ON u.id=e.user_id WHERE e.incident_id=? AND e.user_id=? ORDER BY e.happened_at ASC');
        $st->execute([$incidentId, $userId]);
        $events = $st->fetchAll();
        
        // Calcul des métriques
        [$durSec, $durHuman] = $this->computeInterventionDuration($pdo, $incidentId, $userId);
        $pauseStmt = $pdo->prepare('SELECT COUNT(*) FROM incident_intervention_events WHERE incident_id=? AND user_id=? AND action="pause"');
        $pauseStmt->execute([$incidentId, $userId]);
        $pauseCount = (int)$pauseStmt->fetchColumn();
        
        $techStmt = $pdo->prepare('SELECT name FROM users WHERE id=? LIMIT 1');
        $techStmt->execute([$userId]);
        $techName = $techStmt->fetchColumn() ?: 'Technicien';
        
        $summary = [
            'duration_seconds' => $durSec,
            'duration_human' => $durHuman,
            'pause_count' => $pauseCount,
            'event_count' => count($events),
            'technician_name' => $techName
        ];
        
        $html = $this->renderViewToString('incidents/intervention_metrics_pdf', compact('incident','events','summary'), 'none');
        if (!PDF::available()) {
            header('Content-Type: text/html; charset=utf-8'); echo $html; exit;
        }
        PDF::download($html, 'Metriques_Intervention_' . ($incident['ticket_id'] ?? ('INC_'.$incidentId)) . '_' . str_replace(' ', '_', $techName) . '.pdf');
    }

    public function edit(): void
    {
        if (!Auth::check()) { $this->redirect('/login'); }
        // Désormais: seuls admin et agent peuvent modifier la déclaration.
        // Un technicien ne doit plus accéder à l'édition même s'il est assigné.
        Auth::requireRole(['admin','agent', 'manager', 'superviseur', 'supervisor']);

        $id = (int)($_GET['id'] ?? 0);
        if (!$id) { $this->redirect('/incidents'); }

        $pdo = Database::pdo();
        $stmt = $pdo->prepare('SELECT * FROM incidents WHERE id = ?');
        $stmt->execute([$id]);
        $incident = $stmt->fetch();
        if (!$incident) { $this->redirect('/incidents'); }

        // Suppression de la logique d'autorisation pour technicien: plus d'édition autorisée.
        $u = Auth::user();
        if (($u['role_key'] ?? '') === 'technicien') { $this->redirect('/incidents?error=forbidden'); }

    $clients = $pdo->query('SELECT * FROM clients WHERE active = 1 ORDER BY name')->fetchAll();
        
        // Utiliser les nouvelles tables de référentiels
    // Assurer la table des priorités (seed ignoré si schéma différent)
    $this->ensurePriorityLevelsTable($pdo);
    $locations = $pdo->query('SELECT * FROM locations WHERE active = 1 ORDER BY name')->fetchAll();
    $causes = $pdo->query('SELECT * FROM incident_causes WHERE active = 1 ORDER BY name')->fetchAll();
    $priorities = $this->fetchPrioritiesNormalized($pdo);
    $equipments = $this->fetchEquipmentsNormalized($pdo);
    $sites = $pdo->query('SELECT * FROM sites WHERE active = 1 ORDER BY name')->fetchAll();
    $liaisons = $pdo->query('SELECT * FROM liaisons WHERE active = 1 ORDER BY name')->fetchAll();

        $this->view('incidents/create', [
            'clients' => $clients,
            'locations' => $locations,
            'causes' => $causes,
            'priorities' => $priorities,
            'equipments' => $equipments,
            'sites' => $sites,
            'liaisons' => $liaisons,
            'incident' => $incident,
            'mode' => 'edit',
            '_layout' => 'main',
        ]);
    }

    public function update(): void
    {
        if (!Auth::check()) { $this->redirect('/login'); }
        // Technicien ne peut plus mettre à jour la déclaration.
        Auth::requireRole(['admin','agent', 'manager', 'superviseur', 'supervisor']);
        if ($_SERVER['REQUEST_METHOD'] !== 'POST') { $this->redirect('/incidents'); }

        $id = (int)($_POST['id'] ?? 0);
        $clientId = (int)($_POST['client_id'] ?? 0);
        $locationId = (int)($_POST['location_id'] ?? 0);
        $causeId = (int)($_POST['cause_id'] ?? 0) ?: null;
        $siteId = (int)($_POST['site_id'] ?? 0) ?: null;
        $liaisonId = (int)($_POST['liaison_id'] ?? 0) ?: null;
        $priorityId = (int)($_POST['priority_id'] ?? 0) ?: null;
        $equipmentIds = isset($_POST['equipment_ids']) && is_array($_POST['equipment_ids']) 
            ? implode(',', array_filter(array_map('intval', $_POST['equipment_ids'])))
            : null;
        $title = trim($_POST['title'] ?? '');
        $description = trim($_POST['description'] ?? '');
        $priority = $_POST['priority'] ?? 'Moyenne';
        $incidentNumber = trim($_POST['incident_number'] ?? '');
        $isAjax = (!empty($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest') || (isset($_POST['ajax']) && $_POST['ajax'] == '1');

        if (!$id) { $this->redirect('/incidents'); }
        if (!$clientId || !$locationId || !$title) { if ($isAjax) { $this->respondJson(['status'=>'error','error'=>'missing_fields','message'=>'Champs obligatoires manquants.']); return; } $this->redirect('/incidents/edit?id='.$id.'&error=missing_fields'); }

        $pdo = Database::pdo();

        // Même logique: refus explicite si technicien (sécurité + clarté JSON/AJAX)
        $u = Auth::user();
        if (($u['role_key'] ?? '') === 'technicien') {
            if ($isAjax) { $this->respondJson(['status'=>'error','error'=>'forbidden','message'=>'Accès refusé.']); return; }
            $this->redirect('/incidents?error=forbidden');
        }

        try {
            // Récupérer l'état avant
            $beforeStmt = $pdo->prepare('SELECT * FROM incidents WHERE id=?');
            $beforeStmt->execute([$id]);
            $before = $beforeStmt->fetch() ?: [];
            // S'assurer colonne et unicité
            $this->ensureColumn($pdo, 'incidents', 'incident_number', '`incident_number` VARCHAR(100) NULL');
            if ($incidentNumber !== '') {
                $dup = $pdo->prepare('SELECT COUNT(*) FROM incidents WHERE incident_number = ? AND id <> ?');
                $dup->execute([$incidentNumber, $id]);
                if ((int)$dup->fetchColumn() > 0) {
                    if ($isAjax) { $this->respondJson(['status'=>'error','error'=>'incident_number_exists','message'=>'Ce numéro incident existe déjà.']); return; }
                    $this->redirect('/incidents/edit?id='.$id.'&error=incident_number_exists');
                    return;
                }
                try {
                    $db = $pdo->query('SELECT DATABASE()')->fetchColumn();
                    if ($db) {
                        $idxChk = $pdo->prepare('SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA=? AND TABLE_NAME=? AND INDEX_NAME=?');
                        $idxChk->execute([$db, 'incidents', 'uniq_incident_number']);
                        if ((int)$idxChk->fetchColumn() === 0) { $pdo->exec('ALTER TABLE `incidents` ADD UNIQUE KEY `uniq_incident_number` (`incident_number`)'); }
                    }
                } catch (\Throwable $ie) { /* ignore */ }
            }

            // S'assurer que les nouvelles colonnes existent
            $this->ensureColumn($pdo, 'incidents', 'site_id', '`site_id` INT NULL');
            $this->ensureColumn($pdo, 'incidents', 'liaison_id', '`liaison_id` INT NULL');
            $this->ensureColumn($pdo, 'incidents', 'priority_id', '`priority_id` INT NULL');
            $this->ensureColumn($pdo, 'incidents', 'equipment_ids', '`equipment_ids` TEXT NULL');
            
            $up = $pdo->prepare('UPDATE incidents SET incident_number = ?, client_id = ?, location_id = ?, site_id = ?, liaison_id = ?, cause_id = ?, priority_id = ?, equipment_ids = ?, title = ?, description = ?, priority = ?, updated_at = NOW() WHERE id = ?');
            $up->execute([$incidentNumber !== '' ? $incidentNumber : null, $clientId, $locationId, $siteId, $liaisonId, $causeId, $priorityId, $equipmentIds, $title, $description, $priority, $id]);

            // Historique agrégé (une seule entrée) listant les champs modifiés
            $after = [
                'Numéro incident' => ($incidentNumber !== '' ? $incidentNumber : null),
                'Client' => (function($cid) use ($pdo){ if (!$cid) return null; $st=$pdo->prepare('SELECT name FROM clients WHERE id=?'); $st->execute([(int)$cid]); return $st->fetchColumn() ?: null; })($clientId),
                'Localisation' => (function($lid) use ($pdo){ if (!$lid) return null; $st=$pdo->prepare('SELECT name FROM locations WHERE id=?'); $st->execute([(int)$lid]); return $st->fetchColumn() ?: null; })($locationId),
                'Cause' => (function($cid) use ($pdo){ if (!$cid) return null; $st=$pdo->prepare('SELECT name FROM incident_causes WHERE id=?'); $st->execute([(int)$cid]); return $st->fetchColumn() ?: null; })($causeId),
                'Titre' => $title,
                'Description' => $description,
                'Priorité' => $priority,
            ];
            $beforeReadable = [
                'Numéro incident' => $before['incident_number'] ?? null,
                'Client' => (function($cid) use ($pdo){ if (!$cid) return null; $st=$pdo->prepare('SELECT name FROM clients WHERE id=?'); $st->execute([(int)$cid]); return $st->fetchColumn() ?: null; })($before['client_id'] ?? null),
                'Localisation' => (function($lid) use ($pdo){ if (!$lid) return null; $st=$pdo->prepare('SELECT name FROM locations WHERE id=?'); $st->execute([(int)$lid]); return $st->fetchColumn() ?: null; })($before['location_id'] ?? null),
                'Cause' => (function($cid) use ($pdo){ if (!$cid) return null; $st=$pdo->prepare('SELECT name FROM incident_causes WHERE id=?'); $st->execute([(int)$cid]); return $st->fetchColumn() ?: null; })($before['cause_id'] ?? null),
                'Titre' => $before['title'] ?? null,
                'Description' => $before['description'] ?? null,
                'Priorité' => $before['priority'] ?? null,
            ];
            $changes = [];
            foreach ($after as $label => $newVal) {
                $oldVal = $beforeReadable[$label] ?? null;
                if ($oldVal !== $newVal) {
                    $changes[] = $label . ': ' . ($oldVal!==null ? '"'.str_replace('"','\"',(string)$oldVal).'"' : '—') . ' → ' . ($newVal!==null ? '"'.str_replace('"','\"',(string)$newVal).'"' : '—');
                }
            }
            if (!empty($changes)) {
                $summary = implode("\n", $changes);
                $this->ensureHistoryTable($pdo);
                $hist = $pdo->prepare('INSERT INTO incident_history (incident_id, field_name, old_value, new_value, changed_by) VALUES (?, ?, ?, ?, ?)');
                $hist->execute([$id, 'Modification fiche incident', null, $summary, (int)$u['id']]);
            }

            if ($isAjax) { $this->respondJson(['status'=>'ok','redirect'=>\route_url('/incidents/detail',['id'=>$id])]); return; }
            $this->redirect('/incidents/detail?id=' . $id);
        } catch (\Throwable $e) {
            if ($isAjax) { $this->respondJson(['status'=>'error','error'=>'update_failed','message'=>'Échec de la mise à jour.']); return; }
            $this->redirect('/incidents/edit?id='.$id.'&error=update_failed');
        }
    }

    /**
     * Si expected_response_at / expected_resolution_at sont vides, les calculer
     * à partir du SLA correspondant au type de maintenance et à la priorité,
     * puis les persister et mettre à jour l'array $incident pour l'affichage.
     */
    private function ensureIncidentSLA(\PDO $pdo, array &$incident): void
    {
        $needResp = empty($incident['expected_response_at']);
        $needRes  = empty($incident['expected_resolution_at']);
        if (!$needResp && !$needRes) { return; }

        $priority = $incident['priority'] ?? 'Moyenne';
        $declaredAt = !empty($incident['declared_at']) ? $incident['declared_at'] : null;
        $baseDateResp = $declaredAt ? new \DateTime($declaredAt) : new \DateTime();
        $baseDateRes  = $declaredAt ? new \DateTime($declaredAt) : new \DateTime();

        // Déterminer le type de maintenance à partir de la cause (catégorie) si possible
        $mtName = $this->detectMaintenanceTypeName($incident['cause_category'] ?? null);

        // Vérifier l'existence des tables SLA (sla_policies, maintenance_types); si absentes, ne rien faire
        try {
            $dbName = $pdo->query('SELECT DATABASE()')->fetchColumn() ?: '';
            if ($dbName) {
                $tchk = $pdo->prepare('SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA=? AND TABLE_NAME IN ("sla_policies","maintenance_types")');
                $tchk->execute([$dbName]);
                $tablesCount = (int)$tchk->fetchColumn();
                if ($tablesCount < 2) { return; }
            } else { return; }
        } catch (\Throwable $e) { return; }

        $sla = null;
        try {
            $slaStmt = $pdo->prepare('
            SELECT sp.response_minutes, sp.resolution_minutes
            FROM sla_policies sp
            JOIN maintenance_types mt ON mt.id = sp.maintenance_type_id
            WHERE mt.name = ? AND sp.priority = ?
            LIMIT 1
        ');
            $slaStmt->execute([$mtName, $priority]);
            $sla = $slaStmt->fetch();
        } catch (\Throwable $e) { /* ignore */ }
        if (!$sla) { return; }

        $newResp = $incident['expected_response_at'] ?? (clone $baseDateResp)->add(new \DateInterval('PT' . (int)$sla['response_minutes'] . 'M'))->format('Y-m-d H:i:s');
        $newRes  = $incident['expected_resolution_at'] ?? (clone $baseDateRes)->add(new \DateInterval('PT' . (int)$sla['resolution_minutes'] . 'M'))->format('Y-m-d H:i:s');

        // Persister uniquement les champs manquants
        $upd = $pdo->prepare('UPDATE incidents SET expected_response_at = COALESCE(expected_response_at, ?), expected_resolution_at = COALESCE(expected_resolution_at, ?), updated_at = NOW() WHERE id = ?');
        $upd->execute([$newResp, $newRes, (int)$incident['id']]);

        $incident['expected_response_at'] = $newResp;
        $incident['expected_resolution_at'] = $newRes;
    }

    /**
     * Heuristique simple: mappe la catégorie de cause vers un type de maintenance connu
     * pour les SLA. Par défaut: Maintenance corrective.
     */
    private function detectMaintenanceTypeName(?string $causeCategory): string
    {
        $cat = mb_strtolower(trim((string)$causeCategory));
        if ($cat !== '') {
            if (str_contains($cat, 'prévent') || str_contains($cat, 'prevent')) {
                return 'Maintenance préventive';
            }
            if (str_contains($cat, 'correct') || str_contains($cat, 'curatif') || str_contains($cat, 'incident')) {
                return 'Maintenance corrective';
            }
        }
        // Fallback
        return 'Maintenance corrective';
    }

    public function assign(): void
    {
        if (!Auth::check()) { $this->redirect('/login'); }
        Auth::requireRole(['admin', 'agent', 'manager', 'superviseur', 'supervisor']);
        
        if ($_SERVER['REQUEST_METHOD'] !== 'POST') { $this->redirect('/incidents'); }
        
        $incidentId = (int)($_POST['incident_id'] ?? 0);
        $userId = (int)($_POST['user_id'] ?? 0);
        
        if (!$incidentId || !$userId) { $this->redirect('/incidents'); }
        
        $pdo = Database::pdo();
        // S'assurer que la table des assignations existe
        $this->ensureAssignmentsTable($pdo);
        // Empêcher assignation si incident déjà Traité ou Clôturé
        try {
            $stInfo = $pdo->prepare('SELECT s.key_name, s.label FROM incidents i JOIN incident_statuses s ON s.id=i.status_id WHERE i.id=?');
            $stInfo->execute([$incidentId]);
            $rowSt = $stInfo->fetch();
            if ($rowSt) {
                $lowKey = strtolower($rowSt['key_name'] ?? '');
                $lowLbl = mb_strtolower($rowSt['label'] ?? '');
                $isClosed = in_array($lowKey, ['closed','resolved','cloture','clôturé'], true) || str_contains($lowLbl,'clôtur') || str_contains($lowLbl,'clos');
                $isTreated = in_array($lowKey, ['treated','traite','processed'], true) || str_contains($lowLbl,'trait');
                if ($isClosed || $isTreated) {
                    $this->redirect('/incidents/detail?id=' . $incidentId . '&error=assignment_not_allowed');
                }
            }
        } catch (\Throwable $e) { /* ignorer, ne pas bloquer si erreur */ }
        
        try {
            // Si une assignation active existe déjà, refuser (modèle 1 technicien par incident)
            try {
                $existAny = $pdo->prepare('SELECT COUNT(*) FROM incident_assignments WHERE incident_id = ? AND active = 1');
                $existAny->execute([$incidentId]);
                if ((int)$existAny->fetchColumn() > 0) {
                    $this->redirect('/incidents/detail?id=' . $incidentId . '&error=already_assigned');
                }
            } catch (\Throwable $e) { /* continue si indispo */ }

            // Vérifier si déjà assigné
            $checkStmt = $pdo->prepare('
                SELECT COUNT(*) FROM incident_assignments 
                WHERE incident_id = ? AND user_id = ? AND active = 1
            ');
            $checkStmt->execute([$incidentId, $userId]);
            
            if ($checkStmt->fetchColumn() == 0) {
                // Assigner
                $assignStmt = $pdo->prepare('
                    INSERT INTO incident_assignments (incident_id, user_id, assigned_by, assigned_at, active)
                    VALUES (?, ?, ?, NOW(), 1)
                ');
                $assignStmt->execute([$incidentId, $userId, Auth::user()['id']]);
                
                // Historique
                $this->ensureHistoryTable($pdo);
                $userStmt = $pdo->prepare('SELECT name FROM users WHERE id = ?');
                $userStmt->execute([$userId]);
                $userName = $userStmt->fetchColumn();
                
                $histStmt = $pdo->prepare('
                    INSERT INTO incident_history (incident_id, field_name, new_value, changed_by)
                    VALUES (?, ?, ?, ?)
                ');
                $histStmt->execute([$incidentId, 'Assignment', "Assigné à $userName", Auth::user()['id']]);

                // Mettre à jour le statut incident à "assigned" (prise en charge) pour refléter l'assignation
                try {
                    $curSt = $pdo->prepare('SELECT i.status_id, s.key_name, s.label FROM incidents i LEFT JOIN incident_statuses s ON s.id=i.status_id WHERE i.id=? LIMIT 1');
                    $curSt->execute([$incidentId]);
                    $rowSt2 = $curSt->fetch();
                    $lowKey2 = strtolower($rowSt2['key_name'] ?? '');
                    $lowLbl2 = mb_strtolower($rowSt2['label'] ?? '');
                    $isClosed2 = in_array($lowKey2, ['closed','resolved','cloture','clôturé'], true) || str_contains($lowLbl2,'clôtur') || str_contains($lowLbl2,'clos');
                    $isTreated2 = in_array($lowKey2, ['treated','traite','processed'], true) || str_contains($lowLbl2,'trait');

                    if (!$isClosed2 && !$isTreated2 && $lowKey2 !== 'in_progress') {
                        $assignedId = $this->ensureStatus($pdo, 'assigned', 'Assigné', '#fd7e14', 20);
                        if ($assignedId && (int)($rowSt2['status_id'] ?? 0) !== (int)$assignedId) {
                            $pdo->prepare('UPDATE incidents SET status_id=?, updated_at=NOW() WHERE id=?')->execute([$assignedId, $incidentId]);
                            $histS = $pdo->prepare('INSERT INTO incident_history (incident_id, field_name, old_value, new_value, changed_by) VALUES (?, "Status", ?, ?, ?)');
                            $histS->execute([$incidentId, $rowSt2['label'] ?? null, 'Assigné', (int)(Auth::user()['id'] ?? 0)]);
                        }
                    }
                } catch (\Throwable $e) { /* ignore */ }

                // Notifications (in-app + email) au technicien assigné
                try {
                    // Récupérer infos de l'incident pour le message
                    $iStmt = $pdo->prepare('SELECT ticket_id, title FROM incidents WHERE id = ?');
                    $iStmt->execute([$incidentId]);
                    $i = $iStmt->fetch();
                    $ticket = $i['ticket_id'] ?? ('INC-' . $incidentId);
                    $titleN = 'Assignation d\'incident: ' . $ticket;
                    $bodyN = 'Vous avez été assigné(e) à l\'incident ' . $ticket . ' — ' . ($i['title'] ?? '');
                    $url = \route_url('/incidents/treatment', ['id' => $incidentId]);

                    Notifier::notifyUsers($pdo, [(int)$userId], $titleN, $bodyN, $url);
                    Notifier::pushUsers($pdo, [(int)$userId], $titleN, $bodyN, $url);
                    Notifier::emailUsers($pdo, [(int)$userId], $titleN, nl2br(htmlentities($bodyN)) . '<br><a href="' . $url . '">Ouvrir</a>');
                } catch (\Throwable $ne) { /* ignorer échec de notification */ }
            }
            
            $this->redirect('/incidents/detail?id=' . $incidentId);
            
        } catch (\Throwable $e) {
            $this->redirect('/incidents/detail?id=' . $incidentId . '&error=assignment_failed');
        }
    }

    public function unassign(): void
    {
        if (!Auth::check()) { $this->redirect('/login'); }
        Auth::requireRole(['admin', 'agent', 'manager', 'superviseur', 'supervisor']);

        if ($_SERVER['REQUEST_METHOD'] !== 'POST') { $this->redirect('/incidents'); }

        $incidentId = (int)($_POST['incident_id'] ?? 0);
        $userId = (int)($_POST['user_id'] ?? 0);

        if (!$incidentId || !$userId) { $this->redirect('/incidents'); }

        $pdo = Database::pdo();
        // S'assurer que la table existe
        $this->ensureAssignmentsTable($pdo);

        // Empêcher désassignation si incident déjà Traité/Clôturé (cohérence UX)
        try {
            $stInfo = $pdo->prepare('SELECT s.key_name, s.label FROM incidents i JOIN incident_statuses s ON s.id=i.status_id WHERE i.id=?');
            $stInfo->execute([$incidentId]);
            $rowSt = $stInfo->fetch();
            if ($rowSt) {
                $lowKey = strtolower($rowSt['key_name'] ?? '');
                $lowLbl = mb_strtolower($rowSt['label'] ?? '');
                $isClosed = in_array($lowKey, ['closed','resolved','cloture','clôturé'], true) || str_contains($lowLbl,'clôtur') || str_contains($lowLbl,'clos');
                $isTreated = in_array($lowKey, ['treated','traite','processed'], true) || str_contains($lowLbl,'trait');
                if ($isClosed || $isTreated) {
                    $this->redirect('/incidents/detail?id=' . $incidentId . '&error=unassign_not_allowed');
                }
            }
        } catch (\Throwable $e) { /* ignorer */ }

        try {
            // Désactiver l'assignation active pour cet utilisateur
            $upd = $pdo->prepare('UPDATE incident_assignments SET active = 0 WHERE incident_id = ? AND user_id = ? AND active = 1');
            $upd->execute([$incidentId, $userId]);

            // Historique: "Désassigné: Nom"
            $this->ensureHistoryTable($pdo);
            $userStmt = $pdo->prepare('SELECT name FROM users WHERE id = ?');
            $userStmt->execute([$userId]);
            $userName = $userStmt->fetchColumn();
            $histStmt = $pdo->prepare('INSERT INTO incident_history (incident_id, field_name, new_value, changed_by) VALUES (?, ?, ?, ?)');
            $histStmt->execute([$incidentId, 'Assignment', 'Désassigné: ' . ($userName ?: ('User#' . $userId)), (int)(Auth::user()['id'] ?? 0)]);

            $this->redirect('/incidents/detail?id=' . $incidentId . '&success=unassigned');
        } catch (\Throwable $e) {
            $this->redirect('/incidents/detail?id=' . $incidentId . '&error=unassign_failed');
        }
    }

    private function ensureAssignmentsTable(\PDO $pdo): void
    {
        try {
            $dbName = $pdo->query('SELECT DATABASE()')->fetchColumn() ?: '';
            if (!$dbName) return;
            $chk = $pdo->prepare('SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?');
            $chk->execute([$dbName, 'incident_assignments']);
            if ((int)$chk->fetchColumn() > 0) return;
            $sql = 'CREATE TABLE IF NOT EXISTS incident_assignments (
                        id INT AUTO_INCREMENT PRIMARY KEY,
                        incident_id INT NOT NULL,
                        user_id INT NOT NULL,
                        assigned_by INT NULL,
                        assigned_at DATETIME NULL,
                        active TINYINT(1) NOT NULL DEFAULT 1,
                        KEY idx_incident (incident_id),
                        KEY idx_user (user_id),
                        KEY idx_active (active),
                        CONSTRAINT fk_incident_assignments_incident FOREIGN KEY (incident_id) REFERENCES incidents(id) ON DELETE CASCADE,
                        CONSTRAINT fk_incident_assignments_user FOREIGN KEY (user_id) REFERENCES users(id),
                        CONSTRAINT fk_incident_assignments_assigned_by FOREIGN KEY (assigned_by) REFERENCES users(id)
                    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci';
            $pdo->exec($sql);
        } catch (\Throwable $e) {
            // Ne pas bloquer l'exécution, l'insertion échouera proprement si indisponible
        }
    }

    public function status(): void
    {
        if (!Auth::check()) { $this->redirect('/login'); }
        Auth::requireRole(['admin', 'agent', 'technicien', 'manager', 'superviseur', 'supervisor']);
        
        if ($_SERVER['REQUEST_METHOD'] !== 'POST') { $this->redirect('/incidents'); }
        
        $incidentId = (int)($_POST['incident_id'] ?? 0);
        $statusId = (int)($_POST['status_id'] ?? 0);
        
        if (!$incidentId || !$statusId) { $this->redirect('/incidents'); }
        
        $pdo = Database::pdo();

        // Si technicien, n'autoriser la MAJ que si assigné à cet incident
        $u = Auth::user();
        if (($u['role_key'] ?? '') === 'technicien') {
            $check = $pdo->prepare('SELECT COUNT(*) FROM incident_assignments WHERE incident_id = ? AND user_id = ? AND active = 1');
            $check->execute([$incidentId, (int)$u['id']]);
            if ((int)$check->fetchColumn() === 0) { $this->redirect('/incidents?error=forbidden'); }
        }
        
        try {
            // Récupérer l'ancien et nouveau statut
            $oldStatusStmt = $pdo->prepare('
                SELECT s.label FROM incidents i 
                JOIN incident_statuses s ON s.id = i.status_id 
                WHERE i.id = ?
            ');
            $oldStatusStmt->execute([$incidentId]);
            $oldStatus = $oldStatusStmt->fetchColumn();
            
            $newStatusStmt = $pdo->prepare('SELECT label FROM incident_statuses WHERE id = ?');
            $newStatusStmt->execute([$statusId]);
            $newStatus = $newStatusStmt->fetchColumn();
            
            // Mettre à jour
            $updateStmt = $pdo->prepare('UPDATE incidents SET status_id = ? WHERE id = ?');
            $updateStmt->execute([$statusId, $incidentId]);
            
            // Historique
            $histStmt = $pdo->prepare('
                INSERT INTO incident_history (incident_id, field_name, old_value, new_value, changed_by)
                VALUES (?, ?, ?, ?, ?)
            ');
            $histStmt->execute([$incidentId, 'Status', $oldStatus, $newStatus, Auth::user()['id']]);
            
            $this->redirect('/incidents/detail?id=' . $incidentId);
            
        } catch (\Throwable $e) {
            $this->redirect('/incidents/detail?id=' . $incidentId . '&error=status_update_failed');
        }
    }

    public function comment(): void
    {
        if (!Auth::check()) { $this->redirect('/login'); }
        
        if ($_SERVER['REQUEST_METHOD'] !== 'POST') { $this->redirect('/incidents'); }
        
        $isAjax = (!empty($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest') || (isset($_POST['ajax']) && $_POST['ajax'] == '1');
        $incidentId = (int)($_POST['incident_id'] ?? 0);
        $comment = trim($_POST['comment'] ?? '');
        $isInternal = isset($_POST['is_internal']) ? 1 : 0;
        $parentId = (int)($_POST['parent_id'] ?? 0) ?: null;
        
        if (!$incidentId || !$comment) {
            if ($isAjax) { $this->respondJson(['status'=>'error','error'=>'missing_fields','message'=>'Commentaire manquant.']); return; }
            $this->redirect('/incidents');
        }
        
        $pdo = Database::pdo();

        // Si technicien, commenter uniquement si assigné
        $u = Auth::user();
        $roleKey = (string)($u['role_key'] ?? '');
        $canPostInternal = in_array($roleKey, ['admin','agent','manager','superviseur','supervisor'], true);

        if (($u['role_key'] ?? '') === 'technicien') {
            $check = $pdo->prepare('SELECT COUNT(*) FROM incident_assignments WHERE incident_id = ? AND user_id = ? AND active = 1');
            $check->execute([$incidentId, (int)$u['id']]);
            if ((int)$check->fetchColumn() === 0) { $this->redirect('/incidents?error=forbidden'); }
        }
        
        try {
            $this->ensureCommentTables($pdo);

            // Cohérence threads interne/public: une réponse hérite toujours du parent.
            if ($parentId !== null) {
                try {
                    $pst = $pdo->prepare('SELECT is_internal FROM incident_comments WHERE id=? AND incident_id=? LIMIT 1');
                    $pst->execute([(int)$parentId, (int)$incidentId]);
                    $p = $pst->fetch();
                    if ($p) {
                        $isInternal = (int)($p['is_internal'] ?? 0);
                    } else {
                        $parentId = null;
                    }
                } catch (\Throwable $e) {
                    $parentId = null;
                }
            }

            // Un technicien (ou rôle non autorisé) ne peut pas poster de commentaire interne.
            if (!$canPostInternal) {
                $isInternal = 0;
            }

            // Compat schéma: certaines bases utilisent `content` (parfois NOT NULL), d'autres `comment`.
            $cols = [];
            try {
                $colRows = $pdo->query("SHOW COLUMNS FROM incident_comments")->fetchAll(PDO::FETCH_ASSOC);
                foreach ($colRows as $r) {
                    if (!empty($r['Field'])) { $cols[strtolower((string)$r['Field'])] = true; }
                }
            } catch (\Throwable $e) {
                // Fallback: supposer `comment`
                $cols = ['comment' => true];
            }

            $insertCols = ['incident_id', 'user_id'];
            $insertVals = [$incidentId, Auth::user()['id']];

            if (!empty($cols['comment'])) {
                $insertCols[] = 'comment';
                $insertVals[] = $comment;
            }
            if (!empty($cols['content'])) {
                $insertCols[] = 'content';
                $insertVals[] = $comment;
            }
            if (!empty($cols['is_internal'])) {
                $insertCols[] = 'is_internal';
                $insertVals[] = $isInternal;
            }
            if (!empty($cols['parent_id'])) {
                $insertCols[] = 'parent_id';
                $insertVals[] = $parentId;
            }
            $insertCols[] = 'created_at';

            $placeholders = implode(', ', array_fill(0, count($insertVals), '?'));
            $sql = 'INSERT INTO incident_comments (' . implode(', ', $insertCols) . ') VALUES (' . $placeholders . ', NOW())';
            $stmt = $pdo->prepare($sql);
            $stmt->execute($insertVals);
            $newCommentId = (int)$pdo->lastInsertId();

            // Uploads optionnels
            if (!empty($_FILES['attachments']['name'])) {
                $publicRoot = realpath(__DIR__ . '/../../public');
                $baseDir = $publicRoot . '/storage/uploads/incidents/' . $incidentId;
                if (!is_dir($baseDir)) { @mkdir($baseDir, 0777, true); }
                $names = $_FILES['attachments']['name'];
                if (is_array($names)) {
                    foreach (array_keys($names) as $idx) {
                        $rel = $this->handleUploadArray('attachments', $baseDir, (int)$idx);
                        if ($rel) {
                            $insA = $pdo->prepare('INSERT INTO incident_comment_attachments (comment_id, path, uploaded_at) VALUES (?, ?, NOW())');
                            $insA->execute([$newCommentId, $rel]);
                        }
                    }
                } else {
                    $rel = $this->handleUpload('attachments', $baseDir);
                    if ($rel) {
                        $insA = $pdo->prepare('INSERT INTO incident_comment_attachments (comment_id, path, uploaded_at) VALUES (?, ?, NOW())');
                        $insA->execute([$newCommentId, $rel]);
                    }
                }
            }
            
            // Notifications (in-app + email) pour managers/superviseurs/admin + techniciens assignés (hors auteur)
            try {
                $me = Auth::user();
                $meId = (int)($me['id'] ?? 0);
                $iStmt = $pdo->prepare('SELECT id, ticket_id, title FROM incidents WHERE id=?');
                $iStmt->execute([$incidentId]);
                $inc = $iStmt->fetch();
                $ticket = $inc['ticket_id'] ?? ('INC-' . $incidentId);
                $titleN = 'Nouveau commentaire: ' . $ticket;
                $bodyN = ($me['name'] ?? 'Un utilisateur') . ' a publié un commentaire.';
                $url = \route_url('/incidents/detail', ['id'=>$incidentId]) . '#pane-comments';
                                $slaStmt = $pdo->prepare('
                                    SELECT sp.response_minutes, sp.resolution_minutes
                                    FROM sla_policies sp
                                    JOIN maintenance_types mt ON mt.id = sp.maintenance_type_id
                                    WHERE mt.name = ? AND sp.priority = ?
                                    LIMIT 1
                                ');
                $recRoles = $pdo->query("SELECT id FROM users WHERE active=1 AND role_key IN ('superviseur','supervisor','manager','admin','agent')")->fetchAll(\PDO::FETCH_COLUMN);
                $recBase = array_map('intval', $recRoles ?: []);

                // Public: notifier aussi les techniciens assignés. Interne: notifier uniquement encadrement.
                if ((int)$isInternal === 0) {
                    $recTech = $pdo->prepare('SELECT user_id FROM incident_assignments WHERE incident_id=? AND active=1');
                    $recTech->execute([$incidentId]);
                    $rec2 = array_map('intval', $recTech->fetchAll(\PDO::FETCH_COLUMN) ?: []);
                    $recBase = array_merge($recBase, $rec2);
                }

                // Mentions @user : destinataires mentionnés (filtrés par visibilité)
                $recMentions = $this->resolveMentionedUserIds($pdo, $comment, (int)$incidentId, $roleKey, (int)$isInternal);

                $recBase = array_values(array_unique(array_filter($recBase, fn($id)=>$id && (int)$id!==$meId)));
                $recMentions = array_values(array_unique(array_filter($recMentions, fn($id)=>$id && (int)$id!==$meId)));

                // Éviter double notif: les mentionnés reçoivent la notif "mention", pas la notif générique.
                if (!empty($recMentions)) {
                    $recBase = array_values(array_diff($recBase, $recMentions));
                }

                if (!empty($recBase)) {
                    \App\Core\Notifier::notifyUsers($pdo, $recBase, $titleN, $bodyN, $url);
                    \App\Core\Notifier::emailUsers($pdo, $recBase, $titleN, nl2br(htmlentities($bodyN)) . '<br><a href="' . $url . '">Ouvrir</a>');
                }

                if (!empty($recMentions)) {
                    $titleM = 'Mention: ' . $ticket;
                    $bodyM = ($me['name'] ?? 'Un utilisateur') . ' vous a mentionné dans un commentaire.';
                    \App\Core\Notifier::notifyUsers($pdo, $recMentions, $titleM, $bodyM, $url);
                    \App\Core\Notifier::emailUsers($pdo, $recMentions, $titleM, nl2br(htmlentities($bodyM)) . '<br><a href="' . $url . '">Ouvrir</a>');
                }
            } catch (\Throwable $ne) { /* ignore notif errors */ }

            if ($isAjax) {
                // Retourner les données du commentaire pour MAJ côté client
                $cStmt = $pdo->prepare('SELECT ic.id, COALESCE(ic.comment, ic.content) AS comment, ic.created_at, u.name AS user_name FROM incident_comments ic JOIN users u ON u.id=ic.user_id WHERE ic.id=?');
                $cStmt->execute([$newCommentId]);
                $row = $cStmt->fetch() ?: [];
                // Pièces jointes
                $att = $pdo->prepare('SELECT path FROM incident_comment_attachments WHERE comment_id=? ORDER BY id ASC');
                $att->execute([$newCommentId]);
                $attachments = array_map(fn($r)=>$r['path'], $att->fetchAll() ?: []);
                $this->respondJson(['status'=>'ok','comment'=>[
                    'id'=>$row['id'] ?? $newCommentId,
                    'user_name'=>$row['user_name'] ?? ($me['name'] ?? 'Vous'),
                    'created_at'=>$row['created_at'] ?? date('Y-m-d H:i:s'),
                    'comment'=>$row['comment'] ?? $comment,
                    'attachments'=>$attachments
                ]]);
                return;
            }

            $this->redirect('/incidents/detail?id=' . $incidentId . '#pane-comments');
            
        } catch (\Throwable $e) {
            if ($isAjax) {
                $msg = 'Publication échouée.';
                $host = (string)($_SERVER['HTTP_HOST'] ?? '');
                if (in_array($host, ['localhost', '127.0.0.1', 'localhost:8000'], true)) {
                    $msg = substr($e->getMessage(), 0, 200) ?: $msg;
                }
                $this->respondJson(['status'=>'error','error'=>'comment_failed','message'=>$msg]);
                return;
            }
            $this->redirect('/incidents/detail?id=' . $incidentId . '&error=comment_failed');
        }
    }

    private function extractMentionTokens(string $text): array
    {
        // Formats supportés:
        // - @username
        // - @[Nom Prénom]
        $res = ['handles' => [], 'brackets' => []];
        if ($text === '') { return $res; }

        // @username (ne pas capter @[...])
        if (preg_match_all('/(^|\s)@(?!\[)([A-Za-z0-9._-]{2,40})\b/u', $text, $m1)) {
            $handles = array_map('strval', $m1[2] ?? []);
            $handles = array_values(array_unique(array_filter(array_map(fn($h)=>trim($h), $handles), fn($h)=>$h !== '')));
            $res['handles'] = $handles;
        }

        // @[Nom Prénom]
        if (preg_match_all('/(^|\s)@\[([^\]\r\n]{1,80})\]/u', $text, $m2)) {
            $br = array_map('strval', $m2[2] ?? []);
            $br = array_values(array_unique(array_filter(array_map(fn($t)=>trim($t), $br), fn($t)=>$t !== '')));
            $res['brackets'] = $br;
        }

        return $res;
    }

    private function resolveMentionedUserIds(\PDO $pdo, string $commentText, int $incidentId, string $actorRoleKey, int $isInternal): array
    {
        $tokens = $this->extractMentionTokens($commentText);
        $handles = $tokens['handles'] ?? [];
        $brackets = $tokens['brackets'] ?? [];
        if (empty($handles) && empty($brackets)) { return []; }

        // Détecter colonnes disponibles (username/email)
        $dbName = '';
        try { $dbName = (string)($pdo->query('SELECT DATABASE()')->fetchColumn() ?: ''); } catch (\Throwable $e) { $dbName = ''; }
        $hasUsername = false;
        $hasEmail = false;
        $hasName = true;
        if ($dbName !== '') {
            try {
                $cchk = $pdo->prepare('SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=? AND TABLE_NAME=? AND COLUMN_NAME=?');
                $cchk->execute([$dbName, 'users', 'username']);
                $hasUsername = ((int)$cchk->fetchColumn() > 0);
                $cchk->execute([$dbName, 'users', 'email']);
                $hasEmail = ((int)$cchk->fetchColumn() > 0);
                $cchk->execute([$dbName, 'users', 'name']);
                $hasName = ((int)$cchk->fetchColumn() > 0);
            } catch (\Throwable $e) { /* ignore */ }
        }

        $ids = [];
        try {
            // 1) Résolution via @username
            if (!empty($handles)) {
                if ($hasUsername) {
                    $in = implode(',', array_fill(0, count($handles), '?'));
                    $st = $pdo->prepare("SELECT id FROM users WHERE active=1 AND LOWER(username) IN ($in)");
                    $st->execute(array_map('mb_strtolower', $handles));
                    $ids = array_merge($ids, array_map('intval', $st->fetchAll(\PDO::FETCH_COLUMN) ?: []));
                } elseif ($hasEmail) {
                    // Fallback: @prenom.nom = partie locale de l'email
                    $in = implode(',', array_fill(0, count($handles), '?'));
                    $st = $pdo->prepare("SELECT id FROM users WHERE active=1 AND LOWER(SUBSTRING_INDEX(email,'@',1)) IN ($in)");
                    $st->execute(array_map('mb_strtolower', $handles));
                    $ids = array_merge($ids, array_map('intval', $st->fetchAll(\PDO::FETCH_COLUMN) ?: []));
                }
            }

            // 2) Résolution via @[Nom Prénom]
            if (!empty($brackets)) {
                foreach ($brackets as $tok) {
                    $tokLower = mb_strtolower($tok);
                    $found = [];

                    // Essayer aussi comme username/email local
                    if ($hasUsername) {
                        $st = $pdo->prepare('SELECT id FROM users WHERE active=1 AND LOWER(username)=? LIMIT 2');
                        $st->execute([$tokLower]);
                        $found = array_map('intval', $st->fetchAll(\PDO::FETCH_COLUMN) ?: []);
                    }
                    if (empty($found) && $hasEmail) {
                        $st = $pdo->prepare("SELECT id FROM users WHERE active=1 AND LOWER(SUBSTRING_INDEX(email,'@',1))=? LIMIT 2");
                        $st->execute([$tokLower]);
                        $found = array_map('intval', $st->fetchAll(\PDO::FETCH_COLUMN) ?: []);
                    }
                    if (empty($found) && $hasName) {
                        // Exact sur name
                        $st = $pdo->prepare('SELECT id FROM users WHERE active=1 AND LOWER(name)=? LIMIT 2');
                        $st->execute([$tokLower]);
                        $found = array_map('intval', $st->fetchAll(\PDO::FETCH_COLUMN) ?: []);
                    }
                    if (empty($found) && $hasName) {
                        // Match unique sur name (tolérant)
                        $st = $pdo->prepare('SELECT id FROM users WHERE active=1 AND LOWER(name) LIKE ? LIMIT 2');
                        $st->execute([$tokLower . '%']);
                        $tmp = array_map('intval', $st->fetchAll(\PDO::FETCH_COLUMN) ?: []);
                        if (count($tmp) === 1) { $found = $tmp; }
                    }

                    if (!empty($found)) {
                        $ids = array_merge($ids, $found);
                    }
                }
            }
        } catch (\Throwable $e) {
            $ids = [];
        }

        $ids = array_values(array_unique(array_filter($ids)));
        if (empty($ids)) { return []; }

        // Filtrer selon visibilité: technicien ne peut mentionner que des utilisateurs qui auront accès.
        // - Interne: uniquement encadrement
        // - Public: encadrement + techniciens assignés
        try {
            $allowed = [];
            $roleIds = $pdo->query("SELECT id FROM users WHERE active=1 AND role_key IN ('superviseur','supervisor','manager','admin','agent')")->fetchAll(\PDO::FETCH_COLUMN);
            $allowed = array_map('intval', $roleIds ?: []);
            if ($isInternal === 0) {
                $t = $pdo->prepare('SELECT user_id FROM incident_assignments WHERE incident_id=? AND active=1');
                $t->execute([$incidentId]);
                $allowed = array_merge($allowed, array_map('intval', $t->fetchAll(\PDO::FETCH_COLUMN) ?: []));
            }
            $allowed = array_values(array_unique(array_filter($allowed)));
            $ids = array_values(array_intersect($ids, $allowed));
        } catch (\Throwable $e) {
            // Si on ne peut pas filtrer, ne pas mentionner pour éviter des notifs « mortes ».
            $ids = [];
        }

        return $ids;
    }

    /**
     * Suppression d'un incident non encore pris en charge (statut = "open")
     * Accessible uniquement aux admins et agents
     */
    public function delete(): void
    {
        if (!Auth::check()) { $this->redirect('/login'); }
        Auth::requireRole(['admin','agent']);

        $incidentId = (int)($_POST['incident_id'] ?? 0);
        if (!$incidentId) { $this->redirect('/incidents'); }
        
        $pdo = Database::pdo();
        
        try {
            // Vérifier que l'incident existe et est en statut "open" (non pris en charge)
            $stmt = $pdo->prepare('
                SELECT i.id, s.key_name 
                FROM incidents i 
                JOIN incident_statuses s ON s.id = i.status_id 
                WHERE i.id = ?
            ');
            $stmt->execute([$incidentId]);
            $incident = $stmt->fetch(PDO::FETCH_ASSOC);
            
            if (!$incident) {
                $this->redirect('/incidents?error=incident_not_found');
                return;
            }
            
            // Vérifier que le statut est bien "open" (non pris en charge)
            if ($incident['key_name'] !== 'open') {
                $this->redirect('/incidents/detail?id=' . $incidentId . '&error=cannot_delete_taken');
                return;
            }
            
            // Supprimer l'incident (CASCADE supprimera automatiquement les dépendances)
            $deleteStmt = $pdo->prepare('DELETE FROM incidents WHERE id = ?');
            $deleteStmt->execute([$incidentId]);
            
            $this->redirect('/incidents?success=incident_deleted');
            
        } catch (\Throwable $e) {
            $this->redirect('/incidents?error=delete_failed');
        }
    }

    private function ensureCommentTables(\PDO $pdo): void
    {
        // Table des commentaires (schéma compatible avec plusieurs versions)
        // - Nouveau code: colonnes comment + is_internal
        // - Ancien code: colonnes content + author_name
        $pdo->exec("CREATE TABLE IF NOT EXISTS incident_comments (
            id INT AUTO_INCREMENT PRIMARY KEY,
            incident_id INT NOT NULL,
            user_id INT NOT NULL,
            comment TEXT NULL,
            content TEXT NULL,
            is_internal TINYINT(1) NOT NULL DEFAULT 0,
            author_name VARCHAR(255) NULL,
            parent_id INT NULL,
            created_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
            INDEX (incident_id), INDEX (user_id), INDEX (parent_id), INDEX (created_at)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");

        // Migration douce: ajouter colonnes manquantes si table pré-existante
        $this->ensureColumn($pdo, 'incident_comments', 'comment', '`comment` TEXT NULL');
        $this->ensureColumn($pdo, 'incident_comments', 'content', '`content` TEXT NULL');
        $this->ensureColumn($pdo, 'incident_comments', 'is_internal', '`is_internal` TINYINT(1) NOT NULL DEFAULT 0');
        $this->ensureColumn($pdo, 'incident_comments', 'author_name', '`author_name` VARCHAR(255) NULL');
        $this->ensureColumn($pdo, 'incident_comments', 'parent_id', '`parent_id` INT NULL');

        // Rattrapage données: dupliquer content -> comment (et inversement) si nécessaire
        try {
            $pdo->exec("UPDATE incident_comments SET comment = content WHERE (comment IS NULL OR comment = '') AND content IS NOT NULL AND content <> ''");
        } catch (\Throwable $e) { /* ignore */ }
        try {
            $pdo->exec("UPDATE incident_comments SET content = comment WHERE (content IS NULL OR content = '') AND comment IS NOT NULL AND comment <> ''");
        } catch (\Throwable $e) { /* ignore */ }

        // Table pièces jointes pour commentaires
        $pdo->exec("CREATE TABLE IF NOT EXISTS incident_comment_attachments (
            id INT AUTO_INCREMENT PRIMARY KEY,
            comment_id INT NOT NULL,
            path VARCHAR(255) NOT NULL,
            uploaded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
            INDEX (comment_id)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
    }

    private function generateTicketId(): string
    {
        // Nouvelle nomenclature: MAINTFO-MMDD-N° (compteur quotidien)
        $date = new \DateTime();
        $mmdd = $date->format('md');
        $pdo = Database::pdo();
        // Compter les incidents du jour (basé sur declared_at)
        $today = $date->format('Y-m-d');
        $stmt = $pdo->prepare('SELECT COUNT(*) FROM incidents WHERE DATE(declared_at) = ?');
        $stmt->execute([$today]);
        $count = (int)$stmt->fetchColumn();
        return 'MAINTFO-' . $mmdd . '-' . str_pad($count + 1, 3, '0', STR_PAD_LEFT);
    }

    /**
     * Formulaire de traitement terrain (technicien) — GET
     */
    public function treatment(): void
    {
        if (!Auth::check()) { $this->redirect('/login'); }
        $u = Auth::user();
        $id = (int)($_GET['id'] ?? 0);
        if (!$id) { $this->redirect('/incidents'); }
        $pdo = Database::pdo();
        // Vérifier assignation si technicien
        if (($u['role_key'] ?? '') === 'technicien') {
            $c = $pdo->prepare('SELECT COUNT(*) FROM incident_assignments WHERE incident_id=? AND user_id=? AND active=1');
            $c->execute([$id, (int)$u['id']]);
            if ((int)$c->fetchColumn() === 0) { $this->redirect('/incidents?error=forbidden'); }
        }

        // Charger incident + éventuel rapport existant (tolérer client/localisation/liaisons manquants)
        $hasLiaisonsTable = false;
        $hasLiaisonIdCol = false;
        try { $hasLiaisonsTable = (bool)$pdo->query("SHOW TABLES LIKE 'liaisons'")->fetchColumn(); } catch (\Throwable $e) { $hasLiaisonsTable = false; }
        try { $hasLiaisonIdCol = (bool)$pdo->query("SHOW COLUMNS FROM incidents LIKE 'liaison_id'")->fetchColumn(); } catch (\Throwable $e) { $hasLiaisonIdCol = false; }

        if ($hasLiaisonsTable && $hasLiaisonIdCol) {
            $sql = "SELECT i.*, c.name as client_name, l.name as location_name, li.name as liaison_name
                FROM incidents i
                LEFT JOIN clients c ON c.id=i.client_id
                LEFT JOIN locations l ON l.id=i.location_id
                LEFT JOIN liaisons li ON li.id=i.liaison_id
                WHERE i.id=?";
        } else {
            $sql = "SELECT i.*, c.name as client_name, l.name as location_name, NULL as liaison_name
                FROM incidents i
                LEFT JOIN clients c ON c.id=i.client_id
                LEFT JOIN locations l ON l.id=i.location_id
                WHERE i.id=?";
        }

        $incidentStmt = $pdo->prepare($sql);
        $incidentStmt->execute([$id]);
        $incident = $incidentStmt->fetch();
        if (!$incident) { $this->redirect('/incidents'); }

    // S'assurer que les tables de traitement existent avant toute requête
    $this->ensureTreatmentTables($pdo);

    // S'assurer que la colonne phone existe pour users (pour coordonnées Chef d'Équipe)
    $this->ensureColumn($pdo, 'users', 'phone', '`phone` VARCHAR(30) NULL');

    // Liste des chefs d'équipe potentiels: superviseur/manager (+ admin en fallback)
    $teamLeads = $pdo->query("SELECT id, name, email, role_key, phone FROM users WHERE active=1 AND role_key IN ('manager','superviseur','supervisor','admin') ORDER BY name")
                      ->fetchAll();

    // Rapport (dernier brouillon ou dernier soumis)
        $rep = $pdo->prepare('SELECT * FROM incident_reports WHERE incident_id=? ORDER BY id DESC LIMIT 1');
        $rep->execute([$id]);
        $report = $rep->fetch();

        // Matériels saisis pour le dernier rapport
        $materials = [];
        $interventionPhotos = [];
        $historicalComments = [];
        if ($report && !empty($report['id'])) {
            $m = $pdo->prepare('SELECT * FROM incident_materials WHERE report_id = ? ORDER BY id ASC');
            $m->execute([(int)$report['id']]);
            $materials = $m->fetchAll();
            
            // Charger les photos d'intervention depuis incident_attachments
            $photos = $pdo->prepare('SELECT path, observation, lat, lng, taken_at, uploaded_at FROM incident_attachments WHERE report_id = ? AND type = "intervention_photo" ORDER BY uploaded_at DESC');
            $photos->execute([(int)$report['id']]);
            $interventionPhotos = $photos->fetchAll();
            
            // Charger l'historique des commentaires depuis work_comments
            if (!empty($report['work_comments'])) {
                $historicalComments[] = [
                    'description' => $report['work_comments'],
                    'internal_notes' => $report['supervisor_comments'] ?? null,
                    'created_at' => $report['created_at'],
                    'author_name' => 'Technicien'
                ];
            }
        }

        $this->view('incidents/treatment', compact('incident', 'report', 'teamLeads', 'materials', 'interventionPhotos', 'historicalComments'));
    }

    /**
     * Enregistrement du traitement terrain — POST
     */
    public function treatmentSave(): void
    {
        if (!Auth::check()) { $this->redirect('/login'); }
        $u = Auth::user();
        if ($_SERVER['REQUEST_METHOD'] !== 'POST') { $this->redirect('/incidents'); }

        $incidentId = (int)($_POST['incident_id'] ?? 0);
        if (!$incidentId) { $this->redirect('/incidents'); }

        $pdo = Database::pdo();
        // Restriction technicien: doit être assigné
        if (($u['role_key'] ?? '') === 'technicien') {
            $c = $pdo->prepare('SELECT COUNT(*) FROM incident_assignments WHERE incident_id=? AND user_id=? AND active=1');
            $c->execute([$incidentId, (int)$u['id']]);
            if ((int)$c->fetchColumn() === 0) { $this->redirect('/incidents?error=forbidden'); }
        }

        // Assurer tables
        $this->ensureTreatmentTables($pdo);

        // Inputs principaux
    $teamName = trim($_POST['team_name'] ?? ($u['name'] ?? ''));
        $teamContact = trim($_POST['team_contact'] ?? '');
    $typeOfIncident = trim($_POST['type_of_incident'] ?? '');
        $detectionMode = trim($_POST['detection_mode'] ?? '');
        $cutKm = isset($_POST['cut_distance_km']) ? (float)$_POST['cut_distance_km'] : null;
    // Normaliser entrée décimale (support virgule FR)
    $gpsLat = isset($_POST['gps_lat']) ? (float)str_replace(',', '.', (string)$_POST['gps_lat']) : null;
    $gpsLng = isset($_POST['gps_lng']) ? (float)str_replace(',', '.', (string)$_POST['gps_lng']) : null;
        $faultNature = trim($_POST['fault_nature'] ?? '');
        $equipments = $_POST['impacted_equipment'] ?? [];
        $equipmentsStr = is_array($equipments) ? json_encode(array_values($equipments), JSON_UNESCAPED_UNICODE) : null;
    $takeover = $_POST['takeover_at'] ?? null; // Y-m-d\TH:i
        $start = $_POST['intervention_start'] ?? null; // Y-m-d\TH:i
        $end = $_POST['intervention_end'] ?? null;
        $repairType = trim($_POST['repair_type'] ?? '');
        $workComments = trim($_POST['work_comments'] ?? '');
        $otdrResult = trim($_POST['otdr_result'] ?? '');
        $resolutionStatus = trim($_POST['resolution_status'] ?? '');
        $needAdditional = isset($_POST['need_additional']) ? (int)$_POST['need_additional'] : 0;
        $action = $_POST['action_type'] ?? 'draft'; // draft | submit

        // Durées
        $durationHours = null;
        if ($start && $end) {
            $s = \DateTime::createFromFormat('Y-m-d\TH:i', $start);
            $e = \DateTime::createFromFormat('Y-m-d\TH:i', $end);
            if ($s && $e) {
                $diff = $e->getTimestamp() - $s->getTimestamp();
                if ($diff > 0) { $durationHours = round($diff / 3600, 2); }
            }
        }

    // Gestion des uploads (utilisation de chemin public portable)
    $publicRoot = realpath(__DIR__ . '/../../public');
    $baseDir = $publicRoot . DIRECTORY_SEPARATOR . 'storage' . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR . 'incidents' . DIRECTORY_SEPARATOR . $incidentId;
        if (!is_dir($baseDir)) { @mkdir($baseDir, 0777, true); }

        $otdrBeforePath = $this->handleUpload('otdr_before', $baseDir);
        $otdrAfterPath = $this->handleUpload('otdr_after', $baseDir);
    $quotePath = $this->handleUpload('quote_file', $baseDir);
    
    // Gestion signature électronique (base64) ou fichier
    $signaturePath = null;
    if (!empty($_POST['signature_data'])) {
        // Signature électronique (canvas base64)
        $signatureData = $_POST['signature_data'];
        if (preg_match('/^data:image\/(\w+);base64,(.+)$/', $signatureData, $matches)) {
            $imageType = $matches[1];
            $base64Data = $matches[2];
            $imageData = base64_decode($base64Data);
            if ($imageData !== false) {
                $safeName = 'signature_' . uniqid() . '.' . $imageType;
                $dest = $baseDir . DIRECTORY_SEPARATOR . $safeName;
                if (file_put_contents($dest, $imageData) !== false) {
                    $signaturePath = 'incidents/' . $incidentId . '/' . $safeName;
                }
            }
        }
    } else {
        // Fallback: fichier uploadé (pour compatibilité)
        $signaturePath = $this->handleUpload('signature_file', $baseDir);
    }

        // Insérer/mettre à jour le rapport (un par incident pour simplicité)
        $stmt = $pdo->prepare('SELECT id FROM incident_reports WHERE incident_id=? ORDER BY id DESC LIMIT 1');
        $stmt->execute([$incidentId]);
        $reportId = (int)($stmt->fetchColumn() ?: 0);

        if ($reportId) {
            $up = $pdo->prepare('UPDATE incident_reports SET technician_id=?, team_name=?, team_contact=?, type_of_incident=?, detection_mode=?, cut_distance_km=?, gps_lat=?, gps_lng=?, fault_nature=?, impacted_equipment=?, takeover_at=?, intervention_start=?, intervention_end=?, duration_hours=?, repair_type=?, work_comments=?, otdr_before_path = COALESCE(?, otdr_before_path), otdr_after_path = COALESCE(?, otdr_after_path), otdr_result=?, conformity_status=?, resolution_status=?, need_additional=?, quote_file_path=COALESCE(?, quote_file_path), signature_path=COALESCE(?, signature_path), submitted_at = CASE WHEN ?=\'submit\' THEN NOW() ELSE submitted_at END, updated_at=NOW() WHERE id=?');
            $up->execute([
                (int)$u['id'], $teamName, $teamContact, $typeOfIncident, $detectionMode, $cutKm, $gpsLat, $gpsLng, $faultNature, $equipmentsStr,
                $takeover ? str_replace('T',' ',$takeover).':00' : null,
                $start ? str_replace('T',' ',$start).':00' : null,
                $end ? str_replace('T',' ',$end).':00' : null,
                $durationHours, $repairType, $workComments,
                $otdrBeforePath, $otdrAfterPath, $otdrResult, ($otdrResult==='Conforme'?'Conforme':($otdrResult==='Non conforme'?'Non conforme':null)), $resolutionStatus, $needAdditional, $quotePath, $signaturePath,
                $action, $reportId
            ]);
        } else {
            $ins = $pdo->prepare('INSERT INTO incident_reports (incident_id, technician_id, team_name, team_contact, type_of_incident, detection_mode, cut_distance_km, gps_lat, gps_lng, fault_nature, impacted_equipment, takeover_at, intervention_start, intervention_end, duration_hours, repair_type, work_comments, otdr_before_path, otdr_after_path, otdr_result, conformity_status, resolution_status, need_additional, quote_file_path, signature_path, submitted_at, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?, CASE WHEN ?=\'submit\' THEN NOW() ELSE NULL END, NOW(), NOW())');
            $ins->execute([
                $incidentId, (int)$u['id'], $teamName, $teamContact, $typeOfIncident, $detectionMode, $cutKm, $gpsLat, $gpsLng, $faultNature, $equipmentsStr,
                $takeover ? str_replace('T',' ',$takeover).':00' : null,
                $start ? str_replace('T',' ',$start).':00' : null,
                $end ? str_replace('T',' ',$end).':00' : null,
                $durationHours, $repairType, $workComments, $otdrBeforePath, $otdrAfterPath, $otdrResult, ($otdrResult==='Conforme'?'Conforme':($otdrResult==='Non conforme'?'Non conforme':null)), $resolutionStatus, $needAdditional, $quotePath, $signaturePath,
                $action
            ]);
            $reportId = (int)$pdo->lastInsertId();
        }

        // Matériels (remplacer tout l’ensemble pour simplifier)
        $pdo->prepare('DELETE FROM incident_materials WHERE report_id=?')->execute([$reportId]);
        $m_des = $_POST['materials_designation'] ?? [];
        $m_qty = $_POST['materials_qty'] ?? [];
        $m_ref = $_POST['materials_reference'] ?? [];
        $m_cost = $_POST['materials_unit_cost'] ?? [];
        $matIns = $pdo->prepare('INSERT INTO incident_materials (report_id, designation, qty, reference, unit_cost) VALUES (?,?,?,?,?)');
        for ($i=0; $i < count($m_des); $i++) {
            $des = trim($m_des[$i] ?? ''); if ($des==='') continue;
            $qty = (float)($m_qty[$i] ?? 0);
            $ref = trim($m_ref[$i] ?? '');
            $uc = (float)($m_cost[$i] ?? 0);
            $matIns->execute([$reportId, $des, $qty, $ref, $uc]);
        }

        // Photos multiples (attachments)
        if (!empty($_FILES['photos']['name']) && is_array($_FILES['photos']['name'])) {
            $attIns = $pdo->prepare('INSERT INTO incident_attachments (report_id, type, path, uploaded_at) VALUES (?,?,?,NOW())');
            $count = count($_FILES['photos']['name']);
            for ($i=0; $i<$count; $i++) {
                $p = $this->handleUploadArray('photos', $baseDir, $i);
                if ($p) { $attIns->execute([$reportId, 'photo', $p]); }
            }
        }

        // Photos d'intervention avec observation + géolocalisation
        if (!empty($_FILES['interv_photo']['name']) && is_array($_FILES['interv_photo']['name'])) {
            // S'assurer des colonnes ajoutées
            $this->ensureTreatmentTables($pdo);
            $count = count($_FILES['interv_photo']['name']);
            $obsArr = $_POST['interv_obs'] ?? [];
            $latArr = $_POST['interv_lat'] ?? [];
            $lngArr = $_POST['interv_lng'] ?? [];
            $takenAtArr = $_POST['interv_taken_at'] ?? [];
            $insPh = $pdo->prepare('INSERT INTO incident_attachments (report_id, type, path, observation, lat, lng, taken_by, taken_at, uploaded_at) VALUES (?,?,?,?,?,?,?,?,NOW())');
            for ($i=0; $i<$count; $i++) {
                $p = $this->handleUploadArray('interv_photo', $baseDir, $i);
                if (!$p) { continue; }
                $obs = isset($obsArr[$i]) ? trim((string)$obsArr[$i]) : null;
                $lat = isset($latArr[$i]) && $latArr[$i] !== '' ? (float)str_replace(',', '.', (string)$latArr[$i]) : null;
                $lng = isset($lngArr[$i]) && $lngArr[$i] !== '' ? (float)str_replace(',', '.', (string)$lngArr[$i]) : null;
                $takenBy = $u['name'] ?? null;
                $takenAt = null;
                if (!empty($takenAtArr[$i])) {
                    // attendu: YYYY-mm-ddTHH:ii
                    $d = \DateTime::createFromFormat('Y-m-d\TH:i', (string)$takenAtArr[$i]);
                    if ($d) { $takenAt = $d->format('Y-m-d H:i:00'); }
                }
                $insPh->execute([$reportId, 'intervention_photo', $p, ($obs ?: null), $lat, $lng, $takenBy, $takenAt]);
            }
        }

        // Gestion du fichier de clôture (closure_report)
        $closureReportPath = null;
        $closureNotes = trim($_POST['closure_notes'] ?? '');
        if (!empty($_FILES['closure_report']['name'])) {
            $closureReportPath = $this->handleUpload('closure_report', $baseDir);
        }

        // Si un fichier de clôture est fourni, mettre à jour le statut de l'incident en "Clôturé"
        if ($closureReportPath) {
            // Ajouter une colonne closure_report_path si nécessaire
            $this->ensureColumn($pdo, 'incident_reports', 'closure_report_path', '`closure_report_path` VARCHAR(255) NULL');
            $this->ensureColumn($pdo, 'incident_reports', 'closure_notes', '`closure_notes` TEXT NULL');
            
            // Mettre à jour le rapport avec le fichier de clôture
            $pdo->prepare('UPDATE incident_reports SET closure_report_path=?, closure_notes=? WHERE id=?')
                ->execute([$closureReportPath, $closureNotes, $reportId]);
            
            // Mettre à jour le statut de l'incident en "Clôturé"
            $closedId = $this->ensureStatus($pdo, 'closed', 'Clôturé', '#6c757d', 100);
            if ($closedId) {
                // Récupérer l'ancien statut pour historique
                $oldStatus = $pdo->prepare('SELECT s.label FROM incidents i JOIN incident_statuses s ON s.id=i.status_id WHERE i.id=? LIMIT 1');
                $oldStatus->execute([$incidentId]);
                $oldLbl = $oldStatus->fetchColumn() ?: 'Statut précédent';
                
                // Mettre à jour le statut
                $pdo->prepare('UPDATE incidents SET status_id=?, updated_at=NOW() WHERE id=?')
                    ->execute([$closedId, $incidentId]);
                
                // Historique
                $hist = $pdo->prepare('INSERT INTO incident_history (incident_id, field_name, old_value, new_value, changed_by) VALUES (?, "Status", ?, "Clôturé (rapport final)", ?)');
                $hist->execute([$incidentId, $oldLbl, (int)$u['id']]);
            }
        }

        // Option: mise à jour automatique de statut incident selon resolution_status (uniquement si pas de clôture)
        if (!$closureReportPath && $resolutionStatus) {
            $stKey = ($resolutionStatus === 'Définitive' || strtolower($resolutionStatus)==='definitive') ? 'closed' : 'resolved';
            $stId = $pdo->prepare('SELECT id FROM incident_statuses WHERE key_name=? LIMIT 1');
            $stId->execute([$stKey]);
            $newId = (int)($stId->fetchColumn() ?: 0);
            if ($newId) {
                $pdo->prepare('UPDATE incidents SET status_id=? WHERE id=?')->execute([$newId, $incidentId]);
            }
        }

        // Notifications si soumission
        if ($action === 'submit') {
            try {
                // Infos incident
                $iStmt = $pdo->prepare('SELECT ticket_id, title FROM incidents WHERE id=?');
                $iStmt->execute([$incidentId]);
                $i = $iStmt->fetch();
                $ticket = $i['ticket_id'] ?? ('INC-' . $incidentId);
                $titleN = 'Traitement soumis: ' . $ticket;
                $bodyN = ($u['name'] ?? 'Un utilisateur') . ' a soumis un rapport de traitement pour l\'incident ' . $ticket . '.';
                $url = \route_url('/incidents/treatment', ['id'=>$incidentId,'view'=>1]);

                // Destinataires: superviseurs / managers / admins
                $rec = $pdo->query("SELECT id FROM users WHERE active=1 AND role_key IN ('superviseur','supervisor','manager','admin')")->fetchAll(PDO::FETCH_COLUMN);
                if (!empty($rec)) {
                    Notifier::notifyUsers($pdo, array_map('intval', $rec), $titleN, $bodyN, $url);
                    Notifier::emailUsers($pdo, array_map('intval', $rec), $titleN, nl2br(htmlentities($bodyN)) . '<br><a href="' . $url . '">Voir le rapport</a>');
                }
            } catch (\Throwable $e) { /* ignorer notifications en cas d'échec */ }
        }

        $this->redirect('/incidents/detail?id=' . $incidentId);
    }

    private function ensureTreatmentTables(\PDO $pdo): void
    {
        // incident_reports
        $pdo->exec("CREATE TABLE IF NOT EXISTS incident_reports (
            id INT AUTO_INCREMENT PRIMARY KEY,
            incident_id INT NOT NULL,
            technician_id INT NOT NULL,
            team_name VARCHAR(255) NULL,
            team_contact VARCHAR(255) NULL,
            type_of_incident VARCHAR(50) NULL,
            detection_mode VARCHAR(50) NULL,
            cut_distance_km DECIMAL(10,2) NULL,
            gps_lat DECIMAL(10,8) NULL,
            gps_lng DECIMAL(11,8) NULL,
            fault_nature TEXT NULL,
            impacted_equipment TEXT NULL,
            takeover_at DATETIME NULL,
            intervention_start DATETIME NULL,
            intervention_end DATETIME NULL,
            duration_hours DECIMAL(10,2) NULL,
            repair_type VARCHAR(100) NULL,
            work_comments TEXT NULL,
            otdr_before_path VARCHAR(255) NULL,
            otdr_after_path VARCHAR(255) NULL,
            otdr_result VARCHAR(20) NULL,
            conformity_status VARCHAR(20) NULL,
            resolution_status VARCHAR(20) NULL,
            need_additional TINYINT(1) DEFAULT 0,
            quote_file_path VARCHAR(255) NULL,
            supervisor_comments TEXT NULL,
            signature_path VARCHAR(255) NULL,
            submitted_at DATETIME NULL,
            validated_at DATETIME NULL,
            validated_by INT NULL,
            rejected_at DATETIME NULL,
            rejected_by INT NULL,
            rejection_reason TEXT NULL,
            created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
            updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");

    // Ajout des colonnes manquantes si la table existe déjà (compatible MySQL/MariaDB)
    $this->ensureColumn($pdo, 'incident_reports', 'type_of_incident', '`type_of_incident` VARCHAR(50) NULL');
    $this->ensureColumn($pdo, 'incident_reports', 'takeover_at', '`takeover_at` DATETIME NULL');
    $this->ensureColumn($pdo, 'incident_reports', 'conformity_status', '`conformity_status` VARCHAR(20) NULL');
    $this->ensureColumn($pdo, 'incident_reports', 'validated_at', '`validated_at` DATETIME NULL');
    $this->ensureColumn($pdo, 'incident_reports', 'validated_by', '`validated_by` INT NULL');
    $this->ensureColumn($pdo, 'incident_reports', 'rejected_at', '`rejected_at` DATETIME NULL');
    $this->ensureColumn($pdo, 'incident_reports', 'rejected_by', '`rejected_by` INT NULL');
    $this->ensureColumn($pdo, 'incident_reports', 'rejection_reason', '`rejection_reason` TEXT NULL');

        // incident_materials
        $pdo->exec("CREATE TABLE IF NOT EXISTS incident_materials (
            id INT AUTO_INCREMENT PRIMARY KEY,
            report_id INT NOT NULL,
            designation VARCHAR(255) NOT NULL,
            qty DECIMAL(10,2) DEFAULT 0,
            reference VARCHAR(255) NULL,
            unit_cost DECIMAL(10,2) DEFAULT 0
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");

        // incident_attachments
        $pdo->exec("CREATE TABLE IF NOT EXISTS incident_attachments (
            id INT AUTO_INCREMENT PRIMARY KEY,
            report_id INT NOT NULL,
            type VARCHAR(20) NOT NULL,
            path VARCHAR(255) NOT NULL,
            uploaded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
        // Colonnes enrichies pour les photos d'intervention
        $this->ensureColumn($pdo, 'incident_attachments', 'observation', '`observation` TEXT NULL');
        $this->ensureColumn($pdo, 'incident_attachments', 'lat', '`lat` DECIMAL(10,7) NULL');
        $this->ensureColumn($pdo, 'incident_attachments', 'lng', '`lng` DECIMAL(11,7) NULL');
        $this->ensureColumn($pdo, 'incident_attachments', 'taken_by', '`taken_by` VARCHAR(255) NULL');
        $this->ensureColumn($pdo, 'incident_attachments', 'taken_at', '`taken_at` DATETIME NULL');
    }

    private function ensureFinalReportTables(\PDO $pdo): void
    {
        // Rapport final (un par incident)
        $pdo->exec("CREATE TABLE IF NOT EXISTS incident_final_reports (
            id INT AUTO_INCREMENT PRIMARY KEY,
            incident_id INT NOT NULL UNIQUE,
            supervisor_id INT NULL,
            restoration_at DATETIME NULL,
            client_supervisor VARCHAR(255) NULL,
            comments TEXT NULL,
            validated_at DATETIME NULL,
            validated_by INT NULL,
            created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
            updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
            INDEX(incident_id)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
        $this->ensureColumn($pdo, 'incident_final_reports', 'restoration_at', '`restoration_at` DATETIME NULL');
        $this->ensureColumn($pdo, 'incident_final_reports', 'comments', '`comments` TEXT NULL');
        $this->ensureColumn($pdo, 'incident_final_reports', 'supervisor_id', '`supervisor_id` INT NULL');
        $this->ensureColumn($pdo, 'incident_final_reports', 'client_supervisor', '`client_supervisor` VARCHAR(255) NULL');
        $this->ensureColumn($pdo, 'incident_final_reports', 'validated_at', '`validated_at` DATETIME NULL');
        $this->ensureColumn($pdo, 'incident_final_reports', 'validated_by', '`validated_by` INT NULL');

        // Pièces jointes rapport final
        $pdo->exec("CREATE TABLE IF NOT EXISTS incident_final_report_attachments (
            id INT AUTO_INCREMENT PRIMARY KEY,
            final_report_id INT NOT NULL,
            path VARCHAR(255) NOT NULL,
            uploaded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
            INDEX(final_report_id)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
    }

    private function ensureStatus(\PDO $pdo, string $key, string $label, string $color = '#6c757d', int $sort = 100): int
    {
        try {
            $st = $pdo->prepare('SELECT id FROM incident_statuses WHERE key_name=? LIMIT 1');
            $st->execute([$key]);
            $id = (int)($st->fetchColumn() ?: 0);
            if ($id) { return $id; }
            $ins = $pdo->prepare('INSERT INTO incident_statuses (key_name, label, color, sort_order) VALUES (?,?,?,?)');
            $ins->execute([$key, $label, $color, $sort]);
            return (int)$pdo->lastInsertId();
        } catch (\Throwable $e) { return 0; }
    }

    private function handleUploadArrayWithSub(string $field, string $baseDir, int $index, string $subRel): ?string
    {
        if (!isset($_FILES[$field]['tmp_name'][$index]) || !is_uploaded_file($_FILES[$field]['tmp_name'][$index])) { return null; }
        $name = $_FILES[$field]['name'][$index];
        $ext = strtolower(pathinfo($name, PATHINFO_EXTENSION));
        $safe = uniqid($field.'_') . '.' . $ext;
        if (!is_dir($baseDir)) { @mkdir($baseDir, 0777, true); }
        $dest = $baseDir . DIRECTORY_SEPARATOR . $safe;
        if (@move_uploaded_file($_FILES[$field]['tmp_name'][$index], $dest)) {
            $incidentId = basename(dirname(dirname($dest))) === 'incidents' ? basename(dirname($dest)) : basename(dirname($dest));
            // Forçage du chemin relatif avec sous-dossier
            $rel = 'incidents/' . $incidentId . '/' . trim($subRel,'/') . '/' . basename($dest);
            return $rel;
        }
        return null;
    }

    public function finalReport(): void
    {
        if (!Auth::check()) { $this->redirect('/login'); }
        // Gestion exclusivement par le technicien
        Auth::requireRole(['technicien']);
        $incidentId = (int)($_GET['id'] ?? 0);
        if (!$incidentId) { $this->redirect('/incidents'); }
        $pdo = Database::pdo();
        // Bloquer si déjà clôturé définitivement
        try {
            $ck = $pdo->prepare('SELECT s.key_name FROM incidents i JOIN incident_statuses s ON s.id=i.status_id WHERE i.id=?');
            $ck->execute([$incidentId]);
            $k = $ck->fetchColumn();
            if ($k && in_array($k, ['closed','resolved'])) { $this->redirect('/incidents/detail?id='.$incidentId.'&info=already_closed'); }
        } catch (\Throwable $e) {}
        // Exiger qu'un rapport de traitement soit validé
        try {
            $vr = $pdo->prepare('SELECT COUNT(*) FROM incident_reports WHERE incident_id=? AND validated_at IS NOT NULL');
            $vr->execute([$incidentId]);
            if ((int)$vr->fetchColumn() === 0) { $this->redirect('/incidents/detail?id='.$incidentId.'&error=not_validated'); }
        } catch (\Throwable $e) {}
        // Vérifier assignation du technicien
        try {
            $me = Auth::user();
            $c = $pdo->prepare('SELECT COUNT(*) FROM incident_assignments WHERE incident_id=? AND user_id=? AND active=1');
            $c->execute([$incidentId, (int)$me['id']]);
            if ((int)$c->fetchColumn() === 0) { $this->redirect('/incidents/detail?id='.$incidentId.'&error=forbidden'); }
        } catch (\Throwable $e) {}
        try {
            $this->ensureFinalReportTables($pdo);
            // Créer placeholder si absent
            $chk = $pdo->prepare('SELECT * FROM incident_final_reports WHERE incident_id=? LIMIT 1');
            $chk->execute([$incidentId]);
            $record = $chk->fetch();
            if (!$record) {
                $ins = $pdo->prepare('INSERT INTO incident_final_reports (incident_id, supervisor_id, created_at, updated_at) VALUES (?,?,NOW(),NOW())');
                $ins->execute([$incidentId, (int)Auth::user()['id']]);
                $chk->execute([$incidentId]);
                $record = $chk->fetch();
            }
        } catch (\Throwable $e) { $record = null; }
        // Charger incident basique pour titre
        $iStmt = $pdo->prepare('SELECT id, ticket_id, title FROM incidents WHERE id=?');
        $iStmt->execute([$incidentId]);
        $incident = $iStmt->fetch();
        if (!$incident) { $this->redirect('/incidents'); }
        $this->view('incidents/final_report', [
            'incident' => $incident,
            'finalReport' => $record,
        ]);
    }

    public function finalReportSave(): void
    {
        if (!Auth::check()) { $this->redirect('/login'); }
        // Enregistrement exclusivement par le technicien
        Auth::requireRole(['technicien']);
        if ($_SERVER['REQUEST_METHOD'] !== 'POST') { $this->redirect('/incidents'); }
        $incidentId = (int)($_POST['incident_id'] ?? 0);
        if (!$incidentId) { $this->redirect('/incidents'); }
        $pdo = Database::pdo();
        $this->ensureFinalReportTables($pdo);
        // Empêcher modifications si déjà clôturé
        try {
            $ck = $pdo->prepare('SELECT s.key_name FROM incidents i JOIN incident_statuses s ON s.id=i.status_id WHERE i.id=?');
            $ck->execute([$incidentId]);
            $k = $ck->fetchColumn();
            if ($k && in_array($k, ['closed','resolved'])) { $this->redirect('/incidents/detail?id='.$incidentId+'&info=locked'); }
        } catch (\Throwable $e) {}
        // Exiger qu'un rapport de traitement soit validé
        try {
            $vr = $pdo->prepare('SELECT COUNT(*) FROM incident_reports WHERE incident_id=? AND validated_at IS NOT NULL');
            $vr->execute([$incidentId]);
            if ((int)$vr->fetchColumn() === 0) { $this->redirect('/incidents/detail?id='.$incidentId.'&error=not_validated'); }
        } catch (\Throwable $e) {}
        // Vérifier assignation du technicien
        try {
            $me = Auth::user();
            $c = $pdo->prepare('SELECT COUNT(*) FROM incident_assignments WHERE incident_id=? AND user_id=? AND active=1');
            $c->execute([$incidentId, (int)$me['id']]);
            if ((int)$c->fetchColumn() === 0) { $this->redirect('/incidents/detail?id='.$incidentId.'&error=forbidden'); }
        } catch (\Throwable $e) {}
        $restoration = trim($_POST['restoration_at'] ?? ''); // datetime-local
    $comments = trim($_POST['comments'] ?? '');
    $clientSupervisor = trim($_POST['client_supervisor'] ?? '');
        $dtRestoration = null;
        if ($restoration !== '') {
            $d = \DateTime::createFromFormat('Y-m-d\TH:i', $restoration);
            if ($d) { $dtRestoration = $d->format('Y-m-d H:i:00'); }
        }
        // Récupérer id du rapport final
        $chk = $pdo->prepare('SELECT id FROM incident_final_reports WHERE incident_id=? LIMIT 1');
        $chk->execute([$incidentId]);
        $frId = (int)($chk->fetchColumn() ?: 0);
        if ($frId === 0) {
            $ins = $pdo->prepare('INSERT INTO incident_final_reports (incident_id, supervisor_id, restoration_at, client_supervisor, comments, created_at, updated_at) VALUES (?,?,?,?,?,NOW(),NOW())');
            $ins->execute([$incidentId, (int)Auth::user()['id'], $dtRestoration, $clientSupervisor !== '' ? $clientSupervisor : null, $comments ?: null]);
            $frId = (int)$pdo->lastInsertId();
        } else {
            // En cas de nouvelle sauvegarde par le technicien, on remet la validation à null (nouvelle version)
            $up = $pdo->prepare('UPDATE incident_final_reports SET supervisor_id=?, restoration_at=?, client_supervisor=?, comments=?, validated_at=NULL, validated_by=NULL, updated_at=NOW() WHERE id=?');
            $up->execute([(int)Auth::user()['id'], $dtRestoration, $clientSupervisor !== '' ? $clientSupervisor : null, $comments ?: null, $frId]);
        }
        // Upload pièces jointes
        if (!empty($_FILES['final_attachments']['name']) && is_array($_FILES['final_attachments']['name'])) {
            $publicRoot = realpath(__DIR__ . '/../../public');
            $baseDir = $publicRoot . '/storage/uploads/incidents/' . $incidentId . '/final';
            $count = count($_FILES['final_attachments']['name']);
            $attIns = $pdo->prepare('INSERT INTO incident_final_report_attachments (final_report_id, path) VALUES (?,?)');
            for ($i=0; $i<$count; $i++) {
                $p = $this->handleUploadArrayWithSub('final_attachments', $baseDir, $i, 'final');
                if ($p) { $attIns->execute([$frId, $p]); }
            }
        }
        // Historiser
        try {
            $hist = $pdo->prepare('INSERT INTO incident_history (incident_id, field_name, new_value, changed_by) VALUES (?, "FinalReport", ?, ?)');
            $hist->execute([$incidentId, 'Rapport final mis à jour', (int)Auth::user()['id']]);
        } catch (\Throwable $e) {}
        // Notifications + clôture finale + fin intervention si restoration_at défini
        if ($dtRestoration) {
            try {
                // Utiliser la colonne "reference" réelle (et non un ancien ticket_id inexistant)
                $iStmt = $pdo->prepare('SELECT reference FROM incidents WHERE id=?');
                $iStmt->execute([$incidentId]);
                $ticket = $iStmt->fetchColumn() ?: ('INC-' . $incidentId);
                $titleN = 'Rapport final complété: ' . $ticket;
                $bodyN = (Auth::user()['name'] ?? 'Un technicien') . ' a complété le rapport final (remontée le ' . date('d/m/Y H:i', strtotime($dtRestoration)) . ').';
                $url = \route_url('/incidents/detail', ['id'=>$incidentId]);
                $rec1 = $pdo->query("SELECT id FROM users WHERE active=1 AND role_key IN ('admin','superviseur','supervisor','manager','agent')")->fetchAll(\PDO::FETCH_COLUMN);
                $rec2St = $pdo->prepare('SELECT user_id FROM incident_assignments WHERE incident_id=? AND active=1');
                $rec2St->execute([$incidentId]);
                $rec2 = $rec2St->fetchAll(\PDO::FETCH_COLUMN);
                $rec = array_unique(array_map('intval', array_merge($rec1 ?: [], $rec2 ?: [])));
                if (!empty($rec)) {
                    Notifier::notifyUsers($pdo, $rec, $titleN, $bodyN, $url);
                    Notifier::emailUsers($pdo, $rec, $titleN, nl2br(htmlentities($bodyN)) . '<br><a href="' . $url . '">Consulter</a>');
                }
                $usr = Auth::user();
                // Fin automatique intervention: ajouter événement pause "Clôture rapport final" si pas déjà en pause
                try {
                    $this->ensureInterventionEventsTable($pdo);
                    $lastEv = $pdo->prepare('SELECT action FROM incident_intervention_events WHERE incident_id=? AND user_id=? ORDER BY happened_at DESC LIMIT 1');
                    $lastEv->execute([$incidentId, (int)($usr['id'] ?? 0)]);
                    $lastAction = strtolower((string)$lastEv->fetchColumn());
                    if ($lastAction !== 'pause') {
                        // Utiliser l'horodatage de restauration comme fin réelle si fourni, sinon NOW()
                        $stopTs = $dtRestoration ?: date('Y-m-d H:i:00');
                        $ev = $pdo->prepare('INSERT INTO incident_intervention_events (incident_id, user_id, action, reason, happened_at) VALUES (?,?,?,?,?)');
                        $ev->execute([$incidentId, (int)($usr['id'] ?? 0), 'pause', 'Clôture via rapport final', $stopTs]);
                    }
                    $histI = $pdo->prepare('INSERT INTO incident_history (incident_id, field_name, new_value, changed_by) VALUES (?, "Intervention", ?, ?)');
                    // Historiser explicitement la fin (mot-clés pour détection 'completed')
                    $histI->execute([$incidentId, 'Intervention clôturée via rapport final', (int)($usr['id'] ?? 0)]);
                } catch (\Throwable $ie) { /* ignore */ }
                // Clôture définitive si technicien
                if (($usr['role_key'] ?? '') === 'technicien') {
                    $oldLbl = null;
                    try {
                        $os = $pdo->prepare('SELECT s.label FROM incidents i JOIN incident_statuses s ON s.id=i.status_id WHERE i.id=?');
                        $os->execute([$incidentId]);
                        $oldLbl = $os->fetchColumn() ?: null;
                    } catch (\Throwable $ie2) {}
                    $closedId = $this->ensureStatus($pdo, 'closed', 'Clôturé', '#198754', 90);
                    if ($closedId) {
                        $pdo->prepare('UPDATE incidents SET status_id=?, resolved_at=COALESCE(resolved_at, NOW()) WHERE id=?')->execute([$closedId, $incidentId]);
                        $histC = $pdo->prepare('INSERT INTO incident_history (incident_id, field_name, old_value, new_value, changed_by) VALUES (?, "Status", ?, ?, ?)');
                        $histC->execute([$incidentId, $oldLbl, 'Clôturé', (int)$usr['id']]);
                    }
                }
            } catch (\Throwable $e) {}
        }
        $this->redirect('/incidents/detail?id=' . $incidentId);
    }

    /**
     * Validation du rapport final par superviseur/manager/admin — POST
     */
    public function finalReportValidate(): void
    {
        if (!Auth::check()) { $this->redirect('/login'); }
        Auth::requireRole(['admin','manager','superviseur','supervisor']);
        if ($_SERVER['REQUEST_METHOD'] !== 'POST') { $this->redirect('/incidents'); }

        $incidentId = (int)($_POST['incident_id'] ?? 0);
        if (!$incidentId) { $this->redirect('/incidents'); }

        $pdo = Database::pdo();
        $this->ensureFinalReportTables($pdo);

        // Charger le rapport final pour cet incident
        $st = $pdo->prepare('SELECT id, restoration_at, validated_at FROM incident_final_reports WHERE incident_id=? LIMIT 1');
        $st->execute([$incidentId]);
        $fr = $st->fetch();
        if (!$fr || empty($fr['restoration_at'])) {
            // Aucun rapport final complété à valider
            $this->redirect('/incidents/detail?id=' . $incidentId . '&error=no_final_report');
        }
        if (!empty($fr['validated_at'])) {
            // Déjà validé
            $this->redirect('/incidents/detail?id=' . $incidentId . '&info=already_final_validated');
        }

        $u = Auth::user();

        // Marquer comme validé
        $up = $pdo->prepare('UPDATE incident_final_reports SET validated_at = NOW(), validated_by = ?, updated_at = NOW() WHERE id = ?');
        $up->execute([(int)($u['id'] ?? 0), (int)$fr['id']]);

        // Historiser la validation du rapport final
        try {
            $hist = $pdo->prepare('INSERT INTO incident_history (incident_id, field_name, new_value, changed_by) VALUES (?, "FinalReport", ?, ?)');
            $hist->execute([$incidentId, 'Rapport final validé', (int)($u['id'] ?? 0)]);
        } catch (\Throwable $e) { /* ignore */ }

        // S'assurer que le statut est bien en "closed" (clôturé)
        try {
            $ck = $pdo->prepare('SELECT s.key_name FROM incidents i JOIN incident_statuses s ON s.id=i.status_id WHERE i.id=?');
            $ck->execute([$incidentId]);
            $k = strtolower((string)$ck->fetchColumn());
            if (!in_array($k, ['closed','resolved'], true)) {
                $closedId = $this->ensureStatus($pdo, 'closed', 'Clôturé', '#198754', 90);
                if ($closedId) {
                    $pdo->prepare('UPDATE incidents SET status_id=?, resolved_at=COALESCE(resolved_at, NOW()) WHERE id=?')->execute([$closedId, $incidentId]);
                }
            }
        } catch (\Throwable $e) { /* ignore */ }

        // Notifications IN APP à tous les profils concernés (admin, superviseur, agent, techniciens assignés)
        try {
            // Utiliser la colonne "reference" réelle, avec fallback INC-<id>
            $iStmt = $pdo->prepare('SELECT reference, title FROM incidents WHERE id=?');
            $iStmt->execute([$incidentId]);
            $i = $iStmt->fetch();
            $ticket = $i['reference'] ?? ('INC-' . $incidentId);
            $titleN = 'Clôture validée: ' . $ticket;
            $bodyN = ($u['name'] ?? 'Un superviseur') . ' a validé la clôture de l\'incident ' . $ticket . '.';
            $url = \route_url('/incidents/detail', ['id'=>$incidentId]);

            $rec1 = $pdo->query("SELECT id FROM users WHERE active=1 AND role_key IN ('admin','superviseur','supervisor','manager','agent')")->fetchAll(\PDO::FETCH_COLUMN);
            $rec2St = $pdo->prepare('SELECT user_id FROM incident_assignments WHERE incident_id=? AND active=1');
            $rec2St->execute([$incidentId]);
            $rec2 = $rec2St->fetchAll(\PDO::FETCH_COLUMN);
            $rec = array_unique(array_map('intval', array_merge($rec1 ?: [], $rec2 ?: [])));
            if (!empty($rec)) {
                Notifier::notifyUsers($pdo, $rec, $titleN, $bodyN, $url);
            }
        } catch (\Throwable $e) { /* ignore notifications errors */ }

        $this->redirect('/incidents/detail?id=' . $incidentId);
    }

    private function ensureColumn(\PDO $pdo, string $table, string $column, string $definition): void
    {
        try {
            $db = $pdo->query('SELECT DATABASE()')->fetchColumn();
            if (!$db) { return; }
            $chk = $pdo->prepare('SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? AND COLUMN_NAME = ?');
            $chk->execute([$db, $table, $column]);
            if ((int)$chk->fetchColumn() === 0) {
                $pdo->exec("ALTER TABLE `{$table}` ADD COLUMN {$definition}");
            }
        } catch (\Throwable $e) {
            // Ignorer silencieusement si droits manquants; alternative: logger
        }
    }

    private function ensurePriorityLevelsTable(\PDO $pdo): void
    {
        // Créer la table si absente
        $pdo->exec("CREATE TABLE IF NOT EXISTS priority_levels (
            id INT AUTO_INCREMENT PRIMARY KEY,
            level INT NOT NULL,
            name VARCHAR(100) NOT NULL,
            color VARCHAR(20) NULL,
            active TINYINT(1) DEFAULT 1,
            UNIQUE KEY uniq_level (level)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
        try {
            // Si vide, insérer des valeurs par défaut
            $count = (int)$pdo->query('SELECT COUNT(*) FROM priority_levels')->fetchColumn();
            if ($count === 0) {
                // Tenter un seed basique si les colonnes attendues existent; sinon ignorer silencieusement
                $cols = $pdo->query("DESCRIBE priority_levels")->fetchAll(\PDO::FETCH_COLUMN);
                $hasLevel = in_array('level', $cols, true);
                $hasActive = in_array('active', $cols, true);
                if ($hasLevel) {
                    $sql = $hasActive
                        ? 'INSERT INTO priority_levels (level, name, color, active) VALUES (?,?,?,1)'
                        : 'INSERT INTO priority_levels (level, name, color) VALUES (?,?,?)';
                    $ins = $pdo->prepare($sql);
                    $defaults = [
                        [1, 'Critique', '#dc3545'],
                        [2, 'Haute',    '#fd7e14'],
                        [3, 'Moyenne',  '#0d6efd'],
                        [4, 'Basse',    '#6c757d'],
                    ];
                    foreach ($defaults as $d) { $ins->execute($d); }
                }
            }
        } catch (\Throwable $e) { /* ne pas bloquer l'affichage si droits manquants */ }
    }

    private function fetchPrioritiesNormalized(\PDO $pdo): array
    {
        try {
            $cols = $pdo->query('DESCRIBE priority_levels')->fetchAll(\PDO::FETCH_COLUMN);
            $hasLevel = in_array('level', $cols, true);
            $hasSort = in_array('sort_order', $cols, true);
            $order = $hasLevel ? 'level' : ($hasSort ? 'sort_order' : 'name');
            $stmt = $pdo->query("SELECT * FROM priority_levels ORDER BY {$order} ASC");
            $rows = $stmt->fetchAll();
            // Normaliser: toujours fournir la clé 'level' pour la vue
            foreach ($rows as &$r) {
                if (!array_key_exists('level', $r)) {
                    $r['level'] = $hasSort ? ($r['sort_order'] ?? null) : null;
                }
            }
            unset($r);
            return $rows;
        } catch (\Throwable $e) {
            return [];
        }
    }

    private function fetchEquipmentsNormalized(\PDO $pdo): array
    {
        try {
            $cols = $pdo->query('DESCRIBE equipments')->fetchAll(\PDO::FETCH_COLUMN);
            $hasCategory = in_array('category', $cols, true);
            $hasType = in_array('type', $cols, true); // Au cas où une autre version du schéma existe
            $orderCol = $hasCategory ? 'category' : ($hasType ? 'type' : 'name');
            $stmt = $pdo->query("SELECT * FROM equipments WHERE active = 1 ORDER BY {$orderCol}, name");
            $rows = $stmt->fetchAll();
            // Normaliser champ de groupe et supprimer les doublons (même nom + même groupe)
            $seen = [];
            $unique = [];
            foreach ($rows as $r) {
                if (!isset($r['type'])) { $r['type'] = $r['category'] ?? null; }
                $nameKey = strtolower(trim((string)($r['name'] ?? '')));
                $grpKey = strtolower(trim((string)($r['type'] ?? '')));
                $key = $nameKey . '|' . $grpKey;
                if ($nameKey === '') { continue; }
                if (isset($seen[$key])) { continue; }
                $seen[$key] = true;
                $unique[] = $r;
            }
            return $unique;
        } catch (\Throwable $e) {
            return [];
        }
    }

    private function handleUpload(string $field, string $baseDir): ?string
    {
        if (!isset($_FILES[$field]) || !is_uploaded_file($_FILES[$field]['tmp_name'])) { return null; }
        $name = $_FILES[$field]['name'];
        $ext = strtolower(pathinfo($name, PATHINFO_EXTENSION));
        $safe = uniqid($field.'_') . '.' . $ext;
        $dest = $baseDir . DIRECTORY_SEPARATOR . $safe;
        if (@move_uploaded_file($_FILES[$field]['tmp_name'], $dest)) {
            // Construire un chemin relatif standardisé (sans double slash)
            $incidentId = basename(dirname($dest));
            $rel = 'incidents/' . $incidentId . '/' . basename($dest);
            return $rel;
        }
        return null;
    }

    private function handleUploadArray(string $field, string $baseDir, int $index): ?string
    {
        if (!isset($_FILES[$field]['tmp_name'][$index]) || !is_uploaded_file($_FILES[$field]['tmp_name'][$index])) { return null; }
        $name = $_FILES[$field]['name'][$index];
        $ext = strtolower(pathinfo($name, PATHINFO_EXTENSION));
        $safe = uniqid($field.'_') . '.' . $ext;
        $dest = $baseDir . DIRECTORY_SEPARATOR . $safe;
        if (@move_uploaded_file($_FILES[$field]['tmp_name'][$index], $dest)) {
            $incidentId = basename(dirname($dest));
            $rel = 'incidents/' . $incidentId . '/' . basename($dest);
            return $rel;
        }
        return null;
    }

    /**
     * Validation du traitement par superviseur/manager/admin — POST
     */
    public function treatmentValidate(): void
    {
        if (!Auth::check()) { $this->redirect('/login'); }
        Auth::requireRole(['admin','manager','superviseur','supervisor']);
        if ($_SERVER['REQUEST_METHOD'] !== 'POST') { $this->redirect('/incidents'); }

        $u = Auth::user();
        $incidentId = (int)($_POST['incident_id'] ?? 0);
        if (!$incidentId) { $this->redirect('/incidents'); }

        $pdo = Database::pdo();
        // S'assurer des colonnes
        $this->ensureTreatmentTables($pdo);

        // Récupérer rapport soumis
        $rep = $pdo->prepare('SELECT id, submitted_at, validated_at FROM incident_reports WHERE incident_id=? ORDER BY id DESC LIMIT 1');
        $rep->execute([$incidentId]);
        $report = $rep->fetch();
        if (!$report || empty($report['submitted_at'])) {
            $this->redirect('/incidents/treatment?id=' . $incidentId . '&error=no_submitted_report');
        }
        if (!empty($report['validated_at'])) {
            $this->redirect('/incidents/treatment?id=' . $incidentId . '&info=already_validated');
        }

        $comments = trim($_POST['supervisor_comments'] ?? '');
        // Mettre à jour commentaire et validation
        $up = $pdo->prepare('UPDATE incident_reports SET supervisor_comments = ?, validated_at = NOW(), validated_by = ? WHERE id = ?');
        $up->execute([$comments ?: null, (int)$u['id'], (int)$report['id']]);

        // Nouvelle règle: à la validation, passer en "Traité" (sans clôturer définitivement)
        try {
            // 1) Tenter de créer/récupérer 'treated'
            $treatedId = $this->ensureStatus($pdo, 'treated', 'Traité', '#0d6efd', 60);
            // 2) Fallbacks si création impossible ou non trouvée
            if (!$treatedId) {
                try {
                    $st = $pdo->query("SELECT id FROM incident_statuses WHERE key_name IN ('treated','traite','processed') OR label='Traité' ORDER BY FIELD(key_name,'treated','traite','processed') LIMIT 1");
                    $treatedId = (int)($st->fetchColumn() ?: 0);
                    if (!$treatedId) {
                        $st2 = $pdo->query("SELECT id FROM incident_statuses WHERE key_name='in_progress' LIMIT 1");
                        $treatedId = (int)($st2->fetchColumn() ?: 0);
                    }
                } catch (\Throwable $re) { /* ignore */ }
            }
            if ($treatedId) {
                // Libellé avant/après pour l'historique
                $oldStatusLbl = null; $newStatusLbl = 'Traité';
                try {
                    $os = $pdo->prepare('SELECT s.label FROM incidents i JOIN incident_statuses s ON s.id=i.status_id WHERE i.id=?');
                    $os->execute([$incidentId]);
                    $oldStatusLbl = $os->fetchColumn() ?: null;
                    $nl = $pdo->prepare('SELECT label FROM incident_statuses WHERE id=?');
                    $nl->execute([$treatedId]);
                    $newStatusLbl = $nl->fetchColumn() ?: $newStatusLbl;
                } catch (\Throwable $ie) {}
                $pdo->prepare('UPDATE incidents SET status_id=?, updated_at=NOW() WHERE id=?')->execute([$treatedId, $incidentId]);
                $hist = $pdo->prepare('INSERT INTO incident_history (incident_id, field_name, old_value, new_value, changed_by) VALUES (?, "Status", ?, ?, ?)');
                $hist->execute([$incidentId, $oldStatusLbl, $newStatusLbl, (int)$u['id']]);
            }
        } catch (\Throwable $e) { /* ignore */ }

        // Créer/assurer une action de rapport final
        try {
            $this->ensureFinalReportTables($pdo);
            $chk = $pdo->prepare('SELECT id FROM incident_final_reports WHERE incident_id=? LIMIT 1');
            $chk->execute([$incidentId]);
            $fid = (int)($chk->fetchColumn() ?: 0);
            if ($fid === 0) {
                $ins = $pdo->prepare('INSERT INTO incident_final_reports (incident_id, supervisor_id, created_at, updated_at) VALUES (?,?,NOW(),NOW())');
                $ins->execute([$incidentId, (int)$u['id']]);
            } else {
                $pdo->prepare('UPDATE incident_final_reports SET supervisor_id=COALESCE(supervisor_id, ?), updated_at=NOW() WHERE id=?')->execute([(int)$u['id'], $fid]);
            }
            // Historique pour signaler l'action à réaliser
            $hist2 = $pdo->prepare('INSERT INTO incident_history (incident_id, field_name, new_value, changed_by) VALUES (?, "Action", ?, ?)');
            $hist2->execute([$incidentId, 'Compléter le rapport final (date de remontée, commentaire, PJ)', (int)$u['id']]);
        } catch (\Throwable $e) { /* ignore */ }

        // Notifications: admin, superviseur/manager, agents, techniciens assignés
        try {
            $iStmt = $pdo->prepare('SELECT ticket_id, title FROM incidents WHERE id=?');
            $iStmt->execute([$incidentId]);
            $i = $iStmt->fetch();
            $ticket = $i['ticket_id'] ?? ('INC-' . $incidentId);
            $titleN = 'Traitement validé: ' . $ticket;
            $bodyN = ($u['name'] ?? 'Un superviseur') . ' a validé le rapport de traitement de l\'incident ' . $ticket . ". Un rapport final est à compléter.";
            $url = \route_url('/incidents/treatment', ['id'=>$incidentId,'view'=>1]);

            // Rôles
            $rec1 = $pdo->query("SELECT id FROM users WHERE active=1 AND role_key IN ('admin','superviseur','supervisor','manager','agent')")->fetchAll(PDO::FETCH_COLUMN);
            $rec2St = $pdo->prepare('SELECT user_id FROM incident_assignments WHERE incident_id=? AND active=1');
            $rec2St->execute([$incidentId]);
            $rec2 = $rec2St->fetchAll(PDO::FETCH_COLUMN);
            $rec = array_unique(array_map('intval', array_merge($rec1 ?: [], $rec2 ?: [])));
            if (!empty($rec)) {
                Notifier::notifyUsers($pdo, $rec, $titleN, $bodyN, $url);
                Notifier::emailUsers($pdo, $rec, $titleN, nl2br(htmlentities($bodyN)) . '<br><a href="' . $url . '">Voir le rapport</a>');
            }
        } catch (\Throwable $e) { /* ignorer */ }

        $this->redirect('/incidents/detail?id=' . $incidentId);
    }

    /**
     * Invalidation (refus) du traitement — POST
     */
    public function treatmentInvalidate(): void
    {
        if (!Auth::check()) { $this->redirect('/login'); }
        Auth::requireRole(['admin','manager','superviseur','supervisor']);
        if ($_SERVER['REQUEST_METHOD'] !== 'POST') { $this->redirect('/incidents'); }

        $u = Auth::user();
        $incidentId = (int)($_POST['incident_id'] ?? 0);
        if (!$incidentId) { $this->redirect('/incidents'); }

        $pdo = Database::pdo();
        $this->ensureTreatmentTables($pdo);

        $rep = $pdo->prepare('SELECT id, submitted_at, validated_at FROM incident_reports WHERE incident_id=? ORDER BY id DESC LIMIT 1');
        $rep->execute([$incidentId]);
        $report = $rep->fetch();
        if (!$report || empty($report['submitted_at'])) {
            $this->redirect('/incidents/treatment?id=' . $incidentId . '&error=no_submitted_report');
        }
        if (!empty($report['validated_at'])) {
            // Si déjà validé, on empêche l'invalidation simple ici (processus plus strict à définir)
            $this->redirect('/incidents/treatment?id=' . $incidentId . '&info=already_validated');
        }

        $reason = trim($_POST['rejection_reason'] ?? '');
        $up = $pdo->prepare('UPDATE incident_reports SET rejection_reason = ?, rejected_at = NOW(), rejected_by = ? WHERE id = ?');
        $up->execute([$reason ?: null, (int)$u['id'], (int)$report['id']]);

        // Notifications: mêmes destinataires que validation
        try {
            $iStmt = $pdo->prepare('SELECT ticket_id, title FROM incidents WHERE id=?');
            $iStmt->execute([$incidentId]);
            $i = $iStmt->fetch();
            $ticket = $i['ticket_id'] ?? ('INC-' . $incidentId);
            $titleN = 'Traitement refusé: ' . $ticket;
            $bodyN = ($u['name'] ?? 'Un superviseur') . " a refusé le rapport de traitement de l'incident " . $ticket . ( $reason ? (". Motif: " . $reason) : '' ) . '.';
            $url = \route_url('/incidents/treatment', ['id'=>$incidentId]);
            $rec1 = $pdo->query("SELECT id FROM users WHERE active=1 AND role_key IN ('admin','superviseur','supervisor','manager','agent')")->fetchAll(PDO::FETCH_COLUMN);
            $rec2St = $pdo->prepare('SELECT user_id FROM incident_assignments WHERE incident_id=? AND active=1');
            $rec2St->execute([$incidentId]);
            $rec2 = $rec2St->fetchAll(PDO::FETCH_COLUMN);
            $rec = array_unique(array_map('intval', array_merge($rec1 ?: [], $rec2 ?: [])));
            if (!empty($rec)) {
                Notifier::notifyUsers($pdo, $rec, $titleN, $bodyN, $url);
                Notifier::emailUsers($pdo, $rec, $titleN, nl2br(htmlentities($bodyN)) . '<br><a href="' . $url . '">Ouvrir</a>');
            }
        } catch (\Throwable $e) { /* ignore */ }

        $this->redirect('/incidents/detail?id=' . $incidentId);
    }

    /**
     * Vue imprimable (PDF via impression navigateur)
     */
    public function treatmentPrint(): void
    {
        if (!Auth::check()) { $this->redirect('/login'); }
        $u = Auth::user();
        $id = (int)($_GET['id'] ?? 0);
        if (!$id) { $this->redirect('/incidents'); }
        $pdo = Database::pdo();

        // Restreindre technicien non assigné
        if (($u['role_key'] ?? '') === 'technicien') {
            $c = $pdo->prepare('SELECT COUNT(*) FROM incident_assignments WHERE incident_id=? AND user_id=? AND active=1');
            $c->execute([$id, (int)$u['id']]);
            if ((int)$c->fetchColumn() === 0) { $this->redirect('/incidents?error=forbidden'); }
        }

        $this->ensureTreatmentTables($pdo);

        $incidentStmt = $pdo->prepare('SELECT i.*, c.name as client_name, l.name as location_name FROM incidents i LEFT JOIN clients c ON c.id=i.client_id LEFT JOIN locations l ON l.id=i.location_id WHERE i.id=?');
        $incidentStmt->execute([$id]);
        $incident = $incidentStmt->fetch();
        if (!$incident) { $this->redirect('/incidents'); }

        $rep = $pdo->prepare('SELECT * FROM incident_reports WHERE incident_id=? ORDER BY id DESC LIMIT 1');
        $rep->execute([$id]);
        $report = $rep->fetch();

        $materials = [];
        $interventionPhotos = [];
        if ($report && !empty($report['id'])) {
            $m = $pdo->prepare('SELECT * FROM incident_materials WHERE report_id = ? ORDER BY id ASC');
            $m->execute([(int)$report['id']]);
            $materials = $m->fetchAll();
            // Photos d'intervention rattachées à ce rapport
            try {
                $ph = $pdo->prepare('SELECT path, observation, lat, lng, taken_by, uploaded_at, taken_at FROM incident_attachments WHERE report_id=? AND type="intervention_photo" ORDER BY id ASC');
                $ph->execute([(int)$report['id']]);
                $interventionPhotos = $ph->fetchAll();
            } catch (\Throwable $e) { $interventionPhotos = []; }
        }

        $this->view('incidents/treatment_print', compact('incident','report','materials','interventionPhotos'));
    }

    /**
     * Génération PDF via Dompdf (si disponible). Fallback: redirection vers print.
     */
    public function treatmentPdf(): void
    {
        if (!Auth::check()) { $this->redirect('/login'); }
        $u = Auth::user();
        $id = (int)($_GET['id'] ?? 0);
        if (!$id) { $this->redirect('/incidents'); }
        $pdo = Database::pdo();

        if (($u['role_key'] ?? '') === 'technicien') {
            $c = $pdo->prepare('SELECT COUNT(*) FROM incident_assignments WHERE incident_id=? AND user_id=? AND active=1');
            $c->execute([$id, (int)$u['id']]);
            if ((int)$c->fetchColumn() === 0) { $this->redirect('/incidents?error=forbidden'); }
        }

        $this->ensureTreatmentTables($pdo);

        // Récupération données
        $incidentStmt = $pdo->prepare('SELECT i.*, c.name as client_name, l.name as location_name FROM incidents i LEFT JOIN clients c ON c.id=i.client_id LEFT JOIN locations l ON l.id=i.location_id WHERE i.id=?');
        $incidentStmt->execute([$id]);
        $incident = $incidentStmt->fetch();
        if (!$incident) { $this->redirect('/incidents'); }

        $rep = $pdo->prepare('SELECT * FROM incident_reports WHERE incident_id=? ORDER BY id DESC LIMIT 1');
        $rep->execute([$id]);
        $report = $rep->fetch();

        $materials = [];
        $interventionPhotos = [];
        if ($report && !empty($report['id'])) {
            $m = $pdo->prepare('SELECT * FROM incident_materials WHERE report_id = ? ORDER BY id ASC');
            $m->execute([(int)$report['id']]);
            $materials = $m->fetchAll();
            try {
                $ph = $pdo->prepare('SELECT path, observation, lat, lng, taken_by, uploaded_at, taken_at FROM incident_attachments WHERE report_id=? AND type="intervention_photo" ORDER BY id ASC');
                $ph->execute([(int)$report['id']]);
                $interventionPhotos = $ph->fetchAll();
            } catch (\Throwable $e) { $interventionPhotos = []; }
        }

        // Rendre la vue en string sans layout
        $html = $this->renderViewToString('incidents/treatment_print', compact('incident','report','materials','interventionPhotos'), 'none');
        if (!PDF::available()) {
            $this->redirect('/incidents/treatment/print?id=' . $id);
        }
        $ticket = ($incident['ticket_id'] ?? null) ?: ('INC_' . $id);
        PDF::download($html, 'Rapport_' . $ticket . '.pdf');
    }

    /**
     * Page annexe: carte affichant la position des photos d'intervention
     */
    public function treatmentPhotosMap(): void
    {
        if (!Auth::check()) { $this->redirect('/login'); }
        $u = Auth::user();
        $id = (int)($_GET['id'] ?? 0);
        if (!$id) { $this->redirect('/incidents'); }
        $pdo = Database::pdo();
        // Restreindre technicien non assigné
        if (($u['role_key'] ?? '') === 'technicien') {
            $c = $pdo->prepare('SELECT COUNT(*) FROM incident_assignments WHERE incident_id=? AND user_id=? AND active=1');
            $c->execute([$id, (int)$u['id']]);
            if ((int)$c->fetchColumn() === 0) { $this->redirect('/incidents?error=forbidden'); }
        }
        $this->ensureTreatmentTables($pdo);
        // Ne pas sélectionner de colonnes potentiellement absentes selon les dumps; i.* est plus sûr
        $incidentStmt = $pdo->prepare('SELECT i.* FROM incidents i WHERE i.id=?');
        $incidentStmt->execute([$id]);
        $incident = $incidentStmt->fetch();
        if (!$incident) { $this->redirect('/incidents'); }
        $rep = $pdo->prepare('SELECT id FROM incident_reports WHERE incident_id=? ORDER BY id DESC LIMIT 1');
        $rep->execute([$id]);
        $report = $rep->fetch();
        $photos = [];
        if ($report && !empty($report['id'])) {
            $ph = $pdo->prepare('SELECT path, observation, lat, lng, taken_by, uploaded_at, taken_at FROM incident_attachments WHERE report_id=? AND type="intervention_photo" ORDER BY id ASC');
            $ph->execute([(int)$report['id']]);
            $photos = $ph->fetchAll();
        }
        $this->view('incidents/treatment_photos_map', [
            'incident' => $incident,
            'photos' => $photos
        ]);
    }

    public function finalReportPrint(): void
    {
        if (!Auth::check()) { $this->redirect('/login'); }
        $incidentId = (int)($_GET['id'] ?? 0);
        if (!$incidentId) { $this->redirect('/incidents'); }
        $pdo = Database::pdo();
        $u = Auth::user();
        if (($u['role_key'] ?? '') === 'technicien') {
            $c = $pdo->prepare('SELECT COUNT(*) FROM incident_assignments WHERE incident_id=? AND user_id=? AND active=1');
            $c->execute([$incidentId, (int)$u['id']]);
            if ((int)$c->fetchColumn() === 0) { $this->redirect('/incidents?error=forbidden'); }
        }
        $this->ensureFinalReportTables($pdo);
        $iStmt = $pdo->prepare('SELECT i.*, c.name as client_name, l.name as location_name FROM incidents i JOIN clients c ON c.id=i.client_id JOIN locations l ON l.id=i.location_id WHERE i.id=?');
        $iStmt->execute([$incidentId]);
        $incident = $iStmt->fetch();
        if (!$incident) { $this->redirect('/incidents'); }
        $frS = $pdo->prepare('SELECT * FROM incident_final_reports WHERE incident_id=?');
        $frS->execute([$incidentId]);
        $finalReport = $frS->fetch();
        $atts = [];
        if ($finalReport) {
            $aS = $pdo->prepare('SELECT path FROM incident_final_report_attachments WHERE final_report_id=? ORDER BY id ASC');
            $aS->execute([(int)$finalReport['id']]);
            $atts = $aS->fetchAll();
        }
        
        // Calculer durée intervention et pauses pour le technicien du rapport final
        $interventionData = null;
        if ($finalReport && !empty($finalReport['technician_id'])) {
            $techId = (int)$finalReport['technician_id'];
            $this->ensureInterventionEventsTable($pdo);
            [$durSec, $durHuman] = $this->computeInterventionDuration($pdo, $incidentId, $techId);
            $pauseStmt = $pdo->prepare('SELECT COUNT(*) FROM incident_intervention_events WHERE incident_id=? AND user_id=? AND action="pause"');
            $pauseStmt->execute([$incidentId, $techId]);
            $pauseCount = (int)$pauseStmt->fetchColumn();
            $techStmt = $pdo->prepare('SELECT name FROM users WHERE id=? LIMIT 1');
            $techStmt->execute([$techId]);
            $techName = $techStmt->fetchColumn() ?: 'Technicien';
            $interventionData = [
                'duration_seconds' => $durSec,
                'duration_human' => $durHuman,
                'pause_count' => $pauseCount,
                'technician_name' => $techName
            ];
        }
        
        $this->view('incidents/final_report_print', compact('incident','finalReport','atts','interventionData'));
    }

    public function finalReportPdf(): void
    {
        if (!Auth::check()) { $this->redirect('/login'); }
        $incidentId = (int)($_GET['id'] ?? 0);
        if (!$incidentId) { $this->redirect('/incidents'); }
        $pdo = Database::pdo();
        $u = Auth::user();
        if (($u['role_key'] ?? '') === 'technicien') {
            $c = $pdo->prepare('SELECT COUNT(*) FROM incident_assignments WHERE incident_id=? AND user_id=? AND active=1');
            $c->execute([$incidentId, (int)$u['id']]);
            if ((int)$c->fetchColumn() === 0) { $this->redirect('/incidents?error=forbidden'); }
        }
        $this->ensureFinalReportTables($pdo);
        $iStmt = $pdo->prepare('SELECT i.*, c.name as client_name, l.name as location_name FROM incidents i JOIN clients c ON c.id=i.client_id JOIN locations l ON l.id=i.location_id WHERE i.id=?');
        $iStmt->execute([$incidentId]);
        $incident = $iStmt->fetch();
        if (!$incident) { $this->redirect('/incidents'); }
        $frS = $pdo->prepare('SELECT * FROM incident_final_reports WHERE incident_id=?');
        $frS->execute([$incidentId]);
        $finalReport = $frS->fetch();
        $atts = [];
        if ($finalReport) {
            $aS = $pdo->prepare('SELECT path FROM incident_final_report_attachments WHERE final_report_id=? ORDER BY id ASC');
            $aS->execute([(int)$finalReport['id']]);
            $atts = $aS->fetchAll();
        }
        
        // Calculer durée intervention et pauses pour le technicien du rapport final
        $interventionData = null;
        if ($finalReport && !empty($finalReport['technician_id'])) {
            $techId = (int)$finalReport['technician_id'];
            $this->ensureInterventionEventsTable($pdo);
            [$durSec, $durHuman] = $this->computeInterventionDuration($pdo, $incidentId, $techId);
            $pauseStmt = $pdo->prepare('SELECT COUNT(*) FROM incident_intervention_events WHERE incident_id=? AND user_id=? AND action="pause"');
            $pauseStmt->execute([$incidentId, $techId]);
            $pauseCount = (int)$pauseStmt->fetchColumn();
            $techStmt = $pdo->prepare('SELECT name FROM users WHERE id=? LIMIT 1');
            $techStmt->execute([$techId]);
            $techName = $techStmt->fetchColumn() ?: 'Technicien';
            $interventionData = [
                'duration_seconds' => $durSec,
                'duration_human' => $durHuman,
                'pause_count' => $pauseCount,
                'technician_name' => $techName
            ];
        }
        
        $html = $this->renderViewToString('incidents/final_report_print', compact('incident','finalReport','atts','interventionData'), 'none');
        if (!PDF::available()) {
            header('Content-Type: text/html; charset=utf-8'); echo $html; exit;
        }
        PDF::download($html, 'Rapport_final_' . ($incident['ticket_id'] ?? ('INC_'.$incidentId)) . '.pdf');
    }

    protected function renderViewToString(string $template, array $data = [], string $layout = 'none'): string
    {
        extract($data, EXTR_SKIP);
        $templateFile = __DIR__ . '/../Views/' . str_replace('.', '/', $template) . '.php';
        if (!file_exists($templateFile)) { return '<h1>View introuvable</h1>'; }
        if ($layout === 'none') {
            ob_start();
            include $templateFile;
            return ob_get_clean();
        }
        $layoutFile = __DIR__ . '/../Views/layouts/' . $layout . '.php';
        if (!file_exists($layoutFile)) {
            $layoutFile = __DIR__ . '/../Views/layouts/main.php';
        }
        ob_start();
        include $templateFile;
        $content = ob_get_clean();
        ob_start();
        include $layoutFile;
        return ob_get_clean();
    }
}