<environment_context>
  <cwd>C:\wamp64\www\out</cwd>
  <approval_policy>on-request</approval_policy>
  <sandbox_mode>read-only</sandbox_mode>
  <network_access>restricted</network_access>
  <shell>powershell</shell>
</environment_context>


tu peux checker le fonctionnement du system dans /referentiel


Summary: auto


**Checking read-only access and directory listing**

I'm verifying read-only access and planning to list the /referentiel directory contents to find necessary paths, considering whether French terms matter.


Output:
Exit code: 0
Wall time: 1.9 seconds
Output:
cd : Impossible de trouver le chemin d'accŠs ®ÿC:\referentielÿ¯, car il n'existe pas.
Au caractŠre Ligne:1 : 1
+ cd /referentiel; Get-ChildItem
+ ~~~~~~~~~~~~~~~
    + CategoryInfo          : ObjectNotFound: (C:\referentiel:String) [Set-Location], ItemNotFoundException
    + FullyQualifiedErrorId : PathNotFound,Microsoft.PowerShell.Commands.SetLocationCommand

    R‚pertoireÿ: C:\wamp64\www\out

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
d-----        04/12/2025     17:40                admin71cssp
d-----        05/12/2025     08:42                api
d-----        02/12/2025     08:40                assets
d-----        02/12/2025     15:42                config
d-----        02/12/2025     08:40                cron
d-----        04/12/2025     15:59                data
d-----        04/12/2025     17:41                referentiel
-a----        03/12/2025     17:35          26775 index.php
-a----        03/12/2025     11:33             17 info.php
-a----        03/12/2025     11:33           9359 INSTALLATION.md
-a----        02/12/2025     16:38          23869 luci.html
-a----        03/12/2025     11:33            273 test-curl.php
-a----        04/12/2025     14:09            163 _.htaccess
-a----        04/12/2025     14:10             40 _.htpasswd_cssp


**Confirming directory existence**

The target directory exists despite initial cd issues caused by slash usage; next I'll verify file access and explore contents to gather more context.


Output:
Exit code: 0
Wall time: 0.8 seconds
Output:

    R‚pertoireÿ: C:\wamp64\www\out\referentiel

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
d-----        04/12/2025     17:29                config
d-----        04/12/2025     17:41                export
d-----        04/12/2025     17:22                exports
d-----        04/12/2025     18:23                import
d-----        04/12/2025     17:22                sources
d-----        04/12/2025     17:41                sql
d-----        04/12/2025     11:57                {config,import,export,sql,sources}
-a----        04/12/2025     12:05            164 .gitignore
-a----        04/12/2025     12:44          14845 index.php
-a----        04/12/2025     12:05           5905 README.md
-a----        04/12/2025     12:43          10017 update_referentiel.php


**Checking README for functionality clues**


Output:
Exit code: 0
Wall time: 2.1 seconds
Output:
# RÇ¸fÇ¸rentiel Transport IDF - Accueil V2

## Vue d'ensemble

Ce module gÇùre le rÇ¸fÇ¸rentiel complet des transports en ÇZle-de-France :
- **Lignes** : RER, MÇ¸tro, Transilien, Tramway, Bus, Noctilien
- **ArrÇ¦ts** : Quais, stations, gares avec coordonnÇ¸es
- **Directions** : Sens de circulation par ligne
- **Terminus** : Points d'arrivÇ¸e par direction (multiples pour RER)
- **Association arrÇ¦t-direction** : Quel quai sert quelle direction

## Architecture

```
/referentiel/
ƒ"oƒ"?ƒ"? config/
ƒ"'   ƒ""ƒ"?ƒ"? database.php          # Config MySQL (auto-dÇ¸tection local/prod)
ƒ"oƒ"?ƒ"? import/
ƒ"'   ƒ"oƒ"?ƒ"? download_sources.php  # TÇ¸lÇ¸charge GTFS + rÇ¸fÇ¸rentiels IDFM
ƒ"'   ƒ"oƒ"?ƒ"? import_referentiels.php # Import CSV ƒÅ' BDD
ƒ"'   ƒ""ƒ"?ƒ"? import_gtfs.php       # Parse GTFS ƒÅ' directions/terminus
ƒ"oƒ"?ƒ"? export/
ƒ"'   ƒ""ƒ"?ƒ"? generate_csv.php      # GÇ¸nÇùre JSON/CSV pour frontend
ƒ"oƒ"?ƒ"? sql/
ƒ"'   ƒ""ƒ"?ƒ"? schema.sql            # Structure BDD complÇùte
ƒ"oƒ"?ƒ"? sources/                  # Fichiers tÇ¸lÇ¸chargÇ¸s (ignorÇ¸ par git)
ƒ"'   ƒ"oƒ"?ƒ"? gtfs/                 # GTFS extrait
ƒ"'   ƒ""ƒ"?ƒ"? *.csv                 # RÇ¸fÇ¸rentiels CSV
ƒ"oƒ"?ƒ"? exports/                  # Fichiers exportÇ¸s (optionnel)
ƒ""ƒ"?ƒ"? update_referentiel.php    # Script d'orchestration
```

## Installation

### 1. CrÇ¸er la base de donnÇ¸es

```sql
CREATE DATABASE IF NOT EXISTS astragecssp
CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
```

### 2. Importer le schÇ¸ma

```bash
mysql -u root astragecssp < referentiel/sql/schema.sql
```

Ou via phpMyAdmin : importer `sql/schema.sql`

### 3. Configurer la connexion

Ç%diter `config/database.php` si nÇ¸cessaire :
- **Local (WAMP)** : dÇ¸tection automatique, pas de mot de passe
- **Production (OVH)** : renseigner `DB_PASS`

### 4. PremiÇùre mise Çÿ jour

```bash
php referentiel/update_referentiel.php --force --truncate
```

Ou via le navigateur :
```
https://votre-site.com/referentiel/update_referentiel.php?action=update&force=1&truncate=1
```

## Utilisation

### Mise Çÿ jour complÇùte (recommandÇ¸)

```bash
php update_referentiel.php
```

Options :
- `--force` : re-tÇ¸lÇ¸charge mÇ¦me si fichiers rÇ¸cents (< 7 jours)
- `--truncate` : vide les tables avant import
- `--status` : affiche le statut sans rien faire

### Scripts individuels

```bash
# TÇ¸lÇ¸charger les sources
php import/download_sources.php [--force]

# Importer les rÇ¸fÇ¸rentiels CSV
php import/import_referentiels.php [--truncate]

# Importer le GTFS (directions/terminus)
php import/import_gtfs.php [--truncate]

# GÇ¸nÇ¸rer les exports
php export/generate_csv.php
```

## Structure de la base de donnÇ¸es

### Tables principales

| Table | Description |
|-------|-------------|
| `ref_lignes` | Catalogue des lignes (ID IDFM, code, mode, couleur...) |
| `ref_zones_arret` | Zones commerciales (gares, stations) |
| `ref_arrets` | ArrÇ¦ts physiques (quais, plateformes) |
| `ref_ligne_arret` | Association ligne ƒÅ" arrÇ¦t |
| `ref_directions` | Directions par ligne (0=Aller, 1=Retour) |
| `ref_terminus` | Terminus multiples par direction |
| `ref_arret_direction` | **Table clÇ¸** : quel quai sert quelle direction |
| `ref_import_log` | Journal des imports |

### Vues utiles

| Vue | Description |
|-----|-------------|
| `v_arrets_lignes_directions` | Export complet pour CSV |
| `v_recherche_arrets` | Index de recherche pour admin |

## Fichiers gÇ¸nÇ¸rÇ¸s

AprÇùs export, les fichiers suivants sont crÇ¸Ç¸s/mis Çÿ jour :

### Dans `/data/` (projet principal)

- **`lines_catalog.json`** : Catalogue des lignes groupÇ¸es par mode
- **`search_index.json`** : Index de recherche pour l'admin

### Dans `/referentiel/exports/`

- **`line_stops/[CODE]_[MODE].json`** : ArrÇ¦ts par ligne avec directions
- **`arrets_lignes_directions.csv`** : Export CSV plat

## Sources de donnÇ¸es IDFM

| Source | URL | FrÇ¸quence |
|--------|-----|-----------|
| GTFS Datahub | data.iledefrance-mobilites.fr | Hebdomadaire |
| RÇ¸fÇ¸rentiel lignes | data.iledefrance-mobilites.fr | Mensuelle |
| RÇ¸fÇ¸rentiel arrÇ¦ts | data.iledefrance-mobilites.fr | Mensuelle |
| Zones d'arrÇ¦t | data.iledefrance-mobilites.fr | Mensuelle |
| ArrÇ¦ts-lignes | data.iledefrance-mobilites.fr | Mensuelle |

## Automatisation (CRON)

Pour une mise Çÿ jour mensuelle automatique :

```cron
# Mise Çÿ jour le 1er de chaque mois Çÿ 3h du matin
0 3 1 * * cd /var/www/accueil_v2/referentiel && php update_referentiel.php --force >> /var/log/referentiel_update.log 2>&1
```

## DÇ¸pannage

### Erreur "Fichier GTFS introuvable"

Les sources n'ont pas Ç¸tÇ¸ tÇ¸lÇ¸chargÇ¸es :
```bash
php import/download_sources.php --force
```

### Erreur de connexion BDD

1. VÇ¸rifier les credentials dans `config/database.php`
2. VÇ¸rifier que MySQL est dÇ¸marrÇ¸
3. CrÇ¸er la base si elle n'existe pas

### Import trÇùs lent

Le GTFS contient ~2M de lignes dans `stop_times.txt`.
PrÇ¸voir 10-15 minutes pour l'import complet.

### MÇ¸moire insuffisante

Augmenter la limite dans `php.ini` ou en haut des scripts :
```php
ini_set('memory_limit', '2G');
```

## IntÇ¸gration avec l'admin Accueil V2

L'admin peut utiliser les donnÇ¸es du rÇ¸fÇ¸rentiel pour :

1. **Recherche d'arrÇ¦ts** : via `search_index.json` ou requÇ¦te SQL
2. **SÇ¸lection de lignes** : via `lines_catalog.json`
3. **Choix des directions** : via `ref_arret_direction`

### Exemple de requÇ¦te pour l'admin

```sql
-- ArrÇ¦ts d'une ligne avec leurs directions
SELECT
    a.nom AS arret,
    ad.direction_id,
    ad.libelle_direction,
    ad.stif_stop_ref
FROM ref_arret_direction ad
JOIN ref_arrets a ON a.id = ad.arret_id
JOIN ref_lignes l ON l.id = ad.ligne_id
WHERE l.stif_id = 'STIF:Line::C01742:'
ORDER BY a.nom, ad.direction_id;
```

## Changelog

### v1.0 (DÇ¸cembre 2024)
- CrÇ¸ation du module rÇ¸fÇ¸rentiel
- Import GTFS + rÇ¸fÇ¸rentiels IDFM
- GÇ¸nÇ¸ration des exports JSON/CSV
- Script d'orchestration complet


**Inspecting update script read-only**

I'll check the update script and related files to understand their functioning since running write commands isn't allowed in the read-only environment.


Output:
Exit code: 0
Wall time: 3 seconds
Output:
<?php
/**
 * referentiel/index.php
 * Interface web pour gÇ¸rer le rÇ¸fÇ¸rentiel transport IDF
 * Compatible WAMP Windows
 */

ini_set('display_errors', 1);
error_reporting(E_ALL);

require_once __DIR__ . '/config/database.php';

// =====================
// ACTIONS
// =====================

$action = $_GET['action'] ?? 'dashboard';
$message = '';
$messageType = 'info';

// GÇ¸rer les actions
switch ($action) {
    case 'init_db':
        $result = initializeDatabase();
        if ($result['success']) {
            $message = "Base de donnÇ¸es initialisÇ¸e : " . ($result['message'] ?? 'OK');
            $messageType = 'success';
        } else {
            $message = "Erreur : " . ($result['error'] ?? 'Inconnue');
            $messageType = 'error';
        }
        break;
}

// =====================
// COLLECTE DES STATS
// =====================

$dbStatus = testConnection();
$stats = [];

if ($dbStatus['success'] && $dbStatus['schema_installed']) {
    try {
        $pdo = getDbConnection();
        $stats = [
            'lignes' => $pdo->query("SELECT COUNT(*) FROM ref_lignes")->fetchColumn(),
            'arrets' => $pdo->query("SELECT COUNT(*) FROM ref_arrets")->fetchColumn(),
            'zones' => $pdo->query("SELECT COUNT(*) FROM ref_zones_arret")->fetchColumn(),
            'directions' => $pdo->query("SELECT COUNT(*) FROM ref_directions")->fetchColumn(),
            'arret_directions' => $pdo->query("SELECT COUNT(*) FROM ref_arret_direction")->fetchColumn(),
        ];

        // Derniers imports
        $stats['imports'] = $pdo->query("
            SELECT type_import, date_import, statut
            FROM ref_import_log
            ORDER BY date_import DESC
            LIMIT 5
        ")->fetchAll();

    } catch (Exception $e) {
        $stats['error'] = $e->getMessage();
    }
}

// VÇ¸rifier les fichiers sources
$sourcesDir = __DIR__ . '/sources';
$sources = [];
$sourceFiles = ['referentiel_lignes.csv', 'referentiel_arrets.csv', 'zones_arret.csv', 'arrets_lignes.csv', 'gtfs_idfm.zip'];

foreach ($sourceFiles as $file) {
    $path = $sourcesDir . '/' . $file;
    if (file_exists($path)) {
        $sources[$file] = [
            'exists' => true,
            'size' => round(filesize($path) / 1024 / 1024, 2) . ' Mo',
            'date' => date('d/m/Y H:i', filemtime($path))
        ];
    } else {
        $sources[$file] = ['exists' => false];
    }
}

// GTFS extrait
$gtfsDir = $sourcesDir . '/gtfs';
$gtfsExtracted = is_dir($gtfsDir) && count(glob($gtfsDir . '/*.txt')) > 0;

?>
<!DOCTYPE html>

    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>RÇ¸fÇ¸rentiel Transport IDF - Accueil V2</title>

ÐYsÎ RÇ¸fÇ¸rentiel Transport IDF

        <?php if ($message): ?>
        <div class="alert <?= $messageType ?>">
            <?= htmlspecialchars($message) ?>

        <?php endif; ?>

ÐY"S Ç%tat de la base de donnÇ¸es

                Serveur : <?= DB_HOST ?> |
                Base : <?= DB_NAME ?> |
                Connexion :
                <span class="status <?= $dbStatus['success'] ? 'ok' : 'error' ?>">
                    <?= $dbStatus['success'] ? 'OK' : 'Erreur' ?>

                <?php if ($dbStatus['success']): ?>
                | SchÇ¸ma :
                <span class="status <?= $dbStatus['schema_installed'] ? 'ok' : 'warning' ?>">
                    <?= $dbStatus['schema_installed'] ? 'InstallÇ¸' : 'Non installÇ¸' ?>

                <?php endif; ?>

            <?php if (!$dbStatus['success']): ?>

                    Erreur : <?= htmlspecialchars($dbStatus['error'] ?? 'Connexion impossible') ?>

            <?php elseif (!$dbStatus['schema_installed']): ?>

                    Initialiser la base de donnÇ¸es

            <?php else: ?>

<?= number_format($stats['lignes'] ?? 0) ?>

Lignes

<?= number_format($stats['arrets'] ?? 0) ?>

ArrÇ¦ts

<?= number_format($stats['zones'] ?? 0) ?>

Zones d'arrÇ¦t

<?= number_format($stats['directions'] ?? 0) ?>

Directions

<?= number_format($stats['arret_directions'] ?? 0) ?>

ArrÇ¦t-Directions

            <?php endif; ?>

ÐY"? Fichiers sources IDFM

                <?php foreach ($sources as $file => $info): ?>
                <div class="file-item <?= $info['exists'] ? '' : 'missing' ?>">
                    <?= $file ?>
                    <?php if ($info['exists']): ?>
                        <?= $info['size'] ?> - <?= $info['date'] ?>
                    <?php else: ?>
                        Manquant
                    <?php endif; ?>

                <?php endforeach; ?>

                <div class="file-item <?= $gtfsExtracted ? '' : 'missing' ?>">
                    ÐY"' GTFS extrait
                    <span class="status <?= $gtfsExtracted ? 'ok' : 'warning' ?>">
                        <?= $gtfsExtracted ? 'OK' : 'Non extrait' ?>

ÐY"" Mise Çÿ jour du rÇ¸fÇ¸rentiel

                ExÇ¸cutez ces Ç¸tapes dans l'ordre. Chaque Ç¸tape s'ouvre dans un nouvel onglet.

1

TÇ¸lÇ¸charger les sources IDFM

GTFS (~50 Mo) + rÇ¸fÇ¸rentiels CSV. DurÇ¸e : 1-2 minutes.

                        TÇ¸lÇ¸charger

                        Forcer re-tÇ¸lÇ¸chargement

2

Importer les rÇ¸fÇ¸rentiels CSV

Lignes, arrÇ¦ts, zones d'arrÇ¦t. DurÇ¸e : 1-3 minutes.

                        Importer

                        RÇ¸initialiser + Importer

3

Importer le GTFS (directions & terminus)

Analyse des trips et stop_times. DurÇ¸e : 5-15 minutes.

                        Importer

                        RÇ¸initialiser + Importer

4

GÇ¸nÇ¸rer les exports

CrÇ¸er lines_catalog.json et les fichiers CSV. DurÇ¸e : ~30 sec.

                        GÇ¸nÇ¸rer

                        Voir les stats

        <?php if (!empty($stats['imports'])): ?>

ÐY"o Derniers imports

                        Type
                        Date
                        Statut

                    <?php foreach ($stats['imports'] as $import): ?>

                        <?= htmlspecialchars($import['type_import']) ?>
                        <?= htmlspecialchars($import['date_import']) ?>

                            <span class="status <?= $import['statut'] === 'succes' ? 'ok' : 'error' ?>">
                                <?= htmlspecialchars($import['statut']) ?>

                    <?php endforeach; ?>

        <?php endif; ?>

ÐY"s Documentation

                Voir le README
                Voir le schÇ¸ma SQL


**Verifying database configuration**

I'm checking the database configuration to ensure all necessary settings for functioning are present and none are missing.


Output:
Exit code: 0
Wall time: 0.9 seconds
Output:
<?php
/**
 * referentiel/config/database.php
 * Configuration de la base de donnÇ¸es MySQL
 *
 * DÇ¸tecte automatiquement l'environnement (local WAMP / prod OVH)
 */

// DÇ¸tection de l'environnement
$isLocal = in_array($_SERVER['SERVER_NAME'] ?? 'localhost', ['localhost', '127.0.0.1'])
           || php_sapi_name() === 'cli' && !file_exists('/var/www');

if ($isLocal) {
    // =====================
    // CONFIGURATION LOCALE (WAMP)
    // =====================
    define('DB_HOST', 'localhost');
    define('DB_NAME', 'astragecssp');
    define('DB_USER', 'root');
    define('DB_PASS', '');
    define('DB_CHARSET', 'utf8mb4');
} else {
    // =====================
    // CONFIGURATION PRODUCTION (OVH)
    // =====================
    define('DB_HOST', 'astragecssp.mysql.db');
    define('DB_NAME', 'astragecssp');
    define('DB_USER', 'astragecssp');
    define('DB_PASS', '10Melodie2606'); // Ç? RENSEIGNER !
    define('DB_CHARSET', 'utf8mb4');
}

// =====================
// OPTIONS PDO
// =====================
define('DB_OPTIONS', [
    PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
    PDO::ATTR_EMULATE_PREPARES   => false,
    PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci"
]);

/**
 * Obtenir une connexion PDO Çÿ la base de donnÇ¸es
 *
 * @return PDO
 * @throws PDOException
 */
function getDbConnection(): PDO {
    static $pdo = null;

    if ($pdo === null) {
        $dsn = sprintf(
            'mysql:host=%s;dbname=%s;charset=%s',
            DB_HOST,
            DB_NAME,
            DB_CHARSET
        );

        $pdo = new PDO($dsn, DB_USER, DB_PASS, DB_OPTIONS);
    }

    return $pdo;
}

/**
 * ExÇ¸cuter le schÇ¸ma SQL pour crÇ¸er les tables
 *
 * @return array RÇ¸sultat de l'opÇ¸ration
 */
function initializeDatabase(): array {
    $schemaPath = __DIR__ . '/../sql/schema.sql';

    if (!file_exists($schemaPath)) {
        return [
            'success' => false,
            'error' => 'Fichier schema.sql introuvable'
        ];
    }

    try {
        $pdo = getDbConnection();
        $sql = file_get_contents($schemaPath);

        // ExÇ¸cuter les requÇ¦tes une par une
        $statements = array_filter(
            array_map('trim', explode(';', $sql)),
            fn($s) => !empty($s) && !preg_match('/^(--|SET|#)/', $s)
        );

        $executed = 0;
        foreach ($statements as $statement) {
            if (!empty(trim($statement))) {
                $pdo->exec($statement);
                $executed++;
            }
        }

        return [
            'success' => true,
            'message' => "Base initialisÇ¸e : $executed requÇ¦tes exÇ¸cutÇ¸es",
            'tables_created' => [
                'ref_lignes',
                'ref_zones_arret',
                'ref_arrets',
                'ref_ligne_arret',
                'ref_directions',
                'ref_terminus',
                'ref_arret_direction',
                'ref_import_log'
            ]
        ];

    } catch (PDOException $e) {
        return [
            'success' => false,
            'error' => $e->getMessage()
        ];
    }
}

/**
 * VÇ¸rifier la connexion Çÿ la base
 *
 * @return array
 */
function testConnection(): array {
    try {
        $pdo = getDbConnection();
        $version = $pdo->query('SELECT VERSION()')->fetchColumn();

        // VÇ¸rifier si les tables existent
        $tables = $pdo->query("SHOW TABLES LIKE 'ref_%'")->fetchAll(PDO::FETCH_COLUMN);

        return [
            'success' => true,
            'mysql_version' => $version,
            'database' => DB_NAME,
            'host' => DB_HOST,
            'tables' => $tables,
            'schema_installed' => count($tables) >= 7
        ];

    } catch (PDOException $e) {
        return [
            'success' => false,
            'error' => $e->getMessage(),
            'host' => DB_HOST,
            'database' => DB_NAME
        ];
    }
}


**Preparing to review referentiel scripts**

Planning to analyze the system under /referentiel, including import and update scripts like update_referentiel.php, to check functionality and spot any issues for reporting without modifying code.


Output:
Exit code: 0
Wall time: 0.9 seconds
Output:
<?php
/**
 * referentiel/update_referentiel.php
 * Script d'orchestration pour la mise Çÿ jour complÇùte du rÇ¸fÇ¸rentiel
 *
 * ExÇ¸cute dans l'ordre :
 * 1. TÇ¸lÇ¸chargement des sources IDFM (GTFS + CSV)
 * 2. Import des rÇ¸fÇ¸rentiels CSV (lignes, arrÇ¦ts, zones)
 * 3. Import GTFS (directions, terminus)
 * 4. Export des CSV pour le frontend
 *
 * Usage CLI : php update_referentiel.php [--force] [--truncate]
 * Usage Web : update_referentiel.php?action=update[&force=1][&truncate=1]
 *
 * Options :
 * --force    : Re-tÇ¸lÇ¸charge mÇ¦me si fichiers rÇ¸cents
 * --truncate : Vide les tables avant import
 */

ini_set('max_execution_time', 3600); // 1 heure max
ini_set('memory_limit', '2G');

// =====================
// CONFIGURATION
// =====================

define('REFERENTIEL_DIR', __DIR__);

// =====================
// FONCTIONS
// =====================

function logStep(string $step, string $status = 'START'): void {
    $timestamp = date('Y-m-d H:i:s');
    $icon = match($status) {
        'START' => 'ÐY""',
        'OK' => 'ƒo.',
        'ERROR' => 'ƒ?O',
        'SKIP' => 'ƒ?ð‹÷?',
        default => 'ÐY"O'
    };

    $line = "[$timestamp] $icon $step";

    if (php_sapi_name() === 'cli') {
        echo $line . PHP_EOL;
    } else {
        echo "
$line
";
        // Flush seulement si un buffer existe
        if (ob_get_level() > 0) {
            ob_flush();
        }
        flush();
    }
}

/**
 * ExÇ¸cuter un script PHP via include (compatible Windows/WAMP)
 */
function runScript(string $scriptPath, array $args = []): array {
    if (!file_exists($scriptPath)) {
        return ['success' => false, 'error' => "Script introuvable : $scriptPath"];
    }

    // Simuler les arguments CLI
    $force = in_array('--force', $args);
    $truncate = in_array('--truncate', $args);

    // Capturer la sortie
    ob_start();

    try {
        // DÇ¸terminer quel script exÇ¸cuter et appeler la bonne fonction
        $scriptName = basename($scriptPath, '.php');

        // Inclure le script (dÇ¸finit les fonctions)
        require_once $scriptPath;

        $result = null;

        switch ($scriptName) {
            case 'download_sources':
                $result = downloadAllSources($force);
                break;

            case 'import_referentiels':
                $result = runImport($truncate);
                break;

            case 'import_gtfs':
                require_once REFERENTIEL_DIR . '/config/database.php';
                $pdo = getDbConnection();
                $result = importGTFS($pdo, $truncate);
                break;

            case 'generate_csv':
                $result = runExport();
                break;

            default:
                $result = ['success' => false, 'error' => "Script inconnu : $scriptName"];
        }

        $output = ob_get_clean();

        return [
            'success' => $result['success'] ?? false,
            'result' => $result,
            'output' => $output
        ];

    } catch (Exception $e) {
        $output = ob_get_clean();
        return [
            'success' => false,
            'error' => $e->getMessage(),
            'output' => $output
        ];
    }
}

/**
 * ExÇ¸cution complÇùte de la mise Çÿ jour
 */
function runFullUpdate(bool $force = false, bool $truncate = false): array {
    $startTime = microtime(true);
    $results = [];

    // PrÇ¸parer les arguments
    $downloadArgs = $force ? ['--force'] : [];
    $importArgs = $truncate ? ['--truncate'] : [];

    // =====================
    // Ç%TAPE 1 : TÇ¸lÇ¸chargement
    // =====================
    logStep('TÇ¸lÇ¸chargement des sources IDFM');

    $result = runScript(
        REFERENTIEL_DIR . '/import/download_sources.php',
        $downloadArgs
    );

    $results['download'] = $result;

    if (!$result['success']) {
        logStep('TÇ¸lÇ¸chargement Ç¸chouÇ¸', 'ERROR');
        // On continue quand mÇ¦me si les fichiers existent dÇ¸jÇÿ
    } else {
        logStep('TÇ¸lÇ¸chargement terminÇ¸', 'OK');
    }

    // =====================
    // Ç%TAPE 2 : Import rÇ¸fÇ¸rentiels CSV
    // =====================
    logStep('Import des rÇ¸fÇ¸rentiels CSV (lignes, arrÇ¦ts, zones)');

    $result = runScript(
        REFERENTIEL_DIR . '/import/import_referentiels.php',
        $importArgs
    );

    $results['import_referentiels'] = $result;

    if (!$result['success']) {
        logStep('Import rÇ¸fÇ¸rentiels Ç¸chouÇ¸', 'ERROR');
        return [
            'success' => false,
            'error' => 'Import des rÇ¸fÇ¸rentiels CSV Ç¸chouÇ¸',
            'results' => $results
        ];
    }

    logStep('Import rÇ¸fÇ¸rentiels terminÇ¸', 'OK');

    // =====================
    // Ç%TAPE 3 : Import GTFS
    // =====================
    logStep('Import GTFS (directions, terminus) - peut prendre plusieurs minutes');

    $result = runScript(
        REFERENTIEL_DIR . '/import/import_gtfs.php',
        $importArgs
    );

    $results['import_gtfs'] = $result;

    if (!$result['success']) {
        logStep('Import GTFS Ç¸chouÇ¸', 'ERROR');
        return [
            'success' => false,
            'error' => 'Import GTFS Ç¸chouÇ¸',
            'results' => $results
        ];
    }

    logStep('Import GTFS terminÇ¸', 'OK');

    // =====================
    // Ç%TAPE 4 : Export CSV
    // =====================
    logStep('GÇ¸nÇ¸ration des fichiers CSV/JSON pour le frontend');

    $result = runScript(
        REFERENTIEL_DIR . '/export/generate_csv.php',
        []
    );

    $results['export'] = $result;

    if (!$result['success']) {
        logStep('Export Ç¸chouÇ¸', 'ERROR');
    } else {
        logStep('Export terminÇ¸', 'OK');
    }

    // =====================
    // RÇ%SUMÇ%
    // =====================
    $duration = round(microtime(true) - $startTime, 2);

    logStep("Mise Çÿ jour complÇùte terminÇ¸e en {$duration}s", 'OK');

    return [
        'success' => true,
        'duration' => $duration,
        'results' => $results
    ];
}

/**
 * Afficher le statut actuel
 */
function getStatus(): array {
    require_once REFERENTIEL_DIR . '/config/database.php';

    $status = [
        'database' => [],
        'sources' => [],
        'exports' => []
    ];

    // Test connexion BDD
    try {
        $connTest = testConnection();
        $status['database'] = $connTest;
    } catch (Exception $e) {
        $status['database'] = ['success' => false, 'error' => $e->getMessage()];
    }

    // VÇ¸rifier les sources
    $sourcesDir = REFERENTIEL_DIR . '/sources';
    if (is_dir($sourcesDir)) {
        $files = glob($sourcesDir . '/*.{csv,zip}', GLOB_BRACE);
        foreach ($files as $file) {
            $status['sources'][basename($file)] = [
                'size' => filesize($file),
                'modified' => date('Y-m-d H:i:s', filemtime($file))
            ];
        }

        // GTFS extrait
        $gtfsDir = $sourcesDir . '/gtfs';
        if (is_dir($gtfsDir)) {
            $gtfsFiles = glob($gtfsDir . '/*.txt');
            $status['sources']['gtfs_files'] = count($gtfsFiles);
        }
    }

    // VÇ¸rifier les exports
    $dataDir = realpath(REFERENTIEL_DIR . '/../data');
    if ($dataDir && file_exists($dataDir . '/lines_catalog.json')) {
        $status['exports']['lines_catalog'] = [
            'modified' => date('Y-m-d H:i:s', filemtime($dataDir . '/lines_catalog.json'))
        ];
    }

    return $status;
}

// =====================
// EXECUTION
// =====================

// Mode CLI
if (php_sapi_name() === 'cli') {
    $force = in_array('--force', $argv);
    $truncate = in_array('--truncate', $argv);
    $statusOnly = in_array('--status', $argv);

    echo str_repeat('=', 60) . PHP_EOL;
    echo "   MISE Ç? JOUR DU RÇ%FÇ%RENTIEL TRANSPORT IDF" . PHP_EOL;
    echo str_repeat('=', 60) . PHP_EOL;

    if ($statusOnly) {
        echo "Statut actuel :" . PHP_EOL;
        print_r(getStatus());
        exit;
    }

    echo "Options : force=" . ($force ? 'oui' : 'non') . ", truncate=" . ($truncate ? 'oui' : 'non') . PHP_EOL;
    echo str_repeat('-', 60) . PHP_EOL;

    $results = runFullUpdate($force, $truncate);

    echo PHP_EOL . str_repeat('=', 60) . PHP_EOL;
    echo "RÇ%SULTAT FINAL : " . ($results['success'] ? 'ƒo. SUCCÇ^S' : 'ƒ?O Ç%CHEC') . PHP_EOL;
    echo "DurÇ¸e totale : {$results['duration']}s" . PHP_EOL;
    echo str_repeat('=', 60) . PHP_EOL;

    exit($results['success'] ? 0 : 1);
}

// Mode Web
header('Content-Type: application/json; charset=utf-8');

$action = $_GET['action'] ?? 'status';
$force = isset($_GET['force']);
$truncate = isset($_GET['truncate']);

switch ($action) {
    case 'update':
        // DÇ¸sactiver le buffering pour voir la progression
        if (ob_get_level()) ob_end_clean();
        header('Content-Type: text/html; charset=utf-8');
        echo "<title>Mise Çÿ jour rÇ¸fÇ¸rentiel</title>";
        echo "
Mise Çÿ jour du rÇ¸fÇ¸rentiel en cours...
";

        $results = runFullUpdate($force, $truncate);

        echo "
RÇ¸sultat
";
        echo "
" . json_encode($results, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . "
";
        echo "";
        break;

    case 'status':
    default:
        $status = getStatus();
        echo json_encode([
            'success' => true,
            'status' => $status,
            'actions' => [
                'update' => 'update_referentiel.php?action=update',
                'update_force' => 'update_referentiel.php?action=update&force=1',
                'update_truncate' => 'update_referentiel.php?action=update&truncate=1'
            ]
        ], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
        break;
}


**Reviewing import and download scripts**


Output:
Exit code: 0
Wall time: 0.9 seconds
Output:
<?php
/**
 * referentiel/import/download_sources.php
 * TÇ¸lÇ¸charge les fichiers sources depuis IDFM Open Data
 *
 * Sources :
 * - GTFS Datahub (horaires, routes, trips, stops) - ZIP ~50Mo
 * - RÇ¸fÇ¸rentiel des lignes (CSV)
 * - ArrÇ¦ts et lignes associÇ¸es (CSV)
 * - RÇ¸fÇ¸rentiel des arrÇ¦ts (CSV)
 *
 * Usage CLI : php download_sources.php [--force]
 * Usage Web : download_sources.php?action=download
 */

ini_set('max_execution_time', 600); // 10 minutes
ini_set('memory_limit', '512M');

// =====================
// CONFIGURATION DES SOURCES
// =====================
const SOURCES = [
    'gtfs' => [
        'name' => 'GTFS Datahub IDF',
        // URL officielle transport.data.gouv.fr
        'url_zip' => 'https://transport.data.gouv.fr/resources/80921/download',
        'filename' => 'gtfs_idfm.zip',
        'type' => 'zip'
    ],
    'lignes' => [
        'name' => 'RÇ¸fÇ¸rentiel des lignes',
        'url' => 'https://data.iledefrance-mobilites.fr/api/explore/v2.1/catalog/datasets/referentiel-des-lignes/exports/csv?delimiter=%3B&list_separator=%2C&quote_all=false&with_bom=true',
        'filename' => 'referentiel_lignes.csv',
        'type' => 'csv'
    ],
    'arrets_lignes' => [
        'name' => 'ArrÇ¦ts et lignes associÇ¸es',
        'url' => 'https://data.iledefrance-mobilites.fr/api/explore/v2.1/catalog/datasets/arrets-lignes/exports/csv?delimiter=%3B&list_separator=%2C&quote_all=false&with_bom=true',
        'filename' => 'arrets_lignes.csv',
        'type' => 'csv'
    ],
    'arrets' => [
        'name' => 'RÇ¸fÇ¸rentiel des arrÇ¦ts',
        'url' => 'https://data.iledefrance-mobilites.fr/api/explore/v2.1/catalog/datasets/arrets/exports/csv?delimiter=%3B&list_separator=%2C&quote_all=false&with_bom=true',
        'filename' => 'referentiel_arrets.csv',
        'type' => 'csv'
    ],
    'zones_arret' => [
        'name' => 'Zones d\'arrÇ¦t',
        'url' => 'https://data.iledefrance-mobilites.fr/api/explore/v2.1/catalog/datasets/zones-d-arrets/exports/csv?delimiter=%3B&list_separator=%2C&quote_all=false&with_bom=true',
        'filename' => 'zones_arret.csv',
        'type' => 'csv'
    ]
];

// Dossier de destination
define('SOURCES_DIR', __DIR__ . '/../sources');

// =====================
// FONCTIONS
// =====================

/**
 * Logger les messages
 */
function logMessage(string $message, string $level = 'INFO'): void {
    $timestamp = date('Y-m-d H:i:s');
    $line = "[$timestamp] [$level] $message";

    if (php_sapi_name() === 'cli') {
        echo $line . PHP_EOL;
    } else {
        echo "<pre>$line</pre>";
        flush();
    }
}

/**
 * TÇ¸lÇ¸charger un fichier avec curl
 */
function downloadFile(string $url, string $destination, int $timeout = 300): array {
    logMessage("TÇ¸lÇ¸chargement : $url");

    $ch = curl_init();

    // DÇ¸tection environnement local (WAMP)
    $isLocal = in_array($_SERVER['SERVER_NAME'] ?? 'localhost', ['localhost', '127.0.0.1'])
               || php_sapi_name() === 'cli';

    // Options curl
    curl_setopt_array($ch, [
        CURLOPT_URL => $url,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_FOLLOWLOCATION => true,
        CURLOPT_MAXREDIRS => 5,
        CURLOPT_TIMEOUT => $timeout,
        CURLOPT_CONNECTTIMEOUT => 30,
        // DÇ¸sactiver vÇ¸rification SSL en local (WAMP n'a pas les certificats)
        CURLOPT_SSL_VERIFYPEER => !$isLocal,
        CURLOPT_SSL_VERIFYHOST => $isLocal ? 0 : 2,
        // DÇ¸compression automatique gzip/deflate
        CURLOPT_ENCODING => '',
        CURLOPT_USERAGENT => 'AccueilV2-Referentiel/1.0'
    ]);

    // Ouvrir le fichier en Ç¸criture
    $fp = fopen($destination, 'w');
    if (!$fp) {
        return ['success' => false, 'error' => "Impossible de crÇ¸er $destination"];
    }

    curl_setopt($ch, CURLOPT_FILE, $fp);

    // ExÇ¸cuter
    $success = curl_exec($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    $error = curl_error($ch);
    $size = curl_getinfo($ch, CURLINFO_SIZE_DOWNLOAD);

    curl_close($ch);
    fclose($fp);

    if (!$success || $httpCode !== 200) {
        @unlink($destination);
        return [
            'success' => false,
            'error' => $error ?: "HTTP $httpCode",
            'http_code' => $httpCode
        ];
    }

    return [
        'success' => true,
        'size' => $size,
        'size_human' => formatBytes($size),
        'path' => $destination
    ];
}

/**
 * Formater la taille en bytes
 */
function formatBytes(int $bytes): string {
    $units = ['o', 'Ko', 'Mo', 'Go'];
    $i = 0;
    while ($bytes >= 1024 && $i < count($units) - 1) {
        $bytes /= 1024;
        $i++;
    }
    return round($bytes, 2) . ' ' . $units[$i];
}

/**
 * Extraire un fichier ZIP
 */
function extractZip(string $zipPath, string $destDir): array {
    $zip = new ZipArchive();

    if ($zip->open($zipPath) !== true) {
        return ['success' => false, 'error' => 'Impossible d\'ouvrir le ZIP'];
    }

    $files = [];
    for ($i = 0; $i < $zip->numFiles; $i++) {
        $files[] = $zip->getNameIndex($i);
    }

    if (!$zip->extractTo($destDir)) {
        $zip->close();
        return ['success' => false, 'error' => 'Erreur d\'extraction'];
    }

    $zip->close();

    return [
        'success' => true,
        'files' => $files,
        'count' => count($files)
    ];
}

/**
 * VÇ¸rifier l'Ç½ge d'un fichier
 */
function getFileAge(string $path): ?int {
    if (!file_exists($path)) {
        return null;
    }
    return time() - filemtime($path);
}

/**
 * TÇ¸lÇ¸charger toutes les sources
 */
function downloadAllSources(bool $force = false, int $maxAgeDays = 7): array {
    $results = [];
    $maxAgeSeconds = $maxAgeDays * 86400;

    // CrÇ¸er le dossier sources si nÇ¸cessaire
    if (!is_dir(SOURCES_DIR)) {
        mkdir(SOURCES_DIR, 0755, true);
    }

    foreach (SOURCES as $key => $source) {
        $destPath = SOURCES_DIR . '/' . $source['filename'];
        $age = getFileAge($destPath);

        // VÇ¸rifier si le fichier existe et n'est pas trop vieux
        if (!$force && $age !== null && $age < $maxAgeSeconds) {
            $ageDays = round($age / 86400, 1);
            logMessage("{$source['name']} : fichier existant ({$ageDays}j), skip", 'SKIP');
            $results[$key] = [
                'success' => true,
                'skipped' => true,
                'age_days' => $ageDays,
                'path' => $destPath
            ];
            continue;
        }

        logMessage("{$source['name']} : tÇ¸lÇ¸chargement...");

        // TÇ¸lÇ¸charger
        $url = ($key === 'gtfs') ? $source['url_zip'] : $source['url'];
        $result = downloadFile($url, $destPath);

        if ($result['success']) {
            logMessage("{$source['name']} : OK ({$result['size_human']})", 'OK');

            // Si c'est un ZIP, extraire
            if ($source['type'] === 'zip') {
                $extractDir = SOURCES_DIR . '/gtfs';
                if (!is_dir($extractDir)) {
                    mkdir($extractDir, 0755, true);
                }

                logMessage("Extraction du GTFS...");
                $extractResult = extractZip($destPath, $extractDir);

                if ($extractResult['success']) {
                    logMessage("GTFS extrait : {$extractResult['count']} fichiers", 'OK');
                    $result['extracted'] = $extractResult;
                } else {
                    logMessage("Erreur extraction : {$extractResult['error']}", 'ERROR');
                    $result['extract_error'] = $extractResult['error'];
                }
            }
        } else {
            logMessage("{$source['name']} : ERREUR - {$result['error']}", 'ERROR');
        }

        $results[$key] = $result;
    }

    return $results;
}

/**
 * Lister les fichiers tÇ¸lÇ¸chargÇ¸s
 */
function listDownloadedFiles(): array {
    $files = [];

    if (!is_dir(SOURCES_DIR)) {
        return $files;
    }

    foreach (SOURCES as $key => $source) {
        $path = SOURCES_DIR . '/' . $source['filename'];

        if (file_exists($path)) {
            $files[$key] = [
                'name' => $source['name'],
                'path' => $path,
                'size' => filesize($path),
                'size_human' => formatBytes(filesize($path)),
                'modified' => date('Y-m-d H:i:s', filemtime($path)),
                'age_days' => round(getFileAge($path) / 86400, 1)
            ];
        }
    }

    // VÇ¸rifier les fichiers GTFS extraits
    $gtfsDir = SOURCES_DIR . '/gtfs';
    if (is_dir($gtfsDir)) {
        $gtfsFiles = glob($gtfsDir . '/*.txt');
        $files['gtfs_extracted'] = [
            'name' => 'GTFS (fichiers extraits)',
            'path' => $gtfsDir,
            'files' => array_map('basename', $gtfsFiles),
            'count' => count($gtfsFiles)
        ];
    }

    return $files;
}

// =====================
// EXECUTION
// =====================

// Mode CLI
if (php_sapi_name() === 'cli') {
    $force = in_array('--force', $argv);

    echo "=== TÇ%LÇ%CHARGEMENT DES SOURCES IDFM ===" . PHP_EOL;
    echo "Dossier destination : " . SOURCES_DIR . PHP_EOL;
    echo "Mode force : " . ($force ? 'OUI' : 'NON') . PHP_EOL;
    echo str_repeat('=', 40) . PHP_EOL;

    $results = downloadAllSources($force);

    echo PHP_EOL . "=== RÇ%SUMÇ% ===" . PHP_EOL;
    foreach ($results as $key => $result) {
        $status = $result['success'] ? 'ƒo"' : 'ƒo-';
        $info = $result['skipped'] ?? false ? '(existant)' : ($result['size_human'] ?? '');
        echo "$status $key : $info" . PHP_EOL;
    }

    exit;
}

// Mode Web (API)
header('Content-Type: application/json; charset=utf-8');

$action = $_GET['action'] ?? 'status';

switch ($action) {
    case 'download':
        $force = isset($_GET['force']);
        $results = downloadAllSources($force);
        echo json_encode([
            'success' => true,
            'action' => 'download',
            'results' => $results
        ], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
        break;

    case 'status':
    default:
        $files = listDownloadedFiles();
        echo json_encode([
            'success' => true,
            'action' => 'status',
            'sources_dir' => SOURCES_DIR,
            'files' => $files
        ], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
        break;
}


**Checking import modules**


Output:
Exit code: 0
Wall time: 0.9 seconds
Output:
<?php
/**
 * referentiel/import/import_referentiels.php
 * Importe les rÇ¸fÇ¸rentiels CSV IDFM dans la base de donnÇ¸es
 *
 * Fichiers importÇ¸s :
 * - referentiel_lignes.csv ƒÅ' ref_lignes
 * - referentiel_arrets.csv ƒÅ' ref_arrets
 * - zones_arret.csv ƒÅ' ref_zones_arret
 * - arrets_lignes.csv ƒÅ' ref_ligne_arret
 *
 * Usage CLI : php import_referentiels.php [--truncate]
 * Usage Web : import_referentiels.php?action=import
 */

ini_set('max_execution_time', 600);
ini_set('memory_limit', '1G');

require_once __DIR__ . '/../config/database.php';

define('SOURCES_DIR', __DIR__ . '/../sources');

// =====================
// FONCTIONS UTILITAIRES
// =====================

function logMessage(string $message, string $level = 'INFO'): void {
    $timestamp = date('Y-m-d H:i:s');
    $line = "[$timestamp] [$level] $message";

    if (php_sapi_name() === 'cli') {
        echo $line . PHP_EOL;
    }
}

/**
 * Convertir un mode IDFM en notre enum
 */
function normalizeMode(string $mode, string $code = ''): string {
    $mode = strtolower(trim($mode));
    $codeUpper = strtoupper(trim($code));

    // Mapping des modes IDFM
    $mapping = [
        'metro' => 'metro',
        'tramway' => 'tram',
        'bus' => 'bus',
        'funicular' => 'funicular',
    ];

    // Si c'est un mode connu directement
    if (isset($mapping[$mode])) {
        return $mapping[$mode];
    }

    // Pour rail/localtrain, on doit distinguer RER vs Transilien via le code
    if (in_array($mode, ['rail', 'localtrain', 'regionalrail', 'rapidtransit'])) {
        // RER : codes A, B, C, D, E
        if (in_array($codeUpper, ['A', 'B', 'C', 'D', 'E'])) {
            return 'rer';
        }
        return 'transilien';
    }

    // Tramways dÇ¸tectÇ¸s par le code Txx
    if (preg_match('/^T\d/', $codeUpper)) {
        return 'tram';
    }

    // CÇ½ble C1 (tÇ¸lÇ¸phÇ¸rique CrÇ¸teil) ƒÅ' funicular
    if ($codeUpper === 'C1') {
        return 'funicular';
    }

    return 'other';
}

/**
 * Parser un fichier CSV avec BOM et sÇ¸parateur ;
 */
function parseCSV(string $path, callable $callback, int $batchSize = 1000): array {
    if (!file_exists($path)) {
        return ['success' => false, 'error' => "Fichier introuvable : $path"];
    }

    $handle = fopen($path, 'r');
    if (!$handle) {
        return ['success' => false, 'error' => "Impossible d'ouvrir : $path"];
    }

    // Lire l'en-tÇ¦te
    $header = fgetcsv($handle, 0, ';');

    // Supprimer le BOM si prÇ¸sent
    if ($header && isset($header[0])) {
        $header[0] = preg_replace('/^\xEF\xBB\xBF/', '', $header[0]);
    }

    $lineNumber = 1;
    $processed = 0;
    $errors = 0;
    $batch = [];

    while (($row = fgetcsv($handle, 0, ';')) !== false) {
        $lineNumber++;

        // CrÇ¸er un tableau associatif
        if (count($row) !== count($header)) {
            $errors++;
            continue;
        }

        $data = array_combine($header, $row);
        $batch[] = $data;

        // Traiter par batch
        if (count($batch) >= $batchSize) {
            try {
                $callback($batch);
                $processed += count($batch);
            } catch (Exception $e) {
                logMessage("Erreur batch ligne $lineNumber : " . $e->getMessage(), 'ERROR');
                $errors += count($batch);
            }
            $batch = [];

            if ($processed % 10000 === 0) {
                logMessage("TraitÇ¸ : $processed lignes...");
            }
        }
    }

    // Dernier batch
    if (!empty($batch)) {
        try {
            $callback($batch);
            $processed += count($batch);
        } catch (Exception $e) {
            $errors += count($batch);
        }
    }

    fclose($handle);

    return [
        'success' => true,
        'processed' => $processed,
        'errors' => $errors
    ];
}

// =====================
// IMPORTS SPECIFIQUES
// =====================

/**
 * Importer le rÇ¸fÇ¸rentiel des lignes
 */
function importLignes(PDO $pdo, bool $truncate = false): array {
    $path = SOURCES_DIR . '/referentiel_lignes.csv';
    logMessage("Import des lignes depuis : $path");

    if ($truncate) {
        $pdo->exec('TRUNCATE TABLE ref_lignes');
        logMessage("Table ref_lignes vidÇ¸e");
    }

    $stmt = $pdo->prepare("
        INSERT INTO ref_lignes
        (idfm_id, stif_id, code, nom, mode, operateur, couleur, couleur_texte)
        VALUES
        (:idfm_id, :stif_id, :code, :nom, :mode, :operateur, :couleur, :couleur_texte)
        ON DUPLICATE KEY UPDATE
        nom = VALUES(nom),
        mode = VALUES(mode),
        operateur = VALUES(operateur),
        couleur = VALUES(couleur),
        couleur_texte = VALUES(couleur_texte),
        date_import = NOW()
    ");

    $result = parseCSV($path, function($batch) use ($pdo, $stmt) {
        $pdo->beginTransaction();

        foreach ($batch as $row) {
            // Colonnes rÇ¸elles : id_line, name_line, shortname_line, transportmode, operatorname, colourweb_hexa, textcolourweb_hexa
            $idLine = $row['id_line'] ?? '';
            $name = $row['name_line'] ?? '';
            $code = $row['shortname_line'] ?? '';
            $mode = $row['transportmode'] ?? '';
            $operator = $row['operatorname'] ?? '';
            $color = $row['colourweb_hexa'] ?? '';
            $textColor = $row['textcolourweb_hexa'] ?? 'FFFFFF';

            if (empty($idLine)) continue;

            // Construire l'ID IDFM (format IDFM:Cxxxxx pour matcher arrets_lignes)
            $idfmId = 'IDFM:' . $idLine;

            // Construire l'ID STIF (format PRIM)
            $stifId = "STIF:Line::{$idLine}:";

            // Nettoyer les couleurs
            $color = ltrim($color, '#');
            $textColor = ltrim($textColor, '#');

            $stmt->execute([
                'idfm_id' => $idfmId,
                'stif_id' => $stifId,
                'code' => $code ?: $name,
                'nom' => $name,
                'mode' => normalizeMode($mode, $code),
                'operateur' => $operator,
                'couleur' => $color ?: null,
                'couleur_texte' => $textColor ?: 'FFFFFF'
            ]);
        }

        $pdo->commit();
    });

    $count = $pdo->query("SELECT COUNT(*) FROM ref_lignes")->fetchColumn();
    logMessage("Lignes importÇ¸es : $count");

    return array_merge($result, ['total' => $count]);
}

/**
 * Importer les zones d'arrÇ¦t
 */
function importZonesArret(PDO $pdo, bool $truncate = false): array {
    $path = SOURCES_DIR . '/zones_arret.csv';
    logMessage("Import des zones d'arrÇ¦t depuis : $path");

    if ($truncate) {
        $pdo->exec('SET FOREIGN_KEY_CHECKS = 0');
        $pdo->exec('TRUNCATE TABLE ref_zones_arret');
        $pdo->exec('SET FOREIGN_KEY_CHECKS = 1');
        logMessage("Table ref_zones_arret vidÇ¸e");
    }

    $stmt = $pdo->prepare("
        INSERT INTO ref_zones_arret
        (idfm_id, nom, commune, code_insee, latitude, longitude)
        VALUES
        (:idfm_id, :nom, :commune, :code_insee, :latitude, :longitude)
        ON DUPLICATE KEY UPDATE
        nom = VALUES(nom),
        commune = VALUES(commune),
        latitude = VALUES(latitude),
        longitude = VALUES(longitude),
        date_import = NOW()
    ");

    $result = parseCSV($path, function($batch) use ($pdo, $stmt) {
        $pdo->beginTransaction();

        foreach ($batch as $row) {
            // Colonnes rÇ¸elles : zdaid, zdaname, zdatown, zdapostalregion, zdaxepsg2154, zdayepsg2154
            $zdaId = $row['zdaid'] ?? '';
            $nom = $row['zdaname'] ?? '';

            if (empty($zdaId) || empty($nom)) continue;

            // Ajouter le prÇ¸fixe IDFM: si absent
            $idfmId = $zdaId;
            if (!str_starts_with($zdaId, 'IDFM:')) {
                $idfmId = 'IDFM:' . $zdaId;
            }

            // Pas de coordonnÇ¸es lat/lon directes, on laisse null
            // (les coordonnÇ¸es sont en EPSG:2154, conversion complexe)

            $stmt->execute([
                'idfm_id' => $idfmId,
                'nom' => $nom,
                'commune' => $row['zdatown'] ?? null,
                'code_insee' => $row['zdapostalregion'] ?? null,
                'latitude' => null,
                'longitude' => null
            ]);
        }

        $pdo->commit();
    });

    $count = $pdo->query("SELECT COUNT(*) FROM ref_zones_arret")->fetchColumn();
    logMessage("Zones d'arrÇ¦t importÇ¸es : $count");

    return array_merge($result, ['total' => $count]);
}

/**
 * Importer le rÇ¸fÇ¸rentiel des arrÇ¦ts
 */
function importArrets(PDO $pdo, bool $truncate = false): array {
    $path = SOURCES_DIR . '/referentiel_arrets.csv';
    logMessage("Import des arrÇ¦ts depuis : $path");

    if ($truncate) {
        $pdo->exec('SET FOREIGN_KEY_CHECKS = 0');
        $pdo->exec('TRUNCATE TABLE ref_arrets');
        $pdo->exec('SET FOREIGN_KEY_CHECKS = 1');
        logMessage("Table ref_arrets vidÇ¸e");
    }

    // Charger les zones d'arrÇ¦t pour les FK
    $zones = [];
    $result = $pdo->query("SELECT idfm_id, id FROM ref_zones_arret");
    while ($row = $result->fetch()) {
        $zones[$row['idfm_id']] = $row['id'];
    }
    logMessage("Zones chargÇ¸es : " . count($zones));

    $stmt = $pdo->prepare("
        INSERT INTO ref_arrets
        (idfm_id, stif_id, nom, zone_arret_id, latitude, longitude, commune, code_insee, type_arret)
        VALUES
        (:idfm_id, :stif_id, :nom, :zone_arret_id, :latitude, :longitude, :commune, :code_insee, :type_arret)
        ON DUPLICATE KEY UPDATE
        nom = VALUES(nom),
        zone_arret_id = VALUES(zone_arret_id),
        latitude = VALUES(latitude),
        longitude = VALUES(longitude),
        date_import = NOW()
    ");

    $result = parseCSV($path, function($batch) use ($pdo, $stmt, $zones) {
        $pdo->beginTransaction();

        foreach ($batch as $row) {
            // Colonnes rÇ¸elles : arrid, arrname, arrtown, zdaid, arrgeopoint, arrtype, arrpostalregion
            $arrId = $row['arrid'] ?? '';
            $nom = $row['arrname'] ?? '';

            if (empty($arrId) || empty($nom)) continue;

            // Ajouter le prÇ¸fixe IDFM: si absent
            $idfmId = $arrId;
            if (!str_starts_with($arrId, 'IDFM:')) {
                $idfmId = 'IDFM:' . $arrId;
            }

            // Construire l'ID STIF (format STIF:StopPoint:Q:xxxxx:)
            $stifId = "STIF:StopPoint:Q:{$arrId}:";

            // Parser les coordonnÇ¸es depuis arrgeopoint (format "lat, lon")
            $lat = null;
            $lon = null;
            if (!empty($row['arrgeopoint'])) {
                $coords = explode(',', $row['arrgeopoint']);
                if (count($coords) === 2) {
                    $lat = (float)trim($coords[0]);
                    $lon = (float)trim($coords[1]);
                }
            }

            // Trouver la zone d'arrÇ¦t parente
            $zoneId = null;
            $parentZda = $row['zdaid'] ?? '';
            if (!empty($parentZda)) {
                // Ajouter le prÇ¸fixe IDFM: si absent
                $parentZdaKey = $parentZda;
                if (!str_starts_with($parentZda, 'IDFM:')) {
                    $parentZdaKey = 'IDFM:' . $parentZda;
                }
                $zoneId = $zones[$parentZdaKey] ?? null;
            }

            // Type d'arrÇ¦t
            $typeArret = 'arret_bus';
            $arrType = strtolower($row['arrtype'] ?? '');
            if ($arrType === 'quai' || stripos($nom, 'quai') !== false) {
                $typeArret = 'quai';
            } elseif (stripos($nom, 'gare') !== false) {
                $typeArret = 'gare';
            } elseif (stripos($nom, 'station') !== false) {
                $typeArret = 'station';
            }

            $stmt->execute([
                'idfm_id' => $idfmId,
                'stif_id' => $stifId,
                'nom' => $nom,
                'zone_arret_id' => $zoneId,
                'latitude' => $lat,
                'longitude' => $lon,
                'commune' => $row['arrtown'] ?? null,
                'code_insee' => $row['arrpostalregion'] ?? null,
                'type_arret' => $typeArret
            ]);
        }

        $pdo->commit();
    });

    $count = $pdo->query("SELECT COUNT(*) FROM ref_arrets")->fetchColumn();
    logMessage("ArrÇ¦ts importÇ¸s : $count");

    return array_merge($result, ['total' => $count]);
}

/**
 * Importer les associations arrÇ¦ts-lignes
 * Cette fonction crÇ¸e aussi les arrÇ¦ts s'ils n'existent pas (depuis arrets_lignes.csv)
 */
function importArretsLignes(PDO $pdo, bool $truncate = false): array {
    $path = SOURCES_DIR . '/arrets_lignes.csv';
    logMessage("Import des associations arrÇ¦ts-lignes depuis : $path");

    if ($truncate) {
        $pdo->exec('TRUNCATE TABLE ref_ligne_arret');
        logMessage("Table ref_ligne_arret vidÇ¸e");
    }

    // Charger les ID des lignes
    $lignes = [];
    $result = $pdo->query("SELECT idfm_id, id FROM ref_lignes");
    while ($row = $result->fetch()) {
        $lignes[$row['idfm_id']] = $row['id'];
    }
    logMessage("Lignes chargÇ¸es : " . count($lignes));

    // Charger les ID des arrÇ¦ts existants
    $arrets = [];
    $result = $pdo->query("SELECT idfm_id, id FROM ref_arrets");
    while ($row = $result->fetch()) {
        $arrets[$row['idfm_id']] = $row['id'];
    }
    logMessage("ArrÇ¦ts existants : " . count($arrets));

    // PrÇ¸parer les statements
    $stmtArret = $pdo->prepare("
        INSERT INTO ref_arrets (idfm_id, stif_id, nom, latitude, longitude, commune, code_insee, type_arret)
        VALUES (:idfm_id, :stif_id, :nom, :latitude, :longitude, :commune, :code_insee, 'arret_bus')
        ON DUPLICATE KEY UPDATE
            nom = VALUES(nom),
            latitude = VALUES(latitude),
            longitude = VALUES(longitude),
            date_import = NOW()
    ");

    $stmtLink = $pdo->prepare("
        INSERT IGNORE INTO ref_ligne_arret (ligne_id, arret_id)
        VALUES (:ligne_id, :arret_id)
    ");

    $inserted = 0;
    $skipped = 0;
    $arretsCreated = 0;

    $result = parseCSV($path, function($batch) use ($pdo, $stmtArret, $stmtLink, $lignes, &$arrets, &$inserted, &$skipped, &$arretsCreated) {
        $pdo->beginTransaction();

        foreach ($batch as $row) {
            // Colonnes : id (ligne), stop_id (arrÇ¦t), stop_name, stop_lat, stop_lon, nom_commune, code_insee
            $ligneIdfm = $row['id'] ?? '';
            $arretIdfm = $row['stop_id'] ?? '';
            $stopName = $row['stop_name'] ?? '';
            $stopLat = $row['stop_lat'] ?? null;
            $stopLon = $row['stop_lon'] ?? null;
            $commune = $row['nom_commune'] ?? null;
            $codeInsee = $row['code_insee'] ?? null;

            // Trouver la ligne
            $ligneId = $lignes[$ligneIdfm] ?? null;
            if (!$ligneId) {
                $skipped++;
                continue;
            }

            // Trouver ou crÇ¸er l'arrÇ¦t
            $arretId = $arrets[$arretIdfm] ?? null;

            if (!$arretId && !empty($arretIdfm) && !empty($stopName)) {
                // CrÇ¸er l'arrÇ¦t
                $stifId = null;
                if (preg_match('/IDFM:(\d+)/', $arretIdfm, $m)) {
                    $stifId = "STIF:StopPoint:Q:{$m[1]}:";
                }

                $stmtArret->execute([
                    'idfm_id' => $arretIdfm,
                    'stif_id' => $stifId,
                    'nom' => $stopName,
                    'latitude' => $stopLat ? (float)$stopLat : null,
                    'longitude' => $stopLon ? (float)$stopLon : null,
                    'commune' => $commune,
                    'code_insee' => $codeInsee
                ]);

                // RÇ¸cupÇ¸rer l'ID insÇ¸rÇ¸ ou existant
                $arretId = $pdo->lastInsertId();
                if (!$arretId) {
                    // Si lastInsertId est 0, c'est un UPDATE, on doit rÇ¸cupÇ¸rer l'ID
                    $stmt = $pdo->prepare("SELECT id FROM ref_arrets WHERE idfm_id = ?");
                    $stmt->execute([$arretIdfm]);
                    $arretId = $stmt->fetchColumn();
                }

                $arrets[$arretIdfm] = $arretId;
                $arretsCreated++;
            }

            if (!$arretId) {
                $skipped++;
                continue;
            }

            // CrÇ¸er le lien
            try {
                $stmtLink->execute([
                    'ligne_id' => $ligneId,
                    'arret_id' => $arretId
                ]);
                if ($stmtLink->rowCount() > 0) {
                    $inserted++;
                }
            } catch (PDOException $e) {
                $skipped++;
            }
        }

        $pdo->commit();
    });

    $countArrets = $pdo->query("SELECT COUNT(*) FROM ref_arrets")->fetchColumn();
    $countLinks = $pdo->query("SELECT COUNT(*) FROM ref_ligne_arret")->fetchColumn();
    logMessage("ArrÇ¦ts crÇ¸Ç¸s depuis arrets_lignes : $arretsCreated (total: $countArrets)");
    logMessage("Associations importÇ¸es : $countLinks (insÇ¸rÇ¸es: $inserted, skipped: $skipped)");

    return array_merge($result, [
        'total' => $countLinks,
        'inserted' => $inserted,
        'skipped' => $skipped,
        'arrets_created' => $arretsCreated,
        'arrets_total' => $countArrets
    ]);
}

// =====================
// EXECUTION PRINCIPALE
// =====================

function runImport(bool $truncate = false): array {
    $startTime = microtime(true);
    $results = [];

    try {
        $pdo = getDbConnection();
        logMessage("Connexion BDD OK : " . DB_NAME . "@" . DB_HOST);

        // DÇ¸sactiver les FK pour le truncate
        if ($truncate) {
            $pdo->exec('SET FOREIGN_KEY_CHECKS = 0');
        }

        // Ordre : lignes d'abord, puis arrets_lignes (qui crÇ¸e les arrÇ¦ts)
        $results['lignes'] = importLignes($pdo, $truncate);
        $results['zones_arret'] = importZonesArret($pdo, $truncate);
        $results['arrets'] = importArrets($pdo, $truncate);
        $results['arrets_lignes'] = importArretsLignes($pdo, $truncate);

        // RÇ¸activer les FK
        if ($truncate) {
            $pdo->exec('SET FOREIGN_KEY_CHECKS = 1');
        }

        // Log dans la table d'import
        $pdo->prepare("
            INSERT INTO ref_import_log
            (type_import, fichier_source, nb_lignes_importees, duree_secondes, statut)
            VALUES ('referentiels', 'CSV IDFM', :nb, :duree, 'succes')
        ")->execute([
            'nb' => array_sum(array_column($results, 'total')),
            'duree' => round(microtime(true) - $startTime, 2)
        ]);

    } catch (Exception $e) {
        logMessage("ERREUR : " . $e->getMessage(), 'ERROR');
        return ['success' => false, 'error' => $e->getMessage()];
    }

    $duration = round(microtime(true) - $startTime, 2);
    logMessage("Import terminÇ¸ en {$duration}s");

    return [
        'success' => true,
        'duration' => $duration,
        'results' => $results
    ];
}

// Mode CLI
if (php_sapi_name() === 'cli') {
    $truncate = in_array('--truncate', $argv);

    echo "=== IMPORT DES RÇ%FÇ%RENTIELS IDFM ===" . PHP_EOL;
    echo "Mode truncate : " . ($truncate ? 'OUI' : 'NON') . PHP_EOL;
    echo str_repeat('=', 40) . PHP_EOL;

    $results = runImport($truncate);

    echo PHP_EOL . "=== RÇ%SUMÇ% ===" . PHP_EOL;
    print_r($results);

    exit;
}

// Mode Web
header('Content-Type: application/json; charset=utf-8');

$action = $_GET['action'] ?? 'status';
$truncate = isset($_GET['truncate']);

switch ($action) {
    case 'import':
        $results = runImport($truncate);
        echo json_encode($results, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
        break;

    default:
        echo json_encode([
            'usage' => 'import_referentiels.php?action=import[&truncate]',
            'sources_dir' => SOURCES_DIR
        ]);
        break;
}


**Evaluating GTFS import issues**

I'm assessing if import_gtfs causes local detection failures, encoding problems, and plain DB passwords; inspecting relevant files may clarify these readability and security concerns.


Output:
Exit code: 0
Wall time: 1 seconds
Output:
<?php
/**
 * referentiel/import/import_gtfs.php
 * Parse le GTFS IDFM pour extraire les directions et terminus
 *
 * Fichiers GTFS utilisÇ¸s :
 * - routes.txt ƒÅ' correspondance avec ref_lignes
 * - trips.txt ƒÅ' direction_id + trip_headsign
 * - stops.txt ƒÅ' correspondance avec ref_arrets
 * - stop_times.txt ƒÅ' quel stop dans quel trip (direction)
 *
 * Remplit :
 * - ref_directions (directions par ligne)
 * - ref_terminus (terminus multiples par direction)
 * - ref_arret_direction (quel quai sert quelle direction)
 *
 * Usage CLI : php import_gtfs.php [--truncate]
 */

ini_set('max_execution_time', 1800); // 30 minutes
ini_set('memory_limit', '2G');

require_once __DIR__ . '/../config/database.php';

define('GTFS_DIR', __DIR__ . '/../sources/gtfs');

// =====================
// FONCTIONS UTILITAIRES
// =====================

function logMessage(string $message, string $level = 'INFO'): void {
    $timestamp = date('Y-m-d H:i:s');
    $line = "[$timestamp] [$level] $message";

    if (php_sapi_name() === 'cli') {
        echo $line . PHP_EOL;
    }
}

/**
 * Parser un fichier GTFS (CSV sans BOM, virgule comme sÇ¸parateur)
 */
function parseGTFS(string $filename, callable $rowCallback): int {
    $path = GTFS_DIR . '/' . $filename;

    if (!file_exists($path)) {
        logMessage("Fichier GTFS introuvable : $path", 'ERROR');
        return 0;
    }

    $handle = fopen($path, 'r');
    if (!$handle) {
        return 0;
    }

    // Header
    $header = fgetcsv($handle, 0, ',');
    if (!$header) {
        fclose($handle);
        return 0;
    }

    // Nettoyer le BOM si prÇ¸sent
    $header[0] = preg_replace('/^\xEF\xBB\xBF/', '', $header[0]);

    $count = 0;
    while (($row = fgetcsv($handle, 0, ',')) !== false) {
        if (count($row) !== count($header)) {
            continue;
        }

        $data = array_combine($header, $row);
        $rowCallback($data);
        $count++;

        if ($count % 100000 === 0) {
            logMessage("TraitÇ¸ : $count lignes...");
        }
    }

    fclose($handle);
    return $count;
}

/**
 * Charger un fichier GTFS en mÇ¸moire (pour les petits fichiers)
 */
function loadGTFS(string $filename): array {
    $data = [];
    parseGTFS($filename, function($row) use (&$data) {
        $data[] = $row;
    });
    return $data;
}

/**
 * Charger un fichier GTFS indexÇ¸ par une clÇ¸
 */
function loadGTFSIndexed(string $filename, string $keyField): array {
    $data = [];
    parseGTFS($filename, function($row) use (&$data, $keyField) {
        $key = $row[$keyField] ?? null;
        if ($key !== null) {
            $data[$key] = $row;
        }
    });
    return $data;
}

// =====================
// IMPORT PRINCIPAL
// =====================

function importGTFS(PDO $pdo, bool $truncate = false): array {
    $startTime = microtime(true);
    $results = [];

    // VÇ¸rifier que le dossier GTFS existe
    if (!is_dir(GTFS_DIR)) {
        return ['success' => false, 'error' => 'Dossier GTFS introuvable. Lancez download_sources.php d\'abord.'];
    }

    // Vider les tables si demandÇ¸
    if ($truncate) {
        logMessage("Vidage des tables directions...");
        $pdo->exec('SET FOREIGN_KEY_CHECKS = 0');
        $pdo->exec('TRUNCATE TABLE ref_arret_direction');
        $pdo->exec('TRUNCATE TABLE ref_terminus');
        $pdo->exec('TRUNCATE TABLE ref_directions');
        $pdo->exec('SET FOREIGN_KEY_CHECKS = 1');
    }

    // =====================
    // 1. Charger les correspondances routes GTFS ƒÅ' lignes BDD
    // =====================
    logMessage("Chargement des routes GTFS...");
    $routes = loadGTFSIndexed('routes.txt', 'route_id');
    logMessage("Routes GTFS chargÇ¸es : " . count($routes));

    // Mapper route_id GTFS ƒÅ' ligne_id BDD
    // Le route_id GTFS ressemble Çÿ "IDFM:C01742" ou "IDFM:100100001:1"
    $lignesDb = [];
    $result = $pdo->query("SELECT id, idfm_id, stif_id, code, nom FROM ref_lignes");
    while ($row = $result->fetch()) {
        $lignesDb[$row['idfm_id']] = $row;
        // Aussi indexer par le code interne (C01742)
        if (preg_match('/IDFM:([A-Z0-9]+)/', $row['idfm_id'], $m)) {
            $lignesDb['IDFM:' . $m[1]] = $row;
        }
    }
    logMessage("Lignes BDD chargÇ¸es : " . count($lignesDb));

    // CrÇ¸er le mapping route_id ƒÅ' ligne_id
    $routeToLigne = [];
    foreach ($routes as $routeId => $route) {
        // Essayer de trouver la ligne correspondante
        // route_id peut Ç¦tre "IDFM:C01742" ou "IDFM:100100001:1"
        $idfmId = $routeId;

        // Extraire le code de ligne si format "IDFM:XXXXX:Y"
        if (preg_match('/^(IDFM:[A-Z0-9]+)/', $routeId, $m)) {
            $idfmId = $m[1];
        }

        if (isset($lignesDb[$idfmId])) {
            $routeToLigne[$routeId] = $lignesDb[$idfmId]['id'];
        }
    }
    logMessage("Correspondances routeƒÅ'ligne : " . count($routeToLigne));

    // =====================
    // 2. Charger les stops GTFS et mapper vers ref_arrets
    // =====================
    logMessage("Chargement des stops GTFS...");
    $stopsGtfs = loadGTFSIndexed('stops.txt', 'stop_id');
    logMessage("Stops GTFS chargÇ¸s : " . count($stopsGtfs));

    // Mapper stop_id GTFS ƒÅ' arret_id BDD
    $arretsDb = [];
    $result = $pdo->query("SELECT id, idfm_id, stif_id, nom FROM ref_arrets");
    while ($row = $result->fetch()) {
        $arretsDb[$row['idfm_id']] = $row;
    }
    logMessage("ArrÇ¦ts BDD chargÇ¸s : " . count($arretsDb));

    $stopToArret = [];
    foreach ($stopsGtfs as $stopId => $stop) {
        // stop_id GTFS format : "IDFM:471234" ou "StopPoint:IDFM:471234"
        $idfmId = $stopId;
        if (preg_match('/IDFM:\d+/', $stopId, $m)) {
            $idfmId = $m[0];
        }

        if (isset($arretsDb[$idfmId])) {
            $stopToArret[$stopId] = [
                'arret_id' => $arretsDb[$idfmId]['id'],
                'nom' => $arretsDb[$idfmId]['nom'],
                'stif_id' => $arretsDb[$idfmId]['stif_id']
            ];
        }
    }
    logMessage("Correspondances stopƒÅ'arret : " . count($stopToArret));

    // =====================
    // 3. Charger les trips et analyser les directions
    // =====================
    logMessage("Analyse des trips GTFS...");

    // Structure : $tripsByLigne[ligne_id][direction_id] = [ trip_data, ... ]
    $tripsByLigne = [];

    parseGTFS('trips.txt', function($trip) use (&$tripsByLigne, $routeToLigne) {
        $routeId = $trip['route_id'] ?? '';
        $directionId = (int)($trip['direction_id'] ?? 0);
        $tripId = $trip['trip_id'] ?? '';
        $headsign = $trip['trip_headsign'] ?? '';

        $ligneId = $routeToLigne[$routeId] ?? null;
        if (!$ligneId) return;

        if (!isset($tripsByLigne[$ligneId])) {
            $tripsByLigne[$ligneId] = [0 => [], 1 => []];
        }

        $tripsByLigne[$ligneId][$directionId][] = [
            'trip_id' => $tripId,
            'headsign' => $headsign
        ];
    });

    logMessage("Lignes avec trips : " . count($tripsByLigne));

    // =====================
    // 4. Analyser stop_times pour trouver les terminus et directions par quai
    // =====================
    logMessage("Analyse des stop_times (peut prendre plusieurs minutes)...");

    // Structure pour collecter les donnÇ¸es
    // $stopSequences[trip_id] = [ ['stop_id' => X, 'seq' => N], ... ]
    $tripStops = [];
    $tripMaxSeq = [];

    // Premier passage : collecter tous les stops par trip
    $lineCount = parseGTFS('stop_times.txt', function($st) use (&$tripStops, &$tripMaxSeq) {
        $tripId = $st['trip_id'] ?? '';
        $stopId = $st['stop_id'] ?? '';
        $seq = (int)($st['stop_sequence'] ?? 0);

        if (!isset($tripStops[$tripId])) {
            $tripStops[$tripId] = [];
            $tripMaxSeq[$tripId] = 0;
        }

        $tripStops[$tripId][$stopId] = $seq;

        if ($seq > $tripMaxSeq[$tripId]) {
            $tripMaxSeq[$tripId] = $seq;
        }
    });

    logMessage("Stop_times analysÇ¸s : $lineCount");

    // =====================
    // 5. Calculer les directions et terminus par ligne
    // =====================
    logMessage("Calcul des directions et terminus...");

    $directionsData = []; // [ligne_id][direction_id] => ['headsigns' => [], 'terminus' => []]

    foreach ($tripsByLigne as $ligneId => $directions) {
        $directionsData[$ligneId] = [];

        foreach ($directions as $directionId => $trips) {
            $headsignCounts = [];
            $terminusCounts = [];

            foreach ($trips as $tripData) {
                $tripId = $tripData['trip_id'];
                $headsign = $tripData['headsign'];

                // Compter les headsigns
                if (!empty($headsign)) {
                    $headsignCounts[$headsign] = ($headsignCounts[$headsign] ?? 0) + 1;
                }

                // Trouver le terminus (stop avec max sequence)
                if (isset($tripStops[$tripId])) {
                    $maxSeq = $tripMaxSeq[$tripId];
                    foreach ($tripStops[$tripId] as $stopId => $seq) {
                        if ($seq === $maxSeq) {
                            $terminusCounts[$stopId] = ($terminusCounts[$stopId] ?? 0) + 1;
                            break;
                        }
                    }
                }
            }

            $directionsData[$ligneId][$directionId] = [
                'headsigns' => $headsignCounts,
                'terminus' => $terminusCounts,
                'trip_count' => count($trips)
            ];
        }
    }

    // =====================
    // 6. InsÇ¸rer les directions dans la BDD
    // =====================
    logMessage("Insertion des directions...");

    $stmtDir = $pdo->prepare("
        INSERT INTO ref_directions
        (ligne_id, direction_id, direction_ref, libelle, terminus_arret_id, terminus_nom)
        VALUES (:ligne_id, :direction_id, :direction_ref, :libelle, :terminus_arret_id, :terminus_nom)
        ON DUPLICATE KEY UPDATE
        libelle = VALUES(libelle),
        terminus_arret_id = VALUES(terminus_arret_id),
        terminus_nom = VALUES(terminus_nom),
        date_import = NOW()
    ");

    $stmtTerm = $pdo->prepare("
        INSERT INTO ref_terminus
        (ligne_id, direction_id, arret_id, nom, frequence, principal)
        VALUES (:ligne_id, :direction_id, :arret_id, :nom, :frequence, :principal)
        ON DUPLICATE KEY UPDATE
        frequence = VALUES(frequence),
        principal = VALUES(principal),
        date_import = NOW()
    ");

    $dirCount = 0;
    $termCount = 0;

    foreach ($directionsData as $ligneId => $directions) {
        foreach ($directions as $directionId => $data) {
            // Headsign le plus frÇ¸quent = libellÇ¸ de la direction
            arsort($data['headsigns']);
            $mainHeadsign = array_key_first($data['headsigns']) ?? "Direction $directionId";

            // Terminus le plus frÇ¸quent = terminus principal
            arsort($data['terminus']);
            $mainTerminusStopId = array_key_first($data['terminus']);

            $mainTerminusArretId = null;
            $mainTerminusNom = null;

            if ($mainTerminusStopId && isset($stopToArret[$mainTerminusStopId])) {
                $mainTerminusArretId = $stopToArret[$mainTerminusStopId]['arret_id'];
                $mainTerminusNom = $stopToArret[$mainTerminusStopId]['nom'];
            }

            // Direction_ref : 0 = Aller, 1 = Retour (convention PRIM)
            $directionRef = $directionId == 0 ? 'Aller' : 'Retour';

            // InsÇ¸rer la direction
            $stmtDir->execute([
                'ligne_id' => $ligneId,
                'direction_id' => $directionId,
                'direction_ref' => $directionRef,
                'libelle' => $mainHeadsign,
                'terminus_arret_id' => $mainTerminusArretId,
                'terminus_nom' => $mainTerminusNom
            ]);
            $dirCount++;

            // InsÇ¸rer tous les terminus de cette direction
            $isPrincipal = true;
            foreach ($data['terminus'] as $stopId => $freq) {
                if (isset($stopToArret[$stopId])) {
                    $arretData = $stopToArret[$stopId];

                    $stmtTerm->execute([
                        'ligne_id' => $ligneId,
                        'direction_id' => $directionId,
                        'arret_id' => $arretData['arret_id'],
                        'nom' => $arretData['nom'],
                        'frequence' => $freq,
                        'principal' => $isPrincipal ? 1 : 0
                    ]);
                    $termCount++;
                    $isPrincipal = false;
                }
            }
        }
    }

    logMessage("Directions insÇ¸rÇ¸es : $dirCount");
    logMessage("Terminus insÇ¸rÇ¸s : $termCount");
    $results['directions'] = $dirCount;
    $results['terminus'] = $termCount;

    // =====================
    // 7. Calculer ref_arret_direction (quel quai sert quelle direction)
    // =====================
    logMessage("Calcul des directions par arrÇ¦t...");

    // $arretDirections[ligne_id][arret_id][direction_id] = count
    $arretDirections = [];

    // CrÇ¸er un index trip_id ƒÅ' (ligne_id, direction_id)
    $tripToLigneDir = [];
    foreach ($tripsByLigne as $ligneId => $directions) {
        foreach ($directions as $directionId => $trips) {
            foreach ($trips as $tripData) {
                $tripToLigneDir[$tripData['trip_id']] = [
                    'ligne_id' => $ligneId,
                    'direction_id' => $directionId
                ];
            }
        }
    }

    // Pour chaque trip, noter quels arrÇ¦ts sont traversÇ¸s
    foreach ($tripStops as $tripId => $stops) {
        if (!isset($tripToLigneDir[$tripId])) continue;

        $ligneId = $tripToLigneDir[$tripId]['ligne_id'];
        $directionId = $tripToLigneDir[$tripId]['direction_id'];

        foreach ($stops as $stopId => $seq) {
            if (!isset($stopToArret[$stopId])) continue;

            $arretId = $stopToArret[$stopId]['arret_id'];

            if (!isset($arretDirections[$ligneId])) {
                $arretDirections[$ligneId] = [];
            }
            if (!isset($arretDirections[$ligneId][$arretId])) {
                $arretDirections[$ligneId][$arretId] = [];
            }

            $arretDirections[$ligneId][$arretId][$directionId] =
                ($arretDirections[$ligneId][$arretId][$directionId] ?? 0) + 1;
        }
    }

    // LibÇ¸rer la mÇ¸moire
    unset($tripStops, $tripMaxSeq, $tripToLigneDir);

    // InsÇ¸rer dans ref_arret_direction
    logMessage("Insertion des directions par arrÇ¦t...");

    $stmtArretDir = $pdo->prepare("
        INSERT INTO ref_arret_direction
        (ligne_id, arret_id, direction_id, stif_stop_ref, terminus_liste, libelle_direction)
        VALUES (:ligne_id, :arret_id, :direction_id, :stif_stop_ref, :terminus_liste, :libelle_direction)
        ON DUPLICATE KEY UPDATE
        stif_stop_ref = VALUES(stif_stop_ref),
        terminus_liste = VALUES(terminus_liste),
        libelle_direction = VALUES(libelle_direction),
        date_import = NOW()
    ");

    // Charger les libellÇ¸s de direction
    $dirLabels = [];
    $result = $pdo->query("SELECT ligne_id, direction_id, libelle FROM ref_directions");
    while ($row = $result->fetch()) {
        $dirLabels[$row['ligne_id']][$row['direction_id']] = $row['libelle'];
    }

    // Charger les terminus par direction
    $dirTerminus = [];
    $result = $pdo->query("SELECT ligne_id, direction_id, nom FROM ref_terminus ORDER BY frequence DESC");
    while ($row = $result->fetch()) {
        if (!isset($dirTerminus[$row['ligne_id']][$row['direction_id']])) {
            $dirTerminus[$row['ligne_id']][$row['direction_id']] = [];
        }
        $dirTerminus[$row['ligne_id']][$row['direction_id']][] = $row['nom'];
    }

    // Charger les stif_id des arrÇ¦ts
    $arretStifIds = [];
    $result = $pdo->query("SELECT id, stif_id FROM ref_arrets WHERE stif_id IS NOT NULL");
    while ($row = $result->fetch()) {
        $arretStifIds[$row['id']] = $row['stif_id'];
    }

    $adCount = 0;

    foreach ($arretDirections as $ligneId => $arrets) {
        foreach ($arrets as $arretId => $directions) {
            foreach ($directions as $directionId => $count) {
                $stifStopRef = $arretStifIds[$arretId] ?? null;
                $libelle = $dirLabels[$ligneId][$directionId] ?? "Direction $directionId";
                $terminusListe = $dirTerminus[$ligneId][$directionId] ?? [];

                $stmtArretDir->execute([
                    'ligne_id' => $ligneId,
                    'arret_id' => $arretId,
                    'direction_id' => $directionId,
                    'stif_stop_ref' => $stifStopRef,
                    'terminus_liste' => json_encode($terminusListe, JSON_UNESCAPED_UNICODE),
                    'libelle_direction' => $libelle
                ]);
                $adCount++;
            }
        }
    }

    logMessage("ArrÇ¦t-directions insÇ¸rÇ¸s : $adCount");
    $results['arret_directions'] = $adCount;

    // Log final
    $duration = round(microtime(true) - $startTime, 2);
    logMessage("Import GTFS terminÇ¸ en {$duration}s");

    // Enregistrer dans le log
    $pdo->prepare("
        INSERT INTO ref_import_log
        (type_import, fichier_source, nb_lignes_importees, duree_secondes, statut)
        VALUES ('gtfs', 'GTFS IDFM', :nb, :duree, 'succes')
    ")->execute([
        'nb' => $dirCount + $termCount + $adCount,
        'duree' => $duration
    ]);

    return [
        'success' => true,
        'duration' => $duration,
        'results' => $results
    ];
}

// =====================
// EXECUTION
// =====================

if (php_sapi_name() === 'cli') {
    $truncate = in_array('--truncate', $argv);

    echo "=== IMPORT GTFS IDFM ===" . PHP_EOL;
    echo "Dossier GTFS : " . GTFS_DIR . PHP_EOL;
    echo "Mode truncate : " . ($truncate ? 'OUI' : 'NON') . PHP_EOL;
    echo str_repeat('=', 40) . PHP_EOL;

    try {
        $pdo = getDbConnection();
        $results = importGTFS($pdo, $truncate);

        echo PHP_EOL . "=== RÇ%SUMÇ% ===" . PHP_EOL;
        print_r($results);
    } catch (Exception $e) {
        echo "ERREUR : " . $e->getMessage() . PHP_EOL;
        exit(1);
    }

    exit;
}

// Mode Web
header('Content-Type: application/json; charset=utf-8');

$action = $_GET['action'] ?? 'status';
$truncate = isset($_GET['truncate']);

switch ($action) {
    case 'import':
        try {
            $pdo = getDbConnection();
            $results = importGTFS($pdo, $truncate);
            echo json_encode($results, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
        } catch (Exception $e) {
            echo json_encode(['success' => false, 'error' => $e->getMessage()]);
        }
        break;

    default:
        echo json_encode([
            'usage' => 'import_gtfs.php?action=import[&truncate]',
            'gtfs_dir' => GTFS_DIR,
            'files' => is_dir(GTFS_DIR) ? glob(GTFS_DIR . '/*.txt') : []
        ], JSON_PRETTY_PRINT);
        break;
}


**Checking for existing export script**

I'm reviewing export/generate_csv.php to determine if it provides the needed export functionality to avoid building a new script.


Output:
Exit code: 0
Wall time: 0.9 seconds
Output:
<?php
/**
 * referentiel/export/generate_csv.php
 * GÇ¸nÇùre les fichiers CSV de configuration pour le frontend Accueil V2
 *
 * Fichiers gÇ¸nÇ¸rÇ¸s :
 * - lines_catalog.json : catalogue complet des lignes (remplace l'existant)
 * - arrets_par_ligne.csv : tous les arrÇ¦ts par ligne avec directions
 * - search_index.json : index de recherche rapide pour l'admin
 *
 * Usage CLI : php generate_csv.php
 * Usage Web : generate_csv.php?action=generate
 */

ini_set('max_execution_time', 300);
ini_set('memory_limit', '512M');

require_once __DIR__ . '/../config/database.php';

// Dossier de sortie (data du projet principal)
define('OUTPUT_DIR', realpath(__DIR__ . '/../../data'));
define('EXPORT_DIR', __DIR__ . '/../exports');

// =====================
// FONCTIONS
// =====================

function logMessage(string $message, string $level = 'INFO'): void {
    $timestamp = date('Y-m-d H:i:s');
    $line = "[$timestamp] [$level] $message";

    if (php_sapi_name() === 'cli') {
        echo $line . PHP_EOL;
    }
}

/**
 * GÇ¸nÇ¸rer le catalogue des lignes (JSON)
 */
function generateLinesCatalog(PDO $pdo): array {
    logMessage("GÇ¸nÇ¸ration du catalogue des lignes...");

    $catalog = [
        'version' => '3.0',
        'updated_at' => date('Y-m-d'),
        'source' => 'BDD RÇ¸fÇ¸rentiel IDFM',
        'lines' => []
    ];

    // Grouper par mode
    $modes = ['rer', 'transilien', 'metro', 'tram', 'bus', 'noctilien'];

    foreach ($modes as $mode) {
        $catalog['lines'][$mode] = [];

        $stmt = $pdo->prepare("
            SELECT
                l.idfm_id,
                l.stif_id AS line_ref,
                l.code,
                l.nom AS name,
                l.couleur AS color,
                l.couleur_texte AS text_color,
                l.operateur AS operator,
                COUNT(DISTINCT la.arret_id) AS stop_count
            FROM ref_lignes l
            LEFT JOIN ref_ligne_arret la ON la.ligne_id = l.id
            WHERE l.mode = :mode AND l.actif = 1
            GROUP BY l.id
            ORDER BY
                CASE
                    WHEN l.code REGEXP '^[0-9]+$' THEN LPAD(l.code, 5, '0')
                    ELSE l.code
                END
        ");

        $stmt->execute(['mode' => $mode]);

        while ($row = $stmt->fetch()) {
            $catalog['lines'][$mode][] = [
                'code' => $row['code'],
                'name' => $row['name'],
                'line_ref' => $row['line_ref'],
                'color' => $row['color'] ?: 'CCCCCC',
                'text_color' => $row['text_color'] ?: 'FFFFFF',
                'operator' => $row['operator'],
                'stop_count' => (int)$row['stop_count']
            ];
        }

        logMessage("  $mode : " . count($catalog['lines'][$mode]) . " lignes");
    }

    // Sauvegarder
    $outputPath = OUTPUT_DIR . '/lines_catalog.json';
    file_put_contents(
        $outputPath,
        json_encode($catalog, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)
    );

    logMessage("Catalogue sauvegardÇ¸ : $outputPath");

    return [
        'success' => true,
        'path' => $outputPath,
        'total_lines' => array_sum(array_map('count', $catalog['lines']))
    ];
}

/**
 * GÇ¸nÇ¸rer la liste des arrÇ¦ts par ligne avec directions et quais
 * Format utilisable directement par l'admin pour la sÇ¸lection
 */
function generateArretsByLine(PDO $pdo): array {
    logMessage("GÇ¸nÇ¸ration des arrÇ¦ts par ligne...");

    $outputDir = EXPORT_DIR . '/line_stops';
    if (!is_dir($outputDir)) {
        mkdir($outputDir, 0755, true);
    }

    // GÇ¸nÇ¸rer un fichier par ligne pour les RER/Metro/Transilien/Tram
    $stmt = $pdo->query("
        SELECT
            l.id,
            l.idfm_id,
            l.stif_id AS line_ref,
            l.code,
            l.nom,
            l.mode,
            l.couleur,
            l.couleur_texte
        FROM ref_lignes l
        WHERE l.mode IN ('rer', 'metro', 'transilien', 'tram')
        AND l.actif = 1
        ORDER BY l.mode, l.code
    ");

    $generated = 0;

    while ($ligne = $stmt->fetch()) {
        $ligneId = $ligne['id'];

        // RÇ¸cupÇ¸rer TOUS les arrÇ¦ts de la ligne avec leurs directions
        // Utilise ref_ligne_arret comme base pour avoir tous les arrÇ¦ts
        // puis LEFT JOIN sur ref_arret_direction pour les quais
        $stmtArrets = $pdo->prepare("
            SELECT
                a.id AS arret_id,
                a.idfm_id AS arret_idfm,
                a.stif_id AS arret_stif,
                a.nom AS stop_name,
                a.commune,
                a.latitude,
                a.longitude,
                ad.direction_id,
                ad.libelle_direction,
                ad.stif_stop_ref,
                ad.terminus_liste
            FROM ref_ligne_arret la
            JOIN ref_arrets a ON a.id = la.arret_id
            LEFT JOIN ref_arret_direction ad ON ad.ligne_id = la.ligne_id AND ad.arret_id = la.arret_id
            WHERE la.ligne_id = :ligne_id
            ORDER BY a.nom, ad.direction_id
        ");

        $stmtArrets->execute(['ligne_id' => $ligneId]);
        $rows = $stmtArrets->fetchAll();

        if (empty($rows)) continue;

        // Structurer par arrÇ¦t (clÇ¸ = nom pour regrouper)
        $stopsByName = [];

        foreach ($rows as $row) {
            $stopName = $row['stop_name'];

            if (!isset($stopsByName[$stopName])) {
                $stopsByName[$stopName] = [
                    'name' => $stopName,
                    'commune' => $row['commune'],
                    'latitude' => $row['latitude'],
                    'longitude' => $row['longitude'],
                    'quays' => []
                ];
            }

            // Ajouter le quai/direction si prÇ¸sent
            if ($row['direction_id'] !== null) {
                $terminus = $row['terminus_liste'] ? json_decode($row['terminus_liste'], true) : [];
                $label = $row['libelle_direction'] ?: 'Direction ' . ($row['direction_id'] + 1);

                // Le ref du quai pour les appels PRIM
                $quayRef = $row['stif_stop_ref'] ?: $row['arret_stif'] ?: $row['arret_idfm'];

                $stopsByName[$stopName]['quays'][] = [
                    'ref' => $quayRef,
                    'label' => 'ƒÅ' ' . $label,
                    'direction_id' => (int)$row['direction_id'],
                    'terminus' => $terminus
                ];
            }
        }

        // Pour les arrÇ¦ts sans quais, crÇ¸er un quai par dÇ¸faut
        foreach ($stopsByName as $name => &$stop) {
            if (empty($stop['quays'])) {
                foreach ($rows as $row) {
                    if ($row['stop_name'] === $name) {
                        $stop['quays'][] = [
                            'ref' => $row['arret_stif'] ?: $row['arret_idfm'],
                            'label' => 'ƒÅ' Toutes directions',
                            'direction_id' => null,
                            'terminus' => []
                        ];
                        break;
                    }
                }
            }
        }

        // Construire le JSON de la ligne
        $stopsArray = array_values($stopsByName);
        usort($stopsArray, fn($a, $b) => strcmp($a['name'], $b['name']));

        $lineData = [
            'line_ref' => $ligne['line_ref'],
            'idfm_id' => $ligne['idfm_id'],
            'code' => $ligne['code'],
            'name' => $ligne['nom'],
            'mode' => $ligne['mode'],
            'color' => $ligne['couleur'],
            'text_color' => $ligne['couleur_texte'],
            'generated_at' => date('c'),
            'stops_count' => count($stopsArray),
            'stops' => $stopsArray
        ];

        // Sauvegarder le fichier JSON
        $codeFile = preg_replace('/[^A-Za-z0-9]/', '', $ligne['code']);
        $filename = "{$codeFile}_{$ligne['mode']}.json";
        $filepath = $outputDir . '/' . $filename;

        file_put_contents(
            $filepath,
            json_encode($lineData, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)
        );

        $generated++;
    }

    logMessage("Fichiers ligne gÇ¸nÇ¸rÇ¸s : $generated");

    return [
        'success' => true,
        'generated' => $generated,
        'output_dir' => $outputDir
    ];
}

/**
 * GÇ¸nÇ¸rer un index de recherche pour l'admin
 */
function generateSearchIndex(PDO $pdo): array {
    logMessage("GÇ¸nÇ¸ration de l'index de recherche...");

    // Index des arrÇ¦ts avec leurs lignes
    $stmt = $pdo->query("
        SELECT
            a.id,
            a.idfm_id,
            a.stif_id AS stop_ref,
            a.nom AS stop_name,
            a.commune,
            GROUP_CONCAT(DISTINCT CONCAT(l.mode, '|', l.code, '|', l.stif_id) SEPARATOR ';;') AS lignes
        FROM ref_arrets a
        JOIN ref_ligne_arret la ON la.arret_id = a.id
        JOIN ref_lignes l ON l.id = la.ligne_id
        WHERE l.actif = 1
        GROUP BY a.id
        ORDER BY a.nom
    ");

    $index = [];

    while ($row = $stmt->fetch()) {
        $lignes = [];
        foreach (explode(';;', $row['lignes']) as $ligneStr) {
            $parts = explode('|', $ligneStr);
            if (count($parts) === 3) {
                $lignes[] = [
                    'mode' => $parts[0],
                    'code' => $parts[1],
                    'line_ref' => $parts[2]
                ];
            }
        }

        $index[] = [
            'id' => $row['id'],
            'idfm_id' => $row['idfm_id'],
            'stop_ref' => $row['stop_ref'],
            'name' => $row['stop_name'],
            'commune' => $row['commune'],
            'lines' => $lignes
        ];
    }

    // Sauvegarder
    $outputPath = OUTPUT_DIR . '/search_index.json';
    file_put_contents(
        $outputPath,
        json_encode([
            'updated_at' => date('c'),
            'count' => count($index),
            'stops' => $index
        ], JSON_UNESCAPED_UNICODE)
    );

    logMessage("Index sauvegardÇ¸ : $outputPath (" . count($index) . " arrÇ¦ts)");

    return [
        'success' => true,
        'path' => $outputPath,
        'count' => count($index)
    ];
}

/**
 * GÇ¸nÇ¸rer un fichier CSV flat pour compatibilitÇ¸
 */
function generateFlatCSV(PDO $pdo): array {
    logMessage("GÇ¸nÇ¸ration du CSV plat arrets_lignes_directions...");

    $outputPath = EXPORT_DIR . '/arrets_lignes_directions.csv';

    $fp = fopen($outputPath, 'w');

    // BOM UTF-8 + Header
    fprintf($fp, chr(0xEF).chr(0xBB).chr(0xBF));
    fputcsv($fp, [
        'line_ref', 'line_code', 'line_name', 'mode',
        'stop_ref', 'stop_name', 'zone_name', 'commune',
        'latitude', 'longitude',
        'direction_id', 'direction_ref', 'direction_label',
        'terminus_list'
    ], ';');

    $stmt = $pdo->query("
        SELECT
            l.stif_id AS line_ref,
            l.code AS line_code,
            l.nom AS line_name,
            l.mode,
            a.stif_id AS stop_ref,
            a.nom AS stop_name,
            za.nom AS zone_name,
            a.commune,
            a.latitude,
            a.longitude,
            ad.direction_id,
            CASE WHEN ad.direction_id = 0 THEN 'Aller' ELSE 'Retour' END AS direction_ref,
            ad.libelle_direction AS direction_label,
            ad.terminus_liste
        FROM ref_arret_direction ad
        JOIN ref_lignes l ON l.id = ad.ligne_id
        JOIN ref_arrets a ON a.id = ad.arret_id
        LEFT JOIN ref_zones_arret za ON za.id = a.zone_arret_id
        WHERE l.actif = 1
        ORDER BY l.mode, l.code, a.nom, ad.direction_id
    ");

    $count = 0;
    while ($row = $stmt->fetch()) {
        // Convertir la liste des terminus en string
        $terminus = json_decode($row['terminus_liste'], true) ?: [];
        $row['terminus_list'] = implode(' / ', array_slice($terminus, 0, 3));
        unset($row['terminus_liste']);

        fputcsv($fp, array_values($row), ';');
        $count++;
    }

    fclose($fp);

    logMessage("CSV gÇ¸nÇ¸rÇ¸ : $outputPath ($count lignes)");

    return [
        'success' => true,
        'path' => $outputPath,
        'rows' => $count
    ];
}

/**
 * GÇ¸nÇ¸rer les statistiques du rÇ¸fÇ¸rentiel
 */
function generateStats(PDO $pdo): array {
    $stats = [];

    $stats['lignes'] = $pdo->query("SELECT COUNT(*) FROM ref_lignes WHERE actif = 1")->fetchColumn();
    $stats['lignes_par_mode'] = $pdo->query("
        SELECT mode, COUNT(*) as count
        FROM ref_lignes
        WHERE actif = 1
        GROUP BY mode
    ")->fetchAll(PDO::FETCH_KEY_PAIR);

    $stats['arrets'] = $pdo->query("SELECT COUNT(*) FROM ref_arrets")->fetchColumn();
    $stats['zones_arret'] = $pdo->query("SELECT COUNT(*) FROM ref_zones_arret")->fetchColumn();
    $stats['directions'] = $pdo->query("SELECT COUNT(*) FROM ref_directions")->fetchColumn();
    $stats['terminus'] = $pdo->query("SELECT COUNT(*) FROM ref_terminus")->fetchColumn();
    $stats['arret_directions'] = $pdo->query("SELECT COUNT(*) FROM ref_arret_direction")->fetchColumn();

    $stats['derniers_imports'] = $pdo->query("
        SELECT type_import, MAX(date_import) as last_import
        FROM ref_import_log
        WHERE statut = 'succes'
        GROUP BY type_import
    ")->fetchAll();

    return $stats;
}

// =====================
// EXECUTION PRINCIPALE
// =====================

function runExport(): array {
    $startTime = microtime(true);
    $results = [];

    try {
        $pdo = getDbConnection();
        logMessage("Connexion BDD OK");

        // VÇ¸rifier que les donnÇ¸es existent
        $count = $pdo->query("SELECT COUNT(*) FROM ref_lignes")->fetchColumn();
        if ($count == 0) {
            return [
                'success' => false,
                'error' => 'Aucune donnÇ¸e dans ref_lignes. Lancez les imports d\'abord.'
            ];
        }

        $results['lines_catalog'] = generateLinesCatalog($pdo);
        $results['arrets_by_line'] = generateArretsByLine($pdo);
        $results['search_index'] = generateSearchIndex($pdo);
        $results['flat_csv'] = generateFlatCSV($pdo);
        $results['stats'] = generateStats($pdo);

    } catch (Exception $e) {
        logMessage("ERREUR : " . $e->getMessage(), 'ERROR');
        return ['success' => false, 'error' => $e->getMessage()];
    }

    $duration = round(microtime(true) - $startTime, 2);
    logMessage("Export terminÇ¸ en {$duration}s");

    return [
        'success' => true,
        'duration' => $duration,
        'results' => $results
    ];
}

// Mode CLI
if (php_sapi_name() === 'cli') {
    echo "=== EXPORT DES DONNÇ%ES RÇ%FÇ%RENTIEL ===" . PHP_EOL;
    echo "Output : " . OUTPUT_DIR . PHP_EOL;
    echo "Exports : " . EXPORT_DIR . PHP_EOL;
    echo str_repeat('=', 40) . PHP_EOL;

    $results = runExport();

    echo PHP_EOL . "=== RÇ%SUMÇ% ===" . PHP_EOL;
    print_r($results);

    exit;
}

// Mode Web
header('Content-Type: application/json; charset=utf-8');

$action = $_GET['action'] ?? 'status';

switch ($action) {
    case 'generate':
        $results = runExport();
        echo json_encode($results, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
        break;

    case 'stats':
        try {
            $pdo = getDbConnection();
            $stats = generateStats($pdo);
            echo json_encode(['success' => true, 'stats' => $stats], JSON_PRETTY_PRINT);
        } catch (Exception $e) {
            echo json_encode(['success' => false, 'error' => $e->getMessage()]);
        }
        break;

    default:
        echo json_encode([
            'usage' => [
                'generate' => 'generate_csv.php?action=generate',
                'stats' => 'generate_csv.php?action=stats'
            ],
            'output_dir' => OUTPUT_DIR,
            'export_dir' => EXPORT_DIR
        ], JSON_PRETTY_PRINT);
        break;
}


**Faisant le point sur /referentiel**

J’ai examiné le dossier `/referentiel` pour évaluer le fonctionnement global et relever les points clés :

- Mot de passe en clair dans `config/database.php` (`DB_PASS`), ce qui présente un risque sécurité important ; il faudrait externaliser dans un fichier `.env` non commité.
- Plusieurs fichiers contiennent des caractères corrompus (mojibake) dus à un mauvais encodage (probablement ISO-8859-1 lu en UTF-8), ce qui dégrade l’affichage de l’interface (ex. `README`, pages front, labels).
- Le script de téléchargement des sources désactive la vérification SSL en local, ce qui peut poser un risque de sécurité selon l’environnement.
- Le système s’appuie sur plusieurs dossiers (`sources`, `data`, `config`) devant être présents et accessibles, notamment pour l’import/export de données.
- La procédure principale `update_referentiel` orchestre le téléchargement, l’import et le traitement des données, mais ne semble pas inclure de tests automatiques.
- Certains points techniques : l’utilisation de `TRUNCATE` dans les imports sans gérer les clés étrangères peut poser problème, et la limite mémoire PHP de 2 Go peut être juste pour les gros jeux GTFS.
- Le code contient quelques chaînes mal encodées (`'ƒÅ' . $label`), sans impact fonctionnel mais révélateur du souci d’encodage général.
- Quelques fichiers ou dossiers présentés entre accolades comme `{config,import,...}` semblent être des archives extraites ou des restes, pas de problème critique.

En résumé, le fonctionnement global paraît cohérent mais la sécurisation (mot de passe) et l’encodage des fichiers sont des points importants à corriger. Il faudrait aussi valider en environnement disposant des accès réseau et bases requis. Les prochaines étapes conseillées sont de tester réellement les scripts dans un environnement complet avec base et réseau, et de corriger les points de sécurité et encodage avant mise en production.


J’ai passé en revue `/referentiel` ; rien exécuté (sandbox lecture seule, pas de base dispo). Points notables :

- `referentiel/index.php`: dashboard web qui pilote init BDD + affiche stats/imports + boutons vers chaque étape (téléchargement, imports, exports). Tout repose sur `config/database.php` et les tables `ref_%`.
- Orchestration complète dans `referentiel/update_referentiel.php`: enchaîne download → import référentiels → import GTFS → exports, utilisable en CLI ou via `?action=update[&force][&truncate]`.
- Import des sources IDFM (`import/download_sources.php`), référentiels CSV (`import/import_referentiels.php`) et GTFS (`import/import_gtfs.php`) puis génération des exports (`export/generate_csv.php`) qui écrit dans `../data` (`lines_catalog.json`, `search_index.json`) et `referentiel/exports/line_stops/*.json` + CSV plat.
- Mot de passe production hardcodé dans `config/database.php` (DB_PASS `10Melodie2606`) et versionné en clair : point de sécurité à corriger (à basculer en variables d’environnement/secret).
- Encodage des fichiers manifestement corrompu (ex : `RÇ¸fÇ¸rentiel`, `ÐY"…` dans `index.php`, `README.md`, etc.). Rendu web/UI sera illisible ; vérifier l’encodage (UTF-8 attendu) ou re-générer les fichiers texte.
- Les scripts supposent l’existence des dossiers `sources/gtfs` et `data/`; `download_sources.php` force `curl` avec SSL désactivé en local, à réactiver côté prod.

Prochaines actions suggérées :
1) Régler l’encodage des fichiers PHP/MD/HTML en UTF‑8 et relire l’UI.
2) Externaliser les identifiants BDD (dotenv/config serveur) et retirer le secret du dépôt.
3) Tester le run complet `php referentiel/update_referentiel.php --force --truncate` sur un environnement avec MySQL + accès réseau pour valider les imports/exports.