using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using System.Text.Json;

namespace StellaOps.Cli.Services;

internal sealed class ScannerExecutor : IScannerExecutor
{
    private readonly ILogger<ScannerExecutor> _logger;

    public ScannerExecutor(ILogger<ScannerExecutor> logger)
    {
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
    }

    public async Task<ScannerExecutionResult> RunAsync(
        string runner,
        string entry,
        string targetDirectory,
        string resultsDirectory,
        IReadOnlyList<string> arguments,
        bool verbose,
        CancellationToken cancellationToken)
    {
        if (string.IsNullOrWhiteSpace(targetDirectory))
        {
            throw new ArgumentException("Target directory must be provided.", nameof(targetDirectory));
        }

        runner = string.IsNullOrWhiteSpace(runner) ? "docker" : runner.Trim().ToLowerInvariant();
        entry = entry?.Trim() ?? string.Empty;

        var normalizedTarget = Path.GetFullPath(targetDirectory);
        if (!Directory.Exists(normalizedTarget))
        {
            throw new DirectoryNotFoundException($"Scan target directory '{normalizedTarget}' does not exist.");
        }

        resultsDirectory = string.IsNullOrWhiteSpace(resultsDirectory)
            ? Path.Combine(Directory.GetCurrentDirectory(), "scan-results")
            : Path.GetFullPath(resultsDirectory);

        Directory.CreateDirectory(resultsDirectory);
        var executionTimestamp = DateTimeOffset.UtcNow;
        var baselineFiles = Directory.GetFiles(resultsDirectory, "*", SearchOption.AllDirectories);
        var baseline = new HashSet<string>(baselineFiles, StringComparer.OrdinalIgnoreCase);

        var startInfo = BuildProcessStartInfo(runner, entry, normalizedTarget, resultsDirectory, arguments);
        using var process = new Process { StartInfo = startInfo, EnableRaisingEvents = true };

        var stdout = new List<string>();
        var stderr = new List<string>();

        process.OutputDataReceived += (_, args) =>
        {
            if (args.Data is null)
            {
                return;
            }

            stdout.Add(args.Data);
            if (verbose)
            {
                _logger.LogInformation("[scan] {Line}", args.Data);
            }
        };

        process.ErrorDataReceived += (_, args) =>
        {
            if (args.Data is null)
            {
                return;
            }

            stderr.Add(args.Data);
            _logger.LogError("[scan] {Line}", args.Data);
        };

        _logger.LogInformation("Launching scanner via {Runner} (entry: {Entry})...", runner, entry);
        if (!process.Start())
        {
            throw new InvalidOperationException("Failed to start scanner process.");
        }

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

        await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false);
        var completionTimestamp = DateTimeOffset.UtcNow;

        if (process.ExitCode == 0)
        {
            _logger.LogInformation("Scanner completed successfully.");
        }
        else
        {
            _logger.LogWarning("Scanner exited with code {Code}.", process.ExitCode);
        }

        var resultsPath = ResolveResultsPath(resultsDirectory, executionTimestamp, baseline);
        if (string.IsNullOrWhiteSpace(resultsPath))
        {
            resultsPath = CreatePlaceholderResult(resultsDirectory);
        }

        var metadataPath = WriteRunMetadata(
            resultsDirectory,
            executionTimestamp,
            completionTimestamp,
            runner,
            entry,
            normalizedTarget,
            resultsPath,
            arguments,
            process.ExitCode,
            stdout,
            stderr);

        return new ScannerExecutionResult(process.ExitCode, resultsPath, metadataPath);
    }

    private ProcessStartInfo BuildProcessStartInfo(
        string runner,
        string entry,
        string targetDirectory,
        string resultsDirectory,
        IReadOnlyList<string> args)
    {
        return runner switch
        {
            "self" or "native" => BuildNativeStartInfo(entry, args),
            "dotnet" => BuildDotNetStartInfo(entry, args),
            "docker" => BuildDockerStartInfo(entry, targetDirectory, resultsDirectory, args),
            _ => BuildCustomRunnerStartInfo(runner, entry, args)
        };
    }

    private static ProcessStartInfo BuildNativeStartInfo(string binaryPath, IReadOnlyList<string> args)
    {
        if (string.IsNullOrWhiteSpace(binaryPath) || !File.Exists(binaryPath))
        {
            throw new FileNotFoundException("Scanner entrypoint not found.", binaryPath);
        }

        var startInfo = new ProcessStartInfo
        {
            FileName = binaryPath,
            WorkingDirectory = Directory.GetCurrentDirectory()
        };

        foreach (var argument in args)
        {
            startInfo.ArgumentList.Add(argument);
        }

        startInfo.RedirectStandardError = true;
        startInfo.RedirectStandardOutput = true;
        startInfo.UseShellExecute = false;

        return startInfo;
    }

    private static ProcessStartInfo BuildDotNetStartInfo(string binaryPath, IReadOnlyList<string> args)
    {
        var startInfo = new ProcessStartInfo
        {
            FileName = "dotnet",
            WorkingDirectory = Directory.GetCurrentDirectory()
        };

        startInfo.ArgumentList.Add(binaryPath);
        foreach (var argument in args)
        {
            startInfo.ArgumentList.Add(argument);
        }

        startInfo.RedirectStandardError = true;
        startInfo.RedirectStandardOutput = true;
        startInfo.UseShellExecute = false;

        return startInfo;
    }

    private static ProcessStartInfo BuildDockerStartInfo(string image, string targetDirectory, string resultsDirectory, IReadOnlyList<string> args)
    {
        if (string.IsNullOrWhiteSpace(image))
        {
            throw new ArgumentException("Docker image must be provided when runner is 'docker'.", nameof(image));
        }

        var cwd = Directory.GetCurrentDirectory();

        var startInfo = new ProcessStartInfo
        {
            FileName = "docker",
            WorkingDirectory = cwd
        };

        startInfo.ArgumentList.Add("run");
        startInfo.ArgumentList.Add("--rm");
        startInfo.ArgumentList.Add("-v");
        startInfo.ArgumentList.Add($"{cwd}:{cwd}");
        startInfo.ArgumentList.Add("-v");
        startInfo.ArgumentList.Add($"{targetDirectory}:/scan-target:ro");
        startInfo.ArgumentList.Add("-v");
        startInfo.ArgumentList.Add($"{resultsDirectory}:/scan-results");
        startInfo.ArgumentList.Add("-w");
        startInfo.ArgumentList.Add(cwd);
        startInfo.ArgumentList.Add(image);
        startInfo.ArgumentList.Add("--target");
        startInfo.ArgumentList.Add("/scan-target");
        startInfo.ArgumentList.Add("--output");
        startInfo.ArgumentList.Add("/scan-results/scan.json");

        foreach (var argument in args)
        {
            startInfo.ArgumentList.Add(argument);
        }

        startInfo.RedirectStandardError = true;
        startInfo.RedirectStandardOutput = true;
        startInfo.UseShellExecute = false;

        return startInfo;
    }

    private static ProcessStartInfo BuildCustomRunnerStartInfo(string runner, string entry, IReadOnlyList<string> args)
    {
        var startInfo = new ProcessStartInfo
        {
            FileName = runner,
            WorkingDirectory = Directory.GetCurrentDirectory()
        };

        if (!string.IsNullOrWhiteSpace(entry))
        {
            startInfo.ArgumentList.Add(entry);
        }

        foreach (var argument in args)
        {
            startInfo.ArgumentList.Add(argument);
        }

        startInfo.RedirectStandardError = true;
        startInfo.RedirectStandardOutput = true;
        startInfo.UseShellExecute = false;

        return startInfo;
    }

    private static string ResolveResultsPath(string resultsDirectory, DateTimeOffset startTimestamp, HashSet<string> baseline)
    {
        var candidates = Directory.GetFiles(resultsDirectory, "*", SearchOption.AllDirectories);
        string? newest = null;
        DateTimeOffset newestTimestamp = startTimestamp;

        foreach (var candidate in candidates)
        {
            if (baseline.Contains(candidate))
            {
                continue;
            }

            var info = new FileInfo(candidate);
            if (info.LastWriteTimeUtc >= newestTimestamp)
            {
                newestTimestamp = info.LastWriteTimeUtc;
                newest = candidate;
            }
        }

        return newest ?? string.Empty;
    }

    private static string CreatePlaceholderResult(string resultsDirectory)
    {
        var fileName = $"scan-{DateTimeOffset.UtcNow:yyyyMMddHHmmss}.json";
        var path = Path.Combine(resultsDirectory, fileName);
        File.WriteAllText(path, "{\"status\":\"placeholder\"}");
        return path;
    }

    private static string WriteRunMetadata(
        string resultsDirectory,
        DateTimeOffset startedAt,
        DateTimeOffset completedAt,
        string runner,
        string entry,
        string targetDirectory,
        string resultsPath,
        IReadOnlyList<string> arguments,
        int exitCode,
        IReadOnlyList<string> stdout,
        IReadOnlyList<string> stderr)
    {
        var duration = completedAt - startedAt;
        var payload = new
        {
            runner,
            entry,
            targetDirectory,
            resultsPath,
            arguments,
            exitCode,
            startedAt = startedAt,
            completedAt = completedAt,
            durationSeconds = Math.Round(duration.TotalSeconds, 3, MidpointRounding.AwayFromZero),
            stdout,
            stderr
        };

        var fileName = $"scan-run-{startedAt:yyyyMMddHHmmssfff}.json";
        var path = Path.Combine(resultsDirectory, fileName);
        var options = new JsonSerializerOptions
        {
            WriteIndented = true
        };
        var json = JsonSerializer.Serialize(payload, options);
        File.WriteAllText(path, json);
        return path;
    }
}
