using AngleSharp;
using AngleSharp.Dom;
using Microsoft.AspNetCore.Mvc;
using Nostr.Client.Identifiers;
using Nostr.Client.Messages;
using Nostr.Client.Utils;
using NostrServices.Client;
using NostrServices.Services;
using ProtoBuf;

namespace NostrServices.Controllers;

/// <summary>
/// Add OpenGraph tags to html documents
/// </summary>
[Route("/api/v1/opengraph")]
public class OpenGraphController(ILogger<OpenGraphController> logger, RedisStore redisStore) : Controller
{
    /// <summary>
    /// Inject opengraph tags into provided html
    /// </summary>
    /// <remarks>
    /// POST your HTML content to this endpoint along with the ID of the content and the
    /// returned HTML will contain OpenGraph tags relevant to the event/profile
    /// </remarks>
    /// <param name="id">Nostr identifier nip5/npub/note/nevent/naddr/nprofile</param>
    /// <param name="canonical">Url format for canonical tag <code>https://example.com/%s</code></param>
    /// <returns></returns>
    [HttpPost("{id}")]
    [Consumes("text/html", "text/plain")]
    [Produces("text/html")]
    public async Task<IActionResult> TagPage([FromRoute] string id, [FromQuery] string? canonical)
    {
        var cts = HttpContext.RequestAborted;
        using var sr = new StreamReader(Request.Body);
        var html = await sr.ReadToEndAsync(cts);

        void AddCanonical(List<HeadElement> tags, NostrIdentifier idx)
        {
            if (!string.IsNullOrEmpty(canonical) && canonical.Contains("%s"))
            {
                var uc = new Uri(canonical.Replace("%s", idx.ToBech32()));
                tags.Add(new HeadElement("link", [
                    new("rel", "canonical"),
                    new("href", uc.ToString())
                ]));
            }
        }

        var nid = await Extensions.TryParseIdentifier(id);
        if (nid != default)
        {
            try
            {
                if (nid.Hrp is "nevent" or "note" or "naddr")
                {
                    var ev = await redisStore.GetEvent(nid);
                    if (ev != default)
                    {
                        var tags = MetaTagsToElements(await GetEventTags(ev));
                        AddCanonical(tags, ev.ToIdentifier());

                        var doc = await InjectTags(html, tags);
                        return Content(doc?.ToHtml() ?? html, "text/html");
                    }
                }
                else if (nid.Hrp is "nprofile" or "npub")
                {
                    var profile = await redisStore.GetProfile(nid.Special);

                    var meta = await GetProfileMeta(profile);
                    var tags = MetaTagsToElements([
                        new("og:type", "profile"),
                        new("og:title", meta?.Title ?? ""),
                        new("og:description", meta?.Description ?? ""),
                        new("og:image", meta?.Image ?? $"https://robohash.v0l.io/{nid.Special}.png"),
                        new("og:profile:username", meta?.Profile?.Name ?? ""),
                        new("twitter:card", "summary"),
                        new("twitter:title", meta?.Title ?? ""),
                        new("twitter:description", meta?.Description ?? ""),
                        new("twitter:image", meta?.Image ?? $"https://robohash.v0l.io/{nid.Special}.png"),
                    ]);

                    AddCanonical(tags, profile?.ToIdentifier() ?? new NostrProfileIdentifier(nid.Special, null));
                    var doc = await InjectTags(html, tags);

                    return Content(doc?.ToHtml() ?? html, "text/html");
                }
            }
            catch (Exception ex) when (ex is not TaskCanceledException)
            {
                logger.LogWarning("Failed to inject event tags: {Message}", ex.ToString());
            }
        }

        return Content(html, "text/html");
    }

    private async Task<List<KeyValuePair<string, string>>> GetEventTags(CompactEvent ev)
    {
        var ret = new List<KeyValuePair<string, string>>();

        var profile = await redisStore.GetProfile(ev.PubKey.ToHex());
        var name = profile?.Name ?? "Nostrich";
        switch (ev.Kind)
        {
            case (long)NostrKind.LiveEvent:
            {
                var host = ev.Tags.FirstOrDefault(a => a.Key == "p" && a.Values[^1] == "host")
                               ?.Values[0] ??
                           ev.PubKey.ToHex();
                var hostProfile = await redisStore.GetProfile(host);
                var hostName = hostProfile?.Name ?? profile?.Name ?? "Nostrich";
                var stream = ev.GetFirstTagValue("streaming") ?? ev.GetFirstTagValue("recording") ?? "";
                var image = ev.GetFirstTagValue("image") ??
                            hostProfile?.Picture ?? $"https://robohash.v0l.io/{ev.PubKey.ToHex()}.png";
                ret.AddRange(new KeyValuePair<string, string>[]
                {
                    new("og:type", "video.other"),
                    new("og:title", $"{hostName} is streaming"),
                    new("og:description", ev.GetFirstTagValue("title") ?? ""),
                    new("og:image", image),
                    new("og:video", stream),
                    new("og:video:secure_url", stream),
                    new("og:video:type", "application/vnd.apple.mpegurl"),
                    new("twitter:card", "player"),
                    new("twitter:title", $"{hostName} is streaming"),
                    new("twitter:description", ev.GetFirstTagValue("title") ?? ""),
                    new("twitter:image", image),
                    new("twitter:site", "@zap_stream"),
                    new("twitter:player", $"https://zap.stream/embed/{ev.ToIdentifier().ToBech32()}"),
                    new("twitter:player:width", "640"),
                    new("twitter:player:height", "480"),
                    new("twitter:text:player_height", "480")
                });

                break;
            }
            case 1_313: // stream clip
            {
                var stream = ev.GetFirstTagValue("r")!;
                var image = ev.GetFirstTagValue("image") ??
                            profile?.Picture ?? $"https://robohash.v0l.io/{ev.PubKey.ToHex()}.png";
                ret.AddRange(new KeyValuePair<string, string>[]
                {
                    new("og:type", "video.other"),
                    new("og:title", $"{name} created a clip"),
                    new("og:description", ev.GetFirstTagValue("title") ?? ""),
                    new("og:image", image),
                    new("og:video", stream),
                    new("og:video:secure_url", stream),
                    new("og:video:type", "video/mp4"),
                    new("twitter:card", "summary_large_image"),
                    new("twitter:title", $"{name} created a clip"),
                    new("twitter:description", ev.GetFirstTagValue("title") ?? ""),
                    new("twitter:image", image),
                });

                break;
            }
            default:
            {
                const int maxLen = 160;
                var trimmedContent = ev.Content.Length > maxLen ? ev.Content[..maxLen] : ev.Content;
                var titleContent = $"{profile}: {trimmedContent}";
                var image = profile?.Picture ?? $"https://robohash.v0l.io/{ev.PubKey.ToHex()}.png";
                ret.AddRange(new KeyValuePair<string, string>[]
                {
                    new("og:type", "article"),
                    new("og:title", titleContent),
                    new("og:description", ""),
                    new("og:image", image),
                    new("og:article:published_time", ev.Created.ToString("o")),
                    new("og:article:author:username", profile?.Name ?? ""),
                    new("twitter:card", "summary"),
                    new("twitter:title", titleContent),
                    new("twitter:description", ""),
                    new("twitter:image", image),
                });

                break;
            }
        }

        return ret;
    }

    private async Task<CachedMeta?> GetProfileMeta(CompactProfile? profile)
    {
        var titleContent = $"{profile?.Name ?? "Nostrich"}'s Profile";
        var aboutContent = profile?.About?.Length > 160 ? profile.About[..160] : profile?.About ?? "";
        var imageContent = profile?.Picture ?? "https://snort.social/nostrich_512.png";
        return new CachedMeta
        {
            Title = titleContent,
            Description = aboutContent,
            Image = imageContent,
            Profile = profile
        };
    }

    private async Task<IDocument?> InjectTags(string html, List<HeadElement> tags)
    {
        var config = Configuration.Default;
        var context = BrowsingContext.New(config);
        var doc = await context.OpenAsync(c => c.Content(html));

        foreach (var ex in tags)
        {
            IElement tag = doc.Head?.Children.FirstOrDefault(a =>
            {
                if (ex.Element == "meta")
                {
                    var metaPropertyA = a.Attributes.FirstOrDefault(b => b.Name == "property");
                    var metaPropertyB = ex.Attributes.FirstOrDefault(b => b is { Key: "property" });
                    return metaPropertyA?.Value == metaPropertyB.Value;
                }

                return false;
            }) ?? AddNewTag(ex);

            foreach (var attr in ex.Attributes)
            {
                tag.SetAttribute(attr.Key, attr.Value);
            }

            var isOgTitle = ex.Attributes.Any(a => a is { Key: "property", Value: "og:title" });
            if (isOgTitle && doc.Head != default)
            {
                var titleTag = doc.Head.QuerySelector("title");
                var ogTitle = ex.Attributes.FirstOrDefault(a => a.Key is "content");
                if (!string.IsNullOrEmpty(ogTitle.Value))
                {
                    if (titleTag == default)
                    {
                        var newTitle = doc.CreateElement("title");
                        doc.Head.AppendChild(newTitle);
                        titleTag = newTitle;
                    }

                    titleTag.TextContent = ogTitle.Value;
                }
            }

            var isOgDesc = ex.Attributes.Any(a => a is { Key: "property", Value: "og:description" });
            if (isOgDesc && doc.Head != default)
            {
                var ogDesc = ex.Attributes.FirstOrDefault(a => a.Key is "content");
                var descriptionTag = doc.Head?.QuerySelector("meta[name='description']");
                if (!string.IsNullOrEmpty(ogDesc.Value))
                {
                    if (descriptionTag == default)
                    {
                        var newDesc = doc.CreateElement("meta");
                        newDesc.SetAttribute("name", "description");
                        doc.Head?.AppendChild(newDesc);
                        descriptionTag = newDesc;
                    }

                    descriptionTag.SetAttribute("content", ogDesc.Value);
                }
            }

            continue;

            IElement AddNewTag(HeadElement he)
            {
                var tx = doc!.CreateElement(he.Element);
                doc.Head?.AppendChild(tx);
                return tx;
            }
        }

        return doc;
    }

    private List<HeadElement> MetaTagsToElements(List<KeyValuePair<string, string>> tags)
    {
        var ret = new List<HeadElement>();
        foreach (var tag in tags)
        {
            ret.Add(new("meta", [
                new("property", tag.Key),
                new("content", tag.Value)
            ]));
        }

        return ret;
    }

    record HeadElement(string Element, List<KeyValuePair<string, string>> Attributes);
}

[ProtoContract]
class CachedMeta
{
    [ProtoMember(1)]
    public string? Title { get; init; }

    [ProtoMember(2)]
    public string? Description { get; init; }

    [ProtoMember(3)]
    public string? Image { get; init; }

    [ProtoMember(4)]
    public CompactProfile? Profile { get; init; }

    [ProtoMember(5)]
    public CompactEvent? Event { get; init; }
}