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

namespace Modules.WMS.Picking.Scan;

public sealed class Handler(NpgsqlDataSource dataSource)
{
    public sealed record HandlerResult(int StatusCode, object Body);

    public async Task<HandlerResult> HandleAsync(
        Guid taskId,
        Request request,
        string actor,
        CancellationToken ct)
    {
        if (string.IsNullOrWhiteSpace(request.IdempotencyKey))
            return new(400, KbxValidationProblem.Create(
                new KbxValidationError("idempotencyKey", null, "IDEMPOTENCY_REQUIRED", "Idempotency key가 필요합니다.")));

        var barcode = request.Barcode.Trim();
        if (barcode.Length < 3)
            return new(400, KbxValidationProblem.Create(
                new KbxValidationError("barcode", null, "BARCODE_INVALID", "바코드를 다시 스캔하세요.")));

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

        // One picking task is a sequence. Serialize scanner commands for this task.
        var taskRow = await connection.QuerySingleOrDefaultAsync<TaskRow>(new CommandDefinition("""
            select id, task_no as TaskNo, status, version
              from wms.picking_tasks
             where id=@TaskId
             for update;
            """, new { TaskId = taskId }, tx, cancellationToken: ct));

        if (taskRow is null)
        {
            await tx.RollbackAsync(ct);
            return new(404, new { type = "not-found", title = "피킹 작업을 찾을 수 없습니다." });
        }

        // Idempotency is checked before version. A response-lost retry must replay the committed result.
        var existingJson = await connection.QuerySingleOrDefaultAsync<string>(new CommandDefinition("""
            select response_payload::text
              from wms.scan_receipts
             where task_id=@TaskId and idempotency_key=@Key;
            """, new { TaskId = taskId, Key = request.IdempotencyKey }, tx, cancellationToken: ct));
        if (existingJson is not null)
        {
            var existing = JsonSerializer.Deserialize<Response>(existingJson, JsonOptions)!;
            await tx.RollbackAsync(ct);
            return new(200, existing with { Duplicate = true });
        }

        if (taskRow.Version != request.ExpectedVersion)
        {
            await tx.RollbackAsync(ct);
            return new(409, KbxConflictProblem.Version(taskRow.Version));
        }

        if (taskRow.Status != "IN_PROGRESS")
        {
            await tx.RollbackAsync(ct);
            return new(409, KbxBusinessProblem.Create(
                "PICKING_NOT_ACTIVE",
                "현재 피킹할 수 없는 작업입니다.",
                $"현재 상태: {taskRow.Status}"));
        }

        var line = await connection.QuerySingleOrDefaultAsync<LineRow>(new CommandDefinition("""
            select l.id as LineId,
                   l.line_no as LineNo,
                   l.location_code as LocationCode,
                   l.location_barcode as LocationBarcode,
                   l.location_confirmed as LocationConfirmed,
                   l.item_id as ItemId,
                   l.barcode as Barcode,
                   l.required_qty as RequiredQty,
                   l.picked_qty as PickedQty
              from wms.picking_lines l
             where l.task_id=@TaskId and l.status <> 'COMPLETED'
             order by l.line_no
             limit 1
             for update;
            """, new { TaskId = taskId }, tx, cancellationToken: ct));

        if (line is null)
        {
            await CompleteTaskAsync(connection, tx, taskId, ct);
            var completed = await BuildAndPersistResponse(
                connection, tx, taskId, request, actor, true, "success", "피킹을 완료했습니다.", "TaskRecoveredAsCompleted", ct);
            await tx.CommitAsync(ct);
            return new(200, completed);
        }

        var decision = PickingScanStateMachine.Decide(new PickingScanState(
            line.LocationConfirmed,
            line.LocationCode,
            line.LocationBarcode,
            line.Barcode,
            line.RequiredQty,
            line.PickedQty), barcode);

        switch (decision.Kind)
        {
            case PickingScanDecisionKind.RejectLocation:
            case PickingScanDecisionKind.RejectItem:
            {
                var rejected = await BuildAndPersistResponse(
                    connection, tx, taskId, request, actor,
                    decision.Accepted, decision.Feedback, decision.Message, decision.AuditAction, ct);
                await tx.CommitAsync(ct);
                return new(422, rejected);
            }

            case PickingScanDecisionKind.AcceptLocation:
            {
                await connection.ExecuteAsync(new CommandDefinition("""
                    update wms.picking_lines
                       set location_confirmed=true, updated_at=now()
                     where id=@LineId;
                    update wms.picking_tasks
                       set version=version+1, updated_at=now()
                     where id=@TaskId;
                    """, new { line.LineId, TaskId = taskId }, tx, cancellationToken: ct));

                var accepted = await BuildAndPersistResponse(
                    connection, tx, taskId, request, actor,
                    true, decision.Feedback, decision.Message, decision.AuditAction, ct);
                await tx.CommitAsync(ct);
                return new(200, accepted);
            }

            case PickingScanDecisionKind.AcceptItem:
            {
                await ApplyItemScanAsync(connection, tx, taskId, line, decision, ct);

                var nextExists = await HasIncompleteLineAsync(connection, tx, taskId, ct);
                if (!nextExists)
                    await CompleteTaskAsync(connection, tx, taskId, ct);

                var message = nextExists ? decision.Message : "피킹을 완료했습니다.";
                var accepted = await BuildAndPersistResponse(
                    connection, tx, taskId, request, actor,
                    true, decision.Feedback, message, decision.AuditAction, ct);
                await tx.CommitAsync(ct);
                return new(200, accepted);
            }

            default:
                throw new InvalidOperationException($"Unsupported picking scan decision: {decision.Kind}");
        }
    }

    private static async Task ApplyItemScanAsync(
        NpgsqlConnection connection,
        NpgsqlTransaction tx,
        Guid taskId,
        LineRow line,
        PickingScanDecision decision,
        CancellationToken ct)
    {
        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 completed_at end,
                   updated_at=now()
             where id=@LineId;

            update wms.picking_tasks set version=version+1, updated_at=now() where id=@TaskId;
            """, new {
                PickedQty = decision.NewPickedQty!.Value,
                Completed = decision.LineCompleted,
                line.LineId,
                TaskId = taskId,
            }, tx, cancellationToken: ct));

        if (decision.LineCompleted)
            await PickingTransitions.AutoConfirmNextSameLocationAsync(connection, tx, taskId, line.LocationCode, ct);
    }

    private static Task<bool> HasIncompleteLineAsync(
        NpgsqlConnection connection,
        NpgsqlTransaction tx,
        Guid taskId,
        CancellationToken ct) => 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));

    private static async Task CompleteTaskAsync(
        NpgsqlConnection connection,
        NpgsqlTransaction tx,
        Guid taskId,
        CancellationToken ct)
    {
        await connection.ExecuteAsync(new CommandDefinition("""
            update wms.picking_tasks
               set status='COMPLETED', version=version+1, completed_at=coalesce(completed_at,now()), updated_at=now()
             where id=@TaskId and status <> 'COMPLETED';

            insert into integration.outbox(id,event_type,aggregate_id,payload,occurred_at,status)
            select @EventId,'WmsPickingCompleted',@TaskId,jsonb_build_object('taskId',@TaskId),now(),'PENDING'
             where not exists (
               select 1 from integration.outbox
                where aggregate_id=@TaskId and event_type='WmsPickingCompleted'
             );
            """, new { TaskId = taskId, EventId = Guid.NewGuid() }, tx, cancellationToken: ct));
    }

    private static async Task<Response> BuildAndPersistResponse(
        NpgsqlConnection connection,
        NpgsqlTransaction tx,
        Guid taskId,
        Request request,
        string actor,
        bool accepted,
        string feedback,
        string message,
        string auditAction,
        CancellationToken ct)
    {
        var current = await PickingTaskQueries.GetAsync(connection, tx, taskId, ct)
            ?? throw new InvalidOperationException("Picking task disappeared during transaction.");
        var response = new Response(accepted, false, current, feedback, message);
        var json = JsonSerializer.Serialize(response, JsonOptions);

        await connection.ExecuteAsync(new CommandDefinition("""
            insert into wms.scan_receipts(
                id, task_id, idempotency_key, barcode, source, occurred_at, actor, response_payload)
            values (@Id,@TaskId,@Key,@Barcode,@Source,@OccurredAt,@Actor,cast(@Response as jsonb));

            insert into audit.entries(id,aggregate_type,aggregate_id,action,actor,occurred_at,data)
            values (@AuditId,'WmsPickingTask',@TaskId,@Action,@Actor,now(),
                    jsonb_build_object('barcode',@Barcode,'source',@Source,'idempotencyKey',@Key));
            """, new {
                Id = Guid.NewGuid(),
                AuditId = Guid.NewGuid(),
                TaskId = taskId,
                Key = request.IdempotencyKey,
                request.Barcode,
                request.Source,
                request.OccurredAt,
                Actor = actor,
                Response = json,
                Action = auditAction,
            }, tx, cancellationToken: ct));

        return response;
    }

    private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);

    private sealed record TaskRow(Guid Id, string TaskNo, string Status, long Version);
    private sealed record LineRow(
        Guid LineId,
        int LineNo,
        string LocationCode,
        string LocationBarcode,
        bool LocationConfirmed,
        Guid ItemId,
        string Barcode,
        decimal RequiredQty,
        decimal PickedQty);
}
