using System.Runtime.CompilerServices;
using System.Text.RegularExpressions;
using AsyncKeyedLock;
using Iceshrimp.Backend.Core.Configuration;
using Iceshrimp.Backend.Core.Database;
using Iceshrimp.Backend.Core.Database.Tables;
using Iceshrimp.Backend.Core.Extensions;
using Iceshrimp.Backend.Core.Federation.ActivityStreams.Types;
using Iceshrimp.Backend.Core.Helpers;
using Iceshrimp.Backend.Core.Middleware;
using Iceshrimp.MfmSharp;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;

namespace Iceshrimp.Backend.Core.Services;

public partial class EmojiService(
	DatabaseContext db,
	DriveService driveSvc,
	SystemUserService sysUserSvc,
	IOptions<Config.InstanceSection> config
) : IScopedService
{
	private static readonly AsyncKeyedLocker<string> KeyedLocker = new(o =>
	{
		o.PoolSize        = 100;
		o.PoolInitialFill = 5;
	});

	public async Task<Emoji> CreateEmojiFromStreamAsync(
		Stream input, string fileName, string mimeType, List<string>? tags = null,
		string? category = null
	)
	{
		var name     = fileName.Split(".")[0];
		var existing = await db.Emojis.AnyAsync(p => p.Host == null && p.Name == name);
		if (existing)
			throw GracefulException.Conflict("An emoji with that name already exists.");

		var user = await sysUserSvc.GetInstanceActorAsync();
		var request = new DriveFileCreationRequest
		{
			Filename    = fileName,
			MimeType    = mimeType,
			IsSensitive = false
		};
		var driveFile = await driveSvc.StoreFileAsync(input, user, request, true);

		var id = IdHelpers.GenerateSnowflakeId();
		var emoji = new Emoji
		{
			Id           = id,
			Name         = name,
			Tags         = tags ?? [],
			Category     = category,
			UpdatedAt    = DateTime.UtcNow,
			OriginalUrl  = driveFile.Url,
			RawPublicUrl = driveFile.RawAccessUrl,
			Type         = driveFile.PublicMimeType ?? driveFile.Type,
			Width        = driveFile.Properties.Width,
			Height       = driveFile.Properties.Height,
			Sensitive    = false
		};
		emoji.Uri = emoji.GetPublicUri(config.Value);

		await db.AddAsync(emoji);
		await db.SaveChangesAsync();

		return emoji;
	}

	public async Task<Emoji> CloneEmojiAsync(Emoji existing)
	{
		var user = await sysUserSvc.GetInstanceActorAsync();
		var driveFile = await driveSvc.StoreFileAsync(existing.OriginalUrl, user, false, forceStore: true,
		                                              skipImageProcessing: false)
		                ?? throw new Exception("Error storing emoji file");

		var emoji = new Emoji
		{
			Id           = IdHelpers.GenerateSnowflakeId(),
			Name         = existing.Name,
			UpdatedAt    = DateTime.UtcNow,
			OriginalUrl  = driveFile.Url,
			RawPublicUrl = driveFile.RawAccessUrl,
			Type         = driveFile.PublicMimeType ?? driveFile.Type,
			Width        = driveFile.Properties.Width,
			Height       = driveFile.Properties.Height,
			Sensitive    = existing.Sensitive
		};
		emoji.Uri = emoji.GetPublicUri(config.Value);

		await db.AddAsync(emoji);
		await db.SaveChangesAsync();

		return emoji;
	}

	public async Task DeleteEmojiAsync(string id)
	{
		var emoji = await db.Emojis.FirstOrDefaultAsync(p => p.Host == null && p.Id == id);
		if (emoji == null) throw GracefulException.NotFound("Emoji not found");

		var driveFile = await db.DriveFiles.FirstOrDefaultAsync(p => p.Url == emoji.OriginalUrl);
		if (driveFile != null) await driveSvc.RemoveFileAsync(driveFile.Id);

		db.Remove(emoji);
		await db.SaveChangesAsync();
	}

	public async Task<List<Emoji>> ProcessEmojiAsync(List<ASEmoji>? emoji, string host)
	{
		emoji?.RemoveAll(p => p.Name == null);
		if (emoji is not { Count: > 0 }) return [];

		foreach (var emojo in emoji) emojo.Name = emojo.Name?.Trim(':');
		host = host.ToPunycodeLower();

		var resolved = await db.Emojis.Where(p => p.Host == host && emoji.Select(e => e.Name).Contains(p.Name))
		                       .ToListAsync();
		var existing = emoji.Where(p => resolved.Any(i => i.Name == p.Name))
		                    .ToDictionary(p => p, p => resolved.First(i => i.Name == p.Name));

		foreach (var emojo in emoji)
		{
			// Update emoji entry if modified
			if (existing.TryGetValue(emojo, out var hit))
			{
				var url = emojo.Image?.Url?.Link;
				if (emojo.Image == null || string.IsNullOrWhiteSpace(url))
					continue;

				if (
					hit.OriginalUrl != url
					|| hit.RawPublicUrl != url
					|| hit.Type != emojo.Image.MediaType
					|| hit.Uri != emojo.Id
				)
				{
					using (await KeyedLocker.LockAsync($"emoji:{host}:{emojo.Name}"))
					{
						await db.ReloadEntityAsync(hit);
						hit.OriginalUrl = url;
						hit.RawPublicUrl = url;
						hit.Type = emojo.Image.MediaType;
						hit.Uri = emojo.Id;
						await db.SaveChangesAsync();
					}
				}

				continue;
			}

			using (await KeyedLocker.LockAsync($"emoji:{host}:{emojo.Name}"))
			{
				var dbEmojo = await db.Emojis.FirstOrDefaultAsync(p => p.Host == host && p.Name == emojo.Name);
				if (dbEmojo == null)
				{
					dbEmojo = new Emoji
					{
						Id           = IdHelpers.GenerateSnowflakeId(),
						Host         = host,
						Name         = emojo.Name ?? throw new Exception("emojo.Name must not be null at this stage"),
						UpdatedAt    = DateTime.UtcNow,
						OriginalUrl  = emojo.Image?.Url?.Link ?? throw new Exception("Emoji.Image has no url"),
						RawPublicUrl = emojo.Image.Url.Link,
						Type         = emojo.Image.MediaType,
						Uri          = emojo.Id,
						Sensitive    = false
					};
					await db.AddAsync(dbEmojo);
					await db.SaveChangesAsync();
				}

				resolved.Add(dbEmojo);
			}
		}

		return resolved;
	}

	// This is technically the unicode character 'heavy black heart', but misskey doesn't send the emoji version selector, so here we are.
	private const string MisskeyHeart         = "\u2764";
	private const string EmojiVersionSelector = "\ufe0f";

	public async Task<string> ResolveEmojiNameAsync(string name, string? host)
	{
		if (name == MisskeyHeart)
			return name + EmojiVersionSelector;
		if (EmojiHelpers.IsEmoji(name))
			return name;

		host = host?.ToPunycodeLower();
		var match             = CustomEmojiRegex.Match(name);
		var remoteMatch       = RemoteCustomEmojiRegex.Match(name);
		var localMatchSuccess = !match.Success || match.Groups.Count != 2;
		if (localMatchSuccess && !remoteMatch.Success)
			throw GracefulException.BadRequest("Invalid emoji name");

		// @formatter:off
		var hit = !remoteMatch.Success
			? await db.Emojis.FirstOrDefaultAsync(p => p.Host == host && p.Name == match.Groups[1].Value)
			: await db.Emojis.FirstOrDefaultAsync(p => p.Name == remoteMatch.Groups[1].Value &&
			                                           p.Host == remoteMatch.Groups[2].Value.ToPunycodeLower());
		// @formatter:on

		if (hit == null)
			throw GracefulException.BadRequest("Unknown emoji");

		return hit.Host == null ? $":{hit.Name}:" : $":{hit.Name}@{hit.Host}:";
	}

	public async Task<Emoji?> ResolveEmojiAsync(string fqn)
	{
		if (!fqn.StartsWith(':')) return null;
		var split = fqn.Trim(':').Split('@');
		var name  = split[0];
		var host  = split.Length > 1 ? split[1].ToPunycodeLower() : null;

		return await db.Emojis.FirstOrDefaultAsync(p => p.Host == host && p.Name == name);
	}

	public async Task<List<Emoji>> ResolveEmojiAsync(IMfmNode[] nodes)
	{
		var list = new List<MfmEmojiCodeNode>();
		ResolveChildren(nodes, ref list);
		return await db.Emojis.Where(p => p.Host == null && list.Select(i => i.Name).Contains(p.Name)).ToListAsync();
	}

	private static void ResolveChildren(
		Span<IMfmNode> nodes, ref List<MfmEmojiCodeNode> list
	)
	{
		foreach (var node in nodes)
		{
			if (node is MfmEmojiCodeNode emojiNode) list.Add(emojiNode);
			list.AddRange(node.Children.OfType<MfmEmojiCodeNode>());
			ResolveChildren(node.Children, ref list);
		}
	}

	[OverloadResolutionPriority(1)]
	private static void ResolveChildren(
		Span<IMfmInlineNode> nodes, ref List<MfmEmojiCodeNode> list
	)
	{
		foreach (var node in nodes)
		{
			if (node is MfmEmojiCodeNode emojiNode) list.Add(emojiNode);
			list.AddRange(node.Children.OfType<MfmEmojiCodeNode>());
			ResolveChildren(node.Children, ref list);
		}
	}

	public async Task<Emoji?> UpdateLocalEmojiAsync(
		string id, string? name, List<string>? tags, string? category, string? license, bool? sensitive
	)
	{
		var emoji = await db.Emojis.FirstOrDefaultAsync(p => p.Id == id);
		if (emoji == null) return null;

		if (emoji.Host != null)
		{
			// Only allow changing remote emoji sensitive status
			if (!sensitive.HasValue)
				throw GracefulException.BadRequest("Only sensitive can be updated on remote emojis");

			emoji.Sensitive = sensitive.Value;
			emoji.UpdatedAt = DateTime.UtcNow;
			await db.SaveChangesAsync();

			return emoji;
		}

		emoji.UpdatedAt = DateTime.UtcNow;

		var existing = await db.Emojis.FirstOrDefaultAsync(p => p.Host == null && p.Name == name);

		if (name != null && existing == null && CustomEmojiRegex.IsMatch(name))
		{
			emoji.Name = name;
			emoji.Uri  = emoji.GetPublicUri(config.Value);
		}

		if (tags != null)
			emoji.Tags = tags.Select(p => p.Trim()).Where(p => !string.IsNullOrWhiteSpace(p)).ToList();

		// If category is provided but empty reset to null
		if (category != null) emoji.Category = string.IsNullOrEmpty(category) ? null : category;

		if (license != null) emoji.License = license;

		if (sensitive.HasValue) emoji.Sensitive = sensitive.Value;

		await db.SaveChangesAsync();

		return emoji;
	}

	public static bool IsCustomEmoji(string s) => CustomEmojiRegex.IsMatch(s) || RemoteCustomEmojiRegex.IsMatch(s);

	[GeneratedRegex(@"^:?([\w+-]+)(?:@\.)?:?$", RegexOptions.Compiled)]
	private static partial Regex CustomEmojiRegex { get; }

	[GeneratedRegex(@"^:?([\w+-]+)@([a-zA-Z0-9._\-]+\.[a-zA-Z0-9._\-]+):?$", RegexOptions.Compiled)]
	private static partial Regex RemoteCustomEmojiRegex { get; }
}
