using Dapper;
using KArtSell.BuildingBlocks.Time;
using Microsoft.Extensions.Logging;
using Npgsql;
using System.Collections.Concurrent;
using System.Security.Cryptography;
using System.Text;

namespace KArtSell.Host.Infrastructure;

/// <summary>
/// KIS (Korea Investment & Securities) connection pool with OAuth2 token refresh.
/// Maintains 3-5 concurrent connections with priority queue (BUY > SELL > CANCEL).
/// Idempotent: Token refresh is keyed by connection_id, no double-auth.
/// </summary>
public class KisConnectionPool : IAsyncDisposable
{
    private readonly NpgsqlDataSource _dataSource;
    private readonly HttpClient _httpClient;
    private readonly IClock _clock;
    private readonly ILogger<KisConnectionPool> _logger;

    private readonly ConcurrentDictionary<Guid, KisConnection> _connections;
    private readonly PriorityQueue<Guid, int> _availableConnections;
    private readonly SemaphoreSlim _poolLock;

    private const int MinConnections = 3;
    private const int MaxConnections = 5;
    private const int TokenRefreshIntervalSeconds = 55 * 60;  // 55 minutes (before 1h expiry)

    private static readonly Action<ILogger, int, int, Exception?> LogPoolStatus =
        LoggerMessage.Define<int, int>(
            LogLevel.Information,
            new EventId(1, nameof(LogPoolStatus)),
            "KIS connection pool: {Active} active, {Available} available");

    private static readonly Action<ILogger, Guid, Exception?> LogTokenRefresh =
        LoggerMessage.Define<Guid>(
            LogLevel.Information,
            new EventId(2, nameof(LogTokenRefresh)),
            "KIS token refreshed for connection {ConnectionId}");

    public KisConnectionPool(NpgsqlDataSource dataSource, HttpClient httpClient, IClock clock, ILogger<KisConnectionPool> logger)
    {
        _dataSource = dataSource;
        _httpClient = httpClient;
        _clock = clock;
        _logger = logger;
        _connections = new ConcurrentDictionary<Guid, KisConnection>();
        _availableConnections = new PriorityQueue<Guid, int>();
        _poolLock = new SemaphoreSlim(1, 1);
    }

    /// <summary>
    /// Acquire a connection from the pool (or create new if under limit).
    /// Returns connection with valid OAuth2 token.
    /// Priority: BUY (0) > SELL (1) > CANCEL (2).
    /// </summary>
    public async Task<KisConnection> AcquireAsync(int priority = 0, CancellationToken cancellationToken = default)
    {
        await _poolLock.WaitAsync(cancellationToken);
        try
        {
            // 1. Try to get available connection from priority queue
            while (_availableConnections.Count > 0)
            {
                if (_availableConnections.TryDequeue(out var connId, out _))
                {
                    if (_connections.TryGetValue(connId, out var conn))
                    {
                        // Refresh token if needed
                        if (conn.ExpiresAt < _clock.UtcNow.UtcDateTime.AddMinutes(1))
                        {
                            await RefreshTokenAsync(conn, cancellationToken);
                        }

                        return conn;
                    }
                }
            }

            // 2. If no available, create new if under limit
            if (_connections.Count < MaxConnections)
            {
                var newConn = await CreateConnectionAsync(cancellationToken);
                return newConn;
            }

            // 3. Otherwise wait for available (simplified: return first available)
            throw new InvalidOperationException("KIS connection pool exhausted");
        }
        finally
        {
            _poolLock.Release();
        }
    }

    /// <summary>
    /// Release connection back to pool.
    /// </summary>
    public async Task ReleaseAsync(Guid connectionId, CancellationToken cancellationToken = default)
    {
        await _poolLock.WaitAsync(cancellationToken);
        try
        {
            if (_connections.TryGetValue(connectionId, out var conn))
            {
                conn.State = "idle";
                _availableConnections.Enqueue(connectionId, (int)conn.Priority);
            }
        }
        finally
        {
            _poolLock.Release();
        }
    }

    private async Task<KisConnection> CreateConnectionAsync(CancellationToken cancellationToken)
    {
        var connId = Guid.NewGuid();
        var token = await ObtainOAuth2TokenAsync(cancellationToken);

        var now = _clock.UtcNow.UtcDateTime;
        var conn = new KisConnection
        {
            ConnectionId = connId,
            State = "active",
            Priority = KisOperationPriority.Buy,
            TokenHash = HashToken(token),
            ExpiresAt = now.AddHours(1),
            CreatedAt = now
        };

        // Persist to database
        await SaveConnectionStateAsync(conn, cancellationToken);

        _connections[connId] = conn;
        return conn;
    }

    private async Task RefreshTokenAsync(KisConnection conn, CancellationToken cancellationToken)
    {
        try
        {
            var now = _clock.UtcNow.UtcDateTime;
            var newToken = await ObtainOAuth2TokenAsync(cancellationToken);
            conn.TokenHash = HashToken(newToken);
            conn.ExpiresAt = now.AddHours(1);

            // Log refresh
            const string sql = """
                INSERT INTO kis.token_refresh_log (connection_id, refresh_at, status, new_token_hash, executed_at, published_at)
                VALUES (@connId, @refreshAt, 'success', @tokenHash, @now, @now)
                """;

            await using var connection = await _dataSource.OpenConnectionAsync(cancellationToken);
            await connection.ExecuteAsync(
                sql,
                new { connId = conn.ConnectionId, refreshAt = now, tokenHash = conn.TokenHash, now },
                commandTimeout: 5);

            LogTokenRefresh(_logger, conn.ConnectionId, null);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to refresh KIS token for connection {ConnectionId}", conn.ConnectionId);
            throw;
        }
    }

    private async Task<string> ObtainOAuth2TokenAsync(CancellationToken cancellationToken)
    {
        var appKey = Environment.GetEnvironmentVariable("KIS_APP_KEY") ?? throw new InvalidOperationException("KIS_APP_KEY required");
        var appSecret = Environment.GetEnvironmentVariable("KIS_APP_SECRET") ?? throw new InvalidOperationException("KIS_APP_SECRET required");

        // KIS OAuth2 token endpoint (mock for now)
        var request = new HttpRequestMessage(HttpMethod.Post, "https://openapi.kiwoom.com/oauth2/tokenP");
        request.Content = new FormUrlEncodedContent(new[]
        {
            new KeyValuePair<string, string>("grant_type", "client_credentials"),
            new KeyValuePair<string, string>("appkey", appKey),
            new KeyValuePair<string, string>("appsecret", appSecret)
        });

        var response = await _httpClient.SendAsync(request, cancellationToken);
        response.EnsureSuccessStatusCode();

        var content = await response.Content.ReadAsStringAsync(cancellationToken);
        var json = System.Text.Json.JsonDocument.Parse(content);
        return json.RootElement.GetProperty("access_token").GetString() ?? throw new InvalidOperationException("No access_token in response");
    }

    private async Task SaveConnectionStateAsync(KisConnection conn, CancellationToken cancellationToken)
    {
        const string sql = """
            INSERT INTO kis.connection_pool_state (connection_id, state, priority, token_hash, expires_at, created_at, published_at)
            VALUES (@connId, @state, @priority, @tokenHash, @expiresAt, @createdAt, @now)
            """;

        var now = _clock.UtcNow.UtcDateTime;
        await using var connection = await _dataSource.OpenConnectionAsync(cancellationToken);
        await connection.ExecuteAsync(
            sql,
            new
            {
                connId = conn.ConnectionId,
                state = conn.State,
                priority = (int)conn.Priority,
                tokenHash = conn.TokenHash,
                expiresAt = conn.ExpiresAt,
                createdAt = conn.CreatedAt,
                now
            },
            commandTimeout: 5);
    }

    private static string HashToken(string token)
    {
        using var sha = SHA256.Create();
        var hash = sha.ComputeHash(Encoding.UTF8.GetBytes(token));
        return Convert.ToBase64String(hash);
    }

    public async ValueTask DisposeAsync()
    {
        _poolLock?.Dispose();
        await Task.CompletedTask;
    }
}

public class KisConnection
{
    public Guid ConnectionId { get; set; }
    public string State { get; set; } = "idle";  // idle, active, closed
    public KisOperationPriority Priority { get; set; }
    public string TokenHash { get; set; } = "";
    public DateTime ExpiresAt { get; set; }
    public DateTime CreatedAt { get; set; }
    public DateTime? ReleasedAt { get; set; }
}

public enum KisOperationPriority
{
    Buy = 0,
    Sell = 1,
    Cancel = 2
}
