using System.Data.Common;
using System.Globalization;
using System.Text.Json.Nodes;
using Eis.Infrastructure.Data;

namespace Eis.Infrastructure.Administration;

internal sealed record AdminArrangementSnapshot(
    IReadOnlyList<AdmissionNumberRule> Rules,
    IReadOnlyList<StoredArrangementPlan> Plans,
    string OrganizationName);

internal sealed record AdmissionNumberRule(
    string Id,
    string Code,
    string Name,
    string Description,
    string Separator,
    IReadOnlyList<AdmissionNumberSegment> Segments,
    string Example,
    bool Active,
    string CreatedAt);

internal sealed record AdmissionNumberSegment(string Source, string Label, int? Width);

internal sealed record StoredArrangementPlan(
    string Id,
    string ExamId,
    string NumberRuleId,
    string MixingScope,
    string RandomSeed,
    int CandidateCount,
    int CenterCount,
    int SubjectAssignmentCount,
    int SubjectCombinationCount,
    double SameSchoolCenterRate,
    IReadOnlyList<string> Warnings,
    string? GeneratedBy,
    string GeneratedAt);

internal sealed class AdminArrangementSnapshotLoader(IRelationalConnectionFactory connectionFactory)
{
    public async Task<AdminArrangementSnapshot> LoadAsync(CancellationToken cancellationToken)
    {
        await using var connection = await connectionFactory.OpenAsync(cancellationToken);
        var rules = await QueryAsync(
            connection,
            """
            SELECT id, code, name, description, `separator`, segments_json, example, active, created_at
            FROM admission_number_rules ORDER BY created_at, id
            """,
            reader => new AdmissionNumberRule(
                Text(reader, "id"),
                Text(reader, "code"),
                Text(reader, "name"),
                Text(reader, "description"),
                Optional(reader, "separator") ?? "",
                ParseSegments(Optional(reader, "segments_json")),
                Optional(reader, "example") ?? "",
                Boolean(reader, "active"),
                Text(reader, "created_at")),
            cancellationToken);
        var plans = await QueryAsync(
            connection,
            """
            SELECT id, exam_id, number_rule_id, mixing_scope, random_seed, candidate_count, center_count,
              subject_assignment_count, subject_combination_count, same_school_center_rate, warnings_json,
              generated_by, generated_at
            FROM exam_arrangement_plans ORDER BY generated_at DESC, id
            """,
            reader => new StoredArrangementPlan(
                Text(reader, "id"),
                Text(reader, "exam_id"),
                Text(reader, "number_rule_id"),
                Text(reader, "mixing_scope"),
                Text(reader, "random_seed"),
                Integer(reader, "candidate_count"),
                Integer(reader, "center_count"),
                Integer(reader, "subject_assignment_count"),
                Integer(reader, "subject_combination_count"),
                Number(reader, "same_school_center_rate"),
                ParseStrings(Optional(reader, "warnings_json")),
                Optional(reader, "generated_by"),
                Text(reader, "generated_at")),
            cancellationToken);
        await using var organizationCommand = connection.CreateCommand();
        organizationCommand.CommandText = "SELECT name FROM organization WHERE id = 1";
        var organizationValue = await organizationCommand.ExecuteScalarAsync(cancellationToken);
        var organizationName = organizationValue is null or DBNull
            ? "考试服务平台"
            : Convert.ToString(organizationValue, CultureInfo.InvariantCulture) ?? "考试服务平台";
        return new(rules, plans, organizationName);
    }

    private static IReadOnlyList<AdmissionNumberSegment> ParseSegments(string? value)
    {
        try
        {
            return (JsonNode.Parse(value ?? "[]") as JsonArray ?? [])
                .OfType<JsonObject>()
                .Select(item => new AdmissionNumberSegment(
                    item["source"]?.GetValue<string>() ?? "",
                    item["label"]?.GetValue<string>() ?? "",
                    item.ContainsKey("width") ? item["width"]?.GetValue<int>() ?? 0 : null))
                .ToArray();
        }
        catch
        {
            return [];
        }
    }

    private static IReadOnlyList<string> ParseStrings(string? value)
    {
        try
        {
            return (JsonNode.Parse(value ?? "[]") as JsonArray ?? [])
                .Select(item => item?.GetValue<string>() ?? "")
                .ToArray();
        }
        catch
        {
            return [];
        }
    }

    private static async Task<IReadOnlyList<T>> QueryAsync<T>(
        DbConnection connection,
        string sql,
        Func<DbDataReader, T> map,
        CancellationToken cancellationToken)
    {
        await using var command = connection.CreateCommand();
        command.CommandText = sql;
        await using var reader = await command.ExecuteReaderAsync(cancellationToken);
        var output = new List<T>();
        while (await reader.ReadAsync(cancellationToken)) output.Add(map(reader));
        return output;
    }

    private static string Text(DbDataReader reader, string name) =>
        Convert.ToString(reader.GetValue(reader.GetOrdinal(name)), CultureInfo.InvariantCulture) ?? "";

    private static string? Optional(DbDataReader reader, string name)
    {
        var ordinal = reader.GetOrdinal(name);
        return reader.IsDBNull(ordinal)
            ? null
            : Convert.ToString(reader.GetValue(ordinal), CultureInfo.InvariantCulture);
    }

    private static int Integer(DbDataReader reader, string name) =>
        Convert.ToInt32(reader.GetValue(reader.GetOrdinal(name)), CultureInfo.InvariantCulture);

    private static double Number(DbDataReader reader, string name) =>
        Convert.ToDouble(reader.GetValue(reader.GetOrdinal(name)), CultureInfo.InvariantCulture);

    private static bool Boolean(DbDataReader reader, string name) =>
        Convert.ToInt64(reader.GetValue(reader.GetOrdinal(name)), CultureInfo.InvariantCulture) != 0;
}

internal sealed record ArrangementSummary(
    int CandidateCount,
    int CenterCount,
    int SubjectAssignmentCount,
    int SubjectCombinationCount,
    int SameSchoolCenterCount,
    double SameSchoolCenterRate,
    int ReservedSpareRooms);

internal sealed record ArrangementCard(
    string RegistrationId,
    string Number,
    string CenterId,
    string TestCenter,
    string CenterCode,
    string CenterAddress,
    string GeneratedAt,
    IReadOnlyList<ArrangementAssignment> Assignments);

internal sealed record ArrangementAssignment(
    string RegistrationId,
    string SubjectId,
    string CenterId,
    string CenterName,
    string RoomId,
    string RoomName,
    string RoomCode,
    string Building,
    string Floor,
    string ExamRoomCode,
    string Seat,
    string SubjectSignature);

internal sealed record ArrangementBuildResult(
    OperationalExam Exam,
    AdmissionNumberRule Rule,
    string MixingScope,
    string Seed,
    IReadOnlyList<string> Warnings,
    ArrangementSummary Summary,
    IReadOnlyList<ArrangementCard> Cards);

internal sealed class AdminArrangementException(int statusCode, string message) : Exception(message)
{
    public int StatusCode { get; } = statusCode;
}
