<?php

/**
 * ErrorHandler — shared error-normalisation and logging utilities.
 *
 * Provides:
 *  - userMessage($e) — turns raw exceptions into user-friendly French messages
 *  - log($context, $e, array $extra) — structured error_log with trace
 *
 * Usage from any action/controller:
 *   ErrorHandler::log('thesis_edit', $e, ['thesis_id' => $id]);
 *   App::flash('error', ErrorHandler::userMessage($e));
 */
class ErrorHandler
{
    /**
     * Map SQLite FK constraint errors to human-readable field names.
     *
     * SQLite FK errors reference the *child* table (where the FK column lives),
     * e.g. "INSERT INTO theses" when orientation_id FK fails.
     *
     * We map child table → the form field(s) whose values populate its FK columns.
     */
    private const FK_TABLE_MAP = [
        // Main thesis table — FK columns: orientation_id, ap_program_id,
        // finality_id, access_type_id, license_id
        'theses'              => 'Orientation, AP, Finalité, Type d\'accès ou Licence',
        // Junction tables — each maps to one specific field
        'thesis_authors'      => 'Auteur·ice',
        'thesis_supervisors'  => 'Composition du jury',
        'thesis_languages'    => 'Langue(s)',
        'thesis_formats'      => 'Format(s)',
        'thesis_tags'         => 'Mots-clés',
        'thesis_files'        => 'Fichiers',
        // Admin / system tables
        'share_links'         => 'Lien étudiant·e',
        'file_access_requests' => "Demande d'accès",
        'form_help_blocks'    => "Bloc d'aide",
        'site_settings'       => 'Paramètres',
        'admin_log'           => 'Journal admin',
        // Parent tables (when referenced directly in "table" pattern)
        'orientations'        => 'Orientation',
        'ap_programs'         => 'AP',
        'finality_types'      => 'Finalité',
        'access_types'        => "Type d'accès",
        'license_types'       => 'Licence',
        'authors'             => 'Auteur·ice',
        'supervisors'         => 'Composition du jury',
        'languages'           => 'Langue(s)',
        'format_types'        => 'Format(s)',
        'tags'                => 'Mots-clés',
    ];

    /**
     * Map an exception to a user-facing message.
     *
     * Domain exceptions (validation, duplicates) carry their own message;
     * system exceptions (PDO, generic) get sanitised explanations with
     * specific field identification where possible.
     */
    public static function userMessage(\Throwable $e): string
    {
        // ── Domain exceptions with meaningful messages ──────────────────────
        if ($e instanceof DuplicateThesisException) {
            return $e->getMessage(); // Already user-friendly
        }

        // ── Database errors ─────────────────────────────────────────────────
        if ($e instanceof \PDOException) {
            return self::pdoMessage($e);
        }

        // ── Validation errors (RuntimeException, InvalidArgumentException) ──
        // These are thrown with user-friendly French messages — pass through.
        if ($e instanceof \RuntimeException) {
            return $e->getMessage();
        }

        // ── Everything else — generic fallback ──────────────────────────────
        return 'Une erreur inattendue est survenue.'
             . ' Veuillez réessayer ou contacter l\'équipe.';
    }

    /**
     * Build a user-facing message from a PDOException.
     *
     * For FK constraint failures, the SQLite error message includes the
     * table name (e.g. "FOREIGN KEY constraint failed INSERT INTO thesis_formats").
     * We extract it and map to a field label.
     */
    private static function pdoMessage(\PDOException $e): string
    {
        $msg = $e->getMessage();

        if (str_contains($msg, 'FOREIGN KEY constraint failed')) {
            $field = self::extractFkField($msg);

            if ($field !== null) {
                return "Erreur de base de données : la référence pour « {$field} » est invalide."
                     . ' Vérifiez que la valeur existe dans la base de données.';
            }

            return 'Erreur de base de données : une contrainte de référence est invalide.'
                 . ' Vérifiez les champs Orientation, AP, Finalité, Langues, Formats.';
        }

        // ── UNIQUE constraint ───────────────────────────────────────────────
        if (str_contains($msg, 'UNIQUE constraint failed')) {
            return 'Erreur de base de données : une valeur en double a été détectée.'
                 . ' Veuillez réessayer ou contacter l\'équipe.';
        }

        // ── NOT NULL ────────────────────────────────────────────────────────
        if (str_contains($msg, 'NOT NULL constraint failed')) {
            return 'Erreur de base de données : un champ obligatoire est manquant.'
                 . ' Veuillez réessayer ou contacter l\'équipe.';
        }

        // ── Generic SQL error — don't leak raw SQL to users ────────────────
        return 'Une erreur de base de données est survenue.'
             . ' Veuillez réessayer ou contacter l\'équipe.';
    }

    /**
     * Try to extract the referenced table name from a SQLite FK error message.
     *
     * SQLite FK errors typically contain the table name in the message body:
     *   "FOREIGN KEY constraint failed INSERT INTO thesis_formats (thesis_id, format_id) VALUES (?, ?)"
     *
     * Also handles quoted table names in newer SQLite messages:
     *   "FOREIGN KEY constraint failed (table \"orientations\")"
     */
    private static function extractFkField(string $msg): ?string
    {
        // Pattern 1: "table \"tablename\"" (SQLite 3.37+)
        if (preg_match('/table\s+"([^"]+)"/i', $msg, $m)) {
            $table = strtolower($m[1]);
            return self::FK_TABLE_MAP[$table] ?? null;
        }

        // Pattern 2: "INSERT INTO tablename" or "UPDATE tablename"
        if (preg_match('/(?:INSERT\s+INTO|UPDATE)\s+"?(\w+)"?/i', $msg, $m)) {
            $table = strtolower($m[1]);
            return self::FK_TABLE_MAP[$table] ?? null;
        }

        // Pattern 3: "REFERENCES tablename" — the constraint itself
        if (preg_match('/REFERENCES\s+"?(\w+)"?/i', $msg, $m)) {
            $table = strtolower($m[1]);
            return self::FK_TABLE_MAP[$table] ?? null;
        }

        return null;
    }

    /**
     * Write a structured error log entry.
     *
     * @param string    $context  e.g. 'thesis_edit', 'partage_submit', 'csv_import'
     * @param \Throwable $e
     * @param array     $extra    arbitrary key-value context to include in the log
     */
    public static function log(string $context, \Throwable $e, array $extra = []): void
    {
        $parts = [
            'context=' . $context,
            'exception=' . get_class($e),
            'message=' . $e->getMessage(),
        ];
        foreach ($extra as $k => $v) {
            if (is_scalar($v) || $v === null) {
                $parts[] = $k . '=' . json_encode($v, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
            } elseif (is_array($v)) {
                $parts[] = $k . '=' . json_encode($v, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
            } else {
                $parts[] = $k . '=' . gettype($v);
            }
        }
        $parts[] = 'trace=' . $e->getTraceAsString();

        error_log(implode(' | ', $parts));
    }
}
