using System.Data;
using System.Security.Claims;
using Dapper;
using Modules.OMS.Orders;
using Npgsql;
using Shared.Problems;

namespace Modules.OMS.Orders.Register;

public sealed record RegisterOrderResult(RegisterOrderResponse? Response, object? Problem)
{
    public static RegisterOrderResult Ok(RegisterOrderResponse response) => new(response, null);
    public static RegisterOrderResult Fail(object problem) => new(null, problem);
}

public sealed class RegisterOrderHandler(NpgsqlDataSource dataSource)
{
    public async Task<RegisterOrderResult> HandleAsync(
        RegisterOrderRequest request,
        ClaimsPrincipal user,
        CancellationToken ct)
    {
        await using var connection = await dataSource.OpenConnectionAsync(ct);
        await using var tx = await connection.BeginTransactionAsync(IsolationLevel.ReadCommitted, ct);

        // Lookup/Domain facts are re-read on the server. UI/Zod values are never trusted as business truth.
        var customerExists = await connection.ExecuteScalarAsync<bool>(new CommandDefinition(
            "select exists(select 1 from oms.customers where id = @Id and is_active = true)",
            new { Id = request.CustomerId }, tx, cancellationToken: ct));
        if (!customerExists)
            return RegisterOrderResult.Fail(KbxValidationProblem.Create(
                new("customerId", null, "CUSTOMER_NOT_FOUND", "사용 가능한 거래처가 아닙니다.")));

        var warehouseExists = await connection.ExecuteScalarAsync<bool>(new CommandDefinition(
            "select exists(select 1 from inventory.warehouses where id = @Id and is_active = true)",
            new { Id = request.WarehouseId }, tx, cancellationToken: ct));
        if (!warehouseExists)
            return RegisterOrderResult.Fail(KbxValidationProblem.Create(
                new("warehouseId", null, "WAREHOUSE_NOT_FOUND", "사용 가능한 출고창고가 아닙니다.")));

        var lineErrors = new List<KbxValidationError>();
        foreach (var line in request.Lines)
        {
            if (line.EffectiveOrderQty <= 0)
                lineErrors.Add(new("orderQty", line.ClientId, "QUANTITY_POSITIVE", "수량은 0보다 커야 합니다."));
            if (line.UnitPrice < 0)
                lineErrors.Add(new("unitPrice", line.ClientId, "UNIT_PRICE_NONNEGATIVE", "단가는 0 이상이어야 합니다."));
        }

        var requestedItemIds = request.Lines.Select(x => x.ItemId).Distinct().ToArray();
        var activeItemIds = (await connection.QueryAsync<Guid>(new CommandDefinition(
            "select id from catalog.items where id = any(@Ids) and is_active = true",
            new { Ids = requestedItemIds }, tx, cancellationToken: ct))).ToHashSet();

        foreach (var line in request.Lines.Where(x => !activeItemIds.Contains(x.ItemId)))
            lineErrors.Add(new("itemCode", line.ClientId, "ITEM_NOT_FOUND", "사용 가능한 품목이 아닙니다."));

        if (lineErrors.Count > 0)
            return RegisterOrderResult.Fail(KbxValidationProblem.Create(lineErrors.ToArray()));

        var actor = user.Identity?.Name ?? "unknown";
        var orderId = request.OrderId ?? Guid.NewGuid();
        var nextVersion = 1L;
        string orderNo;

        if (request.OrderId is null)
        {
            orderNo = $"ORD-{DateTime.UtcNow:yyyyMMdd}-{Guid.NewGuid().ToString("N")[..8].ToUpperInvariant()}";
            await connection.ExecuteAsync(new CommandDefinition(@"
insert into oms.orders(id, order_no, order_date, customer_id, warehouse_id, receiver_name, phone, postal_code, address1, address2, status, version, created_at, created_by)
values (@Id, @OrderNo, @OrderDate, @CustomerId, @WarehouseId, @ReceiverName, @Phone, @PostalCode, @Address1, @Address2, @InitialStatus, 1, now(), @Actor);",
                new { Id = orderId, OrderNo = orderNo, request.OrderDate, request.CustomerId, request.WarehouseId, request.ReceiverName, request.Phone, request.PostalCode, request.Address1, request.Address2, Actor = actor, InitialStatus = OrderLifecycleStatus.Draft }, tx, cancellationToken: ct));
        }
        else
        {
            var current = await connection.QuerySingleOrDefaultAsync<(string OrderNo, long Version, string Status)>(new CommandDefinition(
                "select order_no as OrderNo, version as Version, status as Status from oms.orders where id = @Id for update",
                new { Id = orderId }, tx, cancellationToken: ct));
            if (current.OrderNo is null)
                return RegisterOrderResult.Fail(KbxBusinessProblem.Create("ORDER_NOT_FOUND", "주문을 찾을 수 없습니다."));
            if (current.Version != request.Version)
                return RegisterOrderResult.Fail(KbxConflictProblem.Version(current.Version));
            if (current.Status != OrderLifecycleStatus.Draft)
                return RegisterOrderResult.Fail(KbxBusinessProblem.Create("ORDER_NOT_EDITABLE", "주문을 수정할 수 없습니다.", "작성 상태의 주문만 수정할 수 있습니다."));

            orderNo = current.OrderNo;
            nextVersion = current.Version + 1;
            await connection.ExecuteAsync(new CommandDefinition(@"
update oms.orders set order_date=@OrderDate, customer_id=@CustomerId, warehouse_id=@WarehouseId,
receiver_name=@ReceiverName, phone=@Phone, postal_code=@PostalCode, address1=@Address1, address2=@Address2,
version=@Version, updated_at=now(), updated_by=@Actor where id=@Id;",
                new { Id = orderId, request.OrderDate, request.CustomerId, request.WarehouseId, request.ReceiverName, request.Phone, request.PostalCode, request.Address1, request.Address2, Version = nextVersion, Actor = actor }, tx, cancellationToken: ct));
            await connection.ExecuteAsync(new CommandDefinition("delete from oms.order_lines where order_id=@OrderId", new { OrderId = orderId }, tx, cancellationToken: ct));
        }

        var lineNo = 0;
        foreach (var line in request.Lines)
        {
            lineNo++;
            await connection.ExecuteAsync(new CommandDefinition(@"
insert into oms.order_lines(id, order_id, line_no, item_id, quantity, unit_price, amount, remark)
values (@Id, @OrderId, @LineNo, @ItemId, @Quantity, @UnitPrice, @Amount, @Remark);",
                new { Id = Guid.NewGuid(), OrderId = orderId, LineNo = lineNo, line.ItemId, Quantity = line.EffectiveOrderQty, line.UnitPrice, Amount = line.EffectiveOrderQty * line.UnitPrice, line.Remark }, tx, cancellationToken: ct));
        }

        await connection.ExecuteAsync(new CommandDefinition(@"
insert into audit.entries(id, aggregate_type, aggregate_id, action, actor, occurred_at, data)
values (@Id, 'Order', @OrderId, 'ORDER_SAVED', @Actor, now(), jsonb_build_object('version', @Version));",
            new { Id = Guid.NewGuid(), OrderId = orderId, Actor = actor, Version = nextVersion }, tx, cancellationToken: ct));

        // Integration event is recorded atomically; dispatch is handled outside this transaction.
        await connection.ExecuteAsync(new CommandDefinition(@"
insert into integration.outbox(id, event_type, aggregate_id, payload, occurred_at, status)
values (@Id, 'OrderSaved', @OrderId, jsonb_build_object('orderId', @OrderId, 'version', @Version), now(), 'PENDING');",
            new { Id = Guid.NewGuid(), OrderId = orderId, Version = nextVersion }, tx, cancellationToken: ct));

        await tx.CommitAsync(ct);
        return RegisterOrderResult.Ok(new(orderId, orderNo, nextVersion, OrderLifecycleStatus.Draft));
    }
}
