<?php

declare(strict_types=1);

namespace Install\Services;

use PDO;
use Throwable;

final class AdministratorInstaller
{
    private PDO $pdo;

    /** @var array<string,array<string,mixed>> */
    private array $columns = [];

    /**
     * @param array<string,mixed> $database
     */
    public function __construct(array $database)
    {
        $this->pdo = $this->connect($database);
        $this->ensureUsersTable();
        $this->columns = $this->readColumns();
    }

    private function ensureUsersTable(): void
    {
        $this->pdo->exec(
            "CREATE TABLE IF NOT EXISTS utilisateurs ("
            . "id INT UNSIGNED NOT NULL AUTO_INCREMENT,"
            . "nom VARCHAR(120) NOT NULL,"
            . "prenom VARCHAR(120) NOT NULL,"
            . "login VARCHAR(80) NOT NULL,"
            . "email VARCHAR(190) NOT NULL,"
            . "mot_de_passe VARCHAR(255) NOT NULL,"
            . "role VARCHAR(50) NOT NULL DEFAULT 'ADMINISTRATEUR',"
            . "actif TINYINT(1) NOT NULL DEFAULT 1,"
            . "date_creation DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,"
            . "date_modification DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP "
            . "ON UPDATE CURRENT_TIMESTAMP,"
            . "PRIMARY KEY (id),"
            . "UNIQUE KEY uk_utilisateurs_login (login),"
            . "UNIQUE KEY uk_utilisateurs_email (email)"
            . ") ENGINE=InnoDB "
            . "DEFAULT CHARSET=utf8mb4 "
            . "COLLATE=utf8mb4_unicode_ci"
        );
    }

    /**
     * @param array<string,mixed> $data
     *
     * @return array<string,mixed>
     */
    public function create(array $data): array
    {
        $validated = $this->validate($data);
        $mapping = $this->resolveMapping();

        $this->assertUnique(
            $mapping,
            $validated['login'],
            $validated['email']
        );

        $values = [
            $mapping['nom'] => $validated['nom'],
            $mapping['prenom'] => $validated['prenom'],
            $mapping['login'] => $validated['login'],
            $mapping['email'] => $validated['email'],
            $mapping['password'] => password_hash(
                $validated['password'],
                PASSWORD_DEFAULT
            ),
        ];

        if ($mapping['role'] !== null) {
            $values[$mapping['role']] = $this->resolveRoleValue(
                $mapping['role']
            );
        }

        if ($mapping['active'] !== null) {
            $values[$mapping['active']] = $this->resolveActiveValue(
                $mapping['active']
            );
        }

        if ($mapping['created_at'] !== null) {
            $values[$mapping['created_at']] = date('Y-m-d H:i:s');
        }

        if ($mapping['updated_at'] !== null) {
            $values[$mapping['updated_at']] = date('Y-m-d H:i:s');
        }

        $columns = array_keys($values);
        $placeholders = array_map(
            static fn (string $column): string =>
                ':' . $column,
            $columns
        );

        $sql = 'INSERT INTO utilisateurs (`'
            . implode('`, `', $columns)
            . '`) VALUES ('
            . implode(', ', $placeholders)
            . ')';

        try {
            $this->pdo->beginTransaction();

            $statement = $this->pdo->prepare($sql);

            foreach ($values as $column => $value) {
                $statement->bindValue(
                    ':' . $column,
                    $value
                );
            }

            $statement->execute();

            $userId = (int)$this->pdo->lastInsertId();

            if ($userId <= 0 && isset($mapping['id'])) {
                $userId = $this->findInsertedId(
                    $mapping,
                    $validated['login'],
                    $validated['email']
                );
            }

            if ($userId <= 0) {
                throw new \RuntimeException(
                    'Le compte a été créé, mais son identifiant n’a pas pu être récupéré.'
                );
            }

            $storedHash = $this->readStoredPassword(
                $mapping,
                $userId
            );

            if (
                $storedHash === ''
                || !password_verify(
                    $validated['password'],
                    $storedHash
                )
            ) {
                throw new \RuntimeException(
                    'La vérification du mot de passe enregistré a échoué.'
                );
            }

            $this->pdo->commit();

            return [
                'success' => true,
                'user_id' => $userId,
                'nom' => $validated['nom'],
                'prenom' => $validated['prenom'],
                'login' => $validated['login'],
                'email' => $validated['email'],
                'role' => $mapping['role'] !== null
                    ? $values[$mapping['role']]
                    : 'ADMINISTRATEUR',
                'created_at' => date('Y-m-d H:i:s'),
                'password_algorithm' => password_get_info(
                    $storedHash
                )['algoName'] ?? 'PASSWORD_DEFAULT',
            ];
        } catch (Throwable $exception) {
            if ($this->pdo->inTransaction()) {
                $this->pdo->rollBack();
            }

            throw new \RuntimeException(
                'Création de l’administrateur impossible : '
                . $exception->getMessage(),
                0,
                $exception
            );
        }
    }

    /**
     * @return array<string,mixed>
     */
    public function schemaInformation(): array
    {
        $mapping = $this->resolveMapping();

        return [
            'table' => 'utilisateurs',
            'columns_count' => count($this->columns),
            'password_column' => $mapping['password'],
            'role_column' => $mapping['role'],
            'active_column' => $mapping['active'],
            'existing_users' => $this->countUsers(),
        ];
    }

    /**
     * @param array<string,mixed> $data
     *
     * @return array<string,string>
     */
    private function validate(array $data): array
    {
        $nom = trim((string)($data['nom'] ?? ''));
        $prenom = trim((string)($data['prenom'] ?? ''));
        $login = trim((string)($data['login'] ?? ''));
        $email = trim((string)($data['email'] ?? ''));
        $password = (string)($data['password'] ?? '');
        $confirmation = (string)(
            $data['password_confirmation'] ?? ''
        );

        if ($nom === '') {
            throw new \InvalidArgumentException(
                'Le nom est obligatoire.'
            );
        }

        if ($prenom === '') {
            throw new \InvalidArgumentException(
                'Le prénom est obligatoire.'
            );
        }

        if (
            mb_strlen($nom) > 120
            || mb_strlen($prenom) > 120
        ) {
            throw new \InvalidArgumentException(
                'Le nom et le prénom ne doivent pas dépasser 120 caractères.'
            );
        }

        if (
            mb_strlen($login) < 3
            || mb_strlen($login) > 80
            || preg_match(
                '/^[A-Za-z0-9._-]+$/',
                $login
            ) !== 1
        ) {
            throw new \InvalidArgumentException(
                'L’identifiant doit contenir entre 3 et 80 caractères : lettres, chiffres, point, tiret ou soulignement.'
            );
        }

        if (
            filter_var(
                $email,
                FILTER_VALIDATE_EMAIL
            ) === false
        ) {
            throw new \InvalidArgumentException(
                'L’adresse e-mail est invalide.'
            );
        }

        if (mb_strlen($password) < 10) {
            throw new \InvalidArgumentException(
                'Le mot de passe doit contenir au moins 10 caractères.'
            );
        }

        $rules = [
            '/[a-z]/' => 'une lettre minuscule',
            '/[A-Z]/' => 'une lettre majuscule',
            '/\d/' => 'un chiffre',
            '/[^A-Za-z0-9]/' => 'un caractère spécial',
        ];

        foreach ($rules as $pattern => $description) {
            if (preg_match($pattern, $password) !== 1) {
                throw new \InvalidArgumentException(
                    'Le mot de passe doit contenir '
                    . $description
                    . '.'
                );
            }
        }

        if (!hash_equals($password, $confirmation)) {
            throw new \InvalidArgumentException(
                'La confirmation du mot de passe ne correspond pas.'
            );
        }

        return [
            'nom' => $nom,
            'prenom' => $prenom,
            'login' => $login,
            'email' => mb_strtolower($email),
            'password' => $password,
        ];
    }

    /**
     * @return array<string,?string>
     */
    private function resolveMapping(): array
    {
        if ($this->columns === []) {
            throw new \RuntimeException(
                'La table utilisateurs est introuvable ou ne contient aucune colonne.'
            );
        }

        $mapping = [
            'id' => $this->findColumn([
                'id',
                'utilisateur_id',
            ]),
            'nom' => $this->findColumn([
                'nom',
                'last_name',
                'lastname',
            ]),
            'prenom' => $this->findColumn([
                'prenom',
                'first_name',
                'firstname',
            ]),
            'login' => $this->findColumn([
                'login',
                'username',
                'identifiant',
            ]),
            'email' => $this->findColumn([
                'email',
                'adresse_email',
                'mail',
            ]),
            'password' => $this->findColumn([
                'mot_de_passe',
                'password_hash',
                'password',
                'motdepasse',
                'passwd',
            ]),
            'role' => $this->findColumn([
                'role',
                'profil',
                'type_utilisateur',
            ]),
            'active' => $this->findColumn([
                'actif',
                'active',
                'is_active',
                'statut',
                'status',
            ]),
            'created_at' => $this->findColumn([
                'date_creation',
                'created_at',
                'cree_le',
            ]),
            'updated_at' => $this->findColumn([
                'date_modification',
                'updated_at',
                'modifie_le',
            ]),
        ];

        foreach (
            ['id', 'nom', 'prenom', 'login', 'email', 'password']
            as $required
        ) {
            if ($mapping[$required] === null) {
                throw new \RuntimeException(
                    'Colonne obligatoire non reconnue dans utilisateurs : '
                    . $required
                    . '.'
                );
            }
        }

        return $mapping;
    }

    /**
     * @param array<string,?string> $mapping
     */
    private function assertUnique(
        array $mapping,
        string $login,
        string $email
    ): void {
        $sql = 'SELECT COUNT(*) FROM utilisateurs '
            . 'WHERE LOWER(`'
            . $mapping['login']
            . '`) = LOWER(:login) '
            . 'OR LOWER(`'
            . $mapping['email']
            . '`) = LOWER(:email)';

        $statement = $this->pdo->prepare($sql);
        $statement->execute([
            'login' => $login,
            'email' => $email,
        ]);

        if ((int)$statement->fetchColumn() > 0) {
            throw new \RuntimeException(
                'Un utilisateur possède déjà cet identifiant ou cette adresse e-mail.'
            );
        }
    }

    private function resolveRoleValue(
        string $column
    ): string {
        $type = strtolower(
            (string)($this->columns[$column]['Type'] ?? '')
        );

        $candidates = [
            'ADMINISTRATEUR',
            'ADMIN',
            'SUPER_ADMIN',
            'ADMINISTRATOR',
        ];

        if (str_starts_with($type, 'enum(')) {
            preg_match_all(
                "/'([^']+)'/",
                $type,
                $matches
            );

            $allowed = array_map(
                'strtoupper',
                $matches[1] ?? []
            );

            foreach ($candidates as $candidate) {
                $index = array_search(
                    strtoupper($candidate),
                    $allowed,
                    true
                );

                if ($index !== false) {
                    return (string)$matches[1][$index];
                }
            }

            if (!empty($matches[1][0])) {
                return (string)$matches[1][0];
            }
        }

        return 'ADMINISTRATEUR';
    }

    private function resolveActiveValue(
        string $column
    ): int|string {
        $type = strtolower(
            (string)($this->columns[$column]['Type'] ?? '')
        );

        if (str_starts_with($type, 'enum(')) {
            preg_match_all(
                "/'([^']+)'/",
                $type,
                $matches
            );

            $values = $matches[1] ?? [];

            foreach (
                ['ACTIF', 'ACTIVE', '1', 'OUI', 'ENABLED']
                as $candidate
            ) {
                foreach ($values as $value) {
                    if (
                        strtoupper((string)$value)
                        === $candidate
                    ) {
                        return (string)$value;
                    }
                }
            }

            return (string)($values[0] ?? 'ACTIF');
        }

        return 1;
    }

    /**
     * @param array<string,?string> $mapping
     */
    private function findInsertedId(
        array $mapping,
        string $login,
        string $email
    ): int {
        $statement = $this->pdo->prepare(
            'SELECT `'
            . $mapping['id']
            . '` FROM utilisateurs '
            . 'WHERE `'
            . $mapping['login']
            . '` = :login '
            . 'AND `'
            . $mapping['email']
            . '` = :email '
            . 'ORDER BY `'
            . $mapping['id']
            . '` DESC LIMIT 1'
        );

        $statement->execute([
            'login' => $login,
            'email' => $email,
        ]);

        return (int)$statement->fetchColumn();
    }

    /**
     * @param array<string,?string> $mapping
     */
    private function readStoredPassword(
        array $mapping,
        int $userId
    ): string {
        $statement = $this->pdo->prepare(
            'SELECT `'
            . $mapping['password']
            . '` FROM utilisateurs '
            . 'WHERE `'
            . $mapping['id']
            . '` = :id LIMIT 1'
        );

        $statement->execute([
            'id' => $userId,
        ]);

        return (string)$statement->fetchColumn();
    }

    /**
     * @return array<string,array<string,mixed>>
     */
    private function readColumns(): array
    {
        $exists = (int)$this->pdo->query(
            "SELECT COUNT(*) "
            . "FROM information_schema.tables "
            . "WHERE table_schema = DATABASE() "
            . "AND table_name = 'utilisateurs'"
        )->fetchColumn() > 0;

        if (!$exists) {
            throw new \RuntimeException(
                'La table utilisateurs n’existe pas. Vérifiez les migrations.'
            );
        }

        $rows = $this->pdo->query(
            'SHOW COLUMNS FROM utilisateurs'
        )->fetchAll();

        $columns = [];

        foreach ($rows as $row) {
            $name = (string)($row['Field'] ?? '');

            if ($name !== '') {
                $columns[$name] = $row;
            }
        }

        return $columns;
    }

    /**
     * @param array<int,string> $candidates
     */
    private function findColumn(
        array $candidates
    ): ?string {
        $lowerMap = [];

        foreach (array_keys($this->columns) as $column) {
            $lowerMap[mb_strtolower($column)] = $column;
        }

        foreach ($candidates as $candidate) {
            $normalized = mb_strtolower($candidate);

            if (isset($lowerMap[$normalized])) {
                return $lowerMap[$normalized];
            }
        }

        return null;
    }

    private function countUsers(): int
    {
        return (int)$this->pdo->query(
            'SELECT COUNT(*) FROM utilisateurs'
        )->fetchColumn();
    }

    /**
     * @param array<string,mixed> $database
     */
    private function connect(array $database): PDO
    {
        $dsn = 'mysql:host='
            . (string)($database['db_host'] ?? 'localhost')
            . ';port='
            . (int)($database['db_port'] ?? 3306)
            . ';dbname='
            . (string)($database['db_database'] ?? '')
            . ';charset=utf8mb4';

        return new PDO(
            $dsn,
            (string)($database['db_username'] ?? ''),
            (string)($database['db_password'] ?? ''),
            [
                PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
                PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
                PDO::ATTR_TIMEOUT => 10,
                PDO::ATTR_EMULATE_PREPARES => false,
            ]
        );
    }
}
