<?php

declare(strict_types=1);

namespace PrestaShop\Module\LCVPrestaConnector;

/**
 * Gestionnaire de redirections d'URLs basé sur un fichier de règles.
 *
 * Le fichier /var/redirects.list contient des règles de redirection,
 * une par ligne, au format :
 *
 *   <masque_regex> <separateur> <destination>
 *
 * Le séparateur est auto-détecté par ligne : | ; ou tabulation.
 * Les lignes vides et les lignes commençant par # sont ignorées.
 *
 * Les named groups PHP (?P<nom>...) sont référencés dans la destination
 * avec la syntaxe {{nom}}. 
 *
 * Exemple :
 *   /ancienne-categorie/(?P<slug>[^/]+)\.html | /nouvelle-categorie/{{slug}}
 *   /vieux-produit-(\d+)                      ; /produit/$1
 */
class UrlRedirectManager
{
    /**
     * Chemin du fichier de règles, relatif à _PS_ROOT_DIR_
     */
    const REDIRECTS_FILE = '/var/redirects.list';

    /**
     * Module parent
     */
    private \LCVPrestaConnector $module;

    /**
     * Chemin absolu vers le fichier de règles
     */
    private string $filePath;

    public function __construct(\LCVPrestaConnector $module)
    {
        $this->module = $module;
        $this->filePath = _PS_ROOT_DIR_ . self::REDIRECTS_FILE;
    }

    /**
     * Retourne le chemin absolu du fichier de règles
     */
    public function getFilePath(): string
    {
        return $this->filePath;
    }

    /**
     * S'assure que le fichier de règles existe.
     * Le crée vide (avec un commentaire d'en-tête) s'il est absent.
     *
     * @return bool true si le fichier existait déjà ou a pu être créé
     */
    public function ensureFileExists(): bool
    {
        if (file_exists($this->filePath)) {
            return true;
        }

        $dir = dirname($this->filePath);
        if (!is_dir($dir)) {
            @mkdir($dir, 0755, true);
        }

        $header  = "# Fichier de règles de redirections URL - LCVPrestaConnector\n";
        $header .= "#\n";
        $header .= "# Format : <masque_regex> <separateur> <url_destination>\n";
        $header .= "# Le séparateur peut être | ; ou une tabulation (différent à chaque ligne).\n";
        $header .= "# Les groupes nommés (?P<nom>...) sont substitués via {{nom}} dans la destination.\n";
        $header .= "# La destination peut être une URL ou une directive spéciale.\n";
        $header .= "#\n";
        $header .= "# Directives spéciales :\n";
        $header .= "#   category(<id>)  → redirige vers la catégorie PrestaShop n°<id>\n";
        $header .= "#   product(<id>)  → redirige vers le produit PrestaShop n°<id>\n";
        $header .= "#\n";
        $header .= "# Exemples :\n";
        $header .= "#   /ancienne-page/(?P<slug>[^/]+)\\.html | /nouvelle-page/{{slug}}\n";
        $header .= "#   /vieux-produit-(.*)                   ; /produit/\n";
        $header .= "#   /page-deplacee                        | /nouvelle-adresse\n";
        $header .= "#   /fr/catalogue/new/femme/chaussures.awp | category(51)\n";

        return @file_put_contents($this->filePath, $header) !== false;
    }

    /**
     * Charge et retourne le contenu brut du fichier de règles.
     * Crée le fichier s'il n'existe pas.
     */
    public function loadRawContent(): string
    {
        $this->ensureFileExists();
        return (string) @file_get_contents($this->filePath);
    }

    /**
     * Ajoute une règle de redirection à la fin du fichier si elle n'existe pas déjà
     * Ne fait rien si le pattern et la destination sont identiques.
     * Crée le fichier s'il n'existe pas encore.
     *
     * @param string $pattern     Expression régulière de l'ancienne URL
     * @param string $destination URL de destination
     * @param string $separator   Séparateur à utiliser pour une nouvelle ligne (| par défaut)
     *
     * @throws \RuntimeException si l'écriture échoue
     */
    public function appendRule(string $pattern, string $destination, string $separator = '|'): void
    {
        $this->ensureFileExists();
        $lines = @file($this->filePath, FILE_IGNORE_NEW_LINES);
        if ($lines === false) {
            throw new \RuntimeException(
                "Impossible de lire le fichier de règles : " . $this->filePath
            );
        }

        foreach ($lines as $line) {
            $trimmed = trim($line);
            if ($trimmed === '' || $trimmed[0] === '#') {
                continue;
            }
            if (strpos($trimmed, '|') !== false) {
                $sep = '|';
            } elseif (strpos($trimmed, ';') !== false) {
                $sep = ';';
            } elseif (strpos($trimmed, "\t") !== false) {
                $sep = "\t";
            } else {
                continue;
            }
            $parts = explode($sep, $trimmed, 2);
            if (count($parts) !== 2) {
                continue;
            }
            if (trim($parts[0]) === $pattern) {
                return; // Pattern déjà présent, quoi que soit la destination
            }
        }

        // Pattern absent : ajout en fin de fichier
        $line = $pattern . ' ' . $separator . ' ' . $destination . "\n";
        if (@file_put_contents($this->filePath, $line, FILE_APPEND | LOCK_EX) === false) {
            throw new \RuntimeException(
                "Impossible d'écrire dans le fichier de règles : " . $this->filePath
            );
        }
    }

    /**
     * Sauvegarde le contenu brut dans le fichier de règles.
     *
     * @throws \RuntimeException si l'écriture échoue
     */
    public function saveRawContent(string $content): void
    {
        $dir = dirname($this->filePath);
        if (!is_dir($dir)) {
            @mkdir($dir, 0755, true);
        }

        // Normalisation des fins de ligne en LF
        $content = str_replace("\r\n", "\n", $content);
        $content = str_replace("\r", "\n", $content);

        if (@file_put_contents($this->filePath, $content) === false) {
            throw new \RuntimeException(
                "Impossible d'écrire dans le fichier de règles : " . $this->filePath
            );
        }
    }

    /**
     * Parse le contenu brut et retourne un tableau de règles valides.
     *
     * Chaque règle est un tableau ['pattern' => string, 'destination' => string].
     * Les lignes mal formées sont ignorées.
     *
     * @return array<int, array{pattern: string, destination: string}>
     */
    public function parseRules(string $rawContent): array
    {
        $rules = [];
        $lines = explode("\n", $rawContent);

        foreach ($lines as $lineNum => $line) {
            $line = trim($line);

            // Ignorer les lignes vides et les commentaires
            if ($line === '' || $line[0] === '#') {
                continue;
            }

            // Auto-détection du séparateur : | ; ou tabulation
            $separator = null;
            if (strpos($line, '|') !== false) {
                $separator = '|';
            } elseif (strpos($line, ';') !== false) {
                $separator = ';';
            } elseif (strpos($line, "\t") !== false) {
                $separator = "\t";
            }

            if ($separator === null) {
                // Ligne sans séparateur reconnu : ignorée
                continue;
            }

            // On découpe en deux parties seulement (la destination peut contenir des séparateurs)
            $parts = explode($separator, $line, 2);
            if (count($parts) !== 2) {
                continue;
            }

            $pattern     = trim($parts[0]);
            $destination = trim($parts[1]);

            if ($pattern === '' || $destination === '') {
                continue;
            }

            $rules[] = [
                'pattern'     => $pattern,
                'destination' => $destination,
            ];
        }

        return $rules;
    }

    /**
     * Applique la substitution des groupes capturés dans la destination.
     *
     * - Named groups  : {{nom}} → $matches['nom']
     * - Destination category(id) → lien vers catégorie
     */
    private function applySubstitutions(string $destination, array $matches): string
    {
        // Named groups : {{nom}}
        $destination = preg_replace_callback(
            '/\{\{([a-zA-Z_][a-zA-Z0-9_]*)\}\}/',
            function (array $m) use ($matches): string {
                return isset($matches[$m[1]]) ? (string) $matches[$m[1]] : $m[0];
            },
            $destination
        );
        
        // category(id) → lien vers la catégorie Prestashop
        if (preg_match('/^category\((\d+)\)$/', $destination, $m)) {
            $category = new \Category((int)$m[1], \Context::getContext()->language->id);
            if (\Validate::isLoadedObject($category)) {
                $destination = $category->getLink();
            }
            return $destination;
        }

        // product(id) → lien vers le produit PrestaShop
        if (preg_match('/^product\((\d+)\)$/', $destination, $m)) {
            $product = new \Product((int)$m[1]);
            if (\Validate::isLoadedObject($product)) {
                $destination = \Context::getContext()->link->getProductLink($product);
            }
            return $destination;
        }

        return $destination;
    }

    /**
     * Résout une URL donnée en appliquant les règles de redirection.
     * 
     * Retourne la nouvelle URL si existante, ou null si aucune règle ne correspond.
     */
    public function resolve(string $url): ?string
    {
        if (!file_exists($this->filePath)) {
            return null;
        }

        $fh = @fopen($this->filePath, 'r');
        if ($fh === false) {
            return null;
        }        

        try {
            while (($line = fgets($fh)) !== false) {
                $line = trim($line);

                // Ignorer les lignes vides et les commentaires
                if ($line === '' || $line[0] === '#') {
                    continue;
                }

                // Auto-détection du séparateur : | ; ou tabulation
                if (strpos($line, '|') !== false) {
                    $separator = '|';
                } elseif (strpos($line, ';') !== false) {
                    $separator = ';';
                } elseif (strpos($line, "\t") !== false) {
                    $separator = "\t";
                } else {
                    continue; // Pas de séparateur reconnu
                }

                $parts = explode($separator, $line, 2);
                if (count($parts) !== 2) {
                    continue;
                }

                $pattern     = trim($parts[0]);
                $destination = trim($parts[1]);

                if ($pattern === '' || $destination === '') {
                    continue;
                }

                $matches = [];
                // Ancrage automatique : on ajoute ^ et $ si l'utilisateur ne les a pas mis
                // pour éviter qu'un pattern "test" ne matche "testify"
                $anchored = (str_starts_with($pattern, '^') ? '' : '^')
                        . $pattern
                        . (str_ends_with($pattern, '$') ? '' : '$');
                $result  = @preg_match('~' . $anchored . '~', $url, $matches);

                if ($result === false || $result === 0) {
                    continue;
                }

                // Correspondance trouvée : on substitue, on ferme le fichier et on redirige
                fclose($fh);

                return $this->applySubstitutions($destination, $matches);                
            }
        } 
        finally 
        {
            // Le bloc finally garantit que le fichier est toujours fermé,
            // même si une exception non catchée remonte
            if (is_resource($fh)) {
                fclose($fh);
            }
        }

        return null;
    }

    /**
     * Point d'entrée principal : appelé lors d'un hook actionPageNotFound.
     *
     * Lit le fichier de règles ligne par ligne (sans le charger entièrement
     * en mémoire) et effectue une redirection 302 dès la première
     * correspondance, puis stoppe l'exécution.
     *
     * Ne fait rien si le fichier de règles est absent.
     */
    public function handle(): void
    {
        // URL demandée (chemin + éventuelle query string)
        $requestUri = $_SERVER['REQUEST_URI'] ?? '';

        $target = $this->resolve($requestUri);
        if ($target !== null) 
        {
            // Correspondance trouvée
            // On écrase explicitement le 404 déjà envoyé par PageNotFoundController
            // avant que notre hook soit appelé
            http_response_code(302);
            header('HTTP/1.1 302 Found', true, 302);
            header('Location: ' . $target, true, 302);
            die();
        }
    }
}