using System.Globalization;
using System.Text.Json;
using Dapper;
using Npgsql;
using Shared.Excel;
using Kbx.Contracts.Generated;

namespace Modules.OMS.Orders.Import;

public sealed class OrderImportDefinition(NpgsqlDataSource dataSource) : IImportDefinition
{
    private static readonly JsonSerializerOptions Json = new(JsonSerializerDefaults.Web);

    public ImportDefinition Definition { get; } = new(
        Id: "oms.orders.v1",
        ScreenId: "OMS-ORD-003",
        Entity: "order",
        Title: "주문 Excel 업로드",
        Fields: [
            I(KbxFieldKeys.OrderNo),
            I(KbxFieldKeys.OrderDate),
            I(KbxFieldKeys.CustomerCode),
            I(KbxFieldKeys.WarehouseCode),
            I(KbxFieldKeys.ReceiverName),
            I(KbxFieldKeys.Phone),
            I(KbxFieldKeys.PostalCode),
            I(KbxFieldKeys.Address1),
            I(KbxFieldKeys.Address2),
            I(KbxFieldKeys.ItemCode),
            I(KbxFieldKeys.OrderQty),
            I(KbxFieldKeys.UnitPrice),
            I(KbxFieldKeys.Remark),
        ],
        AllowCreate: true,
        AllowUpdate: true,
        MaxFileSizeBytes: 20 * 1024 * 1024,
        MaxRows: 100_000);

    public async Task<IReadOnlyList<ImportRowValidation>> ValidateAsync(
        IReadOnlyList<ImportRawRow> rows,
        IReadOnlyDictionary<string, string> targetToSource,
        NpgsqlConnection connection,
        CancellationToken ct)
    {
        var customerCodes = Values(rows, targetToSource, KbxFieldKeys.CustomerCode);
        var warehouseCodes = Values(rows, targetToSource, KbxFieldKeys.WarehouseCode);
        var itemCodes = Values(rows, targetToSource, KbxFieldKeys.ItemCode);
        var orderNos = Values(rows, targetToSource, KbxFieldKeys.OrderNo);

        var customers = (await connection.QueryAsync<RefRow>(new CommandDefinition(
            "select id,code from oms.customers where is_active and code = any(@Codes)",
            new { Codes = customerCodes }, cancellationToken: ct))).ToDictionary(x => x.Code, StringComparer.OrdinalIgnoreCase);
        var warehouses = (await connection.QueryAsync<RefRow>(new CommandDefinition(
            "select id,code from inventory.warehouses where is_active and code = any(@Codes)",
            new { Codes = warehouseCodes }, cancellationToken: ct))).ToDictionary(x => x.Code, StringComparer.OrdinalIgnoreCase);
        var items = (await connection.QueryAsync<RefRow>(new CommandDefinition(
            "select id,code from catalog.items where is_active and code = any(@Codes)",
            new { Codes = itemCodes }, cancellationToken: ct))).ToDictionary(x => x.Code, StringComparer.OrdinalIgnoreCase);
        var existing = (await connection.QueryAsync<ExistingOrder>(new CommandDefinition(
            """
            select id, order_no as "OrderNo", status
              from oms.orders
             where order_no = any(@OrderNos)
            """,
            new { OrderNos = orderNos }, cancellationToken: ct))).ToDictionary(x => x.OrderNo, StringComparer.OrdinalIgnoreCase);

        var results = new List<ImportRowValidation>(rows.Count);
        foreach (var row in rows)
            results.Add(ValidateRow(row, targetToSource, customers, warehouses, items, existing));

        // One order can span multiple Excel rows. Header values must be identical across its lines.
        foreach (var group in results.Where(x => x.NormalizedData is not null && x.DomainKey is not null).GroupBy(x => x.DomainKey!, StringComparer.OrdinalIgnoreCase))
        {
            var signatures = group.Select(x => HeaderSignature(x.NormalizedData!.RootElement)).Distinct(StringComparer.Ordinal).ToArray();
            if (signatures.Length <= 1) continue;
            foreach (var row in group)
            {
                var issue = new ImportIssue(row.RowNumber, null, null, "ORDER_HEADER_CONFLICT", "같은 주문번호의 주문일·거래처·창고·배송정보가 서로 다릅니다.", "error");
                var index = results.IndexOf(row);
                results[index] = row with { Errors = row.Errors.Append(issue).ToArray() };
            }
        }
        return results;
    }

    public async Task<ImportCommitResult> CommitAsync(Guid sessionId, string actor, CancellationToken ct)
    {
        await using var readConnection = await dataSource.OpenConnectionAsync(ct);
        var staged = (await readConnection.QueryAsync<StagedRow>(new CommandDefinition("""
            select row_number as "RowNumber", domain_key as "DomainKey", normalized_data::text as "NormalizedJson"
              from kbx.import_rows
             where session_id=@SessionId and status='VALID'
             order by domain_key,row_number;
            """, new { SessionId = sessionId }, cancellationToken: ct))).ToArray();

        var created = 0; var updated = 0; var failed = 0;
        foreach (var group in staged.GroupBy(x => x.DomainKey, StringComparer.OrdinalIgnoreCase))
        {
            var lines = group.Select(x => JsonSerializer.Deserialize<NormalizedOrderRow>(x.NormalizedJson, Json)!).ToArray();
            try
            {
                await using var connection = await dataSource.OpenConnectionAsync(ct);
                await using var tx = await connection.BeginTransactionAsync(ct);
                var existing = await connection.QuerySingleOrDefaultAsync<ExistingOrder>(new CommandDefinition(
                    """
                    select id, order_no as "OrderNo", status
                      from oms.orders
                     where order_no=@OrderNo
                     for update
                    """,
                    new { OrderNo = group.Key }, tx, cancellationToken: ct));

                Guid orderId;
                var header = lines[0];
                if (existing is null)
                {
                    orderId = Guid.NewGuid();
                    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,'NEW',1,now(),@Actor);
                        """, new { Id=orderId, header.OrderNo, header.OrderDate, header.CustomerId, header.WarehouseId, header.ReceiverName, header.Phone, header.PostalCode, header.Address1, header.Address2, Actor=actor }, tx, cancellationToken: ct));
                    created++;
                }
                else
                {
                    if (existing.Status is not ("NEW" or "DRAFT")) throw new InvalidOperationException($"{existing.OrderNo} 주문은 현재 상태에서 수정할 수 없습니다.");
                    orderId = existing.Id;
                    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+1,updated_at=now(),updated_by=@Actor where id=@Id;
                        delete from oms.order_lines where order_id=@Id;
                        """, new { Id=orderId, header.OrderDate, header.CustomerId, header.WarehouseId, header.ReceiverName, header.Phone, header.PostalCode, header.Address1, header.Address2, Actor=actor }, tx, cancellationToken: ct));
                    updated++;
                }

                for (var i = 0; i < lines.Length; i++)
                {
                    var line = lines[i];
                    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=i+1, line.ItemId, Quantity=line.OrderQty, line.UnitPrice, Amount=line.OrderQty*line.UnitPrice, line.Remark }, tx, cancellationToken: ct));
                }

                await connection.ExecuteAsync(new CommandDefinition("""
                    update kbx.import_rows set status='COMMITTED'
                     where session_id=@SessionId and domain_key=@OrderNo and status='VALID';
                    insert into audit.entries(id,aggregate_type,aggregate_id,action,actor,occurred_at,data)
                    values(@AuditId,'Order',@OrderId,'ExcelImport',@Actor,now(),jsonb_build_object('sessionId',@SessionText,'orderNo',@OrderNo));
                    insert into integration.outbox(id,event_type,aggregate_id,payload,occurred_at,status)
                    values(@EventId,'OrderImported',@OrderId,jsonb_build_object('sessionId',@SessionText,'orderId',@OrderText,'orderNo',@OrderNo),now(),'PENDING');
                    """, new { SessionId=sessionId, SessionText=sessionId.ToString(), OrderNo=group.Key, AuditId=Guid.NewGuid(), EventId=Guid.NewGuid(), OrderId=orderId, OrderText=orderId.ToString(), Actor=actor }, tx, cancellationToken: ct));
                await tx.CommitAsync(ct);
            }
            catch (Exception ex)
            {
                failed += group.Count();
                await readConnection.ExecuteAsync(new CommandDefinition("""
                    update kbx.import_rows set status='COMMIT_FAILED', errors = errors || jsonb_build_array(jsonb_build_object(
                        'rowNumber',row_number,'code','COMMIT_FAILED','message',@Message,'severity','error'))
                     where session_id=@SessionId and domain_key=@OrderNo and status='VALID';
                    """, new { SessionId=sessionId, OrderNo=group.Key, Message=ex.Message }, cancellationToken: ct));
            }
        }
        return new ImportCommitResult(created, updated, failed);
    }

    private static ImportRowValidation ValidateRow(
        ImportRawRow row,
        IReadOnlyDictionary<string,string> map,
        IReadOnlyDictionary<string,RefRow> customers,
        IReadOnlyDictionary<string,RefRow> warehouses,
        IReadOnlyDictionary<string,RefRow> items,
        IReadOnlyDictionary<string,ExistingOrder> existing)
    {
        var errors = new List<ImportIssue>();
        string? V(string key) => map.TryGetValue(key, out var column) && row.Values.TryGetValue(column, out var value) ? value?.Trim() : null;
        void Required(string key, string label, string? value) { if (string.IsNullOrWhiteSpace(value)) errors.Add(E(row.RowNumber,key,map,label + "을(를) 입력하세요.")); }

        var orderNo=V(KbxFieldKeys.OrderNo); var customerCode=V(KbxFieldKeys.CustomerCode); var warehouseCode=V(KbxFieldKeys.WarehouseCode); var itemCode=V(KbxFieldKeys.ItemCode);
        var receiver=V(KbxFieldKeys.ReceiverName); var phone=V(KbxFieldKeys.Phone); var address1=V(KbxFieldKeys.Address1);
        Required("orderNo","주문번호",orderNo); Required("customerCode","거래처코드",customerCode); Required("warehouseCode","출고창고",warehouseCode);
        Required("itemCode","품목코드",itemCode); Required("receiverName","수취인",receiver); Required("phone","연락처",phone); Required("address1","주소",address1);

        if (!TryDate(V(KbxFieldKeys.OrderDate), out var orderDate)) errors.Add(E(row.RowNumber,KbxFieldKeys.OrderDate,map,"주문일 형식을 확인하세요. (yyyy-MM-dd 또는 yyyyMMdd)"));
        if (!TryDecimal(V(KbxFieldKeys.OrderQty), out var orderQty) || orderQty <= 0) errors.Add(E(row.RowNumber,KbxFieldKeys.OrderQty,map,"수량은 0보다 커야 합니다."));
        if (!TryDecimal(V(KbxFieldKeys.UnitPrice), out var unitPrice) || unitPrice < 0) errors.Add(E(row.RowNumber,KbxFieldKeys.UnitPrice,map,"단가는 0 이상이어야 합니다."));

        customers.TryGetValue(customerCode ?? "", out var customer);
        warehouses.TryGetValue(warehouseCode ?? "", out var warehouse);
        items.TryGetValue(itemCode ?? "", out var item);
        if (!string.IsNullOrWhiteSpace(customerCode) && customer is null) errors.Add(E(row.RowNumber,"customerCode",map,$"존재하지 않거나 사용중지된 거래처코드입니다: {customerCode}"));
        if (!string.IsNullOrWhiteSpace(warehouseCode) && warehouse is null) errors.Add(E(row.RowNumber,"warehouseCode",map,$"존재하지 않거나 사용중지된 창고코드입니다: {warehouseCode}"));
        if (!string.IsNullOrWhiteSpace(itemCode) && item is null) errors.Add(E(row.RowNumber,"itemCode",map,$"존재하지 않거나 사용중지된 품목코드입니다: {itemCode}"));
        if (!string.IsNullOrWhiteSpace(orderNo) && existing.TryGetValue(orderNo, out var old) && old.Status is not ("NEW" or "DRAFT"))
            errors.Add(E(row.RowNumber,"orderNo",map,$"{old.Status} 상태의 기존 주문은 Excel로 수정할 수 없습니다."));

        JsonDocument? normalized = null;
        if (errors.Count == 0)
        {
            normalized = JsonSerializer.SerializeToDocument(new NormalizedOrderRow(
                orderNo!, orderDate, customer!.Id, customerCode!, warehouse!.Id, warehouseCode!, receiver!, phone!, V(KbxFieldKeys.PostalCode), address1!, V(KbxFieldKeys.Address2), item!.Id, itemCode!, orderQty, unitPrice, V(KbxFieldKeys.Remark)), Json);
        }
        return new(row.RowNumber, row.Values, orderNo, normalized, errors, []);
    }

    private static ImportIssue E(int row,string field,IReadOnlyDictionary<string,string> map,string message) =>
        new(row,field,map.TryGetValue(field,out var source)?source:null,"INVALID_VALUE",message,"error");

    private static bool TryDate(string? value, out DateOnly result) =>
        DateOnly.TryParseExact(value, ["yyyy-MM-dd","yyyyMMdd","yyyy/M/d"], CultureInfo.InvariantCulture, DateTimeStyles.None, out result);
    private static bool TryDecimal(string? value, out decimal result) =>
        decimal.TryParse(value?.Replace(",", ""), NumberStyles.Number, CultureInfo.InvariantCulture, out result);
    private static string[] Values(IReadOnlyList<ImportRawRow> rows,IReadOnlyDictionary<string,string> map,string field) =>
        map.TryGetValue(field,out var source) ? rows.Select(x=>x.Values.TryGetValue(source,out var v)?v?.Trim():null).Where(x=>!string.IsNullOrWhiteSpace(x)).Distinct(StringComparer.OrdinalIgnoreCase).Cast<string>().ToArray() : [];
    private static string HeaderSignature(JsonElement e) => string.Join("|", ["orderDate","customerId","warehouseId","receiverName","phone","postalCode","address1","address2"].Select(x=>e.TryGetProperty(x,out var v)?v.ToString():""));
    private static ImportFieldDefinition I(string key) => ImportFieldDefinitionFactory.FromKbxField(key);

    private sealed record RefRow(Guid Id,string Code);
    private sealed record ExistingOrder(Guid Id,string OrderNo,string Status);
    private sealed record StagedRow(int RowNumber,string DomainKey,string NormalizedJson);
    private sealed record NormalizedOrderRow(string OrderNo,DateOnly OrderDate,Guid CustomerId,string CustomerCode,Guid WarehouseId,string WarehouseCode,string ReceiverName,string Phone,string? PostalCode,string Address1,string? Address2,Guid ItemId,string ItemCode,decimal OrderQty,decimal UnitPrice,string? Remark);
}
