using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

namespace StellaOps.Scanner.Sbomer.BuildXPlugin.Manifest;

/// <summary>
/// Loads buildx plug-in manifests from the restart-time plug-in directory.
/// </summary>
public sealed class BuildxPluginManifestLoader
{
    public const string DefaultSearchPattern = "*.manifest.json";

    private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web)
    {
        AllowTrailingCommas = true,
        ReadCommentHandling = JsonCommentHandling.Skip,
        PropertyNameCaseInsensitive = true
    };

    private readonly string manifestDirectory;
    private readonly string searchPattern;

    public BuildxPluginManifestLoader(string manifestDirectory, string? searchPattern = null)
    {
        if (string.IsNullOrWhiteSpace(manifestDirectory))
        {
            throw new ArgumentException("Manifest directory is required.", nameof(manifestDirectory));
        }

        this.manifestDirectory = Path.GetFullPath(manifestDirectory);
        this.searchPattern = string.IsNullOrWhiteSpace(searchPattern)
            ? DefaultSearchPattern
            : searchPattern;
    }

    /// <summary>
    /// Loads all manifests in the configured directory.
    /// </summary>
    public async Task<IReadOnlyList<BuildxPluginManifest>> LoadAsync(CancellationToken cancellationToken)
    {
        if (!Directory.Exists(manifestDirectory))
        {
            return Array.Empty<BuildxPluginManifest>();
        }

        var manifests = new List<BuildxPluginManifest>();

        foreach (var file in Directory.EnumerateFiles(manifestDirectory, searchPattern, SearchOption.TopDirectoryOnly))
        {
            if (IsHiddenPath(file))
            {
                continue;
            }

            var manifest = await DeserializeManifestAsync(file, cancellationToken).ConfigureAwait(false);
            manifests.Add(manifest);
        }

        return manifests
            .OrderBy(static m => m.Id, StringComparer.OrdinalIgnoreCase)
            .ThenBy(static m => m.Version, StringComparer.OrdinalIgnoreCase)
            .ToArray();
    }

    /// <summary>
    /// Loads the manifest with the specified identifier.
    /// </summary>
    public async Task<BuildxPluginManifest> LoadByIdAsync(string manifestId, CancellationToken cancellationToken)
    {
        if (string.IsNullOrWhiteSpace(manifestId))
        {
            throw new ArgumentException("Manifest identifier is required.", nameof(manifestId));
        }

        var manifests = await LoadAsync(cancellationToken).ConfigureAwait(false);
        var manifest = manifests.FirstOrDefault(m => string.Equals(m.Id, manifestId, StringComparison.OrdinalIgnoreCase));
        if (manifest is null)
        {
            throw new BuildxPluginException($"Buildx plug-in manifest '{manifestId}' was not found in '{manifestDirectory}'.");
        }

        return manifest;
    }

    /// <summary>
    /// Loads the first available manifest.
    /// </summary>
    public async Task<BuildxPluginManifest> LoadDefaultAsync(CancellationToken cancellationToken)
    {
        var manifests = await LoadAsync(cancellationToken).ConfigureAwait(false);
        if (manifests.Count == 0)
        {
            throw new BuildxPluginException($"No buildx plug-in manifests were discovered under '{manifestDirectory}'.");
        }

        return manifests[0];
    }

    private static bool IsHiddenPath(string path)
    {
        var directory = Path.GetDirectoryName(path);
        while (!string.IsNullOrEmpty(directory))
        {
            var segment = Path.GetFileName(directory);
            if (segment.StartsWith(".", StringComparison.Ordinal))
            {
                return true;
            }

            directory = Path.GetDirectoryName(directory);
        }

        return false;
    }

    private static async Task<BuildxPluginManifest> DeserializeManifestAsync(string file, CancellationToken cancellationToken)
    {
        await using var stream = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, FileOptions.Asynchronous);
        BuildxPluginManifest? manifest;

        try
        {
            manifest = await JsonSerializer.DeserializeAsync<BuildxPluginManifest>(stream, SerializerOptions, cancellationToken)
                .ConfigureAwait(false);
        }
        catch (JsonException ex)
        {
            throw new BuildxPluginException($"Failed to parse manifest '{file}'.", ex);
        }

        if (manifest is null)
        {
            throw new BuildxPluginException($"Manifest '{file}' is empty or invalid.");
        }

        ValidateManifest(manifest, file);

        var directory = Path.GetDirectoryName(file);
        return manifest with
        {
            SourcePath = file,
            SourceDirectory = directory
        };
    }

    private static void ValidateManifest(BuildxPluginManifest manifest, string file)
    {
        if (!string.Equals(manifest.SchemaVersion, BuildxPluginManifest.CurrentSchemaVersion, StringComparison.OrdinalIgnoreCase))
        {
            throw new BuildxPluginException(
                $"Manifest '{file}' uses unsupported schema version '{manifest.SchemaVersion}'. Expected '{BuildxPluginManifest.CurrentSchemaVersion}'.");
        }

        if (string.IsNullOrWhiteSpace(manifest.Id))
        {
            throw new BuildxPluginException($"Manifest '{file}' must specify a non-empty 'id'.");
        }

        if (manifest.EntryPoint is null)
        {
            throw new BuildxPluginException($"Manifest '{file}' must specify an 'entryPoint'.");
        }

        if (string.IsNullOrWhiteSpace(manifest.EntryPoint.Executable))
        {
            throw new BuildxPluginException($"Manifest '{file}' must specify an executable entry point.");
        }

        if (!manifest.RequiresRestart)
        {
            throw new BuildxPluginException($"Manifest '{file}' must enforce restart-required activation.");
        }

        if (manifest.Cas is null)
        {
            throw new BuildxPluginException($"Manifest '{file}' must define CAS defaults.");
        }

        if (string.IsNullOrWhiteSpace(manifest.Cas.DefaultRoot))
        {
            throw new BuildxPluginException($"Manifest '{file}' must specify a CAS default root directory.");
        }
    }
}
