using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using StellaOps.Feedser.Source.Vndr.Apple.Internal;

namespace StellaOps.Feedser.Source.Vndr.Apple.Tests.Apple;

internal static class AppleFixtureManager
{
    private const string UpdateEnvVar = "UPDATE_APPLE_FIXTURES";
    private const string UpdateSentinelFileName = ".update-apple-fixtures";
    private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web)
    {
        WriteIndented = true,
        Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
        DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
    };

    private static readonly Uri ExampleDetailBaseUri = new("https://support.example.com/en-us/");

    private static readonly AppleFixtureDefinition[] Definitions =
    {
        new(
            "125326",
            new Uri("https://support.apple.com/en-us/125326"),
            Products: new[]
            {
                new AppleFixtureProduct("iOS", "iPhone 16 Pro", "26.0.1", "24A341"),
                new AppleFixtureProduct("iOS", "iPhone 16 Pro", "26.0.1 (a)", "24A341a"),
                new AppleFixtureProduct("iPadOS", "iPad Pro (M4)", "26", "24B120"),
            }),
        new(
            "125328",
            new Uri("https://support.apple.com/en-us/125328"),
            Products: new[]
            {
                new AppleFixtureProduct("macOS", "MacBook Pro (M4)", "26.0.1", "26A123"),
                new AppleFixtureProduct("macOS", "Mac Studio", "26", "26A120b"),
            }),
        new(
            "106355",
            new Uri("https://support.apple.com/en-us/106355"),
            ForceRapidSecurityResponse: true,
            Products: new[]
            {
                new AppleFixtureProduct("macOS Ventura", "macOS Ventura", string.Empty, "22F400"),
                new AppleFixtureProduct("macOS Ventura", "macOS Ventura (Intel)", string.Empty, "22F400a"),
            }),
        new(
            "HT214108",
            new Uri("https://support.apple.com/en-us/HT214108")),
        new(
            "HT215500",
            new Uri("https://support.apple.com/en-us/HT215500"),
            ForceRapidSecurityResponse: true),
    };

    private static readonly Lazy<Task> UpdateTask = new(
        () => UpdateFixturesAsync(CancellationToken.None),
        LazyThreadSafetyMode.ExecutionAndPublication);

    public static IReadOnlyList<AppleFixtureDefinition> Fixtures => Definitions;

    public static Task EnsureUpdatedAsync(CancellationToken cancellationToken = default)
    {
        if (!ShouldUpdateFixtures())
        {
            return Task.CompletedTask;
        }

        Console.WriteLine("[AppleFixtures] UPDATE flag detected; refreshing fixtures");
        return UpdateTask.Value;
    }

    public static string ReadFixture(string name)
    {
        var path = Path.Combine(AppContext.BaseDirectory, "Source", "Vndr", "Apple", "Fixtures", name);
        return File.ReadAllText(path);
    }

    public static AppleDetailDto ReadExpectedDto(string articleId)
    {
        var json = ReadFixture($"{articleId}.expected.json");
        return JsonSerializer.Deserialize<AppleDetailDto>(json, SerializerOptions)
            ?? throw new InvalidOperationException($"Unable to deserialize expected DTO for {articleId}.");
    }

    private static bool ShouldUpdateFixtures()
    {
        var value = Environment.GetEnvironmentVariable(UpdateEnvVar)?.Trim();
        if (string.IsNullOrEmpty(value))
        {
            var sentinelPath = Path.Combine(ResolveFixtureRoot(), UpdateSentinelFileName);
            return File.Exists(sentinelPath);
        }

        if (string.Equals(value, "0", StringComparison.Ordinal)
            || string.Equals(value, "false", StringComparison.OrdinalIgnoreCase))
        {
            return false;
        }

        return true;
    }

    private static async Task UpdateFixturesAsync(CancellationToken cancellationToken)
    {
        var handler = new HttpClientHandler
        {
            AutomaticDecompression = System.Net.DecompressionMethods.All,
            ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator,
        };

        using var httpClient = new HttpClient(handler);
        httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("StellaOpsFixtureUpdater", "1.0"));
        httpClient.Timeout = TimeSpan.FromSeconds(30);

        var indexEntries = new List<object>(Definitions.Length);

        try
        {
            foreach (var definition in Definitions)
            {
                cancellationToken.ThrowIfCancellationRequested();

                try
                {
                var html = await httpClient.GetStringAsync(definition.DetailUri, cancellationToken).ConfigureAwait(false);
                var entryProducts = definition.Products?
                    .Select(product => new AppleIndexProduct(
                        product.Platform,
                        product.Name,
                            product.Version,
                            product.Build))
                        .ToArray() ?? Array.Empty<AppleIndexProduct>();

                    var entry = new AppleIndexEntry(
                        UpdateId: definition.ArticleId,
                        ArticleId: definition.ArticleId,
                        Title: definition.ArticleId,
                        PostingDate: DateTimeOffset.UtcNow,
                        DetailUri: definition.DetailUri,
                    Products: entryProducts,
                    IsRapidSecurityResponse: definition.ForceRapidSecurityResponse ?? false);

                var dto = AppleDetailParser.Parse(html, entry);
                var sanitizedHtml = BuildSanitizedHtml(dto);

                WriteFixture(definition.HtmlFixtureName, sanitizedHtml);
                WriteFixture(definition.ExpectedFixtureName, JsonSerializer.Serialize(dto, SerializerOptions));

                    var exampleDetailUri = new Uri(ExampleDetailBaseUri, definition.ArticleId);
                    indexEntries.Add(new
                    {
                        id = definition.ArticleId,
                        articleId = definition.ArticleId,
                        title = dto.Title,
                        postingDate = dto.Published.ToUniversalTime().ToString("O", CultureInfo.InvariantCulture),
                        detailUrl = exampleDetailUri.ToString(),
                        rapidSecurityResponse = definition.ForceRapidSecurityResponse
                            ?? dto.Title.Contains("Rapid Security Response", StringComparison.OrdinalIgnoreCase),
                        products = dto.Affected
                            .OrderBy(p => p.Name, StringComparer.OrdinalIgnoreCase)
                            .Select(p => new
                            {
                                platform = p.Platform,
                                name = p.Name,
                                version = p.Version,
                                build = p.Build,
                            })
                            .ToArray(),
                    });
                }
                catch (Exception ex) when (ex is HttpRequestException or TaskCanceledException or IOException)
                {
                    Console.WriteLine($"[AppleFixtures] Skipped {definition.ArticleId}: {ex.Message}");
                }
            }
        }
        finally
        {
            var sentinelPath = Path.Combine(ResolveFixtureRoot(), UpdateSentinelFileName);
            if (File.Exists(sentinelPath))
            {
                try
                {
                    File.Delete(sentinelPath);
                }
                catch (IOException)
                {
                    // best effort
                }
            }
        }

        var indexDocument = new { updates = indexEntries };
        WriteFixture("index.json", JsonSerializer.Serialize(indexDocument, SerializerOptions));
    }

    private static string BuildSanitizedHtml(AppleDetailDto dto)
    {
        var builder = new StringBuilder();
        builder.AppendLine("<!DOCTYPE html>");
        builder.AppendLine("<html lang=\"en\">");
        builder.AppendLine("<body>");
        builder.AppendLine("<article>");

        builder.AppendLine($"  <h1 data-testid=\"update-title\">{Escape(dto.Title)}</h1>");

        if (!string.IsNullOrWhiteSpace(dto.Summary))
        {
            builder.AppendLine($"  <p data-testid=\"update-summary\">{Escape(dto.Summary)}</p>");
        }

        var publishedDisplay = dto.Published.ToString("MMMM d, yyyy", CultureInfo.InvariantCulture);
        builder.AppendLine($"  <time data-testid=\"published\" datetime=\"{dto.Published:O}\">{publishedDisplay}</time>");

        if (dto.Updated.HasValue)
        {
            var updatedDisplay = dto.Updated.Value.ToString("MMMM d, yyyy", CultureInfo.InvariantCulture);
            builder.AppendLine($"  <time data-testid=\"updated\" datetime=\"{dto.Updated:O}\">{updatedDisplay}</time>");
        }

        if (dto.CveIds.Count > 0)
        {
            builder.AppendLine("  <section data-component=\"security-update\">");
            builder.AppendLine("    <h2>Security Issues</h2>");
            builder.AppendLine("    <ul data-testid=\"cve-list\">");
            foreach (var cve in dto.CveIds.OrderBy(id => id, StringComparer.OrdinalIgnoreCase))
            {
                builder.AppendLine($"      <li>{Escape(cve)}</li>");
            }
            builder.AppendLine("    </ul>");
            builder.AppendLine("  </section>");
        }

        if (dto.Affected.Count > 0)
        {
            builder.AppendLine("  <table>");
            builder.AppendLine("    <tbody>");
            foreach (var product in dto.Affected
                         .OrderBy(p => p.Platform ?? string.Empty, StringComparer.OrdinalIgnoreCase)
                         .ThenBy(p => p.Name, StringComparer.OrdinalIgnoreCase)
                         .ThenBy(p => p.Version ?? string.Empty, StringComparer.OrdinalIgnoreCase)
                         .ThenBy(p => p.Build ?? string.Empty, StringComparer.OrdinalIgnoreCase))
            {
                var platform = product.Platform is null ? string.Empty : Escape(product.Platform);
                var name = Escape(product.Name);
                var version = product.Version is null ? string.Empty : Escape(product.Version);
                var build = product.Build is null ? string.Empty : Escape(product.Build);

                builder.Append("      <tr data-testid=\"product-row\"");
                builder.Append($" data-platform=\"{platform}\"");
                builder.Append($" data-product=\"{name}\"");
                builder.Append($" data-version=\"{version}\"");
                builder.Append($" data-build=\"{build}\">");
                builder.AppendLine();
                builder.AppendLine($"        <td>{name}</td>");
                builder.AppendLine($"        <td>{version}</td>");
                builder.AppendLine($"        <td>{build}</td>");
                builder.AppendLine("      </tr>");
            }

            builder.AppendLine("    </tbody>");
            builder.AppendLine("  </table>");
        }

        if (dto.References.Count > 0)
        {
            builder.AppendLine("  <section>");
            builder.AppendLine("    <h2>References</h2>");
            foreach (var reference in dto.References
                         .OrderBy(r => r.Url, StringComparer.OrdinalIgnoreCase))
            {
                var title = reference.Title ?? string.Empty;
                builder.AppendLine($"    <a href=\"{Escape(reference.Url)}\">{Escape(title)}</a>");
            }
            builder.AppendLine("  </section>");
        }

        builder.AppendLine("</article>");
        builder.AppendLine("</body>");
        builder.AppendLine("</html>");
        return builder.ToString();
    }

    private static void WriteFixture(string name, string contents)
    {
        var root = ResolveFixtureRoot();
        Directory.CreateDirectory(root);
        var normalized = NormalizeLineEndings(contents);

        var sourcePath = Path.Combine(root, name);
        File.WriteAllText(sourcePath, normalized);

        var outputPath = Path.Combine(AppContext.BaseDirectory, "Source", "Vndr", "Apple", "Fixtures", name);
        Directory.CreateDirectory(Path.GetDirectoryName(outputPath)!);
        File.WriteAllText(outputPath, normalized);

        Console.WriteLine($"[AppleFixtures] Wrote {name}");
    }

    private static string ResolveFixtureRoot()
    {
        var baseDir = AppContext.BaseDirectory;
        // bin/Debug/net10.0/ -> project -> src -> repo root
        var root = Path.GetFullPath(Path.Combine(baseDir, "..", "..", "..", "..", ".."));
        return Path.Combine(root, "src", "StellaOps.Feedser.Source.Vndr.Apple.Tests", "Apple", "Fixtures");
    }

    private static string NormalizeLineEndings(string value)
        => value.Replace("\r\n", "\n", StringComparison.Ordinal);

    private static string Escape(string value)
        => System.Net.WebUtility.HtmlEncode(value);
}

internal sealed record AppleFixtureDefinition(
    string ArticleId,
    Uri DetailUri,
    bool? ForceRapidSecurityResponse = null,
    IReadOnlyList<AppleFixtureProduct>? Products = null)
{
    public string HtmlFixtureName => $"{ArticleId}.html";
    public string ExpectedFixtureName => $"{ArticleId}.expected.json";
}

internal sealed record AppleFixtureProduct(
    string Platform,
    string Name,
    string Version,
    string Build);
