using System.Globalization;
using System.Net;
using System.Net.Http.Json;

namespace Olive.Klipy;

internal sealed class KlipyClient
{
  private const string BaseUrl = "https://api.klipy.com";
  private static readonly HttpClient HttpClient = new() { Timeout = TimeSpan.FromSeconds(12) };

  /// <summary>
  /// Searches Klipy GIFs with the raw query parameters used by Olive and returns converted GIF items plus pagination state.
  /// </summary>
  /// <param name="apiKey">Klipy application key used in the API route.</param>
  /// <param name="query">Search text entered by the user.</param>
  /// <param name="page">One-based Klipy page number to request.</param>
  /// <param name="perPage">Number of items to request from Klipy for this page.</param>
  /// <param name="cancellationToken">Token used to cancel the HTTP request and JSON parsing.</param>
  /// <returns>Converted GIF results and whether Klipy reports another page.</returns>
  public static async Task<KlipySearchResult> SearchAsync(string apiKey, string query, int page, int perPage,
    CancellationToken cancellationToken)
  {
    if (string.IsNullOrWhiteSpace(apiKey))
      throw new InvalidOperationException("The Klipy API key is missing. Set it in Olive settings.");

    var requestUri = BuildSearchUri(apiKey, query, page, perPage);

    using var response = await HttpClient
      .GetAsync(requestUri, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
    if (response.StatusCode == HttpStatusCode.NoContent) return new KlipySearchResult([], false);

    if (!response.IsSuccessStatusCode)
      throw new HttpRequestException($"Klipy returned {(int)response.StatusCode} {response.ReasonPhrase}.", null,
        response.StatusCode);

    var payload = await response.Content
      .ReadFromJsonAsync(KlipyJsonContext.Default.KlipySearchResponse, cancellationToken).ConfigureAwait(false);
    if (payload is null || !payload.Result) throw new HttpRequestException("Klipy did not return a usable result.");

    var rawItems = payload.Data?.Items ?? [];
    var hasMore = payload.Data?.HasNext ?? rawItems.Length >= perPage;
    return new KlipySearchResult(ConvertResults(rawItems), hasMore);
  }

  /// <summary>
  /// Downloads the selected animated GIF file into memory for clipboard use.
  /// </summary>
  /// <param name="gifUrl">Absolute URL of the GIF file to download.</param>
  /// <param name="cancellationToken">Token used to cancel the download.</param>
  /// <returns>Raw GIF file bytes.</returns>
  public static async Task<byte[]> DownloadGifBytesAsync(Uri gifUrl, CancellationToken cancellationToken)
  {
    using var response = await HttpClient.GetAsync(gifUrl, HttpCompletionOption.ResponseHeadersRead, cancellationToken)
      .ConfigureAwait(false);
    if (!response.IsSuccessStatusCode)
      throw new HttpRequestException($"Download failed ({(int)response.StatusCode}).", null, response.StatusCode);

    return await response.Content.ReadAsByteArrayAsync(cancellationToken).ConfigureAwait(false);
  }

  /// <summary>
  /// Builds the Klipy search endpoint URL with only page, per_page, and q query parameters.
  /// </summary>
  /// <param name="apiKey">Klipy application key used in the API route.</param>
  /// <param name="query">Search text to pass as the q query parameter.</param>
  /// <param name="page">One-based Klipy page number to request.</param>
  /// <param name="perPage">Number of items to request from Klipy for this page.</param>
  /// <returns>Fully escaped Klipy search URI.</returns>
  private static Uri BuildSearchUri(string apiKey, string query, int page, int perPage)
  {
    var endpoint = $"{BaseUrl}/api/v1/{Uri.EscapeDataString(apiKey)}/gifs/search";
    var parameters = new Dictionary<string, string>
    {
      ["page"] = page.ToString(CultureInfo.InvariantCulture),
      ["per_page"] = perPage.ToString(CultureInfo.InvariantCulture),
      ["q"] = query
    };

    var queryString = string.Join("&", parameters.Select(pair => string.Create(
      CultureInfo.InvariantCulture,
      $"{Uri.EscapeDataString(pair.Key)}={Uri.EscapeDataString(pair.Value)}")));

    return new Uri($"{endpoint}?{queryString}");
  }

  /// <summary>
  /// Converts raw Klipy response items into Olive GIF models and skips entries without usable GIF URLs.
  /// </summary>
  /// <param name="items">Raw item array from the Klipy search response.</param>
  /// <returns>GIF models that have an ID, thumbnail URL, and full GIF URL.</returns>
  private static List<KlipyGif> ConvertResults(KlipyItem[]? items)
  {
    if (items is null || items.Length == 0) return [];

    var gifs = new List<KlipyGif>(items.Length);
    foreach (var item in items)
    {
      var id = !string.IsNullOrWhiteSpace(item.Slug) ? item.Slug : item.Id?.ToString(CultureInfo.InvariantCulture);
      var thumbnailUrl = item.File?.Sm?.Gif?.Url ?? item.File?.Md?.Gif?.Url ?? item.File?.Hd?.Gif?.Url;
      var fullUrl = item.File?.Hd?.Gif?.Url ?? item.File?.Md?.Gif?.Url ?? item.File?.Sm?.Gif?.Url;

      if (string.IsNullOrWhiteSpace(id)
          || !Uri.TryCreate(thumbnailUrl, UriKind.Absolute, out var thumbnailGifUrl)
          || !Uri.TryCreate(fullUrl, UriKind.Absolute, out var gifUrl))
        continue;

      var title = string.IsNullOrWhiteSpace(item.Title)
        ? "GIF Klipy"
        : WebUtility.HtmlDecode(item.Title).Trim();

      gifs.Add(new KlipyGif(id, title, thumbnailGifUrl, gifUrl));
    }

    return gifs;
  }
}
