using System.Security.Cryptography;
using System.Text;
using AngleSharp;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Memory;
using NostrServices.Client;
using StackExchange.Redis;

namespace NostrServices.Controllers;

[Route("/api/v1/preview")]
public class LinkPreviewController : Controller
{
    private readonly IDatabase _database;
    private readonly IMemoryCache _memoryCache;
    private readonly HttpClient _client;
    private readonly ILogger<LinkPreviewController> _logger;

    public LinkPreviewController(HttpClient client, ILogger<LinkPreviewController> logger, IDatabase database, IMemoryCache memoryCache)
    {
        _client = client;
        _logger = logger;
        _database = database;
        _memoryCache = memoryCache;
        _client.DefaultRequestHeaders.Add("user-agent",
            "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36 Snort/1.0 (LinkPreview; https://snort.social)");

        _client.Timeout = TimeSpan.FromSeconds(30);
    }

    /// <summary>
    /// Fetch page and return parsed header tags
    /// </summary>
    /// <param name="url"></param>
    /// <returns></returns>
    [HttpGet]
    [ResponseCache(Duration = 21600, VaryByQueryKeys = ["url"], Location = ResponseCacheLocation.Any)]
    public async Task<LinkPreviewData?> GetPreview([FromQuery] string url)
    {
        var urlHash = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(url.ToLower())));
        var key = $"link-preview:{urlHash}";
        var keyEmpty = $"{key}:empty";
        if (_memoryCache.Get<bool>(keyEmpty))
        {
            return default;
        }

        var cached = await _database.GetAsync<LinkPreviewData>(key);
        if (cached != default)
        {
            return cached;
        }

        var cts = HttpContext.RequestAborted;
        try
        {
            var rsp = await _client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, cts);
            if (rsp.IsSuccessStatusCode && rsp.Content.Headers.ContentType?.MediaType == "text/html")
            {
                var body = await rsp.Content.ReadAsStringAsync(cts);
                var config = Configuration.Default;
                var context = BrowsingContext.New(config);
                var doc = await context.OpenAsync(c => c.Content(body), cts);
                var ogTags = doc.Head?.QuerySelectorAll("meta[property*='og']").Select(a =>
                {
                    var k = a.Attributes["property"]?.Value;
                    var v = a.Attributes["content"]?.Value;
                    if (!string.IsNullOrEmpty(k) && !string.IsNullOrEmpty(v))
                    {
                        return new KeyValuePair<string, string>(k, v);
                    }

                    return default;
                }).Where(a => !string.IsNullOrEmpty(a.Key)).ToArray();

                var obj = new LinkPreviewData
                {
                    OgTags = ogTags?.Select(a => new KeyValuePair<string, string>(a.Key, a.Value)).ToList() ?? new(),
                    Title = ogTags?.FirstOrDefault(a => a.Key.Equals("og:title")).Value
                            ?? doc.Head?.QuerySelector("title")?.TextContent,
                    Description = ogTags?.FirstOrDefault(a => a.Key.Equals("og:description")).Value
                                  ?? doc.Head?.QuerySelector("meta[name='description']")?.Attributes["content"]?.Value,
                    Image = ogTags?.FirstOrDefault(a => a.Key.Equals("og:image")).Value
                };

                await _database.SetAsync(key, obj, TimeSpan.FromDays(7));
                return obj;
            }
        }
        catch (HttpRequestException hx)
        {
            _logger.LogWarning("{url} returned {status}", url, hx.StatusCode);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to fetch content at {url}", url);
        }

        // cache no result in memory to avoid wasting time on repeated attempts
        _memoryCache.Set(keyEmpty, true, TimeSpan.FromMinutes(10));
        return default;
    }
}
