using System.Text.Json;
using Dapper;
using FastEndpoints;
using Modules.WMS.Picking.Scan;
using Modules.WMS.Picking.Shared;
using Npgsql;
using Shared.Problems;

namespace Modules.WMS.Picking.SetQuantity;

public sealed record Request(
    Guid LineId,
    decimal PickedQty,
    string IdempotencyKey,
    long ExpectedVersion);

public sealed class Endpoint(NpgsqlDataSource dataSource)
    : Endpoint<Request, Modules.WMS.Picking.Scan.Response>
{
    public override void Configure()
    {
        Post("/api/wms/picking/tasks/{taskId:guid}/quantity");
        Permissions("wms.picking.execute");
    }

    public override async Task HandleAsync(Request req, CancellationToken ct)
    {
        var taskId = Route<Guid>("taskId");
        await using var connection = await dataSource.OpenConnectionAsync(ct);
        await using var tx = await connection.BeginTransactionAsync(ct);

        var task = await connection.QuerySingleOrDefaultAsync<TaskRow>(new CommandDefinition(
            "select id, status, version from wms.picking_tasks where id=@TaskId for update",
            new { TaskId = taskId }, tx, cancellationToken: ct));
        if (task is null)
        {
            await tx.RollbackAsync(ct);
            await SendNotFoundAsync(ct);
            return;
        }

        var prior = await connection.QuerySingleOrDefaultAsync<string>(new CommandDefinition("""
            select response_payload::text from wms.quantity_receipts
             where task_id=@TaskId and idempotency_key=@Key;
            """, new { TaskId = taskId, Key = req.IdempotencyKey }, tx, cancellationToken: ct));
        if (prior is not null)
        {
            var replay = JsonSerializer.Deserialize<Modules.WMS.Picking.Scan.Response>(prior, JsonOptions)!;
            await tx.RollbackAsync(ct);
            await Send.OkAsync(replay with { Duplicate = true }, ct);
            return;
        }

        if (task.Version != req.ExpectedVersion)
        {
            await tx.RollbackAsync(ct);
            await Send.ResponseAsync(KbxConflictProblem.Version(task.Version), 409, cancellation: ct);
            return;
        }

        var line = await connection.QuerySingleOrDefaultAsync<LineRow>(new CommandDefinition("""
            select id, line_no as LineNo, location_code as LocationCode, location_confirmed as LocationConfirmed,
                   required_qty as RequiredQty, picked_qty as PickedQty, status
              from wms.picking_lines
             where id=@LineId and task_id=@TaskId
             for update;
            """, new { req.LineId, TaskId = taskId }, tx, cancellationToken: ct));

        if (line is null || line.Status == "COMPLETED")
        {
            await tx.RollbackAsync(ct);
            await Send.ResponseAsync(KbxBusinessProblem.Create("PICK_LINE_NOT_EDITABLE", "현재 피킹 수량을 변경할 수 없습니다."), 409, cancellation: ct);
            return;
        }

        if (!line.LocationConfirmed || line.PickedQty <= 0)
        {
            await tx.RollbackAsync(ct);
            await Send.ResponseAsync(KbxBusinessProblem.Create(
                "ITEM_NOT_VERIFIED",
                "먼저 상품 바코드를 스캔하세요.",
                "수량 직접입력은 상품을 최소 1회 확인한 뒤 사용할 수 있습니다."), 409, cancellation: ct);
            return;
        }

        if (req.PickedQty < line.PickedQty || req.PickedQty > line.RequiredQty)
        {
            await tx.RollbackAsync(ct);
            await Send.ResponseAsync(KbxValidationProblem.Create(
                new KbxValidationError("pickedQty", null, "PICK_QTY_RANGE", $"수량은 현재 피킹수량 {line.PickedQty}부터 필요수량 {line.RequiredQty}까지 입력할 수 있습니다.")), 400, cancellation: ct);
            return;
        }

        var completed = req.PickedQty >= line.RequiredQty;
        await connection.ExecuteAsync(new CommandDefinition("""
            update wms.picking_lines
               set picked_qty=@PickedQty,
                   status=case when @Completed then 'COMPLETED' else 'IN_PROGRESS' end,
                   completed_at=case when @Completed then now() else null end,
                   updated_at=now()
             where id=@LineId;

            update wms.picking_tasks set version=version+1, updated_at=now() where id=@TaskId;
            """, new { req.PickedQty, Completed = completed, req.LineId, TaskId = taskId }, tx, cancellationToken: ct));

        if (completed)
            await PickingTransitions.AutoConfirmNextSameLocationAsync(connection, tx, taskId, line.LocationCode, ct);

        var hasNext = await connection.ExecuteScalarAsync<bool>(new CommandDefinition(
            "select exists(select 1 from wms.picking_lines where task_id=@TaskId and status <> 'COMPLETED')",
            new { TaskId = taskId }, tx, cancellationToken: ct));
        if (!hasNext)
        {
            await connection.ExecuteAsync(new CommandDefinition("""
                update wms.picking_tasks set status='COMPLETED', version=version+1, completed_at=now(), updated_at=now() where id=@TaskId;
                insert into integration.outbox(id,event_type,aggregate_id,payload,occurred_at,status)
                values (@EventId,'WmsPickingCompleted',@TaskId,jsonb_build_object('taskId',@TaskId),now(),'PENDING');
                """, new { TaskId = taskId, EventId = Guid.NewGuid() }, tx, cancellationToken: ct));
        }

        var current = await PickingTaskQueries.GetAsync(connection, tx, taskId, ct) ?? throw new InvalidOperationException();
        var response = new Modules.WMS.Picking.Scan.Response(true, false, current, "success",
            hasNext ? "피킹 수량을 반영했습니다." : "피킹을 완료했습니다.");
        var json = JsonSerializer.Serialize(response, JsonOptions);

        await connection.ExecuteAsync(new CommandDefinition("""
            insert into wms.quantity_receipts(id,task_id,idempotency_key,line_id,picked_qty,actor,response_payload)
            values (@Id,@TaskId,@Key,@LineId,@PickedQty,@Actor,cast(@Response as jsonb));
            insert into audit.entries(id,aggregate_type,aggregate_id,action,actor,occurred_at,data)
            values (@AuditId,'WmsPickingTask',@TaskId,'PickingQuantitySet',@Actor,now(),
                    jsonb_build_object('lineId',@LineId,'pickedQty',@PickedQty,'idempotencyKey',@Key));
            """, new {
                Id = Guid.NewGuid(), AuditId = Guid.NewGuid(), TaskId = taskId, Key = req.IdempotencyKey,
                req.LineId, req.PickedQty, Actor = User.Identity?.Name ?? "unknown", Response = json,
            }, tx, cancellationToken: ct));

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

    private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
    private sealed record TaskRow(Guid Id, string Status, long Version);
    private sealed record LineRow(Guid Id, int LineNo, string LocationCode, bool LocationConfirmed, decimal RequiredQty, decimal PickedQty, string Status);
}
