using System;
using System.Collections.Generic;
using System.Collections.Concurrent;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Json;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using QuantEngine.Core.Interfaces;

namespace QuantEngine.Infrastructure.Services;

/// <summary>
/// KIS (한국투자증권) Open API 클라이언트.
/// 조회(read-only) 전용. 주문 API는 절대 호출하지 않음.
/// </summary>
public class KisApiClient : IKisApiClient
{
    private const string RealDomain = "https://openapi.koreainvestment.com:9443";
    private const string MockDomain = "https://openapivts.koreainvestment.com:29443";
    private const int TokenRefreshSkewMinutes = 10;

    private static readonly string[] ForbiddenPathSubstrings = { "/trading/" };
    private static readonly string[] ForbiddenTrIdPrefixes =
    {
        "TTTC08", "VTTC08", "TTTC01", "VTTC01",
        "TTTC8434R", "VTTC8434R"
    };

    private readonly HttpClient _httpClient;
    private readonly ITokenCache _tokenCache;
    private readonly ILogger<KisApiClient> _logger;
    private static readonly ConcurrentDictionary<string, SemaphoreSlim> TokenLocks = new();
    private readonly SemaphoreSlim _rateLimitSemaphore = new(1, 1);
    private DateTime _lastRequestTime = DateTime.MinValue;

    public KisApiClient(HttpClient httpClient, ITokenCache tokenCache, ILogger<KisApiClient> logger)
    {
        _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
        _tokenCache = tokenCache ?? throw new ArgumentNullException(nameof(tokenCache));
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
    }

    public async Task<Dictionary<string, object>> GetCurrentPriceAsync(string code, string account = "mock")
    {
        return await SendRequestAsync(
            account,
            "/uapi/domestic-stock/v1/quotations/inquire-price",
            "FHKST01010100",
            new Dictionary<string, string>
            {
                { "FID_COND_MRKT_DIV_CODE", "J" },
                { "FID_INPUT_ISCD", code }
            }
        );
    }

    public async Task<Dictionary<string, object>> GetAskingPrice10LevelAsync(string code, string account = "mock")
    {
        return await SendRequestAsync(
            account,
            "/uapi/domestic-stock/v1/quotations/inquire-asking-price-exp-ccn",
            "FHKST01010200",
            new Dictionary<string, string>
            {
                { "FID_COND_MRKT_DIV_CODE", "J" },
                { "FID_INPUT_ISCD", code }
            }
        );
    }

    public async Task<Dictionary<string, object>> GetDailyShortSaleAsync(string code, string startDate, string endDate, string account = "mock")
    {
        return await SendRequestAsync(
            account,
            "/uapi/domestic-stock/v1/quotations/daily-short-sale",
            "FHPST04830000",
            new Dictionary<string, string>
            {
                { "FID_COND_MRKT_DIV_CODE", "J" },
                { "FID_INPUT_ISCD", code },
                { "FID_INPUT_DATE_1", startDate },
                { "FID_INPUT_DATE_2", endDate }
            }
        );
    }

    public async Task<Dictionary<string, object>> GetDailyItemChartPriceAsync(string code, string startDate, string endDate, string period = "D", string account = "mock")
    {
        return await SendRequestAsync(
            account,
            "/uapi/domestic-stock/v1/quotations/inquire-daily-itemchartprice",
            "FHKST03010100",
            new Dictionary<string, string>
            {
                { "FID_COND_MRKT_DIV_CODE", "J" },
                { "FID_INPUT_ISCD", code },
                { "FID_INPUT_DATE_1", startDate },
                { "FID_INPUT_DATE_2", endDate },
                { "FID_PERIOD_DIV_CODE", period },
                { "FID_ORG_ADJ_PRC", "0" }
            }
        );
    }

    public async Task<Dictionary<string, object>> GetInvestorTrendAsync(string code, string account = "mock")
    {
        return await SendRequestAsync(
            account,
            "/uapi/domestic-stock/v1/quotations/inquire-investor",
            "FHKST01010900",
            new Dictionary<string, string>
            {
                { "FID_COND_MRKT_DIV_CODE", "J" },
                { "FID_INPUT_ISCD", code }
            }
        );
    }

    private async Task<Dictionary<string, object>> SendRequestAsync(
        string account,
        string path,
        string trId,
        Dictionary<string, string> parameters)
    {
        AssertReadOnly(path, trId);

        var creds = KisCredentials.Load(account);
        var token = await GetOrRefreshTokenAsync(creds);

        var headers = new Dictionary<string, string>
        {
            { "Authorization", $"Bearer {token}" },
            { "appkey", creds.AppKey },
            { "appsecret", creds.AppSecret },
            { "tr_id", trId },
            { "custtype", "P" }
        };

        var url = $"{creds.Domain}{path}";
        var queryString = string.Join("&", parameters.Select(kvp => $"{kvp.Key}={Uri.EscapeDataString(kvp.Value)}"));
        if (!string.IsNullOrEmpty(queryString))
            url += $"?{queryString}";

        int maxAttempts = 3;
        int delayMs = 1000;

        for (int attempt = 1; attempt <= maxAttempts; attempt++)
        {
            try
            {
                var request = new HttpRequestMessage(HttpMethod.Get, url);
                foreach (var header in headers)
                    request.Headers.Add(header.Key, header.Value);

                await ApplyRateLimitDelayAsync(account);
                var response = await _httpClient.SendAsync(request);
                response.EnsureSuccessStatusCode();

                var result = await response.Content.ReadFromJsonAsync<Dictionary<string, object>>();
                return result ?? new Dictionary<string, object>();
            }
            catch (Exception ex) when (attempt < maxAttempts)
            {
                _logger.LogWarning(ex, "KIS request failed on attempt {Attempt}/{MaxAttempts}. Retrying in {Delay}ms...", attempt, maxAttempts, delayMs);
                await Task.Delay(delayMs);
                delayMs *= 2;
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "KIS request failed after {MaxAttempts} attempts: {Path} / {TrId}", maxAttempts, path, trId);
                throw new InvalidOperationException($"KIS read-only request failed for {path} / {trId} after {maxAttempts} attempts.", ex);
            }
        }

        throw new InvalidOperationException("Unreachable code in KIS client SendRequestAsync");
    }

    private async Task<string> GetOrRefreshTokenAsync(KisCredentials creds)
    {
        var tokenLock = TokenLocks.GetOrAdd(creds.Account, _ => new SemaphoreSlim(1, 1));
        await tokenLock.WaitAsync();
        try
        {
            // Re-check after acquiring the account lock. Another request may
            // have refreshed the shared cache while this request was waiting.
            var cachedToken = await _tokenCache.GetCachedTokenAsync(creds.Account);
            if (!string.IsNullOrEmpty(cachedToken))
                return cachedToken;

            var tokenRequest = new { grant_type = "client_credentials", appkey = creds.AppKey, appsecret = creds.AppSecret };

            int maxAttempts = 3;
            int delayMs = 1000;

            for (int attempt = 1; attempt <= maxAttempts; attempt++)
            {
                try
                {
                    await ApplyRateLimitDelayAsync(creds.Account);
                    var response = await _httpClient.PostAsJsonAsync(
                        $"{creds.Domain}/oauth2/tokenP",
                        tokenRequest
                    );
                    response.EnsureSuccessStatusCode();

                    var tokenData = await response.Content.ReadFromJsonAsync<Dictionary<string, object>>();
                    if (tokenData == null) throw new InvalidOperationException("Token response body is empty");

                    if (!tokenData.TryGetValue("access_token", out var tokenObj) || tokenObj == null)
                        throw new InvalidOperationException("No access_token in response");
                    var accessToken = tokenObj.ToString()!;

                    var expiresInStr = tokenData.TryGetValue("expires_in", out var expiresObj) && expiresObj != null
                        ? expiresObj.ToString()
                        : "86400";
                    var expiresInSec = int.TryParse(expiresInStr, NumberStyles.Integer, CultureInfo.InvariantCulture, out var seconds)
                        ? seconds
                        : 86400;
                    var expiresAt = DateTime.UtcNow.AddSeconds(expiresInSec);

                    await _tokenCache.SaveTokenAsync(creds.Account, accessToken, expiresAt);
                    _logger.LogInformation("KIS token refreshed for {Account}; expires at {ExpiresAtUtc}", creds.Account, expiresAt);
                    return accessToken;
                }
                catch (Exception ex) when (attempt < maxAttempts)
                {
                    _logger.LogWarning(ex, "KIS token refresh failed on attempt {Attempt}/{MaxAttempts}. Retrying in {Delay}ms...", attempt, maxAttempts, delayMs);
                    await Task.Delay(delayMs);
                    delayMs *= 2;
                }
                catch (Exception ex)
                {
                    _logger.LogError(ex, "KIS token refresh failed after {MaxAttempts} attempts", maxAttempts);
                    throw new InvalidOperationException($"KIS token refresh failed after {maxAttempts} attempts; check credentials and API availability.", ex);
                }
            }
            throw new InvalidOperationException("Unreachable code in KIS client TokenRefresh");
        }
        finally
        {
            tokenLock.Release();
        }
    }

    private static void AssertReadOnly(string path, string trId)
    {
        foreach (var forbidden in ForbiddenPathSubstrings)
        {
            if (path.Contains(forbidden, StringComparison.OrdinalIgnoreCase))
                throw new InvalidOperationException(
                    $"BLOCKED: 주문 관련 경로 호출 시도 차단 — path={path}. " +
                    "이 엔진은 매수/매도를 API로 직접 실행하지 않습니다 (governance/rules/06_no_direct_api_trading.yaml)."
                );
        }

        foreach (var prefix in ForbiddenTrIdPrefixes)
        {
            if (trId.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
                throw new InvalidOperationException(
                    $"BLOCKED: 주문 관련 TR_ID 호출 시도 차단 — tr_id={trId}. " +
                    "이 엔진은 매수/매도를 API로 직접 실행하지 않습니다 (governance/rules/06_no_direct_api_trading.yaml)."
                );
        }
    }

    private class KisCredentials
    {
        public string AppKey { get; }
        public string AppSecret { get; }
        public string Account { get; }
        public string Domain { get; }

        private KisCredentials(string appKey, string appSecret, string account)
        {
            AppKey = appKey;
            AppSecret = appSecret;
            Account = account;
            Domain = account == "real" ? RealDomain : MockDomain;
        }

        public static KisCredentials Load(string account = "mock")
        {
            if (account != "real" && account != "mock")
                throw new ArgumentException("account must be 'real' or 'mock'");

            var (keyName, secretName) = account == "real"
                ? ("KIS_APP_Key", "KIS_APP_Secret")
                : ("KIS_APP_Key_TEST", "KIS_APP_Secret_TEST");

            var appKey = ReadEnvVar(keyName);
            var appSecret = ReadEnvVar(secretName);

            if (string.IsNullOrEmpty(appKey) || string.IsNullOrEmpty(appSecret))
                throw new InvalidOperationException(
                    $"{keyName}/{secretName} 환경변수를 찾을 수 없습니다. " +
                    "Windows 환경변수 설정 후 새 셸에서 재시도하거나 HKCU\\Environment 레지스트리를 확인하세요."
                );

            return new KisCredentials(appKey, appSecret, account);
        }

        private static string? ReadEnvVar(string name)
        {
            var value = Environment.GetEnvironmentVariable(name);
            if (!string.IsNullOrEmpty(value))
                return value;

            if (System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows))
            {
                try
                {
                    using var key = Microsoft.Win32.Registry.CurrentUser.OpenSubKey("Environment");
                    var regValue = key?.GetValue(name) as string;
                    if (!string.IsNullOrEmpty(regValue))
                        return regValue;
                }
                catch { }
            }

            return null;
        }
    }

    private async Task ApplyRateLimitDelayAsync(string account)
    {
        await _rateLimitSemaphore.WaitAsync();
        try
        {
            var mode = account.Contains("real", StringComparison.OrdinalIgnoreCase) ? "real" : "mock";
            int minIntervalMs = mode == "real" ? 150 : 400;

            var now = DateTime.UtcNow;
            var elapsed = (now - _lastRequestTime).TotalMilliseconds;
            if (elapsed < minIntervalMs)
            {
                var delay = minIntervalMs - (int)elapsed;
                _logger.LogDebug("Rate limit throttling: delaying for {Delay}ms (Mode: {Mode})", delay, mode);
                await Task.Delay(delay);
            }

            _lastRequestTime = DateTime.UtcNow;
        }
        finally
        {
            _rateLimitSemaphore.Release();
        }
    }
}
