using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text.Json;
using Microsoft.Extensions.Logging;
using StellaOps.Feedser.Models;

namespace StellaOps.Feedser.Source.Vndr.Cisco.Internal;

public class CiscoDtoFactory
{
    private readonly ICiscoCsafClient _csafClient;
    private readonly ILogger<CiscoDtoFactory> _logger;

    public CiscoDtoFactory(ICiscoCsafClient csafClient, ILogger<CiscoDtoFactory> logger)
    {
        _csafClient = csafClient ?? throw new ArgumentNullException(nameof(csafClient));
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
    }

    public async Task<CiscoAdvisoryDto> CreateAsync(CiscoRawAdvisory raw, CancellationToken cancellationToken)
    {
        ArgumentNullException.ThrowIfNull(raw);

        var advisoryId = raw.AdvisoryId?.Trim();
        if (string.IsNullOrWhiteSpace(advisoryId))
        {
            throw new InvalidOperationException("Cisco advisory is missing advisoryId.");
        }

        var title = string.IsNullOrWhiteSpace(raw.AdvisoryTitle) ? advisoryId : raw.AdvisoryTitle!.Trim();
        var severity = SeverityNormalization.Normalize(raw.Sir);
        var published = ParseDate(raw.FirstPublished);
        var updated = ParseDate(raw.LastUpdated);

        CiscoCsafData? csafData = null;
        if (!string.IsNullOrWhiteSpace(raw.CsafUrl))
        {
            var csafContent = await _csafClient.TryFetchAsync(raw.CsafUrl, cancellationToken).ConfigureAwait(false);
            if (!string.IsNullOrWhiteSpace(csafContent))
            {
                try
                {
                    csafData = CiscoCsafParser.Parse(csafContent!);
                }
                catch (JsonException ex)
                {
                    _logger.LogWarning(ex, "Cisco CSAF payload parsing failed for {AdvisoryId}", advisoryId);
                }
            }
        }

        var products = BuildProducts(raw, csafData);
        var cves = NormalizeList(raw.Cves);
        var bugIds = NormalizeList(raw.BugIds);
        var cvss = ParseDouble(raw.CvssBaseScore);

        return new CiscoAdvisoryDto(
            AdvisoryId: advisoryId,
            Title: title,
            Summary: string.IsNullOrWhiteSpace(raw.Summary) ? null : raw.Summary!.Trim(),
            Severity: severity,
            Published: published,
            Updated: updated,
            PublicationUrl: NormalizeUrl(raw.PublicationUrl),
            CsafUrl: NormalizeUrl(raw.CsafUrl),
            CvrfUrl: NormalizeUrl(raw.CvrfUrl),
            CvssBaseScore: cvss,
            Cves: cves,
            BugIds: bugIds,
            Products: products);
    }

    private static IReadOnlyList<CiscoAffectedProductDto> BuildProducts(CiscoRawAdvisory raw, CiscoCsafData? csafData)
    {
        var map = new Dictionary<string, CiscoAffectedProductDto>(StringComparer.OrdinalIgnoreCase);

        if (csafData is not null)
        {
            foreach (var entry in csafData.ProductStatuses)
            {
                var productId = entry.Key;
                var name = csafData.Products.TryGetValue(productId, out var product)
                    ? product.Name
                    : productId;

                var statuses = NormalizeStatuses(entry.Value);
                map[name] = new CiscoAffectedProductDto(
                    Name: name,
                    ProductId: productId,
                    Version: raw.Version?.Trim(),
                    Statuses: statuses);
            }
        }

        var rawProducts = NormalizeList(raw.ProductNames);
        foreach (var productName in rawProducts)
        {
            if (map.ContainsKey(productName))
            {
                continue;
            }

            map[productName] = new CiscoAffectedProductDto(
                Name: productName,
                ProductId: null,
                Version: raw.Version?.Trim(),
                Statuses: new[] { AffectedPackageStatusCatalog.KnownAffected });
        }

        return map.Count == 0
            ? Array.Empty<CiscoAffectedProductDto>()
            : map.Values
                .OrderBy(static p => p.Name, StringComparer.OrdinalIgnoreCase)
                .ThenBy(static p => p.ProductId, StringComparer.OrdinalIgnoreCase)
                .ToArray();
    }

    private static IReadOnlyCollection<string> NormalizeStatuses(IEnumerable<string> statuses)
    {
        var set = new SortedSet<string>(StringComparer.OrdinalIgnoreCase);
        foreach (var status in statuses)
        {
            if (AffectedPackageStatusCatalog.TryNormalize(status, out var normalized))
            {
                set.Add(normalized);
            }
            else if (!string.IsNullOrWhiteSpace(status))
            {
                set.Add(status.Trim().ToLowerInvariant());
            }
        }

        if (set.Count == 0)
        {
            set.Add(AffectedPackageStatusCatalog.KnownAffected);
        }

        return set;
    }

    private static IReadOnlyList<string> NormalizeList(IEnumerable<string>? items)
    {
        if (items is null)
        {
            return Array.Empty<string>();
        }

        var set = new SortedSet<string>(StringComparer.OrdinalIgnoreCase);
        foreach (var item in items)
        {
            if (!string.IsNullOrWhiteSpace(item))
            {
                set.Add(item.Trim());
            }
        }

        return set.Count == 0 ? Array.Empty<string>() : set.ToArray();
    }

    private static double? ParseDouble(string? value)
        => double.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var parsed)
            ? parsed
            : null;

    private static DateTimeOffset? ParseDate(string? value)
    {
        if (string.IsNullOrWhiteSpace(value))
        {
            return null;
        }

        if (DateTimeOffset.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var parsed))
        {
            return parsed.ToUniversalTime();
        }

        return null;
    }

    private static string? NormalizeUrl(string? value)
    {
        if (string.IsNullOrWhiteSpace(value))
        {
            return null;
        }

        return Uri.TryCreate(value.Trim(), UriKind.Absolute, out var uri) ? uri.ToString() : null;
    }
}
