using System.Data.Common;
using System.Globalization;
using System.IO.Compression;
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;
using Eis.Infrastructure.Authentication;

namespace Eis.Infrastructure.Data;

public enum DatabaseMaintenanceMode
{
    Init,
    Reset,
    Seed
}

public sealed record DatabaseMaintenanceResult(
    DatabaseMaintenanceMode Mode,
    string Target,
    bool DryRun,
    string? BackupPath,
    DemoSeedSummary? Summary);

public sealed record DemoSeedSummary(
    int Schools,
    int Users,
    int Candidates,
    int Registrations,
    int Results,
    int AdmissionRecords,
    int FirstRoundPreferences,
    int Operations);

public sealed partial class DatabaseMaintenanceService(DatabaseOptions options)
{
    private const int SchemaVersion = 20;

    private static readonly string[] RelationalTables =
    [
        "schema_metadata",
        "organization",
        "schools",
        "school_classes",
        "school_student_partitions",
        "users",
        "candidate_profiles",
        "notices",
        "exams",
        "exam_data_partitions",
        "exam_subjects",
        "registrations",
        "registration_subjects",
        "admission_number_rules",
        "exam_arrangement_plans",
        "admit_cards",
        "admit_card_subjects",
        "results",
        "test_centers",
        "test_rooms",
        "center_change_requests",
        "center_change_rooms",
        "number_rules",
        "number_rule_segments",
        "candidate_account_batches",
        "candidate_account_batch_items",
        "workflow_definitions",
        "workflow_steps",
        "workflow_instances",
        "workflow_actions",
        "admission_records",
        "audit_logs"
    ];

    private static readonly string[] BusinessTables =
    [
        "schools",
        "school_classes",
        "candidate_profiles",
        "notices",
        "exams",
        "exam_subjects",
        "registrations",
        "registration_subjects",
        "exam_arrangement_plans",
        "admit_cards",
        "admit_card_subjects",
        "results",
        "test_centers",
        "test_rooms",
        "center_change_requests",
        "center_change_rooms",
        "candidate_account_batches",
        "candidate_account_batch_items",
        "workflow_instances",
        "workflow_actions",
        "admission_records",
        "audit_logs"
    ];

    public async Task<DatabaseMaintenanceResult> ExecuteAsync(
        DatabaseMaintenanceMode mode,
        string? confirmedTarget,
        bool force,
        bool dryRun,
        CancellationToken cancellationToken)
    {
        ValidateTarget();
        if (mode is (DatabaseMaintenanceMode.Reset or DatabaseMaintenanceMode.Seed) && !dryRun)
        {
            RequireConfirmedTarget(confirmedTarget);
        }

        if (dryRun)
        {
            if (options.Client == "mysql")
            {
                await ValidateMySqlDryRunAsync(force, cancellationToken);
            }
            return new DatabaseMaintenanceResult(
                mode,
                options.TargetDescription,
                true,
                null,
                mode == DatabaseMaintenanceMode.Seed ? (await LoadDemoSeedAsync(cancellationToken)).Summary : null);
        }

        if (mode == DatabaseMaintenanceMode.Init)
        {
            await CreateInitializer(options).InitializeAsync(cancellationToken);
            return new DatabaseMaintenanceResult(mode, options.TargetDescription, false, null, null);
        }

        return options.Client == "sqlite"
            ? await ReplaceSqliteAsync(mode, cancellationToken)
            : await ReplaceMySqlAsync(mode, force, cancellationToken);
    }

    private async Task<DatabaseMaintenanceResult> ReplaceSqliteAsync(
        DatabaseMaintenanceMode mode,
        CancellationToken cancellationToken)
    {
        var targetPath = options.SqlitePath
                         ?? throw new InvalidOperationException("SQLite 数据库路径不存在");
        var directory = Path.GetDirectoryName(targetPath)
                        ?? throw new InvalidOperationException("SQLite 数据库目录无效");
        Directory.CreateDirectory(directory);
        var stagingPath = Path.Combine(
            directory,
            $".{Path.GetFileName(targetPath)}.staging-{Guid.NewGuid():N}");
        var stagingOptions = DatabaseOptions.CreateSqlite(stagingPath);
        DemoSeedSummary? summary = null;
        try
        {
            await CreateInitializer(stagingOptions).InitializeAsync(cancellationToken);
            if (mode == DatabaseMaintenanceMode.Seed)
            {
                summary = await ImportDemoSeedAsync(stagingOptions, cancellationToken);
            }

            await ValidateSqliteOutputAsync(stagingOptions, mode, cancellationToken);
            string? backupPath = null;
            if (File.Exists(targetPath))
            {
                await CheckpointSqliteAsync(targetPath, cancellationToken);
                backupPath = $"{targetPath}.backup-{DateTimeOffset.UtcNow:yyyyMMddTHHmmssfffZ}";
                File.Copy(targetPath, backupPath, overwrite: false);
            }

            DeleteSqliteSidecars(targetPath);
            File.Move(stagingPath, targetPath, overwrite: true);
            return new DatabaseMaintenanceResult(
                mode,
                targetPath,
                false,
                backupPath,
                summary);
        }
        finally
        {
            DeleteSqliteSidecars(stagingPath);
            if (File.Exists(stagingPath))
            {
                File.Delete(stagingPath);
            }
        }
    }

    private async Task<DatabaseMaintenanceResult> ReplaceMySqlAsync(
        DatabaseMaintenanceMode mode,
        bool force,
        CancellationToken cancellationToken)
    {
        var initializer = CreateInitializer(options);
        await initializer.InitializeAsync(cancellationToken);
        var factory = new RelationalConnectionFactory(options);
        await using var connection = await factory.OpenAsync(cancellationToken);
        var nonEmpty = await FindNonEmptyBusinessTablesAsync(connection, cancellationToken);
        var recognizedDemo = nonEmpty.Count > 0 &&
                             await IsRecognizedDemoDatabaseAsync(connection, cancellationToken);
        if (nonEmpty.Count > 0 && !force && !recognizedDemo)
        {
            throw new InvalidOperationException(
                $"MySQL 数据库已有业务数据（{string.Join(", ", nonEmpty.Take(8))}" +
                $"{(nonEmpty.Count > 8 ? ", ..." : "")}）。仅确认是可覆盖测试库时才可追加 --force");
        }

        var partitionTables = await ReadPartitionTablesAsync(connection, cancellationToken);
        await ExecuteNonQueryAsync(connection, null, "SET FOREIGN_KEY_CHECKS = 0", [], cancellationToken);
        DemoSeedSummary? summary = null;
        try
        {
            await using var transaction = await connection.BeginTransactionAsync(cancellationToken);
            try
            {
                await ClearApplicationDataAsync(connection, transaction, cancellationToken);
                if (mode == DatabaseMaintenanceMode.Seed)
                {
                    summary = await ImportDemoSeedAsync(connection, transaction, cancellationToken);
                }
                else
                {
                    await initializer.SeedBaseStateAsync(connection, transaction, cancellationToken);
                }
                await transaction.CommitAsync(cancellationToken);
            }
            catch
            {
                await transaction.RollbackAsync(cancellationToken);
                throw;
            }
        }
        finally
        {
            await ExecuteNonQueryAsync(connection, null, "SET FOREIGN_KEY_CHECKS = 1", [], cancellationToken);
        }

        foreach (var table in partitionTables)
        {
            await ExecuteNonQueryAsync(
                connection,
                null,
                $"DROP TABLE IF EXISTS `{table}`",
                [],
                cancellationToken);
        }

        return new DatabaseMaintenanceResult(
            mode,
            options.TargetDescription,
            false,
            null,
            summary);
    }

    private async Task ValidateMySqlDryRunAsync(
        bool force,
        CancellationToken cancellationToken)
    {
        var factory = new RelationalConnectionFactory(options);
        await using var connection = await factory.OpenAsync(cancellationToken);
        var existingTables = new HashSet<string>(StringComparer.Ordinal);
        await using (var command = connection.CreateCommand())
        {
            command.CommandText =
                """
                SELECT TABLE_NAME
                FROM information_schema.TABLES
                WHERE TABLE_SCHEMA = DATABASE() AND TABLE_TYPE = 'BASE TABLE'
                """;
            await using var reader = await command.ExecuteReaderAsync(cancellationToken);
            while (await reader.ReadAsync(cancellationToken))
            {
                existingTables.Add(reader.GetString(0));
            }
        }

        var applicationTables = RelationalTables.Where(existingTables.Contains).ToArray();
        if (applicationTables.Length == 0)
        {
            return;
        }
        if (applicationTables.Length != RelationalTables.Length)
        {
            var missing = RelationalTables.Where(item => !existingTables.Contains(item));
            throw new InvalidOperationException(
                $"MySQL 数据库结构不完整，缺少：{string.Join(", ", missing)}");
        }

        var version = await ScalarLongAsync(
            connection,
            "SELECT schema_version FROM schema_metadata WHERE id = 1",
            cancellationToken);
        if (version != SchemaVersion)
        {
            throw new InvalidOperationException(
                $"MySQL 数据库结构版本为 v{version}，当前工具要求 v{SchemaVersion}");
        }

        var nonEmpty = await FindNonEmptyBusinessTablesAsync(connection, cancellationToken);
        var recognizedDemo = nonEmpty.Count > 0 &&
                             await IsRecognizedDemoDatabaseAsync(connection, cancellationToken);
        if (nonEmpty.Count > 0 && !force && !recognizedDemo)
        {
            throw new InvalidOperationException(
                $"MySQL 数据库已有业务数据（{string.Join(", ", nonEmpty.Take(8))}" +
                $"{(nonEmpty.Count > 8 ? ", ..." : "")}）。预检拒绝覆盖；确认是测试库时追加 --force");
        }
    }

    private async Task<DemoSeedSummary> ImportDemoSeedAsync(
        DatabaseOptions targetOptions,
        CancellationToken cancellationToken)
    {
        var factory = new RelationalConnectionFactory(targetOptions);
        await using var connection = await factory.OpenAsync(cancellationToken);
        await ExecuteNonQueryAsync(connection, null, "PRAGMA foreign_keys = OFF", [], cancellationToken);
        try
        {
            await using var transaction = await connection.BeginTransactionAsync(cancellationToken);
            try
            {
                await ClearApplicationDataAsync(connection, transaction, cancellationToken);
                var summary = await ImportDemoSeedAsync(connection, transaction, cancellationToken);
                await transaction.CommitAsync(cancellationToken);
                return summary;
            }
            catch
            {
                await transaction.RollbackAsync(cancellationToken);
                throw;
            }
        }
        finally
        {
            await ExecuteNonQueryAsync(connection, null, "PRAGMA foreign_keys = ON", [], cancellationToken);
        }
    }

    private static async Task<DemoSeedSummary> ImportDemoSeedAsync(
        DbConnection connection,
        DbTransaction transaction,
        CancellationToken cancellationToken)
    {
        var payload = await LoadDemoSeedAsync(cancellationToken);
        if (payload.SchemaVersion != SchemaVersion)
        {
            throw new InvalidOperationException(
                $"演示数据结构版本为 v{payload.SchemaVersion}，当前程序要求 v{SchemaVersion}");
        }

        foreach (var operation in payload.Operations)
        {
            var values = operation.Parameters
                .Select(JsonValue)
                .ToArray();
            await ExecuteNonQueryAsync(
                connection,
                transaction,
                RewriteParameters(operation.Sql, values.Length),
                values,
                cancellationToken);
        }
        return payload.Summary;
    }

    private static async Task ClearApplicationDataAsync(
        DbConnection connection,
        DbTransaction transaction,
        CancellationToken cancellationToken)
    {
        await ExecuteNonQueryAsync(connection, transaction, "DELETE FROM exams", [], cancellationToken);
        foreach (var table in RelationalTables.Reverse()
                     .Where(item => item is not ("schema_metadata" or "exams")))
        {
            await ExecuteNonQueryAsync(
                connection,
                transaction,
                $"DELETE FROM `{table}`",
                [],
                cancellationToken);
        }
    }

    private static async Task ValidateSqliteOutputAsync(
        DatabaseOptions targetOptions,
        DatabaseMaintenanceMode mode,
        CancellationToken cancellationToken)
    {
        var factory = new RelationalConnectionFactory(targetOptions);
        await using var connection = await factory.OpenAsync(cancellationToken);
        var version = await ScalarLongAsync(
            connection,
            "SELECT schema_version FROM schema_metadata WHERE id = 1",
            cancellationToken);
        var users = await ScalarLongAsync(connection, "SELECT COUNT(*) FROM users", cancellationToken);
        if (version != SchemaVersion || users == 0)
        {
            throw new InvalidOperationException("生成的 SQLite 数据库未通过结构和账号校验");
        }

        if (mode == DatabaseMaintenanceMode.Seed)
        {
            var candidates = await ScalarLongAsync(
                connection,
                "SELECT COUNT(*) FROM candidate_profiles",
                cancellationToken);
            var results = await ScalarLongAsync(connection, "SELECT COUNT(*) FROM results", cancellationToken);
            if (candidates != 1200 || results != 10800)
            {
                throw new InvalidOperationException(
                    $"演示数据校验失败：考生 {candidates}，成绩 {results}");
            }
        }
    }

    private static async Task CheckpointSqliteAsync(
        string targetPath,
        CancellationToken cancellationToken)
    {
        var factory = new RelationalConnectionFactory(DatabaseOptions.CreateSqlite(targetPath));
        await using var connection = await factory.OpenAsync(cancellationToken);
        await ExecuteNonQueryAsync(
            connection,
            null,
            "PRAGMA wal_checkpoint(TRUNCATE)",
            [],
            cancellationToken);
    }

    private static void DeleteSqliteSidecars(string path)
    {
        foreach (var suffix in new[] { "-wal", "-shm" })
        {
            var sidecar = path + suffix;
            if (File.Exists(sidecar))
            {
                File.Delete(sidecar);
            }
        }
    }

    private async Task<List<string>> FindNonEmptyBusinessTablesAsync(
        DbConnection connection,
        CancellationToken cancellationToken)
    {
        var result = new List<string>();
        foreach (var table in BusinessTables)
        {
            var count = await ScalarLongAsync(
                connection,
                $"SELECT COUNT(*) FROM `{table}`",
                cancellationToken);
            if (count > 0)
            {
                result.Add($"{table}={count}");
            }
        }

        var users = await ScalarLongAsync(connection, "SELECT COUNT(*) FROM users", cancellationToken);
        if (users > 1)
        {
            result.Add($"users={users}");
        }
        return result;
    }

    private static async Task<bool> IsRecognizedDemoDatabaseAsync(
        DbConnection connection,
        CancellationToken cancellationToken)
    {
        var users = await ScalarLongAsync(
            connection,
            "SELECT COUNT(*) FROM users WHERE id LIKE 'usr_bulk_%'",
            cancellationToken);
        var schools = await ScalarLongAsync(
            connection,
            """
            SELECT COUNT(*) FROM schools
            WHERE id IN ('school_hz1', 'school_hz3', 'school_hz5', 'school_hz7', 'school_hz9')
            """,
            cancellationToken);
        return users >= 1100 && schools == 5;
    }

    private static async Task<string[]> ReadPartitionTablesAsync(
        DbConnection connection,
        CancellationToken cancellationToken)
    {
        var tables = new List<string>();
        await using (var command = connection.CreateCommand())
        {
            command.CommandText =
                "SELECT candidates_table, admissions_table, results_table, centers_table FROM exam_data_partitions";
            await using var reader = await command.ExecuteReaderAsync(cancellationToken);
            while (await reader.ReadAsync(cancellationToken))
            {
                for (var index = 0; index < 4; index++)
                {
                    tables.Add(reader.GetString(index));
                }
            }
        }
        await using (var command = connection.CreateCommand())
        {
            command.CommandText = "SELECT students_table FROM school_student_partitions";
            await using var reader = await command.ExecuteReaderAsync(cancellationToken);
            while (await reader.ReadAsync(cancellationToken))
            {
                tables.Add(reader.GetString(0));
            }
        }

        if (tables.Any(item => !PartitionTablePattern().IsMatch(item)))
        {
            throw new InvalidOperationException("分表登记中存在非法表名，拒绝替换数据库");
        }
        return tables.Distinct(StringComparer.Ordinal).ToArray();
    }

    private void ValidateTarget()
    {
        if (options.Client == "sqlite" && string.IsNullOrWhiteSpace(options.SqlitePath))
        {
            throw new InvalidOperationException("未指定 SQLite 数据库路径");
        }
        if (options.Client == "mysql")
        {
            var name = options.MySqlDatabaseName?.Trim() ?? "";
            var normalized = name.ToLowerInvariant();
            if (normalized.Length == 0 ||
                normalized is "mysql" or "information_schema" or "performance_schema" or "sys")
            {
                throw new InvalidOperationException($"拒绝操作 MySQL 系统数据库：{name}");
            }
        }
    }

    private void RequireConfirmedTarget(string? confirmedTarget)
    {
        var expected = options.Client == "sqlite"
            ? options.SqlitePath
            : options.MySqlDatabaseName;
        var comparison = options.Client == "sqlite" && OperatingSystem.IsWindows()
            ? StringComparison.OrdinalIgnoreCase
            : StringComparison.Ordinal;
        if (string.IsNullOrWhiteSpace(confirmedTarget) ||
            !string.Equals(expected, confirmedTarget, comparison))
        {
            throw new InvalidOperationException(
                $"这是破坏性操作。请使用 --confirm-target \"{expected}\" 明确确认目标");
        }
    }

    private static DatabaseInitializer CreateInitializer(DatabaseOptions targetOptions) =>
        new(
            new RelationalConnectionFactory(targetOptions),
            targetOptions,
            new PasswordCompatibilityService());

    private static async Task<DemoSeedPayload> LoadDemoSeedAsync(CancellationToken cancellationToken)
    {
        var assembly = typeof(DatabaseMaintenanceService).Assembly;
        var name = assembly.GetManifestResourceNames()
            .Single(item => item.EndsWith("demo-seed-operations.json.gz", StringComparison.Ordinal));
        await using var source = assembly.GetManifestResourceStream(name)
                                 ?? throw new InvalidOperationException("内置演示数据包不存在");
        await using var gzip = new GZipStream(source, CompressionMode.Decompress);
        return await JsonSerializer.DeserializeAsync<DemoSeedPayload>(
                   gzip,
                   new JsonSerializerOptions { PropertyNameCaseInsensitive = true },
                   cancellationToken)
               ?? throw new InvalidOperationException("内置演示数据包格式无效");
    }

    private static string RewriteParameters(string sql, int count)
    {
        var index = 0;
        var rewritten = ParameterPattern().Replace(sql, _ => $"@p{index++}");
        if (index != count)
        {
            throw new InvalidOperationException(
                $"演示数据 SQL 参数数量不匹配：预期 {index}，实际 {count}");
        }
        return rewritten;
    }

    private static object JsonValue(JsonElement value) =>
        value.ValueKind switch
        {
            JsonValueKind.Null => DBNull.Value,
            JsonValueKind.String => value.GetString() ?? "",
            JsonValueKind.Number when value.TryGetInt64(out var integer) => integer,
            JsonValueKind.Number => value.GetDouble(),
            JsonValueKind.True => 1,
            JsonValueKind.False => 0,
            _ => value.GetRawText()
        };

    private static async Task ExecuteNonQueryAsync(
        DbConnection connection,
        DbTransaction? transaction,
        string sql,
        IReadOnlyList<object> values,
        CancellationToken cancellationToken)
    {
        await using var command = connection.CreateCommand();
        command.CommandText = sql;
        command.Transaction = transaction;
        for (var index = 0; index < values.Count; index++)
        {
            var parameter = command.CreateParameter();
            parameter.ParameterName = $"@p{index}";
            parameter.Value = values[index];
            command.Parameters.Add(parameter);
        }
        await command.ExecuteNonQueryAsync(cancellationToken);
    }

    private static async Task<long> ScalarLongAsync(
        DbConnection connection,
        string sql,
        CancellationToken cancellationToken)
    {
        await using var command = connection.CreateCommand();
        command.CommandText = sql;
        return Convert.ToInt64(await command.ExecuteScalarAsync(cancellationToken), CultureInfo.InvariantCulture);
    }

    [GeneratedRegex(@"\?", RegexOptions.CultureInvariant)]
    private static partial Regex ParameterPattern();

    [GeneratedRegex(@"^[a-z][a-z0-9_]{0,63}$", RegexOptions.CultureInvariant)]
    private static partial Regex PartitionTablePattern();

    private sealed record DemoSeedPayload(
        int Version,
        int SchemaVersion,
        string GeneratedAt,
        DemoSeedSummary Summary,
        DemoSeedOperation[] Operations);

    private sealed record DemoSeedOperation(
        string Sql,
        [property: JsonPropertyName("params")] JsonElement[] Parameters);
}
