using Hangfire;
using System.Text.Json;
using KArtSell.BuildingBlocks.Time;
using KArtSell.Modules.ModelOperations.Domain;

namespace KArtSell.Host.Features.MarketData;

/// <summary>
/// VS-03 ASYNC: Market Data Ingestion Job
///
/// Scheduled: Daily 9:00 KST (before market open)
/// Responsibility: Fetch, validate, normalize, persist market data
/// Idempotency: By date range (same range = no re-run)
/// </summary>

public class MarketDataSyncedEvent
{
    public Guid EventId { get; set; } = Guid.NewGuid();
    public string EventType { get; set; } = "MarketDataSynced";
    public DateOnly FromDate { get; set; }
    public DateOnly ToDate { get; set; }
    public int RowsProcessed { get; set; }
    public int RowsFailed { get; set; }
    public DateTime SyncedAt { get; set; }
    public string CorrelationId { get; set; } = "";
}

public interface IMarketDataEventPublisher
{
    Task PublishSyncedAsync(MarketDataSyncedEvent evt, CancellationToken ct);
}

public class MarketDataEventPublisher : IMarketDataEventPublisher
{
    private readonly Npgsql.NpgsqlDataSource _dataSource;

    public MarketDataEventPublisher(Npgsql.NpgsqlDataSource dataSource)
    {
        _dataSource = dataSource;
    }

    public async Task PublishSyncedAsync(MarketDataSyncedEvent evt, CancellationToken ct)
    {
        await using var connection = await _dataSource.OpenConnectionAsync(ct);

        const string sql = """
            INSERT INTO shared.outbox (aggregate_id, event_type, payload, published_at, correlation_id)
            VALUES (@aggregateId, @eventType, @payload, CURRENT_TIMESTAMP, @correlationId);
            """;

        await using var cmd = connection.CreateCommand();
        cmd.CommandText = sql;
        cmd.Parameters.AddWithValue("@aggregateId", Guid.NewGuid());
        cmd.Parameters.AddWithValue("@eventType", evt.EventType);
        cmd.Parameters.AddWithValue("@payload", JsonSerializer.Serialize(evt));
        cmd.Parameters.AddWithValue("@correlationId", evt.CorrelationId);

        await cmd.ExecuteNonQueryAsync(ct);
    }
}

/// <summary>
/// VS-03 ASYNC: Daily ingestion job
///
/// Runs at 9:00 KST daily
/// Flow: Fetch → Validate → Normalize → Persist → Event publish
/// </summary>

public class MarketDataIngestionJobHandler : IMarketDataIngestionJob
{
    private readonly IMarketDataDataSourceClient _krxClient;
    private readonly IMarketDataEventPublisher _eventPublisher;
    private readonly Npgsql.NpgsqlDataSource _dataSource;
    private readonly IClock _clock;

    public MarketDataIngestionJobHandler(
        IMarketDataDataSourceClient krxClient,
        IMarketDataEventPublisher eventPublisher,
        Npgsql.NpgsqlDataSource dataSource,
        IClock clock)
    {
        _krxClient = krxClient;
        _eventPublisher = eventPublisher;
        _dataSource = dataSource;
        _clock = clock;
    }

    public async Task ExecuteAsync(
        Guid jobId,
        string dataSource,
        DateOnly fromDate,
        DateOnly toDate,
        string correlationId,
        CancellationToken ct)
    {
        var startTime = _clock.UtcNow;
        var rowsProcessed = 0;
        var rowsFailed = 0;

        try
        {
            // Update job status
            await UpdateJobStatusAsync(jobId, "Running", ct);

            // Fetch prices from data source
            var prices = await _krxClient.FetchPricesAsync(dataSource, fromDate, toDate, ct);

            if (prices.Count == 0)
            {
                await UpdateJobStatusAsync(jobId, "Completed", 0, 0, ct);
                return;
            }

            // Validate & normalize
            var validPrices = new List<DailyPrice>();
            foreach (var price in prices)
            {
                var result = MarketDataPolicy.ValidatePrice(price, toDate);
                if (result.IsValid)
                {
                    var normalized = MarketDataPolicy.NormalizePrice(price);
                    if (normalized != null)
                    {
                        validPrices.Add(normalized);
                        rowsProcessed++;
                    }
                }
                else
                {
                    rowsFailed++;
                }
            }

            // Persist to database
            await PersistPricesAsync(validPrices, ct);

            // Publish event
            var evt = new MarketDataSyncedEvent
            {
                FromDate = fromDate,
                ToDate = toDate,
                RowsProcessed = rowsProcessed,
                RowsFailed = rowsFailed,
                SyncedAt = _clock.UtcNow.DateTime,
                CorrelationId = correlationId,
            };

            await _eventPublisher.PublishSyncedAsync(evt, ct);

            // Mark complete
            var duration = (int)(_clock.UtcNow - startTime).TotalSeconds;
            await UpdateJobStatusAsync(jobId, "Completed", rowsProcessed, rowsFailed, duration, null, ct);
        }
        catch (Exception ex)
        {
            await UpdateJobStatusAsync(jobId, "Failed", rowsProcessed, rowsFailed, null, ex.Message, ct);
            throw;
        }
    }

    private async Task PersistPricesAsync(List<DailyPrice> prices, CancellationToken ct)
    {
        if (prices.Count == 0)
            return;

        const string sql = """
            INSERT INTO market_data.daily_prices
            (symbol, trading_date, open_price, high_price, low_price, close_price, volume, published_at, revision, data_source, correlation_id)
            VALUES (@symbol, @date, @open, @high, @low, @close, @volume, CURRENT_TIMESTAMP, 1, @source, @corrId)
            ON CONFLICT (symbol, trading_date, revision) DO UPDATE SET
              open_price = EXCLUDED.open_price,
              close_price = EXCLUDED.close_price,
              volume = EXCLUDED.volume,
              published_at = CURRENT_TIMESTAMP
            WHERE EXCLUDED.published_at > market_data.daily_prices.published_at;
            """;

        await using var connection = await _dataSource.OpenConnectionAsync(ct);
        foreach (var price in prices)
        {
            await using var cmd = connection.CreateCommand();
            cmd.CommandText = sql;
            cmd.Parameters.AddWithValue("@symbol", price.Symbol);
            cmd.Parameters.AddWithValue("@date", price.TradingDate.ToDateTime(TimeOnly.MinValue));
            cmd.Parameters.AddWithValue("@open", price.OpenPrice);
            cmd.Parameters.AddWithValue("@high", price.HighPrice);
            cmd.Parameters.AddWithValue("@low", price.LowPrice);
            cmd.Parameters.AddWithValue("@close", price.ClosePrice);
            cmd.Parameters.AddWithValue("@volume", price.Volume);
            cmd.Parameters.AddWithValue("@source", price.DataSource);
            cmd.Parameters.AddWithValue("@corrId", price.CorrelationId);

            await cmd.ExecuteNonQueryAsync(ct);
        }
    }

    private async Task UpdateJobStatusAsync(Guid jobId, string status, CancellationToken ct)
        => await UpdateJobStatusAsync(jobId, status, 0, 0, null, null, ct);

    private async Task UpdateJobStatusAsync(
        Guid jobId,
        string status,
        int rowsProcessed,
        int rowsFailed,
        CancellationToken ct)
        => await UpdateJobStatusAsync(jobId, status, rowsProcessed, rowsFailed, null, null, ct);

    private async Task UpdateJobStatusAsync(
        Guid jobId,
        string status,
        int rowsProcessed,
        int rowsFailed,
        int? durationSeconds,
        string? errorMessage,
        CancellationToken ct)
    {
        const string sql = """
            UPDATE market_data.ingestion_jobs
            SET status = @status,
                rows_processed = @rows,
                rows_failed = @failed,
                duration_seconds = @duration,
                last_error_message = @error,
                completed_at = CASE WHEN @status IN ('Completed', 'Failed') THEN CURRENT_TIMESTAMP ELSE NULL END,
                updated_at = CURRENT_TIMESTAMP
            WHERE job_id = @jobId;
            """;

        await using var connection = await _dataSource.OpenConnectionAsync(ct);
        await using var cmd = connection.CreateCommand();
        cmd.CommandText = sql;
        cmd.Parameters.AddWithValue("@jobId", jobId);
        cmd.Parameters.AddWithValue("@status", status);
        cmd.Parameters.AddWithValue("@rows", rowsProcessed);
        cmd.Parameters.AddWithValue("@failed", rowsFailed);
        cmd.Parameters.AddWithValue("@duration", durationSeconds ?? (object)DBNull.Value);
        cmd.Parameters.AddWithValue("@error", errorMessage ?? (object)DBNull.Value);

        await cmd.ExecuteNonQueryAsync(ct);
    }
}

/// <summary>
/// Abstraction: Market data source client (KRX, OpenDart, Stub)
/// </summary>

public interface IMarketDataDataSourceClient
{
    Task<List<DailyPrice>> FetchPricesAsync(string dataSource, DateOnly fromDate, DateOnly toDate, CancellationToken ct);
}

public class StubMarketDataClient : IMarketDataDataSourceClient
{
    public async Task<List<DailyPrice>> FetchPricesAsync(string dataSource, DateOnly fromDate, DateOnly toDate, CancellationToken ct)
    {
        await Task.Delay(100, ct); // Stub delay

        // Return empty for now (real implementation would call KRX/OpenDart)
        return new();
    }
}
