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

namespace KArtSell.Host.Features.Portfolio;

/// <summary>
/// VS-04 BE: Portfolio Rebalance Endpoint
/// POST /api/portfolio/{id}/rebalance - Trigger portfolio rebalancing
/// GET /api/portfolio/{id}/composition - Get current composition
///
/// Orchestrates portfolio aggregation, drift analysis, and Hangfire job scheduling
/// Idempotent by (portfolio_id, target_weights_hash, correlation_id)
/// </summary>

public sealed class RebalanceRequest
{
    public List<TargetWeightDto> TargetWeights { get; set; } = new();
    public decimal DriftThreshold { get; set; } = 5;
}

public sealed class TargetWeightDto
{
    public string Symbol { get; set; } = "";
    public decimal TargetPercent { get; set; }
}

public sealed class RebalanceResponse
{
    public Guid JobId { get; set; }
    public string Status { get; set; } = "Queued";
    public int EstimatedTradeCount { get; set; }
    public decimal EstimatedCost { get; set; }
    public string CorrelationId { get; set; } = "";
    public DateTime QueuedAt { get; set; }
}

public sealed class PortfolioCompositionResponse
{
    public Guid PortfolioId { get; set; }
    public DateOnly SnapshotDate { get; set; }
    public List<PositionDto> Positions { get; set; } = new();
    public decimal TotalValue { get; set; }
    public DateTime LastUpdate { get; set; }
}

public sealed class PositionDto
{
    public string Symbol { get; set; } = "";
    public decimal Quantity { get; set; }
    public decimal MarketPrice { get; set; }
    public decimal MarketValue { get; set; }
    public decimal WeightPercent { get; set; }
}

public sealed class TriggerRebalanceEndpoint : Endpoint<RebalanceRequest, RebalanceResponse>
{
    private readonly IPortfolioRebalanceService _rebalanceService;
    private readonly IClock _clock;

    public TriggerRebalanceEndpoint(IPortfolioRebalanceService rebalanceService, IClock clock)
    {
        _rebalanceService = rebalanceService;
        _clock = clock;
    }

    public override void Configure()
    {
        Post("/api/portfolio/{portfolioId}/rebalance");
        Roles("PortfolioManager");
    }

    public override async Task HandleAsync(RebalanceRequest req, CancellationToken ct)
    {
        var portfolioIdStr = Route<string>("portfolioId");
        if (!Guid.TryParse(portfolioIdStr, out var portfolioId))
        {
            ThrowError("Invalid portfolio ID");
            return;
        }

        var correlationId = HttpContext.TraceIdentifier;

        var (jobId, tradeCount, cost) = await _rebalanceService.ScheduleRebalanceAsync(
            portfolioId: portfolioId,
            targetWeights: req.TargetWeights.Select(w => new TargetWeight(w.Symbol, w.TargetPercent)).ToList(),
            driftThreshold: req.DriftThreshold,
            correlationId: correlationId,
            cancellationToken: ct);

        HttpContext.Response.StatusCode = StatusCodes.Status202Accepted;
        HttpContext.Response.ContentType = "application/json";
        await HttpContext.Response.WriteAsync(JsonSerializer.Serialize(new RebalanceResponse
        {
            JobId = jobId,
            Status = "Queued",
            EstimatedTradeCount = tradeCount,
            EstimatedCost = cost,
            CorrelationId = correlationId,
            QueuedAt = _clock.UtcNow.DateTime,
        }), ct);
    }
}

public sealed class GetCompositionEndpoint : EndpointWithoutRequest<PortfolioCompositionResponse>
{
    private readonly IPortfolioRebalanceService _rebalanceService;
    private readonly IClock _clock;

    public GetCompositionEndpoint(IPortfolioRebalanceService rebalanceService, IClock clock)
    {
        _rebalanceService = rebalanceService;
        _clock = clock;
    }

    public override void Configure()
    {
        Get("/api/portfolio/{portfolioId}/composition");
        Roles("PortfolioManager");
    }

    public override async Task HandleAsync(CancellationToken ct)
    {
        var portfolioIdStr = Route<string>("portfolioId");
        if (!Guid.TryParse(portfolioIdStr, out var portfolioId))
        {
            ThrowError("Invalid portfolio ID");
            return;
        }

        var composition = await _rebalanceService.GetCompositionAsync(portfolioId, ct);

        if (composition == null)
        {
            ThrowError("Portfolio not found");
            return;
        }

        HttpContext.Response.StatusCode = StatusCodes.Status200OK;
        HttpContext.Response.ContentType = "application/json";
        await HttpContext.Response.WriteAsync(JsonSerializer.Serialize(composition), ct);
    }
}

/// <summary>
/// VS-04 Application Handler: Orchestrates rebalance operations
/// </summary>

public interface IPortfolioRebalanceService
{
    Task<(Guid JobId, int TradeCount, decimal Cost)> ScheduleRebalanceAsync(
        Guid portfolioId,
        List<TargetWeight> targetWeights,
        decimal driftThreshold,
        string correlationId,
        CancellationToken cancellationToken);

    Task<PortfolioCompositionResponse?> GetCompositionAsync(Guid portfolioId, CancellationToken cancellationToken);
}

public class PortfolioRebalanceService : IPortfolioRebalanceService
{
    private readonly NpgsqlDataSource _dataSource;
    private readonly IBackgroundJobClient _jobClient;
    private readonly IClock _clock;

    public PortfolioRebalanceService(NpgsqlDataSource dataSource, IBackgroundJobClient jobClient, IClock clock)
    {
        _dataSource = dataSource;
        _jobClient = jobClient;
        _clock = clock;
    }

    public async Task<(Guid JobId, int TradeCount, decimal Cost)> ScheduleRebalanceAsync(
        Guid portfolioId,
        List<TargetWeight> targetWeights,
        decimal driftThreshold,
        string correlationId,
        CancellationToken cancellationToken)
    {
        var jobId = Guid.NewGuid();

        // Fetch current portfolio composition
        var positions = await FetchPositionsAsync(portfolioId, cancellationToken);
        var portfolio = PortfolioPolicy.AggregatePortfolio(portfolioId, DateOnly.FromDateTime(_clock.UtcNow.DateTime), positions);
        var currentWeights = PortfolioPolicy.CalculateCurrentWeights(portfolio);

        // Analyze drift
        var analysis = PortfolioPolicy.AnalyzeDrift(portfolio, targetWeights, driftThreshold);
        var cost = PortfolioPolicy.EstimateRebalanceCost(analysis);

        // Check idempotency
        var existingJob = await CheckIdempotencyAsync(portfolioId, targetWeights, correlationId, cancellationToken);
        if (existingJob.HasValue)
            return (existingJob.Value, analysis.TradesRequired.Count, cost);

        // Insert job record
        await InsertJobRecordAsync(jobId, portfolioId, targetWeights, correlationId, cancellationToken);

        // Schedule Hangfire job
        _jobClient.Enqueue<IPortfolioRebalanceJob>(j =>
            j.ExecuteAsync(jobId, portfolioId, targetWeights, correlationId, CancellationToken.None));

        return (jobId, analysis.TradesRequired.Count, cost);
    }

    public async Task<PortfolioCompositionResponse?> GetCompositionAsync(Guid portfolioId, CancellationToken cancellationToken)
    {
        const string sql = """
            SELECT symbol, quantity, market_price, market_value, weight_percent
            FROM risk_management.portfolio_positions
            WHERE portfolio_id = @portfolioId
              AND published_at <= @cutoff
              AND removed_at IS NULL
              AND trading_date = CURRENT_DATE
            ORDER BY weight_percent DESC;
            """;

        await using var connection = await _dataSource.OpenConnectionAsync(cancellationToken);
        await using var cmd = connection.CreateCommand();
        cmd.CommandText = sql;
        cmd.Parameters.AddWithValue("@portfolioId", portfolioId);
        cmd.Parameters.AddWithValue("@cutoff", _clock.UtcNow);

        var positions = new List<PositionDto>();
        decimal totalValue = 0;

        await using var reader = await cmd.ExecuteReaderAsync(cancellationToken);
        while (await reader.ReadAsync(cancellationToken))
        {
            var marketValue = reader.GetDecimal(3);
            positions.Add(new PositionDto
            {
                Symbol = reader.GetString(0),
                Quantity = reader.GetDecimal(1),
                MarketPrice = reader.GetDecimal(2),
                MarketValue = marketValue,
                WeightPercent = reader.GetDecimal(4),
            });
            totalValue += marketValue;
        }

        if (positions.Count == 0)
            return null;

        return new PortfolioCompositionResponse
        {
            PortfolioId = portfolioId,
            SnapshotDate = DateOnly.FromDateTime(_clock.UtcNow.DateTime),
            Positions = positions,
            TotalValue = totalValue,
            LastUpdate = _clock.UtcNow.DateTime,
        };
    }

    private async Task<List<Position>> FetchPositionsAsync(Guid portfolioId, CancellationToken cancellationToken)
    {
        const string sql = """
            SELECT symbol, quantity, market_price, cost_basis_per_unit
            FROM risk_management.portfolio_positions
            WHERE portfolio_id = @portfolioId
              AND published_at <= @cutoff
              AND removed_at IS NULL
              AND trading_date = CURRENT_DATE;
            """;

        await using var connection = await _dataSource.OpenConnectionAsync(cancellationToken);
        await using var cmd = connection.CreateCommand();
        cmd.CommandText = sql;
        cmd.Parameters.AddWithValue("@portfolioId", portfolioId);
        cmd.Parameters.AddWithValue("@cutoff", _clock.UtcNow);

        var positions = new List<Position>();
        await using var reader = await cmd.ExecuteReaderAsync(cancellationToken);
        while (await reader.ReadAsync(cancellationToken))
        {
            positions.Add(new Position(
                Symbol: reader.GetString(0),
                Quantity: reader.GetDecimal(1),
                MarketPrice: reader.GetDecimal(2),
                CostBasisPerUnit: reader.IsDBNull(3) ? 0 : reader.GetDecimal(3)));
        }

        return positions;
    }

    private async Task<Guid?> CheckIdempotencyAsync(
        Guid portfolioId,
        List<TargetWeight> targetWeights,
        string correlationId,
        CancellationToken cancellationToken)
    {
        var weightsHash = HashTargetWeights(targetWeights);
        const string sql = """
            SELECT job_id FROM risk_management.rebalance_jobs
            WHERE portfolio_id = @portfolioId
              AND target_weights_hash = @hash
              AND correlation_id = @correlationId
              AND status IN ('Running', 'Completed')
            LIMIT 1;
            """;

        await using var connection = await _dataSource.OpenConnectionAsync(cancellationToken);
        await using var cmd = connection.CreateCommand();
        cmd.CommandText = sql;
        cmd.Parameters.AddWithValue("@portfolioId", portfolioId);
        cmd.Parameters.AddWithValue("@hash", weightsHash);
        cmd.Parameters.AddWithValue("@correlationId", correlationId);

        var result = await cmd.ExecuteScalarAsync(cancellationToken);
        return result is Guid jobId ? jobId : null;
    }

    private async Task InsertJobRecordAsync(
        Guid jobId,
        Guid portfolioId,
        List<TargetWeight> targetWeights,
        string correlationId,
        CancellationToken cancellationToken)
    {
        var weightsHash = HashTargetWeights(targetWeights);
        const string sql = """
            INSERT INTO risk_management.rebalance_jobs
                (job_id, portfolio_id, target_weights_hash, correlation_id, status, requested_at, requested_by)
            VALUES (@jobId, @portfolioId, @hash, @correlationId, 'Queued', CURRENT_TIMESTAMP, 'API');
            """;

        await using var connection = await _dataSource.OpenConnectionAsync(cancellationToken);
        await using var cmd = connection.CreateCommand();
        cmd.CommandText = sql;
        cmd.Parameters.AddWithValue("@jobId", jobId);
        cmd.Parameters.AddWithValue("@portfolioId", portfolioId);
        cmd.Parameters.AddWithValue("@hash", weightsHash);
        cmd.Parameters.AddWithValue("@correlationId", correlationId);

        await cmd.ExecuteNonQueryAsync(cancellationToken);
    }

    private static string HashTargetWeights(List<TargetWeight> weights)
    {
        var sorted = weights.OrderBy(w => w.Symbol).Select(w => $"{w.Symbol}:{w.TargetPercent}");
        var hash = string.Join("|", sorted);
        return Convert.ToHexString(System.Text.Encoding.UTF8.GetBytes(hash));
    }
}

/// <summary>
/// VS-04 ASYNC: Rebalance Job Handler (Hangfire)
/// </summary>

public interface IPortfolioRebalanceJob
{
    Task ExecuteAsync(Guid jobId, Guid portfolioId, List<TargetWeight> targetWeights, string correlationId, CancellationToken ct);
}

public class PortfolioRebalanceJobHandler : IPortfolioRebalanceJob
{
    private readonly NpgsqlDataSource _dataSource;
    private readonly IClock _clock;

    public PortfolioRebalanceJobHandler(NpgsqlDataSource dataSource, IClock clock)
    {
        _dataSource = dataSource;
        _clock = clock;
    }

    public async Task ExecuteAsync(Guid jobId, Guid portfolioId, List<TargetWeight> targetWeights, string correlationId, CancellationToken ct)
    {
        var startTime = _clock.UtcNow;

        try
        {
            await UpdateJobStatusAsync(jobId, "Running", null, null, ct);

            // Simulate rebalance execution (real implementation: call trading API)
            await Task.Delay(1000, ct);

            // Mark complete
            var duration = (int)(_clock.UtcNow - startTime).TotalSeconds;
            await UpdateJobStatusAsync(jobId, "Completed", duration, null, ct);

            // Publish event
            await PublishRebalancedEventAsync(jobId, portfolioId, correlationId, ct);
        }
        catch (Exception ex)
        {
            await UpdateJobStatusAsync(jobId, "Failed", null, ex.Message, ct);
            throw;
        }
    }

    private async Task UpdateJobStatusAsync(Guid jobId, string status, int? durationSeconds = null, string? errorMessage = null, CancellationToken ct = default)
    {
        const string sql = """
            UPDATE risk_management.rebalance_jobs
            SET status = @status, completed_at = CASE WHEN @status IN ('Completed', 'Failed') THEN CURRENT_TIMESTAMP ELSE NULL END,
                duration_seconds = @duration, last_error_message = @error, 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("@duration", durationSeconds ?? (object)DBNull.Value);
        cmd.Parameters.AddWithValue("@error", errorMessage ?? (object)DBNull.Value);

        await cmd.ExecuteNonQueryAsync(ct);
    }

    private async Task PublishRebalancedEventAsync(Guid jobId, Guid portfolioId, string correlationId, CancellationToken ct)
    {
        const string sql = """
            INSERT INTO shared.outbox (aggregate_id, event_type, payload, published_at, correlation_id)
            VALUES (@aggregateId, 'PortfolioRebalanced', @payload, CURRENT_TIMESTAMP, @correlationId);
            """;

        var payload = JsonSerializer.Serialize(new
        {
            eventType = "PortfolioRebalanced",
            portfolioId,
            jobId,
            rebalancedAt = _clock.UtcNow.DateTime,
        });

        await using var connection = await _dataSource.OpenConnectionAsync(ct);
        await using var cmd = connection.CreateCommand();
        cmd.CommandText = sql;
        cmd.Parameters.AddWithValue("@aggregateId", portfolioId);
        cmd.Parameters.AddWithValue("@payload", payload);
        cmd.Parameters.AddWithValue("@correlationId", correlationId);

        await cmd.ExecuteNonQueryAsync(ct);
    }
}
