using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

namespace TestersAI;

public sealed class TestersAIClient
{
    private static readonly HttpClient Http = new();
    public Config Config { get; }

    public TestersAIClient(Config? config = null) { Config = config ?? Config.FromEnv(); }

    public Task<AnalysisResult> AnalyzeScreenshotAsync(string path, IReadOnlyDictionary<string, object?>? context = null,
        IEnumerable<object>? checks = null, IEnumerable<object>? testers = null, CancellationToken ct = default)
        => AnalyzeScreenshotAsync(File.ReadAllBytes(path), context, checks, testers, ct);

    public Task<AnalysisResult> AnalyzeScreenshotAsync(byte[] image, IReadOnlyDictionary<string, object?>? context = null,
        IEnumerable<object>? checks = null, IEnumerable<object>? testers = null, CancellationToken ct = default)
    {
        var payload = new Dictionary<string, object?> {
            ["image_base64"] = Convert.ToBase64String(image),
            ["context"] = context ?? new Dictionary<string, object?>(),
        };
        AttachSelection(payload, checks, testers);
        return CallAsync("screenshot", "/v1/analyze/screenshot", payload, ct);
    }

    public Task<AnalysisResult> AnalyzeConsoleAsync(IEnumerable<IDictionary<string, object?>> entries,
        IReadOnlyDictionary<string, object?>? context = null,
        IEnumerable<object>? checks = null, IEnumerable<object>? testers = null, CancellationToken ct = default)
    {
        var payload = new Dictionary<string, object?> { ["entries"] = entries, ["context"] = context ?? new Dictionary<string, object?>() };
        AttachSelection(payload, checks, testers);
        return CallAsync("console", "/v1/analyze/console", payload, ct);
    }

    public Task<AnalysisResult> AnalyzeNetworkAsync(object harOrEntries, IReadOnlyDictionary<string, object?>? context = null,
        IEnumerable<object>? checks = null, IEnumerable<object>? testers = null, CancellationToken ct = default)
    {
        var body = new Dictionary<string, object?> { ["context"] = context ?? new Dictionary<string, object?>() };
        if (harOrEntries is IDictionary<string, object?> d && d.ContainsKey("log")) body["har"] = harOrEntries;
        else body["entries"] = harOrEntries;
        AttachSelection(body, checks, testers);
        return CallAsync("network", "/v1/analyze/network", body, ct);
    }

    public Task<AnalysisResult> AnalyzePageTextAsync(string text, IReadOnlyDictionary<string, object?>? context = null,
        IEnumerable<object>? checks = null, IEnumerable<object>? testers = null, CancellationToken ct = default)
    {
        var payload = new Dictionary<string, object?> { ["text"] = text, ["context"] = context ?? new Dictionary<string, object?>() };
        AttachSelection(payload, checks, testers);
        return CallAsync("page_text", "/v1/analyze/page-text", payload, ct);
    }

    private void AttachSelection(Dictionary<string, object?> payload,
        IEnumerable<object>? checks, IEnumerable<object>? testers)
    {
        var combined = new List<object>();
        if (checks  is not null) combined.AddRange(checks);
        if (testers is not null) combined.AddRange(testers);
        if (combined.Count == 0) return;
        var r = Checks.Resolve(combined);
        if (r.Checks.Count > 0)        payload["checks"] = r.Checks;
        if (r.Testers.Count > 0)       payload["testers"] = r.Testers;
        if (r.CustomTesters.Count > 0) payload["custom_testers"] = r.CustomTesters;
        if (r.Unknown.Count > 0)
            Log($"Unknown check/tester tokens ignored: {string.Join(", ", r.Unknown)}. " +
                "Valid names: https://testers.ai/sdk#checks");
    }

    // ------------------------------------------------------- internal
    private void Log(string msg)
    {
        if (Config.Quiet) return;
        try { Console.Error.WriteLine($"[testersai] {msg}"); } catch { /* ignore */ }
    }

    private static bool LooksInvalid(string? key)
    {
        if (string.IsNullOrEmpty(key) || key!.Length < 16) return true;
        return !(key.StartsWith("sk_") || key.StartsWith("sk-"));
    }

    private async Task<AnalysisResult> CallAsync(string kind, string path, object payload, CancellationToken ct)
    {
        var started = Environment.TickCount64;
        if (!Config.Enabled) return AnalysisResult.Skipped(kind, "disabled", null, 0);
        if (string.IsNullOrWhiteSpace(Config.ApiKey))
        {
            Log("TESTERSAI_API_KEY is not set. Get one at https://testers.ai/sdk "
                + "(click 'Get API key' on the docs site). Set TESTERSAI_QUIET=true to silence this.");
            return Fail(kind, "missing_api_key", started);
        }
        if (LooksInvalid(Config.ApiKey))
        {
            Log("TESTERSAI_API_KEY looks malformed (expected a string starting with 'sk_'). "
                + "Double-check the value from https://testers.ai/sdk.");
        }

        var url = Config.BaseUrl.TrimEnd('/') + path;
        var json = JsonSerializer.Serialize(payload);
        var deadline = started + Config.MaxRetryWaitMs;
        int attempt = 0;
        string? lastErr = null;

        while (true)
        {
            attempt++;
            using var req = new HttpRequestMessage(HttpMethod.Post, url)
            {
                Content = new StringContent(json, Encoding.UTF8, "application/json")
            };
            req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Config.ApiKey);
            req.Headers.UserAgent.ParseAdd("testersai-dotnet/0.1.0");

            try
            {
                using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
                cts.CancelAfter(Config.TimeoutMs);
                var resp = await Http.SendAsync(req, cts.Token).ConfigureAwait(false);
                if (resp.StatusCode == HttpStatusCode.Unauthorized || resp.StatusCode == HttpStatusCode.Forbidden)
                {
                    Log($"Testers.AI API rejected the key (HTTP {(int)resp.StatusCode}). "
                        + "The key may be invalid, expired, or revoked. "
                        + "Get a fresh one at https://testers.ai/sdk.");
                    return Fail(kind, $"http_{(int)resp.StatusCode}", started);
                }
                if (resp.StatusCode == (HttpStatusCode)429 || (int)resp.StatusCode >= 500)
                {
                    lastErr = $"http_{(int)resp.StatusCode}";
                    if (!ShouldRetry(attempt, deadline))
                        return AnalysisResult.Skipped(kind,
                            resp.StatusCode == (HttpStatusCode)429 ? "rate_limited" : "server_error",
                            lastErr, Elapsed(started));
                    var retryAfter = resp.Headers.TryGetValues("Retry-After", out var v) ? string.Join(",", v) : null;
                    await SleepBackoffAsync(attempt, deadline, retryAfter, ct).ConfigureAwait(false);
                    continue;
                }
                if (!resp.IsSuccessStatusCode) return Fail(kind, $"http_{(int)resp.StatusCode}", started);
                var body = await resp.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
                return AnalysisResult.Parse(kind, body, Elapsed(started));
            }
            catch (OperationCanceledException) when (!ct.IsCancellationRequested)
            {
                lastErr = "timeout";
                if (!ShouldRetry(attempt, deadline)) return Fail(kind, lastErr, started);
                await SleepBackoffAsync(attempt, deadline, null, ct).ConfigureAwait(false);
            }
            catch (Exception e)
            {
                lastErr = "network:" + e.GetType().Name;
                if (!ShouldRetry(attempt, deadline)) return Fail(kind, lastErr, started);
                await SleepBackoffAsync(attempt, deadline, null, ct).ConfigureAwait(false);
            }
        }
    }

    private bool ShouldRetry(int attempt, long deadline)
        => attempt <= Config.MaxRetries && Environment.TickCount64 < deadline;

    private async Task SleepBackoffAsync(int attempt, long deadline, string? retryAfter, CancellationToken ct)
    {
        long wait = Math.Min(250L * (1L << (attempt - 1)), 2000L);
        wait += Random.Shared.NextInt64(wait / 2 + 1);
        if (long.TryParse(retryAfter, out var ra)) wait = Math.Min(ra * 1000, wait * 2);
        var remaining = deadline - Environment.TickCount64;
        if (remaining <= 0) return;
        try { await Task.Delay((int)Math.Min(wait, remaining), ct).ConfigureAwait(false); }
        catch (OperationCanceledException) { }
    }

    private AnalysisResult Fail(string kind, string err, long started)
    {
        if (Config.Strict) throw new TestersAIException(err);
        return AnalysisResult.Skipped(kind, "error", err, Elapsed(started));
    }

    private static int Elapsed(long started) => (int)(Environment.TickCount64 - started);
}

public sealed class TestersAIException : Exception
{
    public TestersAIException(string msg) : base(msg) { }
}
