using System.Data.Common;
using System.Globalization;
using System.Net;
using System.Text.Json;
using System.Text.Json.Nodes;
using Eis.Application.Public;
using Eis.Infrastructure.Caching;
using Eis.Infrastructure.Data;
using Eis.Infrastructure.Security;

namespace Eis.Infrastructure.Public;

public sealed partial class PublicQueryService(
    IRelationalConnectionFactory connectionFactory,
    IPublicSiteConfiguration siteConfiguration,
    DocumentVerificationCodeService documentCodes,
    IApplicationCache cache) : IPublicQueryService
{
    private readonly NoticeContentFormatter _noticeFormatter = new();
    private readonly DocumentVerificationCodeService _documentCodes = documentCodes;

    public Task<JsonObject> GetHomeAsync(CancellationToken cancellationToken) =>
        cache.RememberJsonAsync("public", "home", () => LoadHomeAsync(cancellationToken));

    private async Task<JsonObject> LoadHomeAsync(CancellationToken cancellationToken)
    {
        await using var connection = await connectionFactory.OpenAsync(cancellationToken);
        var metadata = await QuerySingleAsync(connection,
            "SELECT self_registration_enabled FROM schema_metadata WHERE id = 1",
            reader => ReadBoolean(reader, "self_registration_enabled"), cancellationToken);
        var schools = await QueryAsync(connection,
            "SELECT id, name, code, address, is_source_school, is_admission_school, active FROM schools ORDER BY name, id",
            ReadSchool, cancellationToken);
        var classes = await QueryAsync(connection,
            "SELECT id, school_id, name, grade, active FROM school_classes ORDER BY school_id, name, id",
            ReadClass, cancellationToken);
        var notices = await QueryAsync(connection,
            "SELECT id, title, summary, content, category, pinned, status, publish_at, created_at, author FROM notices WHERE status = 'published' ORDER BY publish_at, created_at, id",
            ReadNotice, cancellationToken);
        var exams = await QueryAsync(connection,
            "SELECT id, code, name, description, registration_start, registration_end, exam_start, exam_end, admit_download_start, admit_download_end, location, pass_policy, pass_value, status, archived_at, archived_by, created_at FROM exams WHERE status = 'published' AND archived_at IS NULL ORDER BY created_at, id",
            ReadExam, cancellationToken);
        var subjects = await QueryAsync(connection,
            "SELECT id, exam_id, name, subject_date, start_time, end_time, fee, full_score, pass_score, pass_rule, pass_value, position FROM exam_subjects ORDER BY exam_id, position, id",
            ReadSubject, cancellationToken);
        var registrationCounts = await QueryAsync(connection,
            "SELECT exam_id, COUNT(*) AS item_count FROM registrations GROUP BY exam_id",
            reader => new CountByKey(ReadString(reader, "exam_id"), ReadInt64(reader, "item_count")), cancellationToken);
        var candidateCount = await QuerySingleAsync(connection,
            "SELECT COUNT(*) AS item_count FROM candidate_profiles",
            reader => ReadInt64(reader, "item_count"), cancellationToken);
        var registrationCount = await QuerySingleAsync(connection,
            "SELECT COUNT(*) AS item_count FROM registrations",
            reader => ReadInt64(reader, "item_count"), cancellationToken);
        var admissionRecords = await QueryAsync(connection,
            "SELECT id, kind, exam_id, user_id, school_id, status, payload_json, created_at, updated_at FROM admission_records ORDER BY created_at, id",
            ReadAdmissionRecord, cancellationToken);

        var schoolNames = schools.ToDictionary(item => item.Id, item => item.Name, StringComparer.Ordinal);
        var examNames = exams.ToDictionary(item => item.Id, item => item.Name, StringComparer.Ordinal);
        var subjectGroups = subjects.GroupBy(item => item.ExamId).ToDictionary(group => group.Key, group => group.ToList(), StringComparer.Ordinal);
        var countsByExam = registrationCounts.ToDictionary(item => item.Key, item => item.Count, StringComparer.Ordinal);

        var noticeItems = new List<NoticeItem>();
        noticeItems.AddRange(notices.Select(ToManualNotice));
        noticeItems.AddRange(BuildSystemNotices(admissionRecords, examNames, schoolNames));
        noticeItems.Sort(CompareNotices);

        var branding = siteConfiguration.Branding;
        return new JsonObject
        {
            ["ok"] = true,
            ["organization"] = new JsonObject
            {
                ["name"] = branding.Organization.Name,
                ["code"] = branding.Organization.Code,
                ["phone"] = branding.Organization.Phone,
                ["address"] = branding.Organization.Address,
                ["email"] = branding.Organization.Email
            },
            ["siteCopy"] = new JsonObject
            {
                ["heroEyebrow"] = branding.SiteCopy.HeroEyebrow,
                ["heroTitle"] = branding.SiteCopy.HeroTitle,
                ["heroHighlight"] = branding.SiteCopy.HeroHighlight,
                ["heroDescription"] = branding.SiteCopy.HeroDescription,
                ["footerNotice"] = branding.SiteCopy.FooterNotice
            },
            ["schools"] = new JsonArray(schools.Where(item => item.Active && item.IsSourceSchool).Select(ToSchoolJson).ToArray()),
            ["classes"] = new JsonArray(classes.Where(item => item.Active).Select(ToClassJson).ToArray()),
            ["selfRegistrationEnabled"] = metadata,
            ["notices"] = new JsonArray(noticeItems.Select(item => item.Json).ToArray()),
            ["exams"] = new JsonArray(exams.Select(exam => ToExamJson(
                exam,
                subjectGroups.GetValueOrDefault(exam.Id) ?? [],
                countsByExam.GetValueOrDefault(exam.Id))).ToArray()),
            ["stats"] = new JsonObject
            {
                ["candidates"] = candidateCount,
                ["exams"] = exams.Count,
                ["registrations"] = registrationCount
            }
        };
    }

    public async Task<JsonObject?> GetNoticeAsync(string id, CancellationToken cancellationToken)
    {
        // Detail reads deliberately bypass the list cache. During the compatibility
        // window another application instance may publish a notice without a shared
        // Redis cache, and a just-created notice must still be immediately readable.
        var home = await LoadHomeAsync(cancellationToken);
        var notices = home["notices"]?.AsArray();
        return notices?
            .OfType<JsonObject>()
            .FirstOrDefault(item => string.Equals(item["id"]?.GetValue<string>(), id, StringComparison.Ordinal))?
            .DeepClone()
            .AsObject();
    }

    private NoticeItem ToManualNotice(NoticeRow row)
    {
        var sanitized = _noticeFormatter.Sanitize(row.Content);
        var json = new JsonObject
        {
            ["id"] = row.Id,
            ["title"] = row.Title,
            ["summary"] = row.Summary,
            ["content"] = sanitized,
            ["category"] = row.Category,
            ["pinned"] = row.Pinned,
            ["status"] = row.Status,
            ["publishAt"] = JsonValue.Create(row.PublishAt),
            ["createdAt"] = JsonValue.Create(row.CreatedAt),
            ["author"] = row.Author,
            ["contentHtml"] = _noticeFormatter.ContentHtml(row.Content)
        };
        return new NoticeItem(json, row.Pinned, ParseDate(row.PublishAt));
    }

    private static IEnumerable<NoticeItem> BuildSystemNotices(
        IReadOnlyCollection<AdmissionRecordRow> records,
        IReadOnlyDictionary<string, string> examNames,
        IReadOnlyDictionary<string, string> schoolNames)
    {
        string ExamName(string id) => examNames.GetValueOrDefault(id) ?? "未知考试";
        string SchoolName(string? id) => id is not null ? schoolNames.GetValueOrDefault(id) ?? "未知学校" : "未知学校";
        var settings = records.Where(item => item.Kind == "setting")
            .GroupBy(item => item.ExamId)
            .ToDictionary(group => group.Key, group => group.First(), StringComparer.Ordinal);
        var output = new List<NoticeItem>();

        foreach (var record in records)
        {
            var visible = GetBoolean(record.Payload, "publicVisible", true);
            if (!visible)
            {
                continue;
            }

            string? sourceType = null;
            string? category = null;
            string? title = null;
            string? summary = null;
            string? content = null;
            string? publishAt = null;

            if (record.Kind == "plan" && record.Status == "approved")
            {
                sourceType = "plan";
                category = "招生计划";
                title = $"{ExamName(record.ExamId)} · {SchoolName(record.SchoolId)}招生计划公示";
                summary = "招生计划审核通过，类别人数与指标分配已经公开。";
                publishAt = GetString(record.Payload, "reviewedAt") ?? record.UpdatedAt;
                var listItems = GetArray(record.Payload, "categories").OfType<JsonObject>()
                    .Select(item => $"<li>{Html(GetString(item, "name"))}：{GetNumber(item, "quota"):0.##} 人</li>");
                content = $"<p>{Html(SchoolName(record.SchoolId))}招生计划已经审核通过。</p><ul>{string.Concat(listItems)}</ul>";
            }
            else if (record.Kind == "qualification_publication" && record.Status == "published")
            {
                sourceType = "qualification";
                category = "指标资格";
                title = $"{ExamName(record.ExamId)} · {SchoolName(record.SchoolId)}指标分配资格公示";
                summary = "生源学校资格确认完成，系统已生成指标分配资格公示。";
                publishAt = GetString(record.Payload, "publishedAt") ?? record.UpdatedAt;
                content = $"<p>{Html(SchoolName(record.SchoolId))}指标分配资格确认已经完成，共 {GetArray(record.Payload, "rows").Count} 条记录。</p>";
            }
            else if (record.Kind == "notification" && record.Status == "published" &&
                     GetString(record.Payload, "type") == "admission_round_publication")
            {
                var round = Math.Max(1, (int)GetNumber(record.Payload, "round", 1));
                var rowCount = GetArray(record.Payload, "rows").Count;
                sourceType = "admission";
                category = "录取名单";
                title = $"{ExamName(record.ExamId)}第 {round} 轮录取名单公示";
                summary = $"第 {round} 轮录取通知书已签发，共 {rowCount} 名考生进入本轮录取公示。";
                publishAt = GetString(record.Payload, "publishedAt") ?? record.UpdatedAt;
                content = $"<p>{Html(ExamName(record.ExamId))}第 {round} 轮录取工作已经完成，共 {rowCount} 名考生正式录取。</p>";
            }
            else if (record.Kind == "setting" && record.Status == "completed" && GetBoolean(record.Payload, "autoPublish", true))
            {
                sourceType = "admission";
                category = "录取名单";
                title = $"{ExamName(record.ExamId)}最终录取名单";
                summary = "录取与报到决策已经办结，最终录取结果已自动公开。";
                publishAt = GetString(record.Payload, "completedAt") ?? record.UpdatedAt;
                content = $"<p>{Html(ExamName(record.ExamId))}录取工作已经完成，请在招生录取公示中查询脱敏结果。</p>";
            }
            else if (record.Kind == "cutoff_publication" && record.Status == "published" &&
                     (!settings.TryGetValue(record.ExamId, out var setting) || GetBoolean(setting.Payload, "autoPublish", true)))
            {
                sourceType = "cutoff";
                category = "录取分数线";
                title = $"{ExamName(record.ExamId)}录取分数线";
                summary = "各招生学校和类别录取分数线已经由系统汇总发布。";
                publishAt = GetString(record.Payload, "publishedAt") ?? record.UpdatedAt;
                content = $"<p>系统已汇总 {GetArray(record.Payload, "rows").Count} 个学校招生类别的录取分数线。</p>";
            }
            else if (record.Kind == "notification" && record.UserId is null && record.Status == "approved" &&
                     GetString(record.Payload, "type") == "admission_reporting")
            {
                var statistics = GetObject(record.Payload, "statistics");
                var supplement = GetString(record.Payload, "supplementDecision") == "supplement";
                var totalQuota = GetNumber(statistics, "totalQuota");
                var reported = GetNumber(statistics, "reportedCount");
                var reportingRate = GetNumber(statistics, "reportingRate");
                sourceType = "reporting";
                category = "考生报到";
                title = supplement
                    ? $"{ExamName(record.ExamId)} · {SchoolName(record.SchoolId)}考生报到情况及补录说明"
                    : $"{ExamName(record.ExamId)} · {SchoolName(record.SchoolId)}考生报到情况公示";
                summary = $"计划 {totalQuota:0.##} 人，已报到 {reported:0.##} 人，完成率 {reportingRate:0.##}%。";
                publishAt = GetString(record.Payload, "approvedAt") ?? record.UpdatedAt;
                var decision = supplement ? "学校申请补录并已获批准。" : GetString(record.Payload, "decisionNote") ?? "本轮不进行补录。";
                content = $"<p>{Html(summary)}</p><p>{Html(decision)}</p><ul><li>正式录取：{GetNumber(statistics, "finalCount"):0.##} 人</li><li>已报到：{reported:0.##} 人</li><li>未报到：{GetNumber(statistics, "notReportedCount"):0.##} 人</li><li>计划缺额：{GetNumber(statistics, "reportingGap"):0.##} 人</li></ul><p>{Html(GetString(record.Payload, "approvalNote"))}</p>";
            }

            if (sourceType is null || publishAt is null)
            {
                continue;
            }

            var json = new JsonObject
            {
                ["id"] = $"system-{sourceType}-{record.Id}",
                ["noticeId"] = $"system-{sourceType}-{record.Id}",
                ["sourceType"] = sourceType,
                ["schoolId"] = JsonValue.Create(record.SchoolId),
                ["examId"] = record.ExamId,
                ["category"] = category,
                ["title"] = title,
                ["summary"] = summary,
                ["content"] = content,
                ["author"] = "系统自动发布",
                ["publishAt"] = publishAt,
                ["publishedAt"] = publishAt,
                ["pinned"] = false,
                ["visible"] = true,
                ["status"] = "visible"
            };
            output.Add(new NoticeItem(json, false, ParseDate(publishAt)));
        }

        return output;
    }

    private static JsonObject ToExamJson(ExamRow exam, IReadOnlyCollection<SubjectRow> subjects, long registrationCount)
    {
        var subjectJson = subjects.Select(subject =>
        {
            var passRule = subject.PassRule == "score_ratio" ? "rank_percent" : subject.PassRule;
            var passValue = subject.PassValue;
            return new JsonObject
            {
                ["id"] = subject.Id,
                ["name"] = subject.Name,
                ["date"] = subject.Date,
                ["start"] = subject.Start,
                ["end"] = subject.End,
                ["fee"] = subject.Fee,
                ["fullScore"] = subject.FullScore,
                ["passRule"] = passRule,
                ["passValue"] = passValue,
                ["passScore"] = passRule == "fixed_score" ? JsonValue.Create(Math.Round(passValue, 2)) : null,
                ["order"] = subject.Position
            };
        }).ToArray();
        var totalScore = subjects.Sum(subject => subject.FullScore);
        var now = DateTimeOffset.UtcNow;
        var registrationState = exam.ArchivedAt is not null
            ? "archived"
            : now < ParseDate(exam.RegistrationStart)
                ? "upcoming"
                : now > ParseDate(exam.RegistrationEnd) ? "closed" : "open";

        return new JsonObject
        {
            ["id"] = exam.Id,
            ["code"] = exam.Code,
            ["name"] = exam.Name,
            ["description"] = exam.Description,
            ["registrationStart"] = exam.RegistrationStart,
            ["registrationEnd"] = exam.RegistrationEnd,
            ["examStart"] = exam.ExamStart,
            ["examEnd"] = exam.ExamEnd,
            ["admitDownloadStart"] = exam.AdmitDownloadStart,
            ["admitDownloadEnd"] = exam.AdmitDownloadEnd,
            ["location"] = exam.Location,
            ["passPolicy"] = exam.PassPolicy == "score_ratio" ? "rank_percent" : exam.PassPolicy,
            ["passValue"] = exam.PassValue,
            ["status"] = exam.Status,
            ["archivedAt"] = JsonValue.Create(exam.ArchivedAt),
            ["archivedBy"] = JsonValue.Create(exam.ArchivedBy),
            ["createdAt"] = exam.CreatedAt,
            ["subjects"] = new JsonArray(subjectJson),
            ["totalScore"] = totalScore,
            ["registrationState"] = registrationState,
            ["registrationCount"] = registrationCount
        };
    }

    private static JsonObject ToSchoolJson(SchoolRow row) => new()
    {
        ["id"] = row.Id,
        ["name"] = row.Name,
        ["code"] = row.Code,
        ["address"] = row.Address,
        ["isSourceSchool"] = row.IsSourceSchool,
        ["isAdmissionSchool"] = row.IsAdmissionSchool,
        ["active"] = row.Active
    };

    private static JsonObject ToClassJson(ClassRow row) => new()
    {
        ["id"] = row.Id,
        ["schoolId"] = row.SchoolId,
        ["name"] = row.Name,
        ["grade"] = row.Grade,
        ["active"] = row.Active
    };

    private static int CompareNotices(NoticeItem left, NoticeItem right)
    {
        var pinned = right.Pinned.CompareTo(left.Pinned);
        return pinned != 0 ? pinned : right.PublishAt.CompareTo(left.PublishAt);
    }

    private static SchoolRow ReadSchool(DbDataReader reader) => new(
        ReadString(reader, "id"), ReadString(reader, "name"), ReadString(reader, "code"),
        ReadNullableString(reader, "address") ?? string.Empty, ReadBoolean(reader, "is_source_school", true),
        ReadBoolean(reader, "is_admission_school", true), ReadBoolean(reader, "active"));

    private static ClassRow ReadClass(DbDataReader reader) => new(
        ReadString(reader, "id"), ReadString(reader, "school_id"), ReadString(reader, "name"),
        ReadString(reader, "grade"), ReadBoolean(reader, "active"));

    private static NoticeRow ReadNotice(DbDataReader reader) => new(
        ReadString(reader, "id"), ReadString(reader, "title"), ReadString(reader, "summary"),
        ReadString(reader, "content"), ReadString(reader, "category"), ReadBoolean(reader, "pinned"),
        ReadString(reader, "status"), ReadNullableString(reader, "publish_at"), ReadNullableString(reader, "created_at"),
        ReadString(reader, "author"));

    private static ExamRow ReadExam(DbDataReader reader) => new(
        ReadString(reader, "id"), ReadString(reader, "code"), ReadString(reader, "name"), ReadString(reader, "description"),
        ReadString(reader, "registration_start"), ReadString(reader, "registration_end"), ReadString(reader, "exam_start"),
        ReadString(reader, "exam_end"), ReadString(reader, "admit_download_start"), ReadString(reader, "admit_download_end"),
        ReadString(reader, "location"), ReadString(reader, "pass_policy"), ReadDouble(reader, "pass_value"),
        ReadString(reader, "status"), ReadNullableString(reader, "archived_at"), ReadNullableString(reader, "archived_by"),
        ReadString(reader, "created_at"));

    private static SubjectRow ReadSubject(DbDataReader reader) => new(
        ReadString(reader, "id"), ReadString(reader, "exam_id"), ReadString(reader, "name"), ReadString(reader, "subject_date"),
        ReadString(reader, "start_time"), ReadString(reader, "end_time"), ReadDouble(reader, "fee"),
        ReadDouble(reader, "full_score"), ReadString(reader, "pass_rule"), ReadDouble(reader, "pass_value"),
        checked((int)ReadInt64(reader, "position")));

    private static AdmissionRecordRow ReadAdmissionRecord(DbDataReader reader)
    {
        var payloadText = ReadString(reader, "payload_json");
        JsonObject payload;
        try
        {
            payload = JsonNode.Parse(payloadText)?.AsObject() ?? new JsonObject();
        }
        catch (JsonException)
        {
            payload = new JsonObject();
        }

        return new AdmissionRecordRow(
            ReadString(reader, "id"), ReadString(reader, "kind"), ReadString(reader, "exam_id"),
            ReadNullableString(reader, "user_id"), ReadNullableString(reader, "school_id"), ReadString(reader, "status"),
            payload, ReadString(reader, "created_at"), ReadString(reader, "updated_at"));
    }

    private static async Task<List<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 async Task<T> QuerySingleAsync<T>(
        DbConnection connection,
        string sql,
        Func<DbDataReader, T> map,
        CancellationToken cancellationToken)
    {
        var rows = await QueryAsync(connection, sql, map, cancellationToken);
        return rows.Count > 0 ? rows[0] : throw new InvalidOperationException("数据库尚未完成初始化");
    }

    private static string ReadString(DbDataReader reader, string name) =>
        Convert.ToString(reader[name], CultureInfo.InvariantCulture) ?? string.Empty;

    private static string? ReadNullableString(DbDataReader reader, string name) =>
        reader[name] is DBNull ? null : Convert.ToString(reader[name], CultureInfo.InvariantCulture);

    private static bool ReadBoolean(DbDataReader reader, string name, bool fallback = false) =>
        reader[name] is DBNull ? fallback : Convert.ToBoolean(reader[name], CultureInfo.InvariantCulture);

    private static long ReadInt64(DbDataReader reader, string name) =>
        Convert.ToInt64(reader[name], CultureInfo.InvariantCulture);

    private static double ReadDouble(DbDataReader reader, string name) =>
        Convert.ToDouble(reader[name], CultureInfo.InvariantCulture);

    private static DateTimeOffset ParseDate(string? value) =>
        DateTimeOffset.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var parsed)
            ? parsed
            : DateTimeOffset.MinValue;

    private static string? GetString(JsonObject? value, string name) =>
        value?[name] is JsonValue json && json.TryGetValue<string>(out var text) ? text : null;

    private static bool GetBoolean(JsonObject? value, string name, bool fallback) =>
        value?[name] is JsonValue json && json.TryGetValue<bool>(out var result) ? result : fallback;

    private static double GetNumber(JsonObject? value, string name, double fallback = 0)
    {
        if (value?[name] is not JsonValue json)
        {
            return fallback;
        }
        if (json.TryGetValue<double>(out var number))
        {
            return number;
        }
        return json.TryGetValue<string>(out var text) && double.TryParse(text, CultureInfo.InvariantCulture, out number)
            ? number
            : fallback;
    }

    private static JsonArray GetArray(JsonObject? value, string name) => value?[name] as JsonArray ?? [];

    private static JsonObject GetObject(JsonObject? value, string name) => value?[name] as JsonObject ?? [];

    private static string Html(string? value) => WebUtility.HtmlEncode(value ?? string.Empty);

    private sealed record SchoolRow(string Id, string Name, string Code, string Address, bool IsSourceSchool, bool IsAdmissionSchool, bool Active);
    private sealed record ClassRow(string Id, string SchoolId, string Name, string Grade, bool Active);
    private sealed record NoticeRow(string Id, string Title, string Summary, string Content, string Category, bool Pinned, string Status, string? PublishAt, string? CreatedAt, string Author);
    private sealed record ExamRow(string Id, string Code, string Name, string Description, string RegistrationStart, string RegistrationEnd, string ExamStart, string ExamEnd, string AdmitDownloadStart, string AdmitDownloadEnd, string Location, string PassPolicy, double PassValue, string Status, string? ArchivedAt, string? ArchivedBy, string CreatedAt);
    private sealed record SubjectRow(string Id, string ExamId, string Name, string Date, string Start, string End, double Fee, double FullScore, string PassRule, double PassValue, int Position);
    private sealed record CountByKey(string Key, long Count);
    private sealed record AdmissionRecordRow(string Id, string Kind, string ExamId, string? UserId, string? SchoolId, string Status, JsonObject Payload, string CreatedAt, string UpdatedAt);
    private sealed record NoticeItem(JsonObject Json, bool Pinned, DateTimeOffset PublishAt);
}
