using System.Collections.Immutable;
using System.Net;
using System.Net.Http.Headers;
using System.Text.Json;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StellaOps.Excititor.Connectors.Cisco.CSAF.Configuration;
using StellaOps.Excititor.Core;
using System.IO.Abstractions;

namespace StellaOps.Excititor.Connectors.Cisco.CSAF.Metadata;

public sealed class CiscoProviderMetadataLoader
{
    public const string CacheKey = "StellaOps.Excititor.Connectors.Cisco.CSAF.Metadata";

    private readonly IHttpClientFactory _httpClientFactory;
    private readonly IMemoryCache _memoryCache;
    private readonly ILogger<CiscoProviderMetadataLoader> _logger;
    private readonly CiscoConnectorOptions _options;
    private readonly IFileSystem _fileSystem;
    private readonly JsonSerializerOptions _serializerOptions;
    private readonly SemaphoreSlim _semaphore = new(1, 1);

    public CiscoProviderMetadataLoader(
        IHttpClientFactory httpClientFactory,
        IMemoryCache memoryCache,
        IOptions<CiscoConnectorOptions> options,
        ILogger<CiscoProviderMetadataLoader> logger,
        IFileSystem? fileSystem = null)
    {
        _httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory));
        _memoryCache = memoryCache ?? throw new ArgumentNullException(nameof(memoryCache));
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
        ArgumentNullException.ThrowIfNull(options);
        _options = options.Value ?? throw new ArgumentNullException(nameof(options));
        _fileSystem = fileSystem ?? new FileSystem();
        _serializerOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web)
        {
            PropertyNameCaseInsensitive = true,
            ReadCommentHandling = JsonCommentHandling.Skip,
        };
    }

    public async Task<CiscoProviderMetadataResult> LoadAsync(CancellationToken cancellationToken)
    {
        if (_memoryCache.TryGetValue<CacheEntry>(CacheKey, out var cached) && cached is not null && !cached.IsExpired())
        {
            _logger.LogDebug("Returning cached Cisco provider metadata (expires {Expires}).", cached.ExpiresAt);
            return new CiscoProviderMetadataResult(cached.Provider, cached.FetchedAt, cached.FromOffline, true);
        }

        await _semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
        try
        {
            if (_memoryCache.TryGetValue<CacheEntry>(CacheKey, out cached) && cached is not null && !cached.IsExpired())
            {
                return new CiscoProviderMetadataResult(cached.Provider, cached.FetchedAt, cached.FromOffline, true);
            }

            CacheEntry? previous = cached;

            if (!_options.PreferOfflineSnapshot)
            {
                var network = await TryFetchFromNetworkAsync(previous, cancellationToken).ConfigureAwait(false);
                if (network is not null)
                {
                    StoreCache(network);
                    return new CiscoProviderMetadataResult(network.Provider, network.FetchedAt, false, false);
                }
            }

            var offline = TryLoadFromOffline();
            if (offline is not null)
            {
                var entry = offline with
                {
                    FetchedAt = DateTimeOffset.UtcNow,
                    ExpiresAt = DateTimeOffset.UtcNow + _options.MetadataCacheDuration,
                    FromOffline = true,
                };
                StoreCache(entry);
                return new CiscoProviderMetadataResult(entry.Provider, entry.FetchedAt, true, false);
            }

            throw new InvalidOperationException("Unable to load Cisco CSAF provider metadata from network or offline snapshot.");
        }
        finally
        {
            _semaphore.Release();
        }
    }

    private async Task<CacheEntry?> TryFetchFromNetworkAsync(CacheEntry? previous, CancellationToken cancellationToken)
    {
        try
        {
            var client = _httpClientFactory.CreateClient(CiscoConnectorOptions.HttpClientName);
            using var request = new HttpRequestMessage(HttpMethod.Get, _options.MetadataUri);
            if (!string.IsNullOrWhiteSpace(_options.ApiToken))
            {
                request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _options.ApiToken);
            }

            if (!string.IsNullOrWhiteSpace(previous?.ETag) && EntityTagHeaderValue.TryParse(previous.ETag, out var etag))
            {
                request.Headers.IfNoneMatch.Add(etag);
            }

            using var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);

            if (response.StatusCode == HttpStatusCode.NotModified && previous is not null)
            {
                _logger.LogDebug("Cisco provider metadata not modified (etag {ETag}).", previous.ETag);
                return previous with
                {
                    FetchedAt = DateTimeOffset.UtcNow,
                    ExpiresAt = DateTimeOffset.UtcNow + _options.MetadataCacheDuration,
                };
            }

            response.EnsureSuccessStatusCode();
            var payload = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
            var provider = ParseProvider(payload);
            var etagHeader = response.Headers.ETag?.ToString();

            if (_options.PersistOfflineSnapshot && !string.IsNullOrWhiteSpace(_options.OfflineSnapshotPath))
            {
                try
                {
                    _fileSystem.File.WriteAllText(_options.OfflineSnapshotPath, payload);
                    _logger.LogDebug("Persisted Cisco metadata snapshot to {Path}.", _options.OfflineSnapshotPath);
                }
                catch (Exception ex)
                {
                    _logger.LogWarning(ex, "Failed to persist Cisco metadata snapshot to {Path}.", _options.OfflineSnapshotPath);
                }
            }

            return new CacheEntry(
                provider,
                DateTimeOffset.UtcNow,
                DateTimeOffset.UtcNow + _options.MetadataCacheDuration,
                etagHeader,
                FromOffline: false);
        }
        catch (Exception ex) when (ex is not OperationCanceledException && !_options.PreferOfflineSnapshot)
        {
            _logger.LogWarning(ex, "Failed to fetch Cisco provider metadata from {Uri}; falling back to offline snapshot when available.", _options.MetadataUri);
            return null;
        }
    }

    private CacheEntry? TryLoadFromOffline()
    {
        if (string.IsNullOrWhiteSpace(_options.OfflineSnapshotPath))
        {
            return null;
        }

        if (!_fileSystem.File.Exists(_options.OfflineSnapshotPath))
        {
            _logger.LogWarning("Cisco offline snapshot path {Path} does not exist.", _options.OfflineSnapshotPath);
            return null;
        }

        try
        {
            var payload = _fileSystem.File.ReadAllText(_options.OfflineSnapshotPath);
            var provider = ParseProvider(payload);
            return new CacheEntry(provider, DateTimeOffset.UtcNow, DateTimeOffset.UtcNow + _options.MetadataCacheDuration, null, true);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to load Cisco provider metadata from offline snapshot {Path}.", _options.OfflineSnapshotPath);
            return null;
        }
    }

    private VexProvider ParseProvider(string payload)
    {
        if (string.IsNullOrWhiteSpace(payload))
        {
            throw new InvalidOperationException("Cisco provider metadata payload was empty.");
        }

        ProviderMetadataDocument? document;
        try
        {
            document = JsonSerializer.Deserialize<ProviderMetadataDocument>(payload, _serializerOptions);
        }
        catch (JsonException ex)
        {
            throw new InvalidOperationException("Failed to parse Cisco provider metadata.", ex);
        }

        if (document?.Metadata?.Publisher?.ContactDetails is null || string.IsNullOrWhiteSpace(document.Metadata.Publisher.ContactDetails.Id))
        {
            throw new InvalidOperationException("Cisco provider metadata did not include a publisher identifier.");
        }

        var discovery = new VexProviderDiscovery(document.Discovery?.WellKnown, document.Discovery?.RolIe);
        var trust = document.Trust is null
            ? VexProviderTrust.Default
            : new VexProviderTrust(
                document.Trust.Weight ?? 1.0,
                document.Trust.Cosign is null ? null : new VexCosignTrust(document.Trust.Cosign.Issuer ?? string.Empty, document.Trust.Cosign.IdentityPattern ?? string.Empty),
                document.Trust.PgpFingerprints ?? Enumerable.Empty<string>());

        var directories = document.Distributions?.Directories is null
            ? Enumerable.Empty<Uri>()
            : document.Distributions.Directories
                .Where(static s => !string.IsNullOrWhiteSpace(s))
                .Select(static s => Uri.TryCreate(s, UriKind.Absolute, out var uri) ? uri : null)
                .Where(static uri => uri is not null)!
                .Select(static uri => uri!);

        return new VexProvider(
            id: document.Metadata.Publisher.ContactDetails.Id,
            displayName: document.Metadata.Publisher.Name ?? document.Metadata.Publisher.ContactDetails.Id,
            kind: document.Metadata.Publisher.Category?.Equals("vendor", StringComparison.OrdinalIgnoreCase) == true ? VexProviderKind.Vendor : VexProviderKind.Hub,
            baseUris: directories,
            discovery: discovery,
            trust: trust,
            enabled: true);
    }

    private void StoreCache(CacheEntry entry)
    {
        var options = new MemoryCacheEntryOptions
        {
            AbsoluteExpiration = entry.ExpiresAt,
        };
        _memoryCache.Set(CacheKey, entry, options);
    }

    private sealed record CacheEntry(
        VexProvider Provider,
        DateTimeOffset FetchedAt,
        DateTimeOffset ExpiresAt,
        string? ETag,
        bool FromOffline)
    {
        public bool IsExpired() => DateTimeOffset.UtcNow >= ExpiresAt;
    }
}

public sealed record CiscoProviderMetadataResult(
    VexProvider Provider,
    DateTimeOffset FetchedAt,
    bool FromOfflineSnapshot,
    bool ServedFromCache);

#region document models

internal sealed class ProviderMetadataDocument
{
    [System.Text.Json.Serialization.JsonPropertyName("metadata")]
    public ProviderMetadataMetadata Metadata { get; set; } = new();

    [System.Text.Json.Serialization.JsonPropertyName("discovery")]
    public ProviderMetadataDiscovery? Discovery { get; set; }

    [System.Text.Json.Serialization.JsonPropertyName("trust")]
    public ProviderMetadataTrust? Trust { get; set; }

    [System.Text.Json.Serialization.JsonPropertyName("distributions")]
    public ProviderMetadataDistributions? Distributions { get; set; }
}

internal sealed class ProviderMetadataMetadata
{
    [System.Text.Json.Serialization.JsonPropertyName("publisher")]
    public ProviderMetadataPublisher Publisher { get; set; } = new();
}

internal sealed class ProviderMetadataPublisher
{
    [System.Text.Json.Serialization.JsonPropertyName("name")]
    public string? Name { get; set; }

    [System.Text.Json.Serialization.JsonPropertyName("category")]
    public string? Category { get; set; }

    [System.Text.Json.Serialization.JsonPropertyName("contact_details")]
    public ProviderMetadataPublisherContact ContactDetails { get; set; } = new();
}

internal sealed class ProviderMetadataPublisherContact
{
    [System.Text.Json.Serialization.JsonPropertyName("id")]
    public string? Id { get; set; }
}

internal sealed class ProviderMetadataDiscovery
{
    [System.Text.Json.Serialization.JsonPropertyName("well_known")]
    public Uri? WellKnown { get; set; }

    [System.Text.Json.Serialization.JsonPropertyName("rolie")]
    public Uri? RolIe { get; set; }
}

internal sealed class ProviderMetadataTrust
{
    [System.Text.Json.Serialization.JsonPropertyName("weight")]
    public double? Weight { get; set; }

    [System.Text.Json.Serialization.JsonPropertyName("cosign")]
    public ProviderMetadataTrustCosign? Cosign { get; set; }

    [System.Text.Json.Serialization.JsonPropertyName("pgp_fingerprints")]
    public string[]? PgpFingerprints { get; set; }
}

internal sealed class ProviderMetadataTrustCosign
{
    [System.Text.Json.Serialization.JsonPropertyName("issuer")]
    public string? Issuer { get; set; }

    [System.Text.Json.Serialization.JsonPropertyName("identity_pattern")]
    public string? IdentityPattern { get; set; }
}

internal sealed class ProviderMetadataDistributions
{
    [System.Text.Json.Serialization.JsonPropertyName("directories")]
    public string[]? Directories { get; set; }
}

#endregion
