using System.Diagnostics;
using System.Security.Cryptography;
using System.Text;
using Microsoft.Extensions.Logging;
using MySqlConnector;
using PSLauncher.Models;

namespace PSLauncher.Core.Migrations;

/// <summary>
/// Implémentation MySQL/MariaDB du service de migration.
/// Workflow :
/// <code>
///   1. CREATE TABLE IF NOT EXISTS _launcher_migrations (filename, applied_at, checksum, duration_ms)
///   2. SELECT déjà-appliquées
///   3. Liste {installFolder}/_migrations/*.sql triées alphabétiquement
///   4. Pour chaque non-appliquée :
///        START TRANSACTION
///        Exécute le script (peut contenir plusieurs statements séparés par ;)
///        INSERT INTO _launcher_migrations
///        COMMIT
///      Si exception → ROLLBACK + abort + reporte l'erreur
///   5. Pour chaque appliquée dont le checksum a changé : log warning (script modifié après publication)
/// </code>
/// </summary>
public sealed class DatabaseMigrationService : IDatabaseMigrationService
{
    private const string TrackingTable = "_launcher_migrations";

    private readonly Func<DatabaseConfig> _configProvider;
    private readonly Func<string?> _passwordProvider;
    private readonly ILogger<DatabaseMigrationService> _logger;

    public DatabaseMigrationService(
        Func<DatabaseConfig> configProvider,
        Func<string?> passwordProvider,
        ILogger<DatabaseMigrationService> logger)
    {
        _configProvider = configProvider;
        _passwordProvider = passwordProvider;
        _logger = logger;
    }

    public async Task<string?> TestConnectionAsync(CancellationToken ct)
    {
        try
        {
            await using var conn = await OpenAsync(ct).ConfigureAwait(false);
            await using var cmd = new MySqlCommand("SELECT 1", conn);
            await cmd.ExecuteScalarAsync(ct).ConfigureAwait(false);
            return null;
        }
        catch (Exception ex)
        {
            return ex.Message;
        }
    }

    public async Task<MigrationResult> ApplyMigrationsAsync(
        string installFolder,
        IProgress<MigrationProgress>? progress,
        CancellationToken ct)
    {
        var migrationsDir = Path.Combine(installFolder, "_migrations");
        if (!Directory.Exists(migrationsDir))
        {
            _logger.LogInformation("No _migrations/ directory in {Folder}, skipping DB migration step", installFolder);
            return new MigrationResult(Array.Empty<MigrationOutcome>(), AllSucceeded: true, FailureMessage: null);
        }

        // Liste les .sql, ignore les autres extensions (README, etc.)
        var files = Directory.EnumerateFiles(migrationsDir, "*.sql", SearchOption.TopDirectoryOnly)
            .OrderBy(f => Path.GetFileName(f), StringComparer.Ordinal)
            .ToList();

        if (files.Count == 0)
        {
            _logger.LogInformation("_migrations/ exists but empty in {Folder}", installFolder);
            return new MigrationResult(Array.Empty<MigrationOutcome>(), AllSucceeded: true, FailureMessage: null);
        }

        await using var conn = await OpenAsync(ct).ConfigureAwait(false);
        await EnsureTrackingTableAsync(conn, ct).ConfigureAwait(false);

        var applied = await LoadAppliedAsync(conn, ct).ConfigureAwait(false);
        var outcomes = new List<MigrationOutcome>();

        for (int i = 0; i < files.Count; i++)
        {
            ct.ThrowIfCancellationRequested();
            var filepath = files[i];
            var filename = Path.GetFileName(filepath);
            progress?.Report(new MigrationProgress(i, files.Count, filename));

            var sql = await File.ReadAllTextAsync(filepath, Encoding.UTF8, ct).ConfigureAwait(false);
            var checksum = ComputeSha256(sql);

            if (applied.TryGetValue(filename, out var prevChecksum))
            {
                if (!string.Equals(prevChecksum, checksum, StringComparison.OrdinalIgnoreCase))
                {
                    // Détection d'une modif après-publication : on skip mais on log fort.
                    _logger.LogWarning(
                        "Migration {File} was modified after being applied (stored sha256={Stored}, current={Now}). " +
                        "Ignored to preserve historical truth — write a NEW migration file instead.",
                        filename, prevChecksum[..12], checksum[..12]);
                }
                outcomes.Add(new MigrationOutcome(filename, MigrationStatus.Skipped, 0, null));
                continue;
            }

            var sw = Stopwatch.StartNew();
            await using var tx = await conn.BeginTransactionAsync(ct).ConfigureAwait(false);
            try
            {
                await ExecuteScriptAsync(conn, tx, sql, ct).ConfigureAwait(false);
                await using var insertCmd = new MySqlCommand(
                    $"INSERT INTO {TrackingTable} (filename, applied_at, checksum, duration_ms) VALUES (@f, NOW(), @c, @d)",
                    conn, tx);
                insertCmd.Parameters.AddWithValue("@f", filename);
                insertCmd.Parameters.AddWithValue("@c", checksum);
                insertCmd.Parameters.AddWithValue("@d", sw.ElapsedMilliseconds);
                await insertCmd.ExecuteNonQueryAsync(ct).ConfigureAwait(false);
                await tx.CommitAsync(ct).ConfigureAwait(false);
                sw.Stop();
                outcomes.Add(new MigrationOutcome(filename, MigrationStatus.Applied, sw.ElapsedMilliseconds, null));
                _logger.LogInformation("Migration {File} applied in {Ms} ms", filename, sw.ElapsedMilliseconds);
            }
            catch (Exception ex)
            {
                sw.Stop();
                try { await tx.RollbackAsync(ct).ConfigureAwait(false); } catch { /* swallow */ }
                outcomes.Add(new MigrationOutcome(filename, MigrationStatus.Failed, sw.ElapsedMilliseconds, ex.Message));
                _logger.LogError(ex, "Migration {File} FAILED after {Ms} ms — DB state rolled back", filename, sw.ElapsedMilliseconds);
                progress?.Report(new MigrationProgress(i + 1, files.Count, filename));
                return new MigrationResult(outcomes, AllSucceeded: false, FailureMessage: $"{filename} : {ex.Message}");
            }
        }

        progress?.Report(new MigrationProgress(files.Count, files.Count, string.Empty));
        return new MigrationResult(outcomes, AllSucceeded: true, FailureMessage: null);
    }

    public async Task<IReadOnlyList<AppliedMigration>> ListAppliedAsync(CancellationToken ct)
    {
        await using var conn = await OpenAsync(ct).ConfigureAwait(false);
        await EnsureTrackingTableAsync(conn, ct).ConfigureAwait(false);

        var list = new List<AppliedMigration>();
        await using var cmd = new MySqlCommand(
            $"SELECT filename, applied_at, checksum, duration_ms FROM {TrackingTable} ORDER BY id",
            conn);
        await using var reader = await cmd.ExecuteReaderAsync(ct).ConfigureAwait(false);
        while (await reader.ReadAsync(ct).ConfigureAwait(false))
        {
            list.Add(new AppliedMigration(
                reader.GetString(0),
                DateTime.SpecifyKind(reader.GetDateTime(1), DateTimeKind.Utc),
                reader.GetString(2),
                reader.GetInt64(3)));
        }
        return list;
    }

    // ----- helpers -----

    private async Task<MySqlConnection> OpenAsync(CancellationToken ct)
    {
        var cfg = _configProvider();
        var pwd = _passwordProvider() ?? string.Empty;
        var csb = new MySqlConnectionStringBuilder
        {
            Server = cfg.Host,
            Port = cfg.Port,
            UserID = cfg.User,
            Password = pwd,
            Database = cfg.Database,
            // Désactive le pooling : on ouvre une connexion par opération courte,
            // pas besoin de garder un pool ouvert sur la machine du user.
            Pooling = false,
            ConnectionTimeout = 10,
            DefaultCommandTimeout = 600, // 10 min : laisse de la marge pour les grosses migrations
            AllowUserVariables = true,
        };
        var conn = new MySqlConnection(csb.ConnectionString);
        await conn.OpenAsync(ct).ConfigureAwait(false);
        return conn;
    }

    private static async Task EnsureTrackingTableAsync(MySqlConnection conn, CancellationToken ct)
    {
        const string ddl = $"""
            CREATE TABLE IF NOT EXISTS {TrackingTable} (
                id INT PRIMARY KEY AUTO_INCREMENT,
                filename VARCHAR(255) NOT NULL UNIQUE,
                applied_at DATETIME NOT NULL,
                checksum CHAR(64) NOT NULL,
                duration_ms BIGINT NOT NULL DEFAULT 0
            ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
            """;
        await using var cmd = new MySqlCommand(ddl, conn);
        await cmd.ExecuteNonQueryAsync(ct).ConfigureAwait(false);
    }

    private static async Task<Dictionary<string, string>> LoadAppliedAsync(MySqlConnection conn, CancellationToken ct)
    {
        var map = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
        await using var cmd = new MySqlCommand($"SELECT filename, checksum FROM {TrackingTable}", conn);
        await using var reader = await cmd.ExecuteReaderAsync(ct).ConfigureAwait(false);
        while (await reader.ReadAsync(ct).ConfigureAwait(false))
            map[reader.GetString(0)] = reader.GetString(1);
        return map;
    }

    /// <summary>
    /// MySqlCommand exécute UNE commande à la fois. Pour un fichier .sql contenant
    /// plusieurs statements séparés par <c>;</c>, on parse manuellement (en respectant
    /// les chaînes / commentaires) et on exécute statement-par-statement dans la
    /// même transaction. Évite la dépendance à <c>MySqlScript</c> et permet de
    /// supporter <see cref="CancellationToken"/> proprement.
    /// </summary>
    private static async Task ExecuteScriptAsync(MySqlConnection conn, MySqlTransaction tx, string sql, CancellationToken ct)
    {
        foreach (var stmt in SplitStatements(sql))
        {
            ct.ThrowIfCancellationRequested();
            var trimmed = stmt.Trim();
            if (string.IsNullOrEmpty(trimmed)) continue;
            // Skip si juste un commentaire ou une ligne blanche
            if (IsAllCommentsOrWhitespace(trimmed)) continue;
            await using var cmd = new MySqlCommand(trimmed, conn, tx);
            await cmd.ExecuteNonQueryAsync(ct).ConfigureAwait(false);
        }
    }

    private static bool IsAllCommentsOrWhitespace(string s)
    {
        // Heuristique simple : si après strip des commentaires il reste des trucs non-blancs.
        var stripped = System.Text.RegularExpressions.Regex.Replace(s, @"(--[^\n]*)|(/\*[\s\S]*?\*/)", "");
        return string.IsNullOrWhiteSpace(stripped);
    }

    /// <summary>
    /// Splitter SQL minimaliste : sépare par <c>;</c> en respectant les chaînes
    /// (' et "), les identifiants (`...`) et les commentaires (-- ... et /* ... */).
    /// Suffisant pour la plupart des fichiers de migration courants ; pour des
    /// triggers/procs avec DELIMITER, écrire chaque statement dans un fichier séparé.
    /// </summary>
    private static IEnumerable<string> SplitStatements(string sql)
    {
        var sb = new StringBuilder();
        int i = 0;
        char? quote = null;
        bool inLineComment = false;
        bool inBlockComment = false;

        while (i < sql.Length)
        {
            char c = sql[i];
            char next = i + 1 < sql.Length ? sql[i + 1] : '\0';

            if (inLineComment)
            {
                sb.Append(c);
                if (c == '\n') inLineComment = false;
                i++;
                continue;
            }
            if (inBlockComment)
            {
                sb.Append(c);
                if (c == '*' && next == '/') { sb.Append(next); i += 2; inBlockComment = false; continue; }
                i++;
                continue;
            }
            if (quote is not null)
            {
                sb.Append(c);
                if (c == '\\' && i + 1 < sql.Length) { sb.Append(sql[i + 1]); i += 2; continue; }
                if (c == quote) quote = null;
                i++;
                continue;
            }

            // hors-quote, hors-commentaire
            if (c == '-' && next == '-') { sb.Append(c).Append(next); i += 2; inLineComment = true; continue; }
            if (c == '/' && next == '*') { sb.Append(c).Append(next); i += 2; inBlockComment = true; continue; }
            if (c == '\'' || c == '"' || c == '`') { quote = c; sb.Append(c); i++; continue; }

            if (c == ';')
            {
                yield return sb.ToString();
                sb.Clear();
                i++;
                continue;
            }
            sb.Append(c);
            i++;
        }
        if (sb.Length > 0)
        {
            var tail = sb.ToString().Trim();
            if (tail.Length > 0) yield return tail;
        }
    }

    private static string ComputeSha256(string text)
    {
        var bytes = Encoding.UTF8.GetBytes(text);
        var hash = SHA256.HashData(bytes);
        return Convert.ToHexString(hash).ToLowerInvariant();
    }
}
