using System.Data.Common;
using Eis.Infrastructure.Data;

namespace Eis.Infrastructure.Administration;

internal sealed class AdminCenterRepository(IRelationalConnectionFactory connectionFactory)
{
    public Task CreateRequestAsync(
        AdminCenterChange change,
        IReadOnlyList<AdminCenterChangeRoom> rooms,
        AccountWorkflowInstance instance,
        AccountWorkflowAction action,
        AdminAuditEntry audit,
        CancellationToken cancellationToken)
    {
        var operations = new List<SqlOperation> { InsertChange(change), InsertInstance(instance), InsertAction(action) };
        operations.AddRange(rooms.Select(InsertChangeRoom));
        operations.Add(InsertAudit(audit));
        return ExecuteTransactionAsync(operations, cancellationToken);
    }

    public Task ProcessAsync(
        AdminCenterChange change,
        AccountWorkflowInstance instance,
        AccountWorkflowAction action,
        AdminAuditEntry audit,
        CancellationToken cancellationToken) => ExecuteTransactionAsync(
        [UpdateInstance(instance), InsertAction(action), UpdateChange(change), InsertAudit(audit)], cancellationToken);

    public Task ApplyAsync(
        AdminCenterChange change,
        AccountWorkflowInstance instance,
        AccountWorkflowAction action,
        AdminCenter center,
        IReadOnlyList<AdminCenterRoom> rooms,
        AdminAuditEntry audit,
        CancellationToken cancellationToken)
    {
        var operations = new List<SqlOperation>
        {
            UpdateInstance(instance),
            InsertAction(action),
            UpdateChange(change),
            change.RequestType == "create" ? InsertCenter(center) : UpdateCenter(center),
            new("DELETE FROM test_rooms WHERE center_id = @centerId", [new("@centerId", center.Id)])
        };
        operations.AddRange(rooms.Select(InsertRoom));
        operations.Add(InsertAudit(audit));
        return ExecuteTransactionAsync(operations, cancellationToken);
    }

    private async Task ExecuteTransactionAsync(IReadOnlyList<SqlOperation> operations, CancellationToken cancellationToken)
    {
        await using var connection = await connectionFactory.OpenAsync(cancellationToken);
        await using var transaction = await connection.BeginTransactionAsync(cancellationToken);
        try
        {
            foreach (var operation in operations) await ExecuteAsync(connection, transaction, operation, cancellationToken);
            await transaction.CommitAsync(cancellationToken);
        }
        catch
        {
            await transaction.RollbackAsync(cancellationToken);
            throw;
        }
    }

    private static SqlOperation InsertChange(AdminCenterChange item) => new("""
        INSERT INTO center_change_requests (
          id, center_id, school_id, request_type, code, name, province_code, province_name, city_code, city_name,
          district_code, district_name, address, contact, manager_name, manager_phone, emergency_phone,
          gate_open_time, transport, center_status, notes, status, review_note, requested_by, created_at, reviewed_at
        ) VALUES (
          @id, @centerId, @schoolId, @requestType, @code, @name, @provinceCode, @provinceName, @cityCode, @cityName,
          @districtCode, @districtName, @address, @contact, @managerName, @managerPhone, @emergencyPhone,
          @gateOpenTime, @transport, @centerStatus, @notes, @status, @reviewNote, @requestedBy, @createdAt, @reviewedAt
        )
        """, ChangeParameters(item));

    private static SqlOperation UpdateChange(AdminCenterChange item) => new("""
        UPDATE center_change_requests SET status = @status, review_note = @reviewNote, reviewed_at = @reviewedAt WHERE id = @id
        """, [new("@status", item.Status), new("@reviewNote", Optional(item.ReviewNote)), new("@reviewedAt", item.ReviewedAt), new("@id", item.Id)]);

    private static IReadOnlyList<SqlParameterValue> ChangeParameters(AdminCenterChange item) =>
    [
        new("@id", item.Id), new("@centerId", item.CenterId), new("@schoolId", item.SchoolId), new("@requestType", item.RequestType),
        new("@code", item.Code), new("@name", item.Name), new("@provinceCode", item.ProvinceCode), new("@provinceName", item.ProvinceName),
        new("@cityCode", item.CityCode), new("@cityName", item.CityName), new("@districtCode", item.DistrictCode), new("@districtName", item.DistrictName),
        new("@address", item.Address), new("@contact", Optional(item.Contact)), new("@managerName", Optional(item.ManagerName)),
        new("@managerPhone", Optional(item.ManagerPhone)), new("@emergencyPhone", Optional(item.EmergencyPhone)),
        new("@gateOpenTime", Optional(item.GateOpenTime)), new("@transport", Optional(item.Transport)), new("@centerStatus", item.CenterStatus),
        new("@notes", Optional(item.Notes)), new("@status", item.Status), new("@reviewNote", Optional(item.ReviewNote)),
        new("@requestedBy", item.RequestedBy), new("@createdAt", item.CreatedAt), new("@reviewedAt", item.ReviewedAt)
    ];

    private static SqlOperation InsertChangeRoom(AdminCenterChangeRoom item) => new("""
        INSERT INTO center_change_rooms (
          id, request_id, room_id, code, name, building, floor, capacity, seat_plan, seat_start, seat_end, room_type, status, notes
        ) VALUES (
          @id, @requestId, @roomId, @code, @name, @building, @floor, @capacity, @seatPlan, @seatStart, @seatEnd, @roomType, @status, @notes
        )
        """,
        [
            new("@id", item.Id), new("@requestId", item.RequestId), new("@roomId", item.RoomId), new("@code", item.Code),
            new("@name", item.Name), new("@building", item.Building), new("@floor", Optional(item.Floor)), new("@capacity", item.Capacity),
            new("@seatPlan", Optional(item.SeatPlan)), new("@seatStart", item.SeatStart), new("@seatEnd", item.SeatEnd),
            new("@roomType", item.RoomType), new("@status", item.Status), new("@notes", Optional(item.Notes))
        ]);

    private static SqlOperation InsertCenter(AdminCenter item) => new("""
        INSERT INTO test_centers (
          id, school_id, code, name, province_code, province_name, city_code, city_name, district_code, district_name,
          address, contact, manager_name, manager_phone, emergency_phone, gate_open_time, transport, status, notes, rooms, updated_at
        ) VALUES (
          @id, @schoolId, @code, @name, @provinceCode, @provinceName, @cityCode, @cityName, @districtCode, @districtName,
          @address, @contact, @managerName, @managerPhone, @emergencyPhone, @gateOpenTime, @transport, @status, @notes, @rooms, @updatedAt
        )
        """, CenterParameters(item));

    private static SqlOperation UpdateCenter(AdminCenter item) => new("""
        UPDATE test_centers SET code = @code, name = @name, province_code = @provinceCode, province_name = @provinceName,
          city_code = @cityCode, city_name = @cityName, district_code = @districtCode, district_name = @districtName,
          address = @address, contact = @contact, manager_name = @managerName, manager_phone = @managerPhone,
          emergency_phone = @emergencyPhone, gate_open_time = @gateOpenTime, transport = @transport, status = @status,
          notes = @notes, rooms = @rooms, updated_at = @updatedAt WHERE id = @id
        """, CenterParameters(item));

    private static IReadOnlyList<SqlParameterValue> CenterParameters(AdminCenter item) =>
    [
        new("@id", item.Id), new("@schoolId", item.SchoolId), new("@code", item.Code), new("@name", item.Name),
        new("@provinceCode", item.ProvinceCode), new("@provinceName", item.ProvinceName), new("@cityCode", item.CityCode),
        new("@cityName", item.CityName), new("@districtCode", item.DistrictCode), new("@districtName", item.DistrictName),
        new("@address", item.Address), new("@contact", Optional(item.Contact)), new("@managerName", Optional(item.ManagerName)),
        new("@managerPhone", Optional(item.ManagerPhone)), new("@emergencyPhone", Optional(item.EmergencyPhone)),
        new("@gateOpenTime", Optional(item.GateOpenTime)), new("@transport", Optional(item.Transport)), new("@status", item.Status),
        new("@notes", Optional(item.Notes)), new("@rooms", item.Rooms), new("@updatedAt", item.UpdatedAt)
    ];

    private static SqlOperation InsertRoom(AdminCenterRoom item) => new("""
        INSERT INTO test_rooms (
          id, center_id, code, name, building, floor, capacity, seat_plan, seat_start, seat_end, room_type, status, notes
        ) VALUES (
          @id, @ownerId, @code, @name, @building, @floor, @capacity, @seatPlan, @seatStart, @seatEnd, @roomType, @status, @notes
        )
        """,
        [
            new("@id", item.Id), new("@ownerId", item.CenterId), new("@code", item.Code), new("@name", item.Name),
            new("@building", item.Building), new("@floor", Optional(item.Floor)), new("@capacity", item.Capacity),
            new("@seatPlan", Optional(item.SeatPlan)), new("@seatStart", item.SeatStart), new("@seatEnd", item.SeatEnd),
            new("@roomType", item.RoomType), new("@status", item.Status), new("@notes", Optional(item.Notes))
        ]);

    private static SqlOperation InsertInstance(AccountWorkflowInstance item) => new("""
        INSERT INTO workflow_instances (id, workflow_id, business_type, business_id, status, current_step, assignee_id, created_at, completed_at)
        VALUES (@id, @workflowId, @businessType, @businessId, @status, @currentStep, @assigneeId, @createdAt, @completedAt)
        """, [new("@id", item.Id), new("@workflowId", item.WorkflowId), new("@businessType", item.BusinessType), new("@businessId", item.BusinessId), new("@status", item.Status), new("@currentStep", item.CurrentStep), new("@assigneeId", item.AssigneeId), new("@createdAt", item.CreatedAt), new("@completedAt", item.CompletedAt)]);

    private static SqlOperation UpdateInstance(AccountWorkflowInstance item) => new("""
        UPDATE workflow_instances SET status = @status, current_step = @currentStep,
          assignee_id = @assigneeId, completed_at = @completedAt WHERE id = @id
        """, [new("@status", item.Status), new("@currentStep", item.CurrentStep), new("@assigneeId", item.AssigneeId), new("@completedAt", item.CompletedAt), new("@id", item.Id)]);

    private static SqlOperation InsertAction(AccountWorkflowAction item) => new("""
        INSERT INTO workflow_actions (id, instance_id, actor_id, action, note, from_assignee_id, to_assignee_id, created_at)
        VALUES (@id, @instanceId, @actorId, @action, @note, @fromAssigneeId, @toAssigneeId, @createdAt)
        """, [new("@id", item.Id), new("@instanceId", item.InstanceId), new("@actorId", item.ActorId), new("@action", item.Action), new("@note", Optional(item.Note)), new("@fromAssigneeId", item.FromAssigneeId), new("@toAssigneeId", item.ToAssigneeId), new("@createdAt", item.CreatedAt)]);

    private static SqlOperation InsertAudit(AdminAuditEntry item) => new(
        "INSERT INTO audit_logs (id, actor_id, action, detail, created_at) VALUES (@id, @actorId, @action, @detail, @createdAt)",
        [new("@id", item.Id), new("@actorId", item.ActorId), new("@action", item.Action), new("@detail", item.Detail), new("@createdAt", item.CreatedAt)]);

    private static async Task ExecuteAsync(
        DbConnection connection,
        DbTransaction transaction,
        SqlOperation operation,
        CancellationToken cancellationToken)
    {
        await using var command = connection.CreateCommand();
        command.Transaction = transaction;
        command.CommandText = operation.Sql;
        foreach (var item in operation.Parameters)
        {
            var parameter = command.CreateParameter();
            parameter.ParameterName = item.Name;
            parameter.Value = item.Value ?? DBNull.Value;
            command.Parameters.Add(parameter);
        }
        await command.ExecuteNonQueryAsync(cancellationToken);
    }

    private static string? Optional(string value) => value.Length == 0 ? null : value;
    private sealed record SqlOperation(string Sql, IReadOnlyList<SqlParameterValue> Parameters);
    private sealed record SqlParameterValue(string Name, object? Value);
}
