using System.Data.Common;
using System.Globalization;
using System.Reflection;
using System.Text.Json;
using Eis.Infrastructure.Authentication;
using Eis.Infrastructure.Administration;
using Microsoft.Extensions.DependencyInjection;

namespace Eis.Infrastructure.Data;

internal sealed class DatabaseInitializer(
    IRelationalConnectionFactory connectionFactory,
    DatabaseOptions options,
    PasswordCompatibilityService passwords)
{
    private const int SchemaVersion = 20;
    private const string MySqlStatementSeparator = "\n-- EIS-STATEMENT\n";

    public async Task InitializeAsync(CancellationToken cancellationToken)
    {
        await using var connection = await connectionFactory.OpenAsync(cancellationToken);
        var schema = await ReadSchemaAsync(options.Client, cancellationToken);
        if (options.Client == "sqlite")
        {
            await ExecuteAsync(connection, schema, cancellationToken);
        }
        else
        {
            foreach (var statement in schema.ReplaceLineEndings("\n").Split(
                         MySqlStatementSeparator,
                         StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
            {
                await ExecuteAsync(connection, statement, cancellationToken);
            }
        }
        await SeedBaseStateAsync(connection, null, cancellationToken);
    }

    internal async Task SeedBaseStateAsync(
        DbConnection connection,
        DbTransaction? transaction,
        CancellationToken cancellationToken)
    {
        var now = DateTimeOffset.UtcNow.ToString("yyyy-MM-dd'T'HH:mm:ss.fff'Z'", CultureInfo.InvariantCulture);
        if (!await ExistsAsync(connection, transaction, "schema_metadata", cancellationToken))
        {
            await ExecuteAsync(connection,
                """
                INSERT INTO schema_metadata (id, schema_version, app_version, self_registration_enabled, created_at)
                VALUES (1, @version, @version, 0, @createdAt)
                """,
                [("@version", SchemaVersion), ("@createdAt", now)], transaction, cancellationToken);
        }
        else
        {
            var version = await ScalarLongAsync(connection,
                "SELECT schema_version FROM schema_metadata WHERE id = 1", transaction, cancellationToken);
            if (version < SchemaVersion)
            {
                throw new InvalidOperationException(
                    $"数据库结构版本为 {version}，低于 ASP.NET Core 要求的 {SchemaVersion}；请先备份并执行旧版本升级流程");
            }
        }
        if (!await ExistsAsync(connection, transaction, "organization", cancellationToken))
        {
            await ExecuteAsync(connection,
                "INSERT INTO organization (id, name, code, phone, address) VALUES (1, @name, @code, '', '')",
                [("@name", "考试服务平台"), ("@code", "EXAM-SERVICE")], transaction, cancellationToken);
        }
        if (await ScalarLongAsync(connection, "SELECT COUNT(*) FROM users", transaction, cancellationToken) == 0)
        {
            var username = Environment.GetEnvironmentVariable("INITIAL_ADMIN_USERNAME")?.Trim();
            var password = Environment.GetEnvironmentVariable("INITIAL_ADMIN_PASSWORD") ?? "Admin123!";
            var displayName = Environment.GetEnvironmentVariable("INITIAL_ADMIN_DISPLAY_NAME")?.Trim();
            await ExecuteAsync(connection,
                """
                INSERT INTO users (
                  id, username, password_hash, role, admin_level, active, must_change_password,
                  totp_enabled, totp_recovery_codes, display_name, created_at
                ) VALUES (
                  'usr_admin', @username, @passwordHash, 'admin', 'super', 1, 0, 0, '[]', @displayName, @createdAt
                )
                """,
                [
                    ("@username", string.IsNullOrWhiteSpace(username) ? "admin" : username),
                    ("@passwordHash", passwords.Hash(password)),
                    ("@displayName", string.IsNullOrWhiteSpace(displayName) ? "系统管理员" : displayName),
                    ("@createdAt", now)
                ], transaction, cancellationToken);
        }
        await SeedNumberRuleAsync(connection, transaction, now, cancellationToken);
        await SeedAdmissionNumberRulesAsync(connection, transaction, now, cancellationToken);
        await SeedWorkflowsAsync(connection, transaction, now, cancellationToken);
    }

    private static async Task SeedNumberRuleAsync(
        DbConnection connection,
        DbTransaction? transaction,
        string now,
        CancellationToken cancellationToken)
    {
        if (await ScalarLongAsync(connection, "SELECT COUNT(*) FROM number_rules", transaction, cancellationToken) > 0) return;
        await ExecuteAsync(connection,
            """
            INSERT INTO number_rules (id, name, separator, active, created_by, updated_at)
            VALUES ('rule_default', @name, '-', 1, 'usr_admin', @updatedAt)
            """,
            [("@name", "年度学校性别流水号"), ("@updatedAt", now)], transaction, cancellationToken);
        var segments = new[]
        {
            ("segment_year", 1, "year", "", 4),
            ("segment_school", 2, "school_code", "", 0),
            ("segment_gender", 3, "gender", "", 0),
            ("segment_sequence", 4, "sequence", "", 4)
        };
        foreach (var segment in segments)
        {
            await ExecuteAsync(connection,
                """
                INSERT INTO number_rule_segments (id, rule_id, position, type, value, width)
                VALUES (@id, 'rule_default', @position, @type, @value, @width)
                """,
                [
                    ("@id", segment.Item1), ("@position", segment.Item2), ("@type", segment.Item3),
                    ("@value", segment.Item4), ("@width", segment.Item5)
                ], transaction, cancellationToken);
        }
    }

    private static async Task SeedAdmissionNumberRulesAsync(
        DbConnection connection,
        DbTransaction? transaction,
        string now,
        CancellationToken cancellationToken)
    {
        if (await ScalarLongAsync(connection, "SELECT COUNT(*) FROM admission_number_rules", transaction, cancellationToken) > 0) return;
        var rules = new[]
        {
            new AdmissionRule("admit_rule_district_room_seat", "district_room_seat", "县区编号 + 考场号 + 座位号",
                "适合县区统一组织，号码直接反映县区、考试考场与座位。", "32070603108",
                [new("district_code", "县区编号", 6), new("exam_room_code", "考场号", 3), new("seat", "座位号", 2)]),
            new AdmissionRule("admit_rule_district_room_sequence", "district_room_sequence", "县区号 + 考场号 + 流水号",
                "以县区为流水边界，适合不希望座位号直接出现在号码中的场景。", "3207060310028",
                [new("district_code", "县区号", 6), new("exam_room_code", "考场号", 3), new("sequence", "流水号", 4)]),
            new AdmissionRule("admit_rule_center_school_room_seat", "center_school_room_seat", "考点学校代码 + 考场号 + 座位号",
                "号码前缀取考点所属学校代码，便于考点现场快速识别。", "HZ0303108",
                [new("center_school_code", "考点学校代码", null), new("exam_room_code", "考场号", 3), new("seat", "座位号", 2)]),
            new AdmissionRule("admit_rule_candidate_school_room_seat", "candidate_school_room_seat", "考生学校代码 + 考场号 + 座位号",
                "号码前缀保留考生学籍学校代码，适合按生源学校归档。", "HZ0103108",
                [new("candidate_school_code", "考生学校代码", null), new("exam_room_code", "考场号", 3), new("seat", "座位号", 2)])
        };
        foreach (var rule in rules)
        {
            var segments = JsonSerializer.Serialize(rule.Segments.Select(item => new
            {
                source = item.Source, label = item.Label, width = item.Width
            }));
            await ExecuteAsync(connection,
                """
                INSERT INTO admission_number_rules (
                  id, code, name, description, separator, segments_json, example, active, created_at
                ) VALUES (
                  @id, @code, @name, @description, '', @segments, @example, 1, @createdAt
                )
                """,
                [
                    ("@id", rule.Id), ("@code", rule.Code), ("@name", rule.Name),
                    ("@description", rule.Description), ("@segments", segments),
                    ("@example", rule.Example), ("@createdAt", now)
                ], transaction, cancellationToken);
        }
    }

    private static async Task SeedWorkflowsAsync(
        DbConnection connection,
        DbTransaction? transaction,
        string now,
        CancellationToken cancellationToken)
    {
        if (await ScalarLongAsync(connection, "SELECT COUNT(*) FROM workflow_definitions", transaction, cancellationToken) > 0) return;
        var workflows = new[]
        {
            new Workflow("workflow_profile", "profile_change", "考生信息修改审批",
                [new("workflow_profile_step_1", "学校学籍复核", "school"), new("workflow_profile_step_2", "考试中心终审", "super")]),
            new Workflow("workflow_registration", "registration_review", "考试报名审核",
                [new("workflow_registration_step_1", "学校报名初审", "school"), new("workflow_registration_step_2", "考试中心终审", "super")]),
            new Workflow("workflow_center", "center_change", "考点考场变更审批",
                [new("workflow_center_step_1", "考试中心考务终审", "super")]),
            new Workflow("workflow_account_batch", "candidate_account_batch", "批量报名号申领审批",
                [new("workflow_account_batch_step_1", "考试中心账号终审", "super")]),
            new Workflow("workflow_score_appeal", "score_appeal", "考生成绩复议",
                [
                    new("workflow_score_appeal_step_1", "班级情况核验", "class"),
                    new("workflow_score_appeal_step_2", "学校成绩复核", "school"),
                    new("workflow_score_appeal_step_3", "考试中心终审", "super")
                ])
        };
        foreach (var workflow in workflows)
        {
            await ExecuteAsync(connection,
                """
                INSERT INTO workflow_definitions (id, business_type, name, active, updated_by, updated_at)
                VALUES (@id, @type, @name, 1, 'usr_admin', @updatedAt)
                """,
                [
                    ("@id", workflow.Id), ("@type", workflow.Type),
                    ("@name", workflow.Name), ("@updatedAt", now)
                ], transaction, cancellationToken);
            for (var index = 0; index < workflow.Steps.Length; index++)
            {
                var step = workflow.Steps[index];
                await ExecuteAsync(connection,
                    """
                    INSERT INTO workflow_steps (id, workflow_id, position, name, admin_level)
                    VALUES (@id, @workflowId, @position, @name, @level)
                    """,
                    [
                        ("@id", step.Id), ("@workflowId", workflow.Id), ("@position", index + 1),
                        ("@name", step.Name), ("@level", step.Level)
                    ], transaction, cancellationToken);
            }
        }
    }

    private static async Task<bool> ExistsAsync(
        DbConnection connection,
        DbTransaction? transaction,
        string table,
        CancellationToken cancellationToken) =>
        await ScalarLongAsync(connection, $"SELECT COUNT(*) FROM {table}", transaction, cancellationToken) > 0;

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

    private static Task ExecuteAsync(
        DbConnection connection,
        string sql,
        CancellationToken cancellationToken) =>
        ExecuteAsync(connection, sql, [], null, cancellationToken);

    private static async Task ExecuteAsync(
        DbConnection connection,
        string sql,
        IReadOnlyList<(string Name, object Value)> values,
        DbTransaction? transaction,
        CancellationToken cancellationToken)
    {
        await using var command = connection.CreateCommand();
        command.CommandText = sql;
        command.Transaction = transaction;
        foreach (var value in values)
        {
            var parameter = command.CreateParameter();
            parameter.ParameterName = value.Name;
            parameter.Value = value.Value;
            command.Parameters.Add(parameter);
        }
        await command.ExecuteNonQueryAsync(cancellationToken);
    }

    private static async Task<string> ReadSchemaAsync(string client, CancellationToken cancellationToken)
    {
        var assembly = typeof(DatabaseInitializer).Assembly;
        var resourceSuffix = $"Schema.{client}.sql";
        var name = assembly.GetManifestResourceNames()
            .Single(item => item.EndsWith(resourceSuffix, StringComparison.Ordinal));
        await using var stream = assembly.GetManifestResourceStream(name)
                                 ?? throw new InvalidOperationException("内置数据库结构资源不存在");
        using var reader = new StreamReader(stream);
        return await reader.ReadToEndAsync(cancellationToken);
    }

    private sealed record AdmissionRule(
        string Id, string Code, string Name, string Description, string Example, AdmissionSegment[] Segments);
    private sealed record AdmissionSegment(string Source, string Label, int? Width);
    private sealed record Workflow(string Id, string Type, string Name, WorkflowStep[] Steps);
    private sealed record WorkflowStep(string Id, string Name, string Level);
}

public static class DatabaseInitializationExtensions
{
    public static async Task InitializeEisDatabaseAsync(
        this IServiceProvider serviceProvider,
        CancellationToken cancellationToken = default)
    {
        await using var scope = serviceProvider.CreateAsyncScope();
        await scope.ServiceProvider.GetRequiredService<DatabaseInitializer>()
            .InitializeAsync(cancellationToken);
        await scope.ServiceProvider.GetRequiredService<AdminResultStatisticsBackfillService>()
            .EnsureAsync(cancellationToken);
    }
}
