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

namespace Eis.Infrastructure.Administration;

internal sealed record AdminNotice(string Id, string Title, string Summary, string Content, string Category, bool Pinned, string Status, string? PublishAt, string? CreatedAt, string Author);

internal sealed class AdminNoticeRepository(
    IRelationalConnectionFactory connectionFactory,
    IApplicationCache cache)
{
    public async Task<AdminNoticeManagementSnapshot> LoadManagementAsync(CancellationToken cancellationToken)
    {
        await using var connection = await connectionFactory.OpenAsync(cancellationToken);
        var notices = new List<AdminNotice>();
        await using (var command = connection.CreateCommand())
        {
            command.CommandText = "SELECT id, title, summary, content, category, pinned, status, publish_at, created_at, author FROM notices ORDER BY created_at, id";
            await using var reader = await command.ExecuteReaderAsync(cancellationToken);
            while (await reader.ReadAsync(cancellationToken)) notices.Add(Read(reader));
        }
        var records = new List<AdminPublicationRecord>();
        await using (var command = connection.CreateCommand())
        {
            command.CommandText = "SELECT id, kind, exam_id, user_id, school_id, status, payload_json, created_at, updated_at FROM admission_records ORDER BY created_at, id";
            await using var reader = await command.ExecuteReaderAsync(cancellationToken);
            while (await reader.ReadAsync(cancellationToken)) records.Add(ReadPublication(reader));
        }
        var examNames = await ReadNamesAsync(connection, "SELECT id, name FROM exams ORDER BY created_at, id", cancellationToken);
        var schoolNames = await ReadNamesAsync(connection, "SELECT id, name FROM schools ORDER BY name, id", cancellationToken);
        return new AdminNoticeManagementSnapshot(notices, records, examNames, schoolNames);
    }

    public async Task<AdminNotice?> FindAsync(string id, CancellationToken cancellationToken)
    {
        await using var connection = await connectionFactory.OpenAsync(cancellationToken);
        await using var command = connection.CreateCommand();
        command.CommandText = "SELECT id, title, summary, content, category, pinned, status, publish_at, created_at, author FROM notices WHERE id = @id LIMIT 1";
        Add(command, "@id", id);
        await using var reader = await command.ExecuteReaderAsync(cancellationToken);
        return await reader.ReadAsync(cancellationToken) ? Read(reader) : null;
    }

    public Task CreateAsync(AdminNotice notice, AdminAuditEntry audit, CancellationToken cancellationToken) =>
        ExecuteAsync("""
            INSERT INTO notices (id, title, summary, content, category, pinned, status, publish_at, created_at, author)
            VALUES (@id, @title, @summary, @content, @category, @pinned, @status, @publishAt, @createdAt, @author)
            """, notice, audit, cancellationToken);

    public Task UpdateAsync(AdminNotice notice, AdminAuditEntry audit, CancellationToken cancellationToken) =>
        ExecuteAsync("""
            UPDATE notices SET title = @title, summary = @summary, content = @content, category = @category,
              pinned = @pinned, status = @status, publish_at = @publishAt WHERE id = @id
            """, notice, audit, cancellationToken);

    public async Task UpdatePublicationAsync(
        AdminPublicationRecord publication,
        AdminAuditEntry audit,
        CancellationToken cancellationToken)
    {
        await using var connection = await connectionFactory.OpenAsync(cancellationToken);
        await using var transaction = await connection.BeginTransactionAsync(cancellationToken);
        try
        {
            await using (var command = connection.CreateCommand())
            {
                command.Transaction = transaction;
                command.CommandText = "UPDATE admission_records SET payload_json = @payload, updated_at = @updatedAt WHERE id = @id";
                Add(command, "@payload", publication.Payload.ToJsonString());
                Add(command, "@updatedAt", publication.UpdatedAt);
                Add(command, "@id", publication.Id);
                await command.ExecuteNonQueryAsync(cancellationToken);
            }
            await InsertAuditAsync(connection, transaction, audit, cancellationToken);
            await transaction.CommitAsync(cancellationToken);
            await cache.InvalidateAsync("public");
        }
        catch
        {
            await transaction.RollbackAsync(cancellationToken);
            throw;
        }
    }

    private async Task ExecuteAsync(string sql, AdminNotice notice, AdminAuditEntry audit, CancellationToken cancellationToken)
    {
        await using var connection = await connectionFactory.OpenAsync(cancellationToken);
        await using var transaction = await connection.BeginTransactionAsync(cancellationToken);
        try
        {
            await using (var command = connection.CreateCommand())
            {
                command.Transaction = transaction;
                command.CommandText = sql;
                Add(command, "@id", notice.Id); Add(command, "@title", notice.Title); Add(command, "@summary", notice.Summary);
                Add(command, "@content", notice.Content); Add(command, "@category", notice.Category); Add(command, "@pinned", notice.Pinned ? 1 : 0);
                Add(command, "@status", notice.Status); Add(command, "@publishAt", notice.PublishAt); Add(command, "@createdAt", notice.CreatedAt); Add(command, "@author", notice.Author);
                await command.ExecuteNonQueryAsync(cancellationToken);
            }
            await InsertAuditAsync(connection, transaction, audit, cancellationToken);
            await transaction.CommitAsync(cancellationToken);
            await cache.InvalidateAsync("public");
        }
        catch
        {
            await transaction.RollbackAsync(cancellationToken);
            throw;
        }
    }

    private static AdminNotice Read(DbDataReader reader) => new(
        Text(reader, "id"), Text(reader, "title"), Text(reader, "summary"), Text(reader, "content"), Text(reader, "category"),
        Convert.ToInt64(reader.GetValue(reader.GetOrdinal("pinned")), CultureInfo.InvariantCulture) != 0, Text(reader, "status"),
        Optional(reader, "publish_at"), Optional(reader, "created_at"), Text(reader, "author"));

    private static AdminPublicationRecord ReadPublication(DbDataReader reader) => new(
        Text(reader, "id"),
        Text(reader, "kind"),
        Text(reader, "exam_id"),
        Optional(reader, "user_id"),
        Optional(reader, "school_id"),
        Text(reader, "status"),
        JsonNode.Parse(Text(reader, "payload_json")) as JsonObject ?? new JsonObject(),
        Text(reader, "created_at"),
        Text(reader, "updated_at"));

    private static async Task<IReadOnlyDictionary<string, string>> ReadNamesAsync(
        DbConnection connection,
        string sql,
        CancellationToken cancellationToken)
    {
        var names = new Dictionary<string, string>(StringComparer.Ordinal);
        await using var command = connection.CreateCommand();
        command.CommandText = sql;
        await using var reader = await command.ExecuteReaderAsync(cancellationToken);
        while (await reader.ReadAsync(cancellationToken)) names[Text(reader, "id")] = Text(reader, "name");
        return names;
    }

    private static async Task InsertAuditAsync(
        DbConnection connection,
        DbTransaction transaction,
        AdminAuditEntry audit,
        CancellationToken cancellationToken)
    {
        await using var command = connection.CreateCommand();
        command.Transaction = transaction;
        command.CommandText = "INSERT INTO audit_logs (id, actor_id, action, detail, created_at) VALUES (@id, @actorId, @action, @detail, @createdAt)";
        Add(command, "@id", audit.Id);
        Add(command, "@actorId", audit.ActorId);
        Add(command, "@action", audit.Action);
        Add(command, "@detail", audit.Detail);
        Add(command, "@createdAt", audit.CreatedAt);
        await command.ExecuteNonQueryAsync(cancellationToken);
    }

    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 index = reader.GetOrdinal(name); return reader.IsDBNull(index) ? null : Convert.ToString(reader.GetValue(index), CultureInfo.InvariantCulture); }
    private static void Add(DbCommand command, string name, object? value) { var parameter = command.CreateParameter(); parameter.ParameterName = name; parameter.Value = value ?? DBNull.Value; command.Parameters.Add(parameter); }
}
