using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using ClawdDotNet.Core.Api.Models;
using Microsoft.Extensions.Logging;

namespace ClawdDotNet.Core.Api;

public sealed class OpenRouterClient : IChatCompletionClient, IDisposable
{
    private const string BaseUrl = "https://openrouter.ai/api/v1/";

    private readonly HttpClient _http;
    private readonly ILogger _logger;
    private readonly RetryPolicy _retry;

    /// <summary>Wartefunktion — in Tests ersetzbar, damit nicht wirklich gewartet wird.</summary>
    private readonly Func<TimeSpan, CancellationToken, Task> _delay;

    private static readonly JsonSerializerOptions JsonOptions = new()
    {
        DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull
    };

    public OpenRouterClient(
        string apiKey,
        ILogger logger,
        HttpClient? httpClient = null,
        RetryPolicy? retryPolicy = null,
        Func<TimeSpan, CancellationToken, Task>? delay = null)
    {
        _logger = logger;
        _retry = retryPolicy ?? RetryPolicy.Default;
        _delay = delay ?? ((d, ct) => Task.Delay(d, ct));
        _http = httpClient ?? new HttpClient();
        _http.BaseAddress = new Uri(BaseUrl);
        _http.Timeout = TimeSpan.FromMinutes(5); // LLM-Calls können bei großen Prompts lange dauern
        _http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);
        _http.DefaultRequestHeaders.Add("HTTP-Referer", "ClawdDotNet");
        _http.DefaultRequestHeaders.Add("X-Title", "ClawdDotNet");
    }

    public async Task<ChatResponse> CompleteAsync(ChatRequest request, CancellationToken ct)
    {
        request.Stream = false;

        var json = JsonSerializer.Serialize(request, JsonOptions);

        _logger.LogDebug("Sending request to OpenRouter: model={Model}, messages={Count}",
            request.Model, request.Messages.Count);

        var (statusCode, responseBody) = await SendWithRetryAsync(json, ct);

        if (statusCode is < 200 or > 299)
        {
            _logger.LogError("OpenRouter API error {StatusCode}: {Body}", statusCode, responseBody);
            throw new OpenRouterException(
                $"API request failed with status {statusCode}",
                statusCode,
                responseBody);
        }

        var result = JsonSerializer.Deserialize<ChatResponse>(responseBody, JsonOptions)
            ?? throw new OpenRouterException("Empty response from OpenRouter", 0, responseBody);

        if (result.Error is not null)
        {
            _logger.LogError("OpenRouter returned error: {Error}", result.Error.Message);
            throw new OpenRouterException(result.Error.Message, result.Error.Code ?? 0, responseBody);
        }

        _logger.LogDebug("OpenRouter response received: tokens={Tokens}",
            result.Usage?.TotalTokens ?? 0);

        return result;
    }

    /// <summary>
    /// Sendet die Anfrage und wiederholt sie bei vorübergehenden Störungen.
    ///
    /// Ohne das beendet ein einzelnes HTTP 429 einen kompletten geplanten Lauf —
    /// Rate-Limits und kurzzeitige 5xx sind bei OpenRouter Normalbetrieb.
    /// </summary>
    private async Task<(int StatusCode, string Body)> SendWithRetryAsync(string json, CancellationToken ct)
    {
        Exception? lastNetworkError = null;

        for (var attempt = 1; attempt <= _retry.MaxAttempts; attempt++)
        {
            ct.ThrowIfCancellationRequested();

            // Content muss je Versuch neu erzeugt werden — ein bereits gesendeter
            // HttpContent lässt sich nicht erneut verwenden.
            using var content = new StringContent(json, Encoding.UTF8, "application/json");

            try
            {
                using var response = await _http.PostAsync("chat/completions", content, ct);
                var body = await response.Content.ReadAsStringAsync(ct);
                var status = (int)response.StatusCode;

                if (response.IsSuccessStatusCode || !_retry.ShouldRetry(response.StatusCode))
                    return (status, body);

                if (attempt == _retry.MaxAttempts)
                {
                    _logger.LogWarning(
                        "OpenRouter antwortete {Status} — letzter von {Max} Versuchen aufgebraucht.",
                        status, _retry.MaxAttempts);
                    return (status, body);
                }

                var wait = _retry.GetDelay(attempt, response.Headers.RetryAfter?.Delta);
                _logger.LogWarning(
                    "OpenRouter antwortete {Status}. Versuch {Attempt}/{Max}, erneut in {Delay:N1}s.",
                    status, attempt, _retry.MaxAttempts, wait.TotalSeconds);

                await _delay(wait, ct);
            }
            catch (HttpRequestException ex)
            {
                lastNetworkError = ex;

                if (attempt == _retry.MaxAttempts)
                    break;

                var wait = _retry.GetDelay(attempt);
                _logger.LogWarning(ex,
                    "Netzwerkfehler beim Aufruf von OpenRouter. Versuch {Attempt}/{Max}, erneut in {Delay:N1}s.",
                    attempt, _retry.MaxAttempts, wait.TotalSeconds);

                await _delay(wait, ct);
            }
        }

        throw new OpenRouterException(
            $"OpenRouter nach {_retry.MaxAttempts} Versuchen nicht erreichbar: {lastNetworkError?.Message}",
            0, "");
    }

    /// <summary>
    /// Ruft die verfügbaren Modelle von OpenRouter ab (/models Endpoint).
    /// Gibt eine Liste von Modell-IDs zurück, sortiert nach Name.
    /// </summary>
    public async Task<List<ModelInfo>> GetAvailableModelsAsync(CancellationToken ct = default)
    {
        _logger.LogDebug("Fetching available models from OpenRouter...");

        using var response = await _http.GetAsync("models", ct);
        var body = await response.Content.ReadAsStringAsync(ct);

        if (!response.IsSuccessStatusCode)
        {
            _logger.LogError("Failed to fetch models: {StatusCode} {Body}",
                (int)response.StatusCode, body);
            return [];
        }

        var doc = JsonDocument.Parse(body);

        if (!doc.RootElement.TryGetProperty("data", out var dataArray))
            return [];

        var models = new List<ModelInfo>();

        foreach (var item in dataArray.EnumerateArray())
        {
            var id = item.GetProperty("id").GetString() ?? "";
            var name = item.TryGetProperty("name", out var nameProp) ? nameProp.GetString() ?? id : id;

            models.Add(new ModelInfo { Id = id, Name = name, Pricing = ReadPricing(item) });
        }

        models.Sort((a, b) => string.Compare(a.Id, b.Id, StringComparison.OrdinalIgnoreCase));

        _logger.LogDebug("Fetched {Count} models from OpenRouter", models.Count);
        return models;
    }

    /// <summary>
    /// Liest die Preisangaben eines Modell-Eintrags. OpenRouter liefert sie als Text
    /// und als Preis pro einzelnem Token.
    /// </summary>
    internal static ModelPricing? ReadPricing(JsonElement item)
    {
        if (!item.TryGetProperty("pricing", out var pricing) || pricing.ValueKind != JsonValueKind.Object)
            return null;

        var input = ModelPricing.ParsePerToken(
            pricing.TryGetProperty("prompt", out var p) ? p.GetString() : null);
        var output = ModelPricing.ParsePerToken(
            pricing.TryGetProperty("completion", out var c) ? c.GetString() : null);

        // Nur wenn beide Werte vorliegen, ist eine Kostenrechnung belastbar.
        return input is { } i && output is { } o ? new ModelPricing(i, o) : null;
    }

    public void Dispose()
    {
        _http.Dispose();
    }
}

public sealed class OpenRouterException(string message, int statusCode, string responseBody)
    : Exception(message)
{
    public int StatusCode { get; } = statusCode;
    public string ResponseBody { get; } = responseBody;
}
