using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using System.Xml;
using System.Xml.Linq;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StellaOps.Feedser.Source.Jvn.Configuration;

namespace StellaOps.Feedser.Source.Jvn.Internal;

public sealed class MyJvnClient
{
    private static readonly XNamespace RssNamespace = "http://purl.org/rss/1.0/";
    private static readonly XNamespace DcTermsNamespace = "http://purl.org/dc/terms/";
    private static readonly XNamespace SecNamespace = "http://jvn.jp/rss/mod_sec/3.0/";
    private static readonly XNamespace RdfNamespace = "http://www.w3.org/1999/02/22-rdf-syntax-ns#";
    private static readonly XNamespace StatusNamespace = "http://jvndb.jvn.jp/myjvn/Status";

    private static readonly TimeSpan TokyoOffset = TimeSpan.FromHours(9);

    private readonly IHttpClientFactory _httpClientFactory;
    private readonly JvnOptions _options;
    private readonly ILogger<MyJvnClient> _logger;

    public MyJvnClient(IHttpClientFactory httpClientFactory, IOptions<JvnOptions> options, ILogger<MyJvnClient> logger)
    {
        _httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory));
        _options = (options ?? throw new ArgumentNullException(nameof(options))).Value ?? throw new ArgumentNullException(nameof(options));
        _options.Validate();
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
    }

    internal async Task<IReadOnlyList<JvnOverviewItem>> GetOverviewAsync(DateTimeOffset windowStart, DateTimeOffset windowEnd, CancellationToken cancellationToken)
    {
        if (windowEnd <= windowStart)
        {
            throw new ArgumentException("windowEnd must be greater than windowStart", nameof(windowEnd));
        }

        var items = new List<JvnOverviewItem>();
        var client = _httpClientFactory.CreateClient(JvnOptions.HttpClientName);

        var startItem = 1;
        var pagesFetched = 0;

        while (pagesFetched < _options.MaxOverviewPagesPerFetch)
        {
            cancellationToken.ThrowIfCancellationRequested();

            var requestUri = BuildOverviewUri(windowStart, windowEnd, startItem);
            using var response = await client.GetAsync(requestUri, cancellationToken).ConfigureAwait(false);
            response.EnsureSuccessStatusCode();

            await using var contentStream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
            using var reader = XmlReader.Create(contentStream, new XmlReaderSettings { Async = true, IgnoreWhitespace = true, IgnoreComments = true });
            var document = await XDocument.LoadAsync(reader, LoadOptions.None, cancellationToken).ConfigureAwait(false);

            var page = ParseOverviewPage(document);
            if (page.Items.Count == 0)
            {
                _logger.LogDebug("JVN overview page starting at {StartItem} returned zero results", startItem);
                break;
            }

            items.AddRange(page.Items);
            pagesFetched++;

            if (page.ReturnedCount < _options.PageSize || startItem + _options.PageSize > page.TotalResults)
            {
                break;
            }

            startItem += _options.PageSize;

            if (_options.RequestDelay > TimeSpan.Zero)
            {
                try
                {
                    await Task.Delay(_options.RequestDelay, cancellationToken).ConfigureAwait(false);
                }
                catch (TaskCanceledException)
                {
                    break;
                }
            }
        }

        return items;
    }

    private Uri BuildOverviewUri(DateTimeOffset windowStart, DateTimeOffset windowEnd, int startItem)
    {
        var (startYear, startMonth, startDay) = ToTokyoDateParts(windowStart);
        var (endYear, endMonth, endDay) = ToTokyoDateParts(windowEnd);

        var parameters = new[]
        {
            new KeyValuePair<string, string>("method", "getVulnOverviewList"),
            new KeyValuePair<string, string>("feed", "hnd"),
            new KeyValuePair<string, string>("lang", "en"),
            new KeyValuePair<string, string>("rangeDatePublished", "n"),
            new KeyValuePair<string, string>("rangeDatePublic", "n"),
            new KeyValuePair<string, string>("rangeDateFirstPublished", "n"),
            new KeyValuePair<string, string>("dateFirstPublishedStartY", startYear),
            new KeyValuePair<string, string>("dateFirstPublishedStartM", startMonth),
            new KeyValuePair<string, string>("dateFirstPublishedStartD", startDay),
            new KeyValuePair<string, string>("dateFirstPublishedEndY", endYear),
            new KeyValuePair<string, string>("dateFirstPublishedEndM", endMonth),
            new KeyValuePair<string, string>("dateFirstPublishedEndD", endDay),
            new KeyValuePair<string, string>("startItem", startItem.ToString(CultureInfo.InvariantCulture)),
            new KeyValuePair<string, string>("maxCountItem", _options.PageSize.ToString(CultureInfo.InvariantCulture)),
        };

        var query = BuildQueryString(parameters);

        var builder = new UriBuilder(_options.BaseEndpoint)
        {
            Query = query,
        };
        return builder.Uri;
    }

    private static (string Year, string Month, string Day) ToTokyoDateParts(DateTimeOffset timestamp)
    {
        var local = timestamp.ToOffset(TokyoOffset).Date;
        return (
            local.Year.ToString("D4", CultureInfo.InvariantCulture),
            local.Month.ToString("D2", CultureInfo.InvariantCulture),
            local.Day.ToString("D2", CultureInfo.InvariantCulture));
    }

    private static JvnOverviewPage ParseOverviewPage(XDocument document)
    {
        var items = new List<JvnOverviewItem>();

        foreach (var item in document.Descendants(RssNamespace + "item"))
        {
            var identifier = item.Element(SecNamespace + "identifier")?.Value?.Trim();
            if (string.IsNullOrWhiteSpace(identifier))
            {
                continue;
            }

            Uri? detailUri = null;
            var linkValue = item.Element(RssNamespace + "link")?.Value?.Trim();
            if (!string.IsNullOrWhiteSpace(linkValue))
            {
                Uri.TryCreate(linkValue, UriKind.Absolute, out detailUri);
            }

            if (detailUri is null)
            {
                var aboutValue = item.Attribute(RdfNamespace + "about")?.Value?.Trim();
                if (!string.IsNullOrWhiteSpace(aboutValue))
                {
                    Uri.TryCreate(aboutValue, UriKind.Absolute, out detailUri);
                }
            }

            if (detailUri is null)
            {
                continue;
            }

            var title = item.Element(RssNamespace + "title")?.Value?.Trim();
            if (string.IsNullOrWhiteSpace(title))
            {
                title = identifier;
            }

            var firstPublished = TryParseDate(item.Element(DcTermsNamespace + "issued")?.Value);
            var lastUpdated = TryParseDate(item.Element(DcTermsNamespace + "modified")?.Value);

            items.Add(new JvnOverviewItem(identifier, detailUri, title!, firstPublished, lastUpdated));
        }

        var statusElement = document.Root?.Element(StatusNamespace + "Status")
            ?? document.Descendants(StatusNamespace + "Status").FirstOrDefault();

        var totalResults = TryParseInt(statusElement?.Attribute("totalRes")?.Value) ?? items.Count;
        var returned = TryParseInt(statusElement?.Attribute("totalResRet")?.Value) ?? items.Count;
        var firstResult = TryParseInt(statusElement?.Attribute("firstRes")?.Value) ?? 1;

        return new JvnOverviewPage(items, totalResults, returned, firstResult);
    }

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

        return DateTimeOffset.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal, out var parsed)
            ? parsed.ToUniversalTime()
            : null;
    }

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

        return int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsed) ? parsed : null;
    }

    internal Uri BuildDetailUri(string vulnerabilityId)
    {
        ArgumentException.ThrowIfNullOrEmpty(vulnerabilityId);

        var query = BuildQueryString(new[]
        {
            new KeyValuePair<string, string>("method", "getVulnDetailInfo"),
            new KeyValuePair<string, string>("feed", "hnd"),
            new KeyValuePair<string, string>("lang", "en"),
            new KeyValuePair<string, string>("vulnId", vulnerabilityId.Trim()),
        });
        var builder = new UriBuilder(_options.BaseEndpoint)
        {
            Query = query,
        };

        return builder.Uri;
    }

    private static string BuildQueryString(IEnumerable<KeyValuePair<string, string>> parameters)
    {
        return string.Join(
            "&",
            parameters.Select(parameter =>
                $"{WebUtility.UrlEncode(parameter.Key)}={WebUtility.UrlEncode(parameter.Value)}"));
    }
}
