using System.Text.Json;
using Dapper;
using FastEndpoints;
using Modules.OMS.Orders;
using Npgsql;
using Shared.Problems;

namespace Modules.OMS.Orders.Ship;

public sealed record ShipOrdersFilter(
    DateOnly From,
    DateOnly To,
    Guid? ChannelId,
    string? Status,
    string? Keyword,
    bool ExceptionOnly = false);

public sealed record ShipOrdersRequest(
    string Mode,
    IReadOnlyList<Guid>? Ids,
    ShipOrdersFilter? Filter,
    IReadOnlyList<Guid>? ExcludedIds);

public sealed record ShipOrdersResponse(int Requested, int Accepted, int Rejected);

public sealed class Endpoint(NpgsqlDataSource dataSource)
    : Endpoint<ShipOrdersRequest, ShipOrdersResponse>
{
    public override void Configure()
    {
        Post("/api/oms/orders/ship");
        Permissions("oms.order.ship");
    }

    public override async Task HandleAsync(ShipOrdersRequest req, CancellationToken ct)
    {
        var key = HttpContext.Request.Headers["Idempotency-Key"].FirstOrDefault();
        if (string.IsNullOrWhiteSpace(key))
        {
            await Send.ResponseAsync(KbxValidationProblem.Create(
                new KbxValidationError(null, null, "IDEMPOTENCY_KEY_REQUIRED", "안전한 재처리를 위해 Idempotency-Key가 필요합니다.")), 400, cancellation: ct);
            return;
        }

        var mode = req.Mode?.Trim().ToLowerInvariant();
        if (mode is not ("ids" or "filter") || (mode == "ids" && (req.Ids is null || req.Ids.Count == 0)) || (mode == "filter" && req.Filter is null))
        {
            await Send.ResponseAsync(KbxValidationProblem.Create(
                new KbxValidationError("selection", null, "ORDER_SELECTION_REQUIRED", "출고지시할 주문을 선택하세요.")), 400, cancellation: ct);
            return;
        }

        await using var connection = await dataSource.OpenConnectionAsync(ct);
        await using var tx = await connection.BeginTransactionAsync(ct);

        var replay = await connection.QuerySingleOrDefaultAsync<string?>(new CommandDefinition(
            "select response_json::text from kbx.command_receipts where operation_id=@OperationId and idempotency_key=@Key",
            new { OperationId = "oms.orders.ship", Key = key }, tx, cancellationToken: ct));
        if (replay is not null)
        {
            await tx.RollbackAsync(ct);
            await Send.OkAsync(JsonSerializer.Deserialize<ShipOrdersResponse>(replay)!, ct);
            return;
        }

        const string idsTarget = "select distinct unnest(@Ids::uuid[]) as id";
        const string filterTarget = """
            select o.id
              from oms_order_search_projection o
             where o.ordered_at >= @From
               and o.ordered_at < @ToExclusive
               and (@ChannelId is null or o.channel_id = @ChannelId)
               and (@Status is null or o.shipment_status = @Status)
               and (@Keyword is null or o.search_text ilike '%' || @Keyword || '%')
               and (@ExceptionOnly = false or o.exception_count > 0)
               and not (o.id = any(@ExcludedIds))
            """;
        var targetSql = mode == "ids" ? idsTarget : filterTarget;

        var filter = req.Filter;
        var args = new
        {
            Ids = (req.Ids ?? Array.Empty<Guid>()).ToArray(),
            From = filter is null ? (DateTime?)null : filter.From.ToDateTime(TimeOnly.MinValue),
            ToExclusive = filter is null ? (DateTime?)null : filter.To.AddDays(1).ToDateTime(TimeOnly.MinValue),
            ChannelId = filter?.ChannelId,
            Status = string.IsNullOrWhiteSpace(filter?.Status) ? null : filter!.Status,
            Keyword = string.IsNullOrWhiteSpace(filter?.Keyword) ? null : filter!.Keyword!.Trim(),
            ExceptionOnly = filter?.ExceptionOnly ?? false,
            ExcludedIds = (req.ExcludedIds ?? Array.Empty<Guid>()).ToArray(),
            RequiredOrderStatus = OrderLifecycleStatus.Confirmed,
            RequiredShipmentStatus = OrderShipmentStatus.Ready,
        };

        var result = await connection.QuerySingleAsync<ShipMutationResult>(new CommandDefinition($"""
            with target as materialized (
                {targetSql}
            ),
            accepted as materialized (
                update oms.orders o
                   set version=version+1, updated_at=now(), updated_by='web'
                  from target t
                  join oms_order_search_projection p on p.id=t.id
                 where o.id=t.id
                   and o.status=@RequiredOrderStatus
                   and p.shipment_status=@RequiredShipmentStatus
                returning o.id
            ),
            audit_insert as (
                insert into audit.entries(id,aggregate_type,aggregate_id,action,actor,occurred_at,data)
                select gen_random_uuid(),'Order',id,'ShipRequested','web',now(),'{}'::jsonb
                  from accepted
                returning 1
            ),
            outbox_insert as (
                insert into integration.outbox(id,event_type,aggregate_id,payload,occurred_at,status)
                select gen_random_uuid(),'OmsOrderShipRequested',id,jsonb_build_object('orderId',id),now(),'PENDING'
                  from accepted
                returning 1
            )
            select (select count(*)::int from target) as Requested,
                   (select count(*)::int from accepted) as Accepted;
            """, args, tx, cancellationToken: ct));

        if (result.Requested == 0)
        {
            await tx.RollbackAsync(ct);
            await Send.ResponseAsync(KbxValidationProblem.Create(
                new KbxValidationError("selection", null, "ORDER_SELECTION_EMPTY", "현재 검색조건에서 처리할 주문이 없습니다.")), 400, cancellation: ct);
            return;
        }

        var response = new ShipOrdersResponse(result.Requested, result.Accepted, result.Requested - result.Accepted);
        await connection.ExecuteAsync(new CommandDefinition("""
            insert into kbx.command_receipts(operation_id,idempotency_key,response_json)
            values(@OperationId,@Key,cast(@Response as jsonb));
            """, new { OperationId = "oms.orders.ship", Key = key, Response = JsonSerializer.Serialize(response) }, tx, cancellationToken: ct));

        await tx.CommitAsync(ct);
        await Send.OkAsync(response, ct);
    }

    private sealed record ShipMutationResult(int Requested, int Accepted);
}
