namespace KArtSell.Modules.ModelOperations.PortfolioReconciliation;

using System;
using System.Data;
using System.Data.Common;
using System.Text.Json;
using System.Threading.Tasks;
using KArtSell.BuildingBlocks.Hashing;
using KArtSell.BuildingBlocks.Reliability;
using KArtSell.BuildingBlocks.Time;

/// <summary>
/// Handles trade reconciliation command.
/// Updates holdings, calculates cost basis, detects mismatches.
/// Publishes reconciliation event to Outbox.
/// </summary>
public class ReconcileTradeCommand
{
    public Guid TradeId { get; set; }
    public Guid SecurityId { get; set; }
    public int ExecutedQuantity { get; set; }
    public decimal ExecutedPrice { get; set; }
    public int ApprovedQuantity { get; set; }
    public decimal ApprovedPrice { get; set; }
    public DateTime TradeDate { get; set; }
    public DateTime ExpectedSettlementDate { get; set; }
    public DateTime? ActualSettlementDate { get; set; }
    public Guid CorrelationId { get; set; }
    public string? IdempotencyKey { get; set; }
}

public class ReconcileTradeHandler
{
    private readonly ReconciliationEngine _engine;
    private readonly IDbConnection _connection;
    private readonly IOutboxWriter _outboxWriter;
    private readonly IClock _clock;

    public ReconcileTradeHandler(
        ReconciliationEngine engine,
        IDbConnection connection,
        IOutboxWriter outboxWriter,
        IClock clock)
    {
        _engine = engine ?? throw new ArgumentNullException(nameof(engine));
        _connection = connection ?? throw new ArgumentNullException(nameof(connection));
        _outboxWriter = outboxWriter ?? throw new ArgumentNullException(nameof(outboxWriter));
        _clock = clock ?? throw new ArgumentNullException(nameof(clock));
    }

    public async Task HandleAsync(ReconcileTradeCommand command)
    {
        ArgumentNullException.ThrowIfNull(command);
        if (string.IsNullOrWhiteSpace(command.IdempotencyKey))
        {
            throw new ArgumentException("IdempotencyKey is required for replay-safe reconciliation.", nameof(command));
        }

        // DEBT-018: share one connection/transaction across the engine's holding/log writes and
        // the outbox event(s) below, instead of the engine writing on its own connection and the
        // outbox writing on a second, separately-committed one. _connection is scoped per HTTP
        // request (see Program.cs), the same instance ReconciliationEngine/ReconciliationSql use,
        // so this is the same underlying connection, not a new one.
        using var transaction = _connection.BeginTransaction();

        var result = await _engine.ReconcileTradeAsync(
            command.TradeId,
            command.SecurityId,
            command.ExecutedQuantity,
            command.ExecutedPrice,
            command.ApprovedQuantity,
            command.ApprovedPrice,
            command.TradeDate,
            command.ExpectedSettlementDate,
            command.ActualSettlementDate,
            command.CorrelationId,
            transaction);

        if (!result.Success)
        {
            transaction.Rollback();
            throw new InvalidOperationException($"Reconciliation failed: {result.Error}");
        }

        // Publish event to Outbox for async processing
        var @event = new TradeReconciledEvent
        {
            EventId = Guid.NewGuid(),
            TradeId = command.TradeId,
            HoldingId = result.Holding!.Id,
            SecurityId = command.SecurityId,
            QuantityAfter = result.Holding.Quantity,
            CostBasisAfter = result.Holding.TotalCostBasis,
            MismatchDetected = result.MismatchDetected,
            MismatchSummary = result.MismatchDetected
                ? FormatMismatchSummary(result.Mismatches)
                : null,
            ReconciliationTimestamp = _clock.UtcNow.UtcDateTime,
            CorrelationId = command.CorrelationId,
            IdempotencyKey = command.IdempotencyKey
        };

        await PublishAsync(transaction, "TradeReconciled", @event, command.CorrelationId, CancellationToken.None);

        // If mismatches detected, publish alert event
        if (result.MismatchDetected)
        {
            var alertEvent = new ReconciliationMismatchAlertEvent
            {
                EventId = Guid.NewGuid(),
                TradeId = command.TradeId,
                HoldingId = result.Holding.Id,
                MismatchCount = result.Mismatches.Count,
                HighSeverityCount = CountBysSeverity(result.Mismatches, MismatchSeverity.High),
                MismatchDetails = FormatMismatchDetails(result.Mismatches),
                AlertedAt = _clock.UtcNow.UtcDateTime,
                CorrelationId = command.CorrelationId
            };

            await PublishAsync(transaction, "ReconciliationMismatchAlert", alertEvent, command.CorrelationId, CancellationToken.None);
        }

        transaction.Commit();
    }

    /// <summary>
    /// DEBT-018 (fixed): writes to the same transaction the holding/log writes used above, so a
    /// crash between the entity write and the outbox write can no longer leave one committed
    /// without the other.
    /// </summary>
    private async Task PublishAsync<T>(IDbTransaction transaction, string eventType, T @event, Guid correlationId, CancellationToken ct) where T : class
    {
        var payload = JsonSerializer.Serialize(@event);
        var message = new OutboxMessage(
            Guid.NewGuid(),
            eventType,
            1,
            payload,
            correlationId.ToString(),
            _clock.UtcNow,
            ContentHasher.Sha256(payload));

        await _outboxWriter.AddAsync((DbConnection)_connection, (DbTransaction)transaction, message, ct);
    }

    private int CountBysSeverity(List<Mismatch> mismatches, MismatchSeverity severity)
    {
        return mismatches.Count(m => m.Severity == severity);
    }

    private string FormatMismatchSummary(List<Mismatch> mismatches)
    {
        return string.Join("; ", mismatches.ConvertAll(m => m.Type.ToString()));
    }

    private string FormatMismatchDetails(List<Mismatch> mismatches)
    {
        return string.Join("\n", mismatches.ConvertAll(m => $"{m.Type}: {m.Description}"));
    }
}

/// <summary>
/// Event published when trade is reconciled.
/// </summary>
public class TradeReconciledEvent
{
    public Guid EventId { get; set; }
    public Guid TradeId { get; set; }
    public Guid HoldingId { get; set; }
    public Guid SecurityId { get; set; }
    public int QuantityAfter { get; set; }
    public decimal CostBasisAfter { get; set; }
    public bool MismatchDetected { get; set; }
    public string? MismatchSummary { get; set; }
    public DateTime ReconciliationTimestamp { get; set; }
    public Guid CorrelationId { get; set; }
    public string IdempotencyKey { get; set; } = string.Empty;
}

/// <summary>
/// Event published when reconciliation mismatches are detected.
/// </summary>
public class ReconciliationMismatchAlertEvent
{
    public Guid EventId { get; set; }
    public Guid TradeId { get; set; }
    public Guid HoldingId { get; set; }
    public int MismatchCount { get; set; }
    public int HighSeverityCount { get; set; }
    public string MismatchDetails { get; set; } = string.Empty;
    public DateTime AlertedAt { get; set; }
    public Guid CorrelationId { get; set; }
}
