using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace StellaOps.Feedser.Exporter.TrivyDb;

public sealed class TrivyDbOrasPusher : ITrivyDbOrasPusher
{
    private readonly TrivyDbExportOptions _options;
    private readonly ILogger<TrivyDbOrasPusher> _logger;

    public TrivyDbOrasPusher(IOptions<TrivyDbExportOptions> options, ILogger<TrivyDbOrasPusher> logger)
    {
        _options = options?.Value ?? throw new ArgumentNullException(nameof(options));
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
    }

    public async Task PushAsync(string layoutPath, string reference, string exportId, CancellationToken cancellationToken)
    {
        var orasOptions = _options.Oras;
        if (!orasOptions.Enabled)
        {
            return;
        }

        if (string.IsNullOrWhiteSpace(reference))
        {
            throw new InvalidOperationException("ORAS push requested but reference is empty.");
        }

        if (!Directory.Exists(layoutPath))
        {
            throw new DirectoryNotFoundException($"OCI layout directory '{layoutPath}' does not exist.");
        }

        var executable = string.IsNullOrWhiteSpace(orasOptions.ExecutablePath) ? "oras" : orasOptions.ExecutablePath;
        var tag = ResolveTag(reference, exportId);
        var layoutReference = $"{layoutPath}:{tag}";

        var startInfo = new ProcessStartInfo
        {
            FileName = executable,
            RedirectStandardOutput = true,
            RedirectStandardError = true,
            UseShellExecute = false,
        };

        startInfo.ArgumentList.Add("cp");
        startInfo.ArgumentList.Add("--from-oci-layout");
        startInfo.ArgumentList.Add(layoutReference);
        if (orasOptions.SkipTlsVerify)
        {
            startInfo.ArgumentList.Add("--insecure");
        }
        if (orasOptions.UseHttp)
        {
            startInfo.ArgumentList.Add("--plain-http");
        }

        if (orasOptions.AdditionalArguments is { Count: > 0 })
        {
            foreach (var arg in orasOptions.AdditionalArguments)
            {
                if (!string.IsNullOrWhiteSpace(arg))
                {
                    startInfo.ArgumentList.Add(arg);
                }
            }
        }

        startInfo.ArgumentList.Add(reference);

        if (!string.IsNullOrWhiteSpace(orasOptions.WorkingDirectory))
        {
            startInfo.WorkingDirectory = orasOptions.WorkingDirectory;
        }

        if (!orasOptions.InheritEnvironment)
        {
            startInfo.Environment.Clear();
        }

        if (orasOptions.Environment is { Count: > 0 })
        {
            foreach (var kvp in orasOptions.Environment)
            {
                if (!string.IsNullOrEmpty(kvp.Key))
                {
                    startInfo.Environment[kvp.Key] = kvp.Value;
                }
            }
        }

        using var process = new Process { StartInfo = startInfo };
        var stdout = new StringBuilder();
        var stderr = new StringBuilder();
        var stdoutCompletion = new TaskCompletionSource<object?>();
        var stderrCompletion = new TaskCompletionSource<object?>();

        process.OutputDataReceived += (_, e) =>
        {
            if (e.Data is null)
            {
                stdoutCompletion.TrySetResult(null);
            }
            else
            {
                stdout.AppendLine(e.Data);
            }
        };

        process.ErrorDataReceived += (_, e) =>
        {
            if (e.Data is null)
            {
                stderrCompletion.TrySetResult(null);
            }
            else
            {
                stderr.AppendLine(e.Data);
            }
        };

        _logger.LogInformation("Pushing Trivy DB export {ExportId} to {Reference} using {Executable}", exportId, reference, executable);

        try
        {
            if (!process.Start())
            {
                throw new InvalidOperationException($"Failed to start '{executable}'.");
            }
        }
        catch (Exception ex)
        {
            throw new InvalidOperationException($"Failed to start '{executable}'.", ex);
        }

        process.BeginOutputReadLine();
        process.BeginErrorReadLine();

        using var registration = cancellationToken.Register(() =>
        {
            try
            {
                if (!process.HasExited)
                {
                    process.Kill(entireProcessTree: true);
                }
            }
            catch
            {
                // ignore
            }
        });

#if NET8_0_OR_GREATER
        await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false);
#else
        await Task.Run(() => process.WaitForExit(), cancellationToken).ConfigureAwait(false);
#endif

        await Task.WhenAll(stdoutCompletion.Task, stderrCompletion.Task).ConfigureAwait(false);

        if (process.ExitCode != 0)
        {
            _logger.LogError("ORAS push for {Reference} failed with code {Code}. stderr: {Stderr}", reference, process.ExitCode, stderr.ToString());
            throw new InvalidOperationException($"'{executable}' exited with code {process.ExitCode}.");
        }

        if (stdout.Length > 0)
        {
            _logger.LogDebug("ORAS push output: {Stdout}", stdout.ToString());
        }

        if (stderr.Length > 0)
        {
            _logger.LogWarning("ORAS push warnings: {Stderr}", stderr.ToString());
        }
    }

    private static string ResolveTag(string reference, string fallback)
    {
        if (string.IsNullOrWhiteSpace(reference))
        {
            return fallback;
        }

        var atIndex = reference.IndexOf('@');
        if (atIndex >= 0)
        {
            reference = reference[..atIndex];
        }

        var slashIndex = reference.LastIndexOf('/');
        var colonIndex = reference.LastIndexOf(':');
        if (colonIndex > slashIndex && colonIndex >= 0)
        {
            return reference[(colonIndex + 1)..];
        }

        return fallback;
    }
}
