using System.Security.Claims;
using System.Text.Json;
using Dapper;
using FastEndpoints;
using Npgsql;
using Shared.Problems;

namespace Modules.ERP.ItemPrices.BulkSave;

public sealed record SaveItemPriceRow(string ClientId, Guid ItemId, DateOnly EffectiveDate, decimal UnitPrice, string? Remark);
public sealed record SaveItemPricesRequest(IReadOnlyList<SaveItemPriceRow> Rows);
public sealed record SaveItemPricesResponse(int Requested, int Saved, int Created, int Updated);

public sealed class Endpoint(NpgsqlDataSource dataSource) : Endpoint<SaveItemPricesRequest, SaveItemPricesResponse>
{
    public override void Configure() { Post("/api/erp/item-prices/bulk"); Permissions("erp.item.price.write"); }

    public override async Task HandleAsync(SaveItemPricesRequest 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;}
        if(req.Rows is null||req.Rows.Count==0){await Send.ResponseAsync(KbxValidationProblem.Create(new KbxValidationError(null,null,"PRICE_ROWS_REQUIRED","저장할 단가를 한 건 이상 입력하세요.")),400,cancellation:ct);return;}
        if(req.Rows.Count>5000){await Send.ResponseAsync(KbxValidationProblem.Create(new KbxValidationError(null,null,"PRICE_ROWS_LIMIT","한 번에 최대 5,000건까지 저장할 수 있습니다.")),400,cancellation:ct);return;}

        var errors=new List<KbxValidationError>();
        foreach(var row in req.Rows){if(row.UnitPrice<0)errors.Add(new("unitPrice",row.ClientId,"PRICE_NONNEGATIVE","단가는 0 이상이어야 합니다."));}
        foreach(var group in req.Rows.GroupBy(x=>new{x.ItemId,x.EffectiveDate}).Where(x=>x.Count()>1)) foreach(var row in group) errors.Add(new("effectiveDate",row.ClientId,"DUPLICATE_ITEM_DATE","같은 품목과 적용일이 중복되었습니다."));

        if(errors.Count>0){await Send.ResponseAsync(KbxValidationProblem.Create(errors.ToArray()),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="erp.itemPrices.bulkSave",Key=key},tx,cancellationToken:ct));
        if(replay is not null){await tx.RollbackAsync(ct);await Send.OkAsync(JsonSerializer.Deserialize<SaveItemPricesResponse>(replay)!,ct);return;}

        var ids=req.Rows.Select(x=>x.ItemId).Distinct().ToArray();
        var active=(await connection.QueryAsync<Guid>(new CommandDefinition("select id from catalog.items where id=any(@Ids) and is_active=true",new{Ids=ids},tx,cancellationToken:ct))).ToHashSet();
        foreach(var row in req.Rows.Where(x=>!active.Contains(x.ItemId)))errors.Add(new("itemCode",row.ClientId,"ITEM_NOT_FOUND","사용 가능한 품목이 아닙니다."));
        if(errors.Count>0){await tx.RollbackAsync(ct);await Send.ResponseAsync(KbxValidationProblem.Create(errors.ToArray()),400,cancellation:ct);return;}

        var actor=User.Identity?.Name??"unknown";
        var rowsJson=JsonSerializer.Serialize(req.Rows.Select(x=>new{clientId=x.ClientId,itemId=x.ItemId,effectiveDate=x.EffectiveDate.ToString("yyyy-MM-dd"),unitPrice=x.UnitPrice,remark=x.Remark}));
        var result=await connection.QuerySingleAsync<MutationResult>(new CommandDefinition("""
        with input as materialized (
          select x.client_id, x.item_id, x.effective_date, x.unit_price, nullif(trim(x.remark),'') as remark
            from jsonb_to_recordset(cast(@RowsJson as jsonb)) as x(client_id text,item_id uuid,effective_date date,unit_price numeric,remark text)
        ), before_state as materialized (
          select i.*, p.unit_price as old_unit_price, p.id as existing_id
            from input i left join erp.item_prices p on p.item_id=i.item_id and p.effective_date=i.effective_date
        ), upserted as materialized (
          insert into erp.item_prices(id,item_id,effective_date,unit_price,remark,version,created_at,created_by,updated_at,updated_by)
          select coalesce(existing_id,gen_random_uuid()),item_id,effective_date,unit_price,remark,case when existing_id is null then 1 else 2 end,now(),@Actor,case when existing_id is null then null else now() end,case when existing_id is null then null else @Actor end
            from before_state
          on conflict(item_id,effective_date) do update set unit_price=excluded.unit_price,remark=excluded.remark,version=erp.item_prices.version+1,updated_at=now(),updated_by=@Actor
          returning item_id,effective_date
        ), audit_insert as (
          insert into audit.entries(id,aggregate_type,aggregate_id,action,actor,occurred_at,data)
          select gen_random_uuid(),'Item',b.item_id,'ITEM_PRICE_SAVED',@Actor,now(),jsonb_build_object('effectiveDate',b.effective_date,'before',b.old_unit_price,'after',b.unit_price)
            from before_state b returning 1
        ), outbox_insert as (
          insert into integration.outbox(id,event_type,aggregate_id,payload,occurred_at,status)
          select gen_random_uuid(),'ErpItemPriceChanged',b.item_id,jsonb_build_object('itemId',b.item_id,'effectiveDate',b.effective_date,'unitPrice',b.unit_price),now(),'PENDING'
            from before_state b returning 1
        )
        select count(*)::int as Saved,
               count(*) filter(where existing_id is null)::int as Created,
               count(*) filter(where existing_id is not null)::int as Updated
          from before_state;
        """,new{RowsJson=rowsJson,Actor=actor},tx,cancellationToken:ct));

        var response=new SaveItemPricesResponse(req.Rows.Count,result.Saved,result.Created,result.Updated);
        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="erp.itemPrices.bulkSave",Key=key,Response=JsonSerializer.Serialize(response)},tx,cancellationToken:ct));
        await tx.CommitAsync(ct); await Send.OkAsync(response,ct);
    }
    private sealed record MutationResult(int Saved,int Created,int Updated);
}
