using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Linq;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Globalization;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using StellaOps.Auth.Abstractions;
using StellaOps.Auth.Client;
using StellaOps.Cli.Configuration;
using StellaOps.Cli.Services.Models;
using StellaOps.Cli.Services.Models.Transport;

namespace StellaOps.Cli.Services;

internal sealed class BackendOperationsClient : IBackendOperationsClient
{
    private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web);
    private static readonly TimeSpan TokenRefreshSkew = TimeSpan.FromSeconds(30);

    private readonly HttpClient _httpClient;
    private readonly StellaOpsCliOptions _options;
    private readonly ILogger<BackendOperationsClient> _logger;
    private readonly IStellaOpsTokenClient? _tokenClient;
    private readonly object _tokenSync = new();
    private string? _cachedAccessToken;
    private DateTimeOffset _cachedAccessTokenExpiresAt = DateTimeOffset.MinValue;

    public BackendOperationsClient(HttpClient httpClient, StellaOpsCliOptions options, ILogger<BackendOperationsClient> logger, IStellaOpsTokenClient? tokenClient = null)
    {
        _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
        _options = options ?? throw new ArgumentNullException(nameof(options));
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
        _tokenClient = tokenClient;

        if (!string.IsNullOrWhiteSpace(_options.BackendUrl) && httpClient.BaseAddress is null)
        {
            if (Uri.TryCreate(_options.BackendUrl, UriKind.Absolute, out var baseUri))
            {
                httpClient.BaseAddress = baseUri;
            }
        }
    }

    public async Task<ScannerArtifactResult> DownloadScannerAsync(string channel, string outputPath, bool overwrite, bool verbose, CancellationToken cancellationToken)
    {
        EnsureBackendConfigured();

        channel = string.IsNullOrWhiteSpace(channel) ? "stable" : channel.Trim();
        outputPath = ResolveArtifactPath(outputPath, channel);
        Directory.CreateDirectory(Path.GetDirectoryName(outputPath)!);

        if (!overwrite && File.Exists(outputPath))
        {
            var existing = new FileInfo(outputPath);
            _logger.LogInformation("Scanner artifact already cached at {Path} ({Size} bytes).", outputPath, existing.Length);
            return new ScannerArtifactResult(outputPath, existing.Length, true);
        }

        var attempt = 0;
        var maxAttempts = Math.Max(1, _options.ScannerDownloadAttempts);

        while (true)
        {
            attempt++;
            try
            {
                using var request = CreateRequest(HttpMethod.Get, $"api/scanner/artifacts/{channel}");
                await AuthorizeRequestAsync(request, cancellationToken).ConfigureAwait(false);
                using var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
                if (!response.IsSuccessStatusCode)
                {
                    var failure = await CreateFailureMessageAsync(response, cancellationToken).ConfigureAwait(false);
                    throw new InvalidOperationException(failure);
                }

                return await ProcessScannerResponseAsync(response, outputPath, channel, verbose, cancellationToken).ConfigureAwait(false);
            }
            catch (Exception ex) when (attempt < maxAttempts)
            {
                var backoffSeconds = Math.Pow(2, attempt);
                _logger.LogWarning(ex, "Scanner download attempt {Attempt}/{MaxAttempts} failed. Retrying in {Delay:F0}s...", attempt, maxAttempts, backoffSeconds);
                await Task.Delay(TimeSpan.FromSeconds(backoffSeconds), cancellationToken).ConfigureAwait(false);
            }
        }
    }

    private async Task<ScannerArtifactResult> ProcessScannerResponseAsync(HttpResponseMessage response, string outputPath, string channel, bool verbose, CancellationToken cancellationToken)
    {
        var tempFile = outputPath + ".tmp";
        await using (var payloadStream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false))
        await using (var fileStream = File.Create(tempFile))
        {
            await payloadStream.CopyToAsync(fileStream, cancellationToken).ConfigureAwait(false);
        }

        var expectedDigest = ExtractHeaderValue(response.Headers, "X-StellaOps-Digest");
        var signatureHeader = ExtractHeaderValue(response.Headers, "X-StellaOps-Signature");

        var digestHex = await ValidateDigestAsync(tempFile, expectedDigest, cancellationToken).ConfigureAwait(false);
        await ValidateSignatureAsync(signatureHeader, digestHex, verbose, cancellationToken).ConfigureAwait(false);

        if (verbose)
        {
            var signatureNote = string.IsNullOrWhiteSpace(signatureHeader) ? "no signature" : "signature validated";
            _logger.LogDebug("Scanner digest sha256:{Digest} ({SignatureNote}).", digestHex, signatureNote);
        }

        if (File.Exists(outputPath))
        {
            File.Delete(outputPath);
        }

        File.Move(tempFile, outputPath);

        PersistMetadata(outputPath, channel, digestHex, signatureHeader, response);

        var downloaded = new FileInfo(outputPath);
        _logger.LogInformation("Scanner downloaded to {Path} ({Size} bytes).", outputPath, downloaded.Length);

        return new ScannerArtifactResult(outputPath, downloaded.Length, false);
    }

    public async Task UploadScanResultsAsync(string filePath, CancellationToken cancellationToken)
    {
        EnsureBackendConfigured();

        if (!File.Exists(filePath))
        {
            throw new FileNotFoundException("Scan result file not found.", filePath);
        }

        var maxAttempts = Math.Max(1, _options.ScanUploadAttempts);
        var attempt = 0;

        while (true)
        {
            attempt++;
            try
            {
                using var content = new MultipartFormDataContent();
                await using var fileStream = File.OpenRead(filePath);
                var streamContent = new StreamContent(fileStream);
                streamContent.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
                content.Add(streamContent, "file", Path.GetFileName(filePath));

                using var request = CreateRequest(HttpMethod.Post, "api/scanner/results");
                await AuthorizeRequestAsync(request, cancellationToken).ConfigureAwait(false);
                request.Content = content;

                using var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
                if (response.IsSuccessStatusCode)
                {
                    _logger.LogInformation("Scan results uploaded from {Path}.", filePath);
                    return;
                }

                var failure = await CreateFailureMessageAsync(response, cancellationToken).ConfigureAwait(false);
                if (attempt >= maxAttempts)
                {
                    throw new InvalidOperationException(failure);
                }

                var delay = GetRetryDelay(response, attempt);
                _logger.LogWarning(
                    "Scan upload attempt {Attempt}/{MaxAttempts} failed ({Reason}). Retrying in {Delay:F1}s...",
                    attempt,
                    maxAttempts,
                    failure,
                    delay.TotalSeconds);
                await Task.Delay(delay, cancellationToken).ConfigureAwait(false);
            }
            catch (Exception ex) when (attempt < maxAttempts)
            {
                var delay = TimeSpan.FromSeconds(Math.Pow(2, attempt));
                _logger.LogWarning(
                    ex,
                    "Scan upload attempt {Attempt}/{MaxAttempts} threw an exception. Retrying in {Delay:F1}s...",
                    attempt,
                    maxAttempts,
                    delay.TotalSeconds);
                await Task.Delay(delay, cancellationToken).ConfigureAwait(false);
            }
        }
    }

    public async Task<JobTriggerResult> TriggerJobAsync(string jobKind, IDictionary<string, object?> parameters, CancellationToken cancellationToken)
    {
        EnsureBackendConfigured();

        if (string.IsNullOrWhiteSpace(jobKind))
        {
            throw new ArgumentException("Job kind must be provided.", nameof(jobKind));
        }

        var requestBody = new JobTriggerRequest
        {
            Trigger = "cli",
            Parameters = parameters is null ? new Dictionary<string, object?>(StringComparer.Ordinal) : new Dictionary<string, object?>(parameters, StringComparer.Ordinal)
        };

        var request = CreateRequest(HttpMethod.Post, $"jobs/{jobKind}");
        await AuthorizeRequestAsync(request, cancellationToken).ConfigureAwait(false);
        request.Content = JsonContent.Create(requestBody, options: SerializerOptions);

        using var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
        if (response.StatusCode == HttpStatusCode.Accepted)
        {
            JobRunResponse? run = null;
            if (response.Content.Headers.ContentLength is > 0)
            {
                try
                {
                    run = await response.Content.ReadFromJsonAsync<JobRunResponse>(SerializerOptions, cancellationToken).ConfigureAwait(false);
                }
                catch (JsonException ex)
                {
                    _logger.LogWarning(ex, "Failed to deserialize job run response for job kind {Kind}.", jobKind);
                }
            }

            var location = response.Headers.Location?.ToString();
            return new JobTriggerResult(true, "Accepted", location, run);
        }

        var failureMessage = await CreateFailureMessageAsync(response, cancellationToken).ConfigureAwait(false);
        return new JobTriggerResult(false, failureMessage, null, null);
    }

    private HttpRequestMessage CreateRequest(HttpMethod method, string relativeUri)
    {
        if (!Uri.TryCreate(relativeUri, UriKind.RelativeOrAbsolute, out var requestUri))
        {
            throw new InvalidOperationException($"Invalid request URI '{relativeUri}'.");
        }

        if (requestUri.IsAbsoluteUri)
        {
            // Nothing to normalize.
        }
        else
        {
            requestUri = new Uri(relativeUri.TrimStart('/'), UriKind.Relative);
        }

        return new HttpRequestMessage(method, requestUri);
    }

    private async Task AuthorizeRequestAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        var token = await ResolveAccessTokenAsync(cancellationToken).ConfigureAwait(false);
        if (!string.IsNullOrWhiteSpace(token))
        {
            request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
        }
    }

    private async Task<string?> ResolveAccessTokenAsync(CancellationToken cancellationToken)
    {
        if (!string.IsNullOrWhiteSpace(_options.ApiKey))
        {
            return _options.ApiKey;
        }

        if (_tokenClient is null || string.IsNullOrWhiteSpace(_options.Authority.Url))
        {
            return null;
        }

        var now = DateTimeOffset.UtcNow;

        lock (_tokenSync)
        {
            if (!string.IsNullOrEmpty(_cachedAccessToken) && now < _cachedAccessTokenExpiresAt - TokenRefreshSkew)
            {
                return _cachedAccessToken;
            }
        }

        var cacheKey = AuthorityTokenUtilities.BuildCacheKey(_options);
        var cachedEntry = await _tokenClient.GetCachedTokenAsync(cacheKey, cancellationToken).ConfigureAwait(false);
        if (cachedEntry is not null && now < cachedEntry.ExpiresAtUtc - TokenRefreshSkew)
        {
            lock (_tokenSync)
            {
                _cachedAccessToken = cachedEntry.AccessToken;
                _cachedAccessTokenExpiresAt = cachedEntry.ExpiresAtUtc;
                return _cachedAccessToken;
            }
        }

        var scope = AuthorityTokenUtilities.ResolveScope(_options);

        StellaOpsTokenResult token;
        if (!string.IsNullOrWhiteSpace(_options.Authority.Username))
        {
            if (string.IsNullOrWhiteSpace(_options.Authority.Password))
            {
                throw new InvalidOperationException("Authority password must be configured when username is provided.");
            }

            token = await _tokenClient.RequestPasswordTokenAsync(
                _options.Authority.Username,
                _options.Authority.Password!,
                scope,
                cancellationToken).ConfigureAwait(false);
        }
        else
        {
            token = await _tokenClient.RequestClientCredentialsTokenAsync(scope, cancellationToken).ConfigureAwait(false);
        }

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

        lock (_tokenSync)
        {
            _cachedAccessToken = token.AccessToken;
            _cachedAccessTokenExpiresAt = token.ExpiresAtUtc;
            return _cachedAccessToken;
        }
    }

    private void EnsureBackendConfigured()
    {
        if (_httpClient.BaseAddress is null)
        {
            throw new InvalidOperationException("Backend URL is not configured. Provide STELLAOPS_BACKEND_URL or configure appsettings.");
        }
    }

    private string ResolveArtifactPath(string outputPath, string channel)
    {
        if (!string.IsNullOrWhiteSpace(outputPath))
        {
            return Path.GetFullPath(outputPath);
        }

        var directory = string.IsNullOrWhiteSpace(_options.ScannerCacheDirectory)
            ? Directory.GetCurrentDirectory()
            : Path.GetFullPath(_options.ScannerCacheDirectory);

        Directory.CreateDirectory(directory);
        var fileName = $"stellaops-scanner-{channel}.tar.gz";
        return Path.Combine(directory, fileName);
    }

    private async Task<string> CreateFailureMessageAsync(HttpResponseMessage response, CancellationToken cancellationToken)
    {
        var statusCode = (int)response.StatusCode;
        var builder = new StringBuilder();
        builder.Append("Backend request failed with status ");
        builder.Append(statusCode);
        builder.Append(' ');
        builder.Append(response.ReasonPhrase ?? "Unknown");

        if (response.Content.Headers.ContentLength is > 0)
        {
            try
            {
                var problem = await response.Content.ReadFromJsonAsync<ProblemDocument>(SerializerOptions, cancellationToken).ConfigureAwait(false);
                if (problem is not null)
                {
                    if (!string.IsNullOrWhiteSpace(problem.Title))
                    {
                        builder.AppendLine().Append(problem.Title);
                    }

                    if (!string.IsNullOrWhiteSpace(problem.Detail))
                    {
                        builder.AppendLine().Append(problem.Detail);
                    }
                }
            }
            catch (JsonException)
            {
                var raw = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
                if (!string.IsNullOrWhiteSpace(raw))
                {
                    builder.AppendLine().Append(raw);
                }
            }
        }

        return builder.ToString();
    }

    private static string? ExtractHeaderValue(HttpResponseHeaders headers, string name)
    {
        if (headers.TryGetValues(name, out var values))
        {
            return values.FirstOrDefault();
        }

        return null;
    }

    private async Task<string> ValidateDigestAsync(string filePath, string? expectedDigest, CancellationToken cancellationToken)
    {
        string digestHex;
        await using (var stream = File.OpenRead(filePath))
        {
            var hash = await SHA256.HashDataAsync(stream, cancellationToken).ConfigureAwait(false);
            digestHex = Convert.ToHexString(hash).ToLowerInvariant();
        }

        if (!string.IsNullOrWhiteSpace(expectedDigest))
        {
            var normalized = NormalizeDigest(expectedDigest);
            if (!normalized.Equals(digestHex, StringComparison.OrdinalIgnoreCase))
            {
                File.Delete(filePath);
                throw new InvalidOperationException($"Scanner digest mismatch. Expected sha256:{normalized}, calculated sha256:{digestHex}.");
            }
        }
        else
        {
            _logger.LogWarning("Scanner download missing X-StellaOps-Digest header; relying on computed digest only.");
        }

        return digestHex;
    }

    private static string NormalizeDigest(string digest)
    {
        if (digest.StartsWith("sha256:", StringComparison.OrdinalIgnoreCase))
        {
            return digest[7..];
        }

        return digest;
    }

    private async Task ValidateSignatureAsync(string? signatureHeader, string digestHex, bool verbose, CancellationToken cancellationToken)
    {
        if (string.IsNullOrWhiteSpace(_options.ScannerSignaturePublicKeyPath))
        {
            if (!string.IsNullOrWhiteSpace(signatureHeader))
            {
                _logger.LogDebug("Signature header present but no public key configured; skipping validation.");
            }
            return;
        }

        if (string.IsNullOrWhiteSpace(signatureHeader))
        {
            throw new InvalidOperationException("Scanner signature missing while a public key is configured.");
        }

        var publicKeyPath = Path.GetFullPath(_options.ScannerSignaturePublicKeyPath);
        if (!File.Exists(publicKeyPath))
        {
            throw new FileNotFoundException("Scanner signature public key not found.", publicKeyPath);
        }

        var signatureBytes = Convert.FromBase64String(signatureHeader);
        var digestBytes = Convert.FromHexString(digestHex);

        var pem = await File.ReadAllTextAsync(publicKeyPath, cancellationToken).ConfigureAwait(false);
        using var rsa = RSA.Create();
        rsa.ImportFromPem(pem);

        var valid = rsa.VerifyHash(digestBytes, signatureBytes, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
        if (!valid)
        {
            throw new InvalidOperationException("Scanner signature validation failed.");
        }

        if (verbose)
        {
            _logger.LogDebug("Scanner signature validated using key {KeyPath}.", publicKeyPath);
        }
    }

    private void PersistMetadata(string outputPath, string channel, string digestHex, string? signatureHeader, HttpResponseMessage response)
    {
        var metadata = new
        {
            channel,
            digest = $"sha256:{digestHex}",
            signature = signatureHeader,
            downloadedAt = DateTimeOffset.UtcNow,
            source = response.RequestMessage?.RequestUri?.ToString(),
            sizeBytes = new FileInfo(outputPath).Length,
            headers = new
            {
                etag = response.Headers.ETag?.Tag,
                lastModified = response.Content.Headers.LastModified,
                contentType = response.Content.Headers.ContentType?.ToString()
            }
        };

        var metadataPath = outputPath + ".metadata.json";
        var json = JsonSerializer.Serialize(metadata, new JsonSerializerOptions
        {
            WriteIndented = true
        });

        File.WriteAllText(metadataPath, json);
    }

    private static TimeSpan GetRetryDelay(HttpResponseMessage response, int attempt)
    {
        if (response.Headers.TryGetValues("Retry-After", out var retryValues))
        {
            var value = retryValues.FirstOrDefault();
            if (!string.IsNullOrWhiteSpace(value))
            {
                if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var seconds) && seconds >= 0)
                {
                    return TimeSpan.FromSeconds(Math.Min(seconds, 300));
                }

                if (DateTimeOffset.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var when))
                {
                    var delta = when - DateTimeOffset.UtcNow;
                    if (delta > TimeSpan.Zero)
                    {
                        return delta < TimeSpan.FromMinutes(5) ? delta : TimeSpan.FromMinutes(5);
                    }
                }
            }
        }

        var fallbackSeconds = Math.Min(60, Math.Pow(2, attempt));
        return TimeSpan.FromSeconds(fallbackSeconds);
    }
}
