using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Spectre.Console;
using StellaOps.Auth.Client;
using StellaOps.Cli.Configuration;
using StellaOps.Cli.Prompts;
using StellaOps.Cli.Services;
using StellaOps.Cli.Services.Models;
using StellaOps.Cli.Telemetry;

namespace StellaOps.Cli.Commands;

internal static class CommandHandlers
{
    public static async Task HandleScannerDownloadAsync(
        IServiceProvider services,
        string channel,
        string? output,
        bool overwrite,
        bool install,
        bool verbose,
        CancellationToken cancellationToken)
    {
        await using var scope = services.CreateAsyncScope();
        var client = scope.ServiceProvider.GetRequiredService<IBackendOperationsClient>();
        var logger = scope.ServiceProvider.GetRequiredService<ILoggerFactory>().CreateLogger("scanner-download");
        var verbosity = scope.ServiceProvider.GetRequiredService<VerbosityState>();
        var previousLevel = verbosity.MinimumLevel;
        verbosity.MinimumLevel = verbose ? LogLevel.Debug : LogLevel.Information;
        using var activity = CliActivitySource.Instance.StartActivity("cli.scanner.download", ActivityKind.Client);
        activity?.SetTag("stellaops.cli.command", "scanner download");
        activity?.SetTag("stellaops.cli.channel", channel);
        using var duration = CliMetrics.MeasureCommandDuration("scanner download");

        try
        {
            var result = await client.DownloadScannerAsync(channel, output ?? string.Empty, overwrite, verbose, cancellationToken).ConfigureAwait(false);

            if (result.FromCache)
            {
                logger.LogInformation("Using cached scanner at {Path}.", result.Path);
            }
            else
            {
                logger.LogInformation("Scanner downloaded to {Path} ({Size} bytes).", result.Path, result.SizeBytes);
            }

            CliMetrics.RecordScannerDownload(channel, result.FromCache);

            if (install)
            {
                var installer = scope.ServiceProvider.GetRequiredService<IScannerInstaller>();
                await installer.InstallAsync(result.Path, verbose, cancellationToken).ConfigureAwait(false);
                CliMetrics.RecordScannerInstall(channel);
            }

            Environment.ExitCode = 0;
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Failed to download scanner bundle.");
            Environment.ExitCode = 1;
        }
        finally
        {
            verbosity.MinimumLevel = previousLevel;
        }
    }

    public static async Task HandleScannerRunAsync(
        IServiceProvider services,
        string runner,
        string entry,
        string targetDirectory,
        IReadOnlyList<string> arguments,
        bool verbose,
        CancellationToken cancellationToken)
    {
        await using var scope = services.CreateAsyncScope();
        var executor = scope.ServiceProvider.GetRequiredService<IScannerExecutor>();
        var logger = scope.ServiceProvider.GetRequiredService<ILoggerFactory>().CreateLogger("scanner-run");
        var verbosity = scope.ServiceProvider.GetRequiredService<VerbosityState>();
        var previousLevel = verbosity.MinimumLevel;
        verbosity.MinimumLevel = verbose ? LogLevel.Debug : LogLevel.Information;
        using var activity = CliActivitySource.Instance.StartActivity("cli.scan.run", ActivityKind.Internal);
        activity?.SetTag("stellaops.cli.command", "scan run");
        activity?.SetTag("stellaops.cli.runner", runner);
        activity?.SetTag("stellaops.cli.entry", entry);
        activity?.SetTag("stellaops.cli.target", targetDirectory);
        using var duration = CliMetrics.MeasureCommandDuration("scan run");

        try
        {
            var options = scope.ServiceProvider.GetRequiredService<StellaOpsCliOptions>();
            var resultsDirectory = options.ResultsDirectory;

            var executionResult = await executor.RunAsync(
                runner,
                entry,
                targetDirectory,
                resultsDirectory,
                arguments,
                verbose,
                cancellationToken).ConfigureAwait(false);

            Environment.ExitCode = executionResult.ExitCode;
            CliMetrics.RecordScanRun(runner, executionResult.ExitCode);

            if (executionResult.ExitCode == 0)
            {
                var backend = scope.ServiceProvider.GetRequiredService<IBackendOperationsClient>();
                logger.LogInformation("Uploading scan artefact {Path}...", executionResult.ResultsPath);
                await backend.UploadScanResultsAsync(executionResult.ResultsPath, cancellationToken).ConfigureAwait(false);
                logger.LogInformation("Scan artefact uploaded.");
                activity?.SetTag("stellaops.cli.results", executionResult.ResultsPath);
            }
            else
            {
                logger.LogWarning("Skipping automatic upload because scan exited with code {Code}.", executionResult.ExitCode);
            }

            logger.LogInformation("Run metadata written to {Path}.", executionResult.RunMetadataPath);
            activity?.SetTag("stellaops.cli.run_metadata", executionResult.RunMetadataPath);
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Scanner execution failed.");
            Environment.ExitCode = 1;
        }
        finally
        {
            verbosity.MinimumLevel = previousLevel;
        }
    }

    public static async Task HandleScanUploadAsync(
        IServiceProvider services,
        string file,
        bool verbose,
        CancellationToken cancellationToken)
    {
        await using var scope = services.CreateAsyncScope();
        var client = scope.ServiceProvider.GetRequiredService<IBackendOperationsClient>();
        var logger = scope.ServiceProvider.GetRequiredService<ILoggerFactory>().CreateLogger("scanner-upload");
        var verbosity = scope.ServiceProvider.GetRequiredService<VerbosityState>();
        var previousLevel = verbosity.MinimumLevel;
        verbosity.MinimumLevel = verbose ? LogLevel.Debug : LogLevel.Information;
        using var activity = CliActivitySource.Instance.StartActivity("cli.scan.upload", ActivityKind.Client);
        activity?.SetTag("stellaops.cli.command", "scan upload");
        activity?.SetTag("stellaops.cli.file", file);
        using var duration = CliMetrics.MeasureCommandDuration("scan upload");

        try
        {
            var path = Path.GetFullPath(file);
            await client.UploadScanResultsAsync(path, cancellationToken).ConfigureAwait(false);
            logger.LogInformation("Scan results uploaded successfully.");
            Environment.ExitCode = 0;
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Failed to upload scan results.");
            Environment.ExitCode = 1;
        }
        finally
        {
            verbosity.MinimumLevel = previousLevel;
        }
    }

    public static async Task HandleConnectorJobAsync(
        IServiceProvider services,
        string source,
        string stage,
        string? mode,
        bool verbose,
        CancellationToken cancellationToken)
    {
        await using var scope = services.CreateAsyncScope();
        var client = scope.ServiceProvider.GetRequiredService<IBackendOperationsClient>();
        var logger = scope.ServiceProvider.GetRequiredService<ILoggerFactory>().CreateLogger("db-connector");
        var verbosity = scope.ServiceProvider.GetRequiredService<VerbosityState>();
        var previousLevel = verbosity.MinimumLevel;
        verbosity.MinimumLevel = verbose ? LogLevel.Debug : LogLevel.Information;
        using var activity = CliActivitySource.Instance.StartActivity("cli.db.fetch", ActivityKind.Client);
        activity?.SetTag("stellaops.cli.command", "db fetch");
        activity?.SetTag("stellaops.cli.source", source);
        activity?.SetTag("stellaops.cli.stage", stage);
        if (!string.IsNullOrWhiteSpace(mode))
        {
            activity?.SetTag("stellaops.cli.mode", mode);
        }
        using var duration = CliMetrics.MeasureCommandDuration("db fetch");

        try
        {
            var jobKind = $"source:{source}:{stage}";
            var parameters = new Dictionary<string, object?>(StringComparer.Ordinal);
            if (!string.IsNullOrWhiteSpace(mode))
            {
                parameters["mode"] = mode;
            }

            await TriggerJobAsync(client, logger, jobKind, parameters, cancellationToken).ConfigureAwait(false);
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Connector job failed.");
            Environment.ExitCode = 1;
        }
        finally
        {
            verbosity.MinimumLevel = previousLevel;
        }
    }

    public static async Task HandleMergeJobAsync(
        IServiceProvider services,
        bool verbose,
        CancellationToken cancellationToken)
    {
        await using var scope = services.CreateAsyncScope();
        var client = scope.ServiceProvider.GetRequiredService<IBackendOperationsClient>();
        var logger = scope.ServiceProvider.GetRequiredService<ILoggerFactory>().CreateLogger("db-merge");
        var verbosity = scope.ServiceProvider.GetRequiredService<VerbosityState>();
        var previousLevel = verbosity.MinimumLevel;
        verbosity.MinimumLevel = verbose ? LogLevel.Debug : LogLevel.Information;
        using var activity = CliActivitySource.Instance.StartActivity("cli.db.merge", ActivityKind.Client);
        activity?.SetTag("stellaops.cli.command", "db merge");
        using var duration = CliMetrics.MeasureCommandDuration("db merge");

        try
        {
            await TriggerJobAsync(client, logger, "merge:reconcile", new Dictionary<string, object?>(StringComparer.Ordinal), cancellationToken).ConfigureAwait(false);
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Merge job failed.");
            Environment.ExitCode = 1;
        }
        finally
        {
            verbosity.MinimumLevel = previousLevel;
        }
    }

    public static async Task HandleExportJobAsync(
        IServiceProvider services,
        string format,
        bool delta,
        bool? publishFull,
        bool? publishDelta,
        bool? includeFull,
        bool? includeDelta,
        bool verbose,
        CancellationToken cancellationToken)
    {
        await using var scope = services.CreateAsyncScope();
        var client = scope.ServiceProvider.GetRequiredService<IBackendOperationsClient>();
        var logger = scope.ServiceProvider.GetRequiredService<ILoggerFactory>().CreateLogger("db-export");
        var verbosity = scope.ServiceProvider.GetRequiredService<VerbosityState>();
        var previousLevel = verbosity.MinimumLevel;
        verbosity.MinimumLevel = verbose ? LogLevel.Debug : LogLevel.Information;
        using var activity = CliActivitySource.Instance.StartActivity("cli.db.export", ActivityKind.Client);
        activity?.SetTag("stellaops.cli.command", "db export");
        activity?.SetTag("stellaops.cli.format", format);
        activity?.SetTag("stellaops.cli.delta", delta);
        using var duration = CliMetrics.MeasureCommandDuration("db export");
        activity?.SetTag("stellaops.cli.publish_full", publishFull);
        activity?.SetTag("stellaops.cli.publish_delta", publishDelta);
        activity?.SetTag("stellaops.cli.include_full", includeFull);
        activity?.SetTag("stellaops.cli.include_delta", includeDelta);

        try
        {
            var jobKind = format switch
            {
                "trivy-db" or "trivy" => "export:trivy-db",
                _ => "export:json"
            };

            var isTrivy = jobKind == "export:trivy-db";
            if (isTrivy
                && !publishFull.HasValue
                && !publishDelta.HasValue
                && !includeFull.HasValue
                && !includeDelta.HasValue
                && AnsiConsole.Profile.Capabilities.Interactive)
            {
                var overrides = TrivyDbExportPrompt.PromptOverrides();
                publishFull = overrides.publishFull;
                publishDelta = overrides.publishDelta;
                includeFull = overrides.includeFull;
                includeDelta = overrides.includeDelta;
            }

            var parameters = new Dictionary<string, object?>(StringComparer.Ordinal)
            {
                ["delta"] = delta
            };
            if (publishFull.HasValue)
            {
                parameters["publishFull"] = publishFull.Value;
            }
            if (publishDelta.HasValue)
            {
                parameters["publishDelta"] = publishDelta.Value;
            }
            if (includeFull.HasValue)
            {
                parameters["includeFull"] = includeFull.Value;
            }
            if (includeDelta.HasValue)
            {
                parameters["includeDelta"] = includeDelta.Value;
            }

            await TriggerJobAsync(client, logger, jobKind, parameters, cancellationToken).ConfigureAwait(false);
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Export job failed.");
            Environment.ExitCode = 1;
        }
        finally
        {
            verbosity.MinimumLevel = previousLevel;
        }
    }

    public static async Task HandleAuthLoginAsync(
        IServiceProvider services,
        StellaOpsCliOptions options,
        bool verbose,
        bool force,
        CancellationToken cancellationToken)
    {
        await using var scope = services.CreateAsyncScope();
        var logger = scope.ServiceProvider.GetRequiredService<ILoggerFactory>().CreateLogger("auth-login");
        Environment.ExitCode = 0;

        if (string.IsNullOrWhiteSpace(options.Authority?.Url))
        {
            logger.LogError("Authority URL is not configured. Set STELLAOPS_AUTHORITY_URL or update your configuration.");
            Environment.ExitCode = 1;
            return;
        }

        var tokenClient = scope.ServiceProvider.GetService<IStellaOpsTokenClient>();
        if (tokenClient is null)
        {
            logger.LogError("Authority client is not available. Ensure AddStellaOpsAuthClient is registered in Program.cs.");
            Environment.ExitCode = 1;
            return;
        }

        var cacheKey = AuthorityTokenUtilities.BuildCacheKey(options);
        if (string.IsNullOrWhiteSpace(cacheKey))
        {
            logger.LogError("Authority configuration is incomplete; unable to determine cache key.");
            Environment.ExitCode = 1;
            return;
        }

        try
        {
            if (force)
            {
                await tokenClient.ClearCachedTokenAsync(cacheKey, cancellationToken).ConfigureAwait(false);
            }

            var scopeName = AuthorityTokenUtilities.ResolveScope(options);
            StellaOpsTokenResult token;

            if (!string.IsNullOrWhiteSpace(options.Authority.Username))
            {
                if (string.IsNullOrWhiteSpace(options.Authority.Password))
                {
                    logger.LogError("Authority password must be provided when username is configured.");
                    Environment.ExitCode = 1;
                    return;
                }

                token = await tokenClient.RequestPasswordTokenAsync(
                    options.Authority.Username,
                    options.Authority.Password!,
                    scopeName,
                    cancellationToken).ConfigureAwait(false);
            }
            else
            {
                token = await tokenClient.RequestClientCredentialsTokenAsync(scopeName, cancellationToken).ConfigureAwait(false);
            }

            await tokenClient.CacheTokenAsync(cacheKey, token.ToCacheEntry(), cancellationToken).ConfigureAwait(false);

            if (verbose)
            {
                logger.LogInformation("Authenticated with {Authority} (scopes: {Scopes}).", options.Authority.Url, string.Join(", ", token.Scopes));
            }

            logger.LogInformation("Login successful. Access token expires at {Expires}.", token.ExpiresAtUtc.ToString("u"));
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Authentication failed: {Message}", ex.Message);
            Environment.ExitCode = 1;
        }
    }

    public static async Task HandleAuthLogoutAsync(
        IServiceProvider services,
        StellaOpsCliOptions options,
        bool verbose,
        CancellationToken cancellationToken)
    {
        await using var scope = services.CreateAsyncScope();
        var logger = scope.ServiceProvider.GetRequiredService<ILoggerFactory>().CreateLogger("auth-logout");
        Environment.ExitCode = 0;

        var tokenClient = scope.ServiceProvider.GetService<IStellaOpsTokenClient>();
        if (tokenClient is null)
        {
            logger.LogInformation("No authority client registered; nothing to remove.");
            return;
        }

        var cacheKey = AuthorityTokenUtilities.BuildCacheKey(options);
        if (string.IsNullOrWhiteSpace(cacheKey))
        {
            logger.LogInformation("Authority configuration missing; no cached tokens to remove.");
            return;
        }

        await tokenClient.ClearCachedTokenAsync(cacheKey, cancellationToken).ConfigureAwait(false);
        if (verbose)
        {
            logger.LogInformation("Cleared cached token for {Authority}.", options.Authority?.Url ?? "authority");
        }
    }

    public static async Task HandleAuthStatusAsync(
        IServiceProvider services,
        StellaOpsCliOptions options,
        bool verbose,
        CancellationToken cancellationToken)
    {
        await using var scope = services.CreateAsyncScope();
        var logger = scope.ServiceProvider.GetRequiredService<ILoggerFactory>().CreateLogger("auth-status");
        Environment.ExitCode = 0;

        if (string.IsNullOrWhiteSpace(options.Authority?.Url))
        {
            logger.LogInformation("Authority URL not configured. Set STELLAOPS_AUTHORITY_URL and run 'auth login'.");
            Environment.ExitCode = 1;
            return;
        }

        var tokenClient = scope.ServiceProvider.GetService<IStellaOpsTokenClient>();
        if (tokenClient is null)
        {
            logger.LogInformation("Authority client not registered; no cached tokens available.");
            Environment.ExitCode = 1;
            return;
        }

        var cacheKey = AuthorityTokenUtilities.BuildCacheKey(options);
        if (string.IsNullOrWhiteSpace(cacheKey))
        {
            logger.LogInformation("Authority configuration incomplete; no cached tokens available.");
            Environment.ExitCode = 1;
            return;
        }

        var entry = await tokenClient.GetCachedTokenAsync(cacheKey, cancellationToken).ConfigureAwait(false);
        if (entry is null)
        {
            logger.LogInformation("No cached token for {Authority}. Run 'auth login' to authenticate.", options.Authority.Url);
            Environment.ExitCode = 1;
            return;
        }

        logger.LogInformation("Cached token for {Authority} expires at {Expires}.", options.Authority.Url, entry.ExpiresAtUtc.ToString("u"));
        if (verbose)
        {
            logger.LogInformation("Scopes: {Scopes}", string.Join(", ", entry.Scopes));
        }
    }

    private static async Task TriggerJobAsync(
        IBackendOperationsClient client,
        ILogger logger,
        string jobKind,
        IDictionary<string, object?> parameters,
        CancellationToken cancellationToken)
    {
        JobTriggerResult result = await client.TriggerJobAsync(jobKind, parameters, cancellationToken).ConfigureAwait(false);
        if (result.Success)
        {
            if (!string.IsNullOrWhiteSpace(result.Location))
            {
                logger.LogInformation("Job accepted. Track status at {Location}.", result.Location);
            }
            else if (result.Run is not null)
            {
                logger.LogInformation("Job accepted. RunId: {RunId} Status: {Status}", result.Run.RunId, result.Run.Status);
            }
            else
            {
                logger.LogInformation("Job accepted.");
            }

            Environment.ExitCode = 0;
        }
        else
        {
            logger.LogError("Job '{JobKind}' failed: {Message}", jobKind, result.Message);
            Environment.ExitCode = 1;
        }
    }
}
