using System.Net.Http.Headers;
using Microsoft.Extensions.Logging;
using MaksIT.Results;
using MaksIT.Core.Extensions;
using MaksIT.Core.Security.JWS;
using MaksIT.CertsUI.Engine.Domain.LetsEncrypt;
using MaksIT.CertsUI.Engine.Domain.LetsEncrypt.Jws;
using MaksIT.CertsUI.Engine.Dto.LetsEncrypt.Interfaces;
using MaksIT.CertsUI.Engine.Dto.LetsEncrypt.Responses;


namespace MaksIT.CertsUI.Engine.Services;

public partial class LetsEncryptService {

  #region Internal helpers

  private async Task<State> LoadAcmeSessionStateAsync(Guid sessionId, CancellationToken cancellationToken) {
    var result = await acmeSessionPersistence.LoadAsync(sessionId, cancellationToken).ConfigureAwait(false);
    if (!result.IsSuccess) {
      logger.LogWarning(
        "ACME session load failed for {SessionId}: {Messages}",
        sessionId,
        result.Messages != null ? string.Join("; ", result.Messages) : "(no detail)");
      return new State();
    }

    return result.Value ?? new State();
  }

  private async Task PersistAcmeSessionStateAsync(Guid sessionId, State state, CancellationToken cancellationToken) {
    var result = await acmeSessionPersistence.SaveAsync(sessionId, state, cancellationToken).ConfigureAwait(false);
    if (!result.IsSuccess) {
      logger.LogError(
        "ACME session save failed for {SessionId}: {Messages}",
        sessionId,
        result.Messages != null ? string.Join("; ", result.Messages) : "(no detail)");
    }
  }

  private async Task<Result> WithPersistedSessionAsync(
    Guid sessionId,
    CancellationToken cancellationToken,
    Func<State, Task<Result>> body) {
    var state = await LoadAcmeSessionStateAsync(sessionId, cancellationToken).ConfigureAwait(false);
    try {
      return await body(state).ConfigureAwait(false);
    }
    finally {
      await PersistAcmeSessionStateAsync(sessionId, state, cancellationToken).ConfigureAwait(false);
    }
  }

  private async Task<Result<T?>> WithPersistedSessionAsync<T>(
    Guid sessionId,
    CancellationToken cancellationToken,
    Func<State, Task<Result<T?>>> body) {
    var state = await LoadAcmeSessionStateAsync(sessionId, cancellationToken).ConfigureAwait(false);
    try {
      return await body(state).ConfigureAwait(false);
    }
    finally {
      await PersistAcmeSessionStateAsync(sessionId, state, cancellationToken).ConfigureAwait(false);
    }
  }

  private async Task<Result<string?>> GetNonceAsync(State state, Uri uri) {
    if (uri == null)
      return Result<string?>.InternalServerError(null, "URI is null");

    try {

      logger.LogInformation($"Executing {nameof(GetNonceAsync)}...");

      if (state.Directory is not { NewNonce: { } newNonceUri })
        return Result<string?>.InternalServerError(null, "Directory or NewNonce URL is null.");

      var result = await httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Head, newNonceUri));

      var nonce = result.Headers.GetValues(ReplayNonceHeader).FirstOrDefault();

      if (nonce == null)
        return Result<string?>.InternalServerError(null, "Nonce is null");

      return Result<string?>.Ok(nonce);
    }
    catch (Exception ex) {
      return HandleUnhandledException<string?>(ex);
    }
  }

  private async Task<Result<SendResult<T>?>> SendAcmeRequest<T>(HttpRequestMessage request, State state, HttpMethod method) {
    try {
      var response = await httpClient.SendAsync(request);

      var responseText = await response.Content.ReadAsStringAsync();

      HandleProblemResponseAsync(response, responseText);

      var sendResult = ProcessResponseContent<T>(response, responseText);

      return Result<SendResult<T>?>.Ok(sendResult);
    }

    catch (LetsEncrytException ex) {
      return MapLetsEncryptException<SendResult<T>?>(state, ex, null);
    }
    catch (Exception ex) {
      return HandleUnhandledException<SendResult<T>?>(ex);
    }
  }

  private Result<string?> EncodeMessage(State state, bool isPostAsGet, object? requestModel, ACMEJwsHeader protectedHeader) {
    if (!state.TryGetAccountKey(out var rsa, out var jwk))
      return Result<string?>.InternalServerError(AccountKeyMissingMessage);

    if (isPostAsGet) {
      if (!JwsGenerator.TryEncode(rsa, jwk, protectedHeader, out var jwsPostAsGet, out var errPostAsGet))
        return Result<string?>.InternalServerError(errPostAsGet);

      return Result<string?>.Ok(jwsPostAsGet.ToJson());
    }

    if (!JwsGenerator.TryEncode(rsa, jwk, protectedHeader, requestModel, out var jwsWithPayload, out var errWithPayload))
      return Result<string?>.InternalServerError(errWithPayload);

    return Result<string?>.Ok(jwsWithPayload.ToJson());
  }

  private static string GetContentType(ContentType type) => type.GetDisplayName();

  private void PrepareRequestContent(HttpRequestMessage request, string json, HttpMethod method) {
    request.Content = new StringContent(json ?? string.Empty);
    var contentType = method == HttpMethod.Post
      ? GetContentType(ContentType.JoseJson)
      : GetContentType(ContentType.Json);
    request.Content.Headers.ContentType = new MediaTypeHeaderValue(contentType);
  }

  private async Task<Result> PollChallengeStatus(State state, AuthorizationChallengeChallenge challenge) {
    if (challenge?.Url == null)
      return Result.InternalServerError("Challenge URL is null");

    var start = DateTime.UtcNow;

    while (true) {
      var pollRequest = new HttpRequestMessage(HttpMethod.Post, challenge.Url);

      var nonceResult = await GetNonceAsync(state, challenge.Url);
      if (!nonceResult.IsSuccess || nonceResult.Value == null)
        return nonceResult;

      var nonce = nonceResult.Value;

      var pollJsonResult = EncodeMessage(state, true, null, new ACMEJwsHeader {
        Url = challenge.Url.ToString(),
        Nonce = nonce
      });

      if (!pollJsonResult.IsSuccess || pollJsonResult.Value == null)
        return pollJsonResult;

      var pollJson = pollJsonResult.Value;

      PrepareRequestContent(pollRequest, pollJson, HttpMethod.Post);

      var pollResponse = await httpClient.SendAsync(pollRequest);

      var pollResponseText = await pollResponse.Content.ReadAsStringAsync();

      HandleProblemResponseAsync(pollResponse, pollResponseText);

      var authChallenge = ProcessResponseContent<AuthorizationChallengeResponse>(pollResponse, pollResponseText);

      if (authChallenge.Result?.Status != "pending")
        return authChallenge.Result?.Status == "valid" ? Result.Ok() : Result.InternalServerError();

      if ((DateTime.UtcNow - start).Seconds > 120)
        return Result.InternalServerError("Timeout");

      await Task.Delay(1000);
    }
  }

  private void HandleProblemResponseAsync(HttpResponseMessage response, string responseText) {
    if (response.Content.Headers.ContentType?.MediaType == GetContentType(ContentType.ProblemJson)) {
      var problem = responseText.ToObject<Problem>();

      throw new LetsEncrytException(problem, response);
    }

    if (response.Content.Headers.ContentType?.MediaType == GetContentType(ContentType.Json)) {
      var authorizationChallengeChallenge = responseText.ToObject<AuthorizationChallengeChallenge>();

      if (authorizationChallengeChallenge?.Status == "invalid") {
        throw new LetsEncrytException(new Problem {
          Type = authorizationChallengeChallenge.Error?.Type,
          Detail = authorizationChallengeChallenge.Error?.Detail,
          RawJson = responseText
        }, response);
      }
    }
  }

  private SendResult<TResult> ProcessResponseContent<TResult>(HttpResponseMessage response, string responseText) {
    if (response.Content.Headers.ContentType?.MediaType == GetContentType(ContentType.PemCertificateChain) && typeof(TResult) == typeof(string)) {
      return new SendResult<TResult> {
        Result = (TResult)(object)responseText
      };
    }
    var responseContent = responseText.ToObject<TResult>();
    if (responseContent is IHasLocation ihl && response.Headers.Location != null) {
      ihl.Location = response.Headers.Location;
    }
    return new SendResult<TResult> {
      Result = responseContent,
      ResponseText = responseText
    };
  }

  private static bool StatusEquals(string? status, OrderStatus expected) => status == expected.GetDisplayName();

  private Result MapLetsEncryptException(State state, LetsEncrytException ex) =>
    MapLetsEncryptExceptionCore<Result>(state, ex, m => Result.TooManyRequests(m), m => Result.InternalServerError(m));

  private Result<T?> MapLetsEncryptException<T>(State state, LetsEncrytException ex, T? defaultValue) =>
    MapLetsEncryptExceptionCore(state, ex, m => Result<T?>.TooManyRequests(defaultValue, m), m => Result<T?>.InternalServerError(defaultValue, m));

  private TResult MapLetsEncryptExceptionCore<TResult>(
      State state,
      LetsEncrytException ex,
      Func<string, TResult> tooManyRequests,
      Func<string, TResult> internalError) {
    try {
      if (ex.ProblemKind == AcmeProblemKind.RateLimited) {
        var when = ex.RetryAfterUtc ?? DateTimeOffset.UtcNow.AddHours(1);
        var id = ex.RateLimitedIdentifier;
        if (state.Cache != null && !string.IsNullOrEmpty(id)) {
          var key = id.ToLowerInvariant();
          state.Cache.AcmeRenewalNotBeforeUtcByHostname ??= new Dictionary<string, DateTimeOffset>(StringComparer.OrdinalIgnoreCase);
          if (state.Cache.AcmeRenewalNotBeforeUtcByHostname.TryGetValue(key, out var existing))
            when = when > existing ? when : existing;
          state.Cache.AcmeRenewalNotBeforeUtcByHostname[key] = when;
        }

        logger.LogWarning(
            "ACME rate limited: Kind {AcmeProblemKind}, Type {AcmeProblemType}, Hostname {Hostname}, RetryNotBefore {RetryNotBeforeUtc:o}. Detail: {Detail}",
            ex.ProblemKind, ex.Problem?.Type, id, when, ex.Problem?.Detail);

        var msg = string.IsNullOrEmpty(id)
          ? $"Let's Encrypt rate limit. Do not retry certificate operations before {when:u} UTC."
          : $"Let's Encrypt rate limit for hostname '{id}'. Do not retry before {when:u} UTC.";
        return tooManyRequests(msg);
      }

      logger.LogWarning(ex,
          "Let's Encrypt ACME problem: Kind {AcmeProblemKind}, Type {AcmeProblemType}. {Detail}",
          ex.ProblemKind, ex.Problem?.Type, ex.Problem?.Detail);
      var fallback = string.IsNullOrEmpty(ex.Message) ? "Let's Encrypt request failed." : ex.Message;
      return internalError(fallback);
    }
    finally {
      ex.Response?.Dispose();
    }
  }

  private Result HandleUnhandledException(Exception ex, string defaultMessage = "Let's Encrypt client unhandled exception") {
    logger.LogError(ex, defaultMessage);
    return Result.InternalServerError([defaultMessage, .. ex.ExtractMessages()]);
  }

  private Result<T?> HandleUnhandledException<T>(Exception ex, T? defaultValue = default, string defaultMessage = "Let's Encrypt client unhandled exception") {
    logger.LogError(ex, defaultMessage);
    return Result<T?>.InternalServerError(defaultValue, [.. ex.ExtractMessages()]);
  }
  #endregion
}
