using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using StellaOps.Feedser.Models;
using StellaOps.Feedser.Source.Common.Packages;
using StellaOps.Feedser.Storage.Mongo.Documents;
using StellaOps.Feedser.Storage.Mongo.Dtos;
using StellaOps.Feedser.Storage.Mongo.PsirtFlags;

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

internal static class OracleMapper
{
    private static readonly Regex FixedVersionRegex = new("(?:Fixed|Fix)\\s+(?:in|available in|for)\\s+(?<value>[A-Za-z0-9._-]+)", RegexOptions.IgnoreCase | RegexOptions.Compiled);
    private static readonly Regex PatchNumberRegex = new("Patch\\s+(?<value>\\d+)", RegexOptions.IgnoreCase | RegexOptions.Compiled);

    public static (Advisory Advisory, PsirtFlagRecord Flag) Map(
        OracleDto dto,
        DocumentRecord document,
        DtoRecord dtoRecord,
        string sourceName,
        DateTimeOffset mappedAt)
    {
        ArgumentNullException.ThrowIfNull(dto);
        ArgumentNullException.ThrowIfNull(document);
        ArgumentNullException.ThrowIfNull(dtoRecord);
        ArgumentException.ThrowIfNullOrEmpty(sourceName);

        var advisoryKey = $"oracle/{dto.AdvisoryId}";
        var fetchProvenance = new AdvisoryProvenance(sourceName, "document", document.Uri, document.FetchedAt.ToUniversalTime());
        var mappingProvenance = new AdvisoryProvenance(sourceName, "mapping", dto.AdvisoryId, mappedAt.ToUniversalTime());

        var aliases = BuildAliases(dto);
        var references = BuildReferences(dto, sourceName, mappedAt);
        var affectedPackages = BuildAffectedPackages(dto, sourceName, mappedAt);

        var advisory = new Advisory(
            advisoryKey,
            dto.Title,
            dto.Content,
            language: "en",
            published: dto.Published.ToUniversalTime(),
            modified: null,
            severity: null,
            exploitKnown: false,
            aliases,
            references,
            affectedPackages,
            Array.Empty<CvssMetric>(),
            new[] { fetchProvenance, mappingProvenance });

        var flag = new PsirtFlagRecord(
            advisoryKey,
            "Oracle",
            sourceName,
            dto.AdvisoryId,
            mappedAt.ToUniversalTime());

        return (advisory, flag);
    }

    private static IReadOnlyList<string> BuildAliases(OracleDto dto)
    {
        var aliases = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
        {
            $"ORACLE:{dto.AdvisoryId}".ToUpperInvariant(),
        };

        foreach (var cve in dto.CveIds)
        {
            if (!string.IsNullOrWhiteSpace(cve))
            {
                aliases.Add(cve.Trim().ToUpperInvariant());
            }
        }

        return aliases
            .OrderBy(static alias => alias, StringComparer.OrdinalIgnoreCase)
            .ToArray();
    }

    private static IReadOnlyList<AdvisoryReference> BuildReferences(OracleDto dto, string sourceName, DateTimeOffset recordedAt)
    {
        var comparer = StringComparer.OrdinalIgnoreCase;
        var entries = new List<(AdvisoryReference Reference, int Priority)>
        {
            (new AdvisoryReference(
                dto.DetailUrl,
                "advisory",
                "oracle",
                dto.Title,
                new AdvisoryProvenance(sourceName, "reference", dto.DetailUrl, recordedAt.ToUniversalTime())), 0),
        };

        foreach (var document in dto.PatchDocuments)
        {
            var summary = document.Title ?? document.Product;
            entries.Add((new AdvisoryReference(
                document.Url,
                "patch",
                "oracle",
                summary,
                new AdvisoryProvenance(sourceName, "reference", document.Url, recordedAt.ToUniversalTime())), 1));
        }

        foreach (var url in dto.References)
        {
            entries.Add((new AdvisoryReference(
                url,
                "reference",
                null,
                null,
                new AdvisoryProvenance(sourceName, "reference", url, recordedAt.ToUniversalTime())), 2));
        }

        foreach (var cve in dto.CveIds)
        {
            if (string.IsNullOrWhiteSpace(cve))
            {
                continue;
            }

            var cveUrl = $"https://www.cve.org/CVERecord?id={cve}";
            entries.Add((new AdvisoryReference(
                cveUrl,
                "advisory",
                cve,
                null,
                new AdvisoryProvenance(sourceName, "reference", cveUrl, recordedAt.ToUniversalTime())), 3));
        }

        return entries
            .GroupBy(tuple => tuple.Reference.Url, comparer)
            .Select(group => group
                .OrderBy(t => t.Priority)
                .ThenBy(t => t.Reference.Kind ?? string.Empty, comparer)
                .ThenBy(t => t.Reference.Url, comparer)
                .First())
            .OrderBy(t => t.Priority)
            .ThenBy(t => t.Reference.Kind ?? string.Empty, comparer)
            .ThenBy(t => t.Reference.Url, comparer)
            .Select(t => t.Reference)
            .ToArray();
    }

    private static IReadOnlyList<AffectedPackage> BuildAffectedPackages(OracleDto dto, string sourceName, DateTimeOffset recordedAt)
    {
        if (dto.Affected.Count == 0)
        {
            return Array.Empty<AffectedPackage>();
        }

        var packages = new List<AffectedPackage>();
        var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

        foreach (var entry in dto.Affected)
        {
            if (entry is null)
            {
                continue;
            }

            var component = NormalizeComponent(entry.Component);
            var notes = entry.Notes;

            foreach (var segment in SplitSupportedVersions(entry.Product, entry.SupportedVersions))
            {
                if (string.IsNullOrWhiteSpace(segment.Product))
                {
                    continue;
                }

                var identifier = CreateIdentifier(segment.Product, component);
                var baseExpression = segment.Versions ?? entry.SupportedVersions ?? string.Empty;
                var composedExpression = baseExpression;

                if (!string.IsNullOrEmpty(notes))
                {
                    composedExpression = string.IsNullOrEmpty(composedExpression)
                        ? $"notes: {notes}"
                        : $"{composedExpression} (notes: {notes})";
                }

                var rangeExpression = string.IsNullOrWhiteSpace(composedExpression) ? null : composedExpression;
                var (fixedVersion, patchNumber) = ExtractFixMetadata(notes);
                var rangeProvenance = new AdvisoryProvenance(sourceName, "range", identifier, recordedAt.ToUniversalTime());
                var rangePrimitives = BuildVendorRangePrimitives(entry, segment, component, baseExpression, rangeExpression, notes, fixedVersion, patchNumber);

                var ranges = rangeExpression is null && string.IsNullOrEmpty(fixedVersion)
                    ? Array.Empty<AffectedVersionRange>()
                    : new[]
                    {
                        new AffectedVersionRange(
                            rangeKind: "vendor",
                            introducedVersion: null,
                            fixedVersion: fixedVersion,
                            lastAffectedVersion: null,
                            rangeExpression: rangeExpression,
                            provenance: rangeProvenance,
                            primitives: rangePrimitives),
                    };

                var provenance = new[]
                {
                    new AdvisoryProvenance(sourceName, "affected", identifier, recordedAt.ToUniversalTime()),
                };

                var package = new AffectedPackage(
                    AffectedPackageTypes.Vendor,
                    identifier,
                    component,
                    ranges,
                    statuses: Array.Empty<AffectedPackageStatus>(),
                    provenance: provenance);

                var key = $"{identifier}::{component}::{ranges.FirstOrDefault()?.CreateDeterministicKey()}";
                if (seen.Add(key))
                {
                    packages.Add(package);
                }
            }
        }

        return packages.Count == 0 ? Array.Empty<AffectedPackage>() : packages;
    }

    private static IEnumerable<(string Product, string? Versions)> SplitSupportedVersions(string product, string? supportedVersions)
    {
        var normalizedProduct = string.IsNullOrWhiteSpace(product) ? "Oracle Product" : product.Trim();

        if (string.IsNullOrWhiteSpace(supportedVersions))
        {
            yield return (normalizedProduct, null);
            yield break;
        }

        var segments = supportedVersions.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
        if (segments.Length <= 1)
        {
            yield return (normalizedProduct, supportedVersions.Trim());
            yield break;
        }

        foreach (var segment in segments)
        {
            var text = segment.Trim();
            if (text.Length == 0)
            {
                continue;
            }

            var colonIndex = text.IndexOf(':');
            if (colonIndex > 0)
            {
                var name = text[..colonIndex].Trim();
                var versions = text[(colonIndex + 1)..].Trim();
                yield return (string.IsNullOrEmpty(name) ? normalizedProduct : name, versions);
            }
            else
            {
                yield return (normalizedProduct, text);
            }
        }
    }

    private static RangePrimitives? BuildVendorRangePrimitives(
        OracleAffectedEntry entry,
        (string Product, string? Versions) segment,
        string? component,
        string? baseExpression,
        string? rangeExpression,
        string? notes,
        string? fixedVersion,
        string? patchNumber)
    {
        var extensions = new Dictionary<string, string>(StringComparer.Ordinal);

        AddExtension(extensions, "oracle.product", segment.Product);
        AddExtension(extensions, "oracle.productRaw", entry.Product);
        AddExtension(extensions, "oracle.component", component);
        AddExtension(extensions, "oracle.componentRaw", entry.Component);
        AddExtension(extensions, "oracle.segmentVersions", segment.Versions);
        AddExtension(extensions, "oracle.supportedVersions", entry.SupportedVersions);
        AddExtension(extensions, "oracle.rangeExpression", rangeExpression);
        AddExtension(extensions, "oracle.baseExpression", baseExpression);
        AddExtension(extensions, "oracle.notes", notes);
        AddExtension(extensions, "oracle.fixedVersion", fixedVersion);
        AddExtension(extensions, "oracle.patchNumber", patchNumber);

        var versionTokens = ExtractVersionTokens(baseExpression);
        if (versionTokens.Count > 0)
        {
            extensions["oracle.versionTokens"] = string.Join('|', versionTokens);

            var normalizedTokens = versionTokens
                .Select(NormalizeSemVerToken)
                .Where(static token => !string.IsNullOrEmpty(token))
                .Cast<string>()
                .Distinct(StringComparer.Ordinal)
                .ToArray();

            if (normalizedTokens.Length > 0)
            {
                extensions["oracle.versionTokens.normalized"] = string.Join('|', normalizedTokens);
            }
        }

        if (extensions.Count == 0)
        {
            return null;
        }

        return new RangePrimitives(null, null, null, extensions);
    }

    private static IReadOnlyList<string> ExtractVersionTokens(string? baseExpression)
    {
        if (string.IsNullOrWhiteSpace(baseExpression))
        {
            return Array.Empty<string>();
        }

        var tokens = new List<string>();
        foreach (var token in baseExpression.Split(new[] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries))
        {
            var value = token.Trim();
            if (value.Length == 0 || !value.Any(char.IsDigit))
            {
                continue;
            }

            tokens.Add(value);
        }

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

    private static string? NormalizeSemVerToken(string token)
    {
        if (string.IsNullOrWhiteSpace(token))
        {
            return null;
        }

        if (PackageCoordinateHelper.TryParseSemVer(token, out _, out var normalized) && !string.IsNullOrWhiteSpace(normalized))
        {
            return normalized;
        }

        if (Version.TryParse(token, out var parsed))
        {
            if (parsed.Build >= 0 && parsed.Revision >= 0)
            {
                return $"{parsed.Major}.{parsed.Minor}.{parsed.Build}.{parsed.Revision}";
            }

            if (parsed.Build >= 0)
            {
                return $"{parsed.Major}.{parsed.Minor}.{parsed.Build}";
            }

            return $"{parsed.Major}.{parsed.Minor}";
        }

        return null;
    }

    private static void AddExtension(Dictionary<string, string> extensions, string key, string? value)
    {
        if (string.IsNullOrWhiteSpace(value))
        {
            return;
        }

        extensions[key] = value.Trim();
    }

    private static string? NormalizeComponent(string? component)
    {
        if (string.IsNullOrWhiteSpace(component))
        {
            return null;
        }

        var trimmed = component.Trim();
        return trimmed.Length == 0 ? null : trimmed;
    }

    private static string CreateIdentifier(string product, string? component)
    {
        var normalizedProduct = product.Trim();
        if (string.IsNullOrEmpty(component))
        {
            return normalizedProduct;
        }

        return $"{normalizedProduct}::{component}";
    }

    private static (string? FixedVersion, string? PatchNumber) ExtractFixMetadata(string? notes)
    {
        if (string.IsNullOrWhiteSpace(notes))
        {
            return (null, null);
        }

        string? fixedVersion = null;
        string? patchNumber = null;

        var match = FixedVersionRegex.Match(notes);
        if (match.Success)
        {
            fixedVersion = match.Groups["value"].Value.Trim();
        }

        match = PatchNumberRegex.Match(notes);
        if (match.Success)
        {
            patchNumber = match.Groups["value"].Value.Trim();
            fixedVersion ??= patchNumber;
        }

        return (fixedVersion, patchNumber);
    }
}
