using Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities;
using Iceshrimp.Backend.Controllers.Pleroma.Schemas.Entities;
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.Helpers;
using Iceshrimp.Backend.Core.Helpers.LibMfm.Conversion;
using Iceshrimp.Backend.Core.Services;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;

namespace Iceshrimp.Backend.Controllers.Mastodon.Renderers;

public class NoteRenderer(
	IOptions<Config.InstanceSection> config,
	IOptionsSnapshot<Config.SecuritySection> security,
	UserRenderer userRenderer,
	PollRenderer pollRenderer,
	MfmConverter mfmConverter,
	DatabaseContext db,
	EmojiService emojiSvc,
	AttachmentRenderer attachmentRenderer,
	FlagService flags
) : IScopedService
{
	private static readonly FilterResultEntity InaccessibleFilter = new()
	{
		Filter = new FilterEntity
		{
			Title        = "HideInaccessible",
			FilterAction = "hide",
			Id           = "0",
			Context      = ["home", "thread", "notifications", "account", "public"],
			Keywords     = [new FilterKeyword("RE: \ud83d\udd12", 0, 0)],
			ExpiresAt    = null
		},
		KeywordMatches = ["RE: \ud83d\udd12"] // lock emoji
	};

	public async Task<StatusEntity> RenderAsync(
		Note note, User? user, Filter.FilterContext? filterContext = null, NoteRendererDto? data = null, int recurse = 2
	)
	{
		var uri = note.Uri ?? note.GetPublicUri(config.Value);
		var renote = note is { Renote: not null, IsQuote: false } && recurse > 1
			? await RenderAsync(note.Renote, user, null, data, --recurse)
			: null;
		var quote = note is { Renote: not null, IsQuote: true } && recurse > 0
			? await RenderAsync(note.Renote, user, null, data, 0)
			: null;
		var     text     = note.Text;
		string? quoteUri = null;

		if (note is { Renote: not null, IsQuote: true })
		{
			var qUri = note.Renote?.Url ?? note.Renote?.Uri ?? note.Renote?.GetPublicUriOrNull(config.Value);
			var alt  = note.Renote?.Uri;
			var t    = text ?? "";

			if (qUri != null && !t.Contains(qUri) && (alt == null || qUri == alt || !t.Contains(alt)))
				quoteUri = qUri;
		}

		var liked = data?.LikedNotes?.Contains(note.Id) ??
		            await db.NoteLikes.AnyAsync(p => p.Note == note && p.User == user);
		var bookmarked = data?.BookmarkedNotes?.Contains(note.Id) ??
		                 await db.NoteBookmarks.AnyAsync(p => p.Note == note && p.User == user);
		var muted = data?.MutedNotes?.Contains(note.ThreadId) ??
		            await db.NoteThreadMutings.AnyAsync(p => p.ThreadId == note.ThreadId && p.User == user);
		var pinned = data?.PinnedNotes?.Contains(note.Id) ??
		             await db.UserNotePins.AnyAsync(p => p.Note == note && p.User == user);
		var renoted = data?.Renotes?.Contains(note.Id) ??
		              await db.Notes.AnyAsync(p => p.Renote == note && p.User == user && p.IsPureRenote);

		var noteEmoji = data?.Emoji?.Where(p => note.Emojis.Contains(p.Id)).ToList() ?? await GetEmojiAsync([note]);

		var mentions = data?.Mentions == null
			? await GetMentionsAsync([note])
			: [..data.Mentions.Where(p => note.Mentions.Contains(p.Id))];

		var attachments = data?.Attachments == null
			? await GetAttachmentsAsync([note])
			: [..data.Attachments.Where(p => note.FileIds.Contains(p.Id))];

		if (user == null && security.Value.PublicPreview == Enums.PublicPreview.RestrictedNoMedia)
			attachments = [];

		var reactions = data?.Reactions == null
			? await GetReactionsAsync([note], user)
			: [..data.Reactions.Where(p => p.NoteId == note.Id)];

		var tags = note.Tags.Select(tag => new StatusTags
		               {
			               Name = tag,
			               Url  = $"https://{config.Value.WebDomain}/tags/{tag}"
		               })
		               .ToList();

		var mentionedUsers = mentions.Select(p => new Note.MentionedUser
		                             {
			                             Host     = p.Host ?? config.Value.AccountDomain,
			                             Uri      = p.Uri,
			                             Username = p.Username,
			                             Url      = p.Url
		                             })
		                             .ToList();

		var replyInaccessible =
			note.Reply == null && ((note.ReplyId != null && recurse == 2) || note.ReplyUri != null);
		var quoteInaccessible =
			note.Renote == null && ((note.RenoteId != null && recurse > 0) || note.RenoteUri != null);

		var sensitive = note.Cw != null || attachments.Any(p => p.Sensitive);

		var inlineMedia = attachments.Select(p => new MfmInlineMedia(p.Type switch
		{
			AttachmentType.Audio                       => MfmInlineMedia.MediaType.Audio,
			AttachmentType.Video                       => MfmInlineMedia.MediaType.Video,
			AttachmentType.Image or AttachmentType.Gif => MfmInlineMedia.MediaType.Image,
			_                                          => MfmInlineMedia.MediaType.Other
		}, p.RemoteUrl ?? p.Url, p.Description)).ToList();
		
		var filters = data?.Filters ?? await GetFiltersAsync(user, filterContext);

		List<FilterResultEntity> filterResult;
		if (filters.Count > 0 && filterContext == null)
		{
			var filtered = FilterHelper.CheckFilters([note, note.Reply, note.Renote, note.Renote?.Renote], filters);
			filterResult = GetFilterResult(filtered);
		}
		else
		{
			var filtered = FilterHelper.IsFiltered([note, note.Reply, note.Renote, note.Renote?.Renote], filters);
			filterResult = GetFilterResult(filtered.HasValue ? [filtered.Value] : []);
		}

		if ((user?.UserSettings?.FilterInaccessible ?? false) && (replyInaccessible || quoteInaccessible))
			filterResult.Insert(0, InaccessibleFilter);

		var cw = note.Cw;
		if (replyInaccessible && !string.IsNullOrEmpty(cw))
		{
			// prefix with lock emoji
			cw = "RE: \ud83d\udd12, " + cw;

			// prevent duplicating inaccessible marker in the body
			replyInaccessible = false;
		}

		string? content = null;
		if (data?.Source != true)
		{
			if (text != null || quoteUri != null || quoteInaccessible || replyInaccessible)
			{
				(content, inlineMedia) = await mfmConverter.ToHtmlAsync(text ?? "", mentionedUsers, note.UserHost, quoteUri,
				                                          quoteInaccessible, replyInaccessible, media: inlineMedia);

				attachments.RemoveAll(attachment => inlineMedia.Any(inline => inline.Src == (attachment.RemoteUrl ?? attachment.Url)));
			}
			else
			{
				content = "";
			}
		}

		var account = data?.Accounts?.FirstOrDefault(p => p.Id == note.UserId) ??
		              await userRenderer.RenderAsync(note.User, user);

		var poll = note.HasPoll
			? (data?.Polls ?? await GetPollsAsync([note], user)).FirstOrDefault(p => p.Id == note.Id)
			: null;

		var visibility = flags.IsPleroma.Value && note.LocalOnly
			? "local"
			: StatusEntity.EncodeVisibility(note.Visibility);

		var pleromaExtensions = flags.IsPleroma.Value
			? new PleromaStatusExtensions
			{
				LocalOnly      = note.LocalOnly,
				Reactions      = reactions,
				ConversationId = note.ThreadId
			}
			: null;

		var res = new StatusEntity
		{
			Id               = note.Id,
			Uri              = uri,
			Url              = note.Url ?? uri,
			Account          = account,
			ReplyId          = note.ReplyId,
			ReplyUserId      = note.MastoReplyUserId ?? note.ReplyUserId,
			MastoReplyUserId = note.MastoReplyUserId,
			Renote           = renote,
			Quote            = quote,
			QuoteId          = note.IsQuote ? note.RenoteId : null,
			ContentType      = "text/x.misskeymarkdown",
			CreatedAt        = note.CreatedAt.ToStringIso8601Like(),
			EditedAt         = note.UpdatedAt?.ToStringIso8601Like(),
			RepliesCount     = note.RepliesCount,
			RenoteCount      = note.RenoteCount,
			FavoriteCount    = note.LikeCount,
			IsFavorited      = liked,
			IsRenoted        = renoted,
			IsBookmarked     = bookmarked,
			IsMuted          = muted,
			IsSensitive      = sensitive,
			ContentWarning   = cw ?? "",
			Visibility       = visibility,
			Content          = content,
			Text             = text,
			Mentions         = mentions,
			IsPinned         = pinned,
			Attachments      = attachments,
			Emojis           = noteEmoji,
			Poll             = poll,
			Reactions        = reactions,
			Tags             = tags,
			Filtered         = filterResult,
			Pleroma          = pleromaExtensions
		};

		return res;
	}

	public async Task<List<StatusEdit>> RenderHistoryAsync(Note note, User? user)
	{
		var edits = await db.NoteEdits.Where(p => p.Note == note).OrderBy(p => p.Id).ToListAsync();
		edits.Add(RenderEdit(note));

		var attachments = await GetAttachmentsAsync(note.FileIds.Concat(edits.SelectMany(p => p.FileIds)));
		var mentions    = await GetMentionsAsync([note]);
		var mentionedUsers = mentions.Select(p => new Note.MentionedUser
		                             {
			                             Host     = p.Host ?? config.Value.AccountDomain,
			                             Uri      = p.Uri,
			                             Username = p.Username,
			                             Url      = p.Url
		                             })
		                             .ToList();

		var account  = await userRenderer.RenderAsync(note.User, user);
		var lastDate = note.CreatedAt;

		List<StatusEdit> history = [];
		foreach (var edit in edits)
		{
			var files   = attachments.Where(p => edit.FileIds.Contains(p.Id)).ToList();

			var inlineMedia = files.Select(p => new MfmInlineMedia(p.Type switch
			{
				AttachmentType.Audio                       => MfmInlineMedia.MediaType.Audio,
				AttachmentType.Video                       => MfmInlineMedia.MediaType.Video,
				AttachmentType.Image or AttachmentType.Gif => MfmInlineMedia.MediaType.Image,
				_                                          => MfmInlineMedia.MediaType.Other
			}, p.RemoteUrl ?? p.Url, p.Description)).ToList();

			(var content, inlineMedia) = await mfmConverter.ToHtmlAsync(edit.Text ?? "", mentionedUsers, note.UserHost, media: inlineMedia);
			files.RemoveAll(attachment => inlineMedia.Any(inline => inline.Src == (attachment.RemoteUrl ?? attachment.Url)));

			var entry = new StatusEdit
			{
				Account        = account,
				Content        = content,
				CreatedAt      = lastDate.ToStringIso8601Like(),
				Emojis         = [],
				IsSensitive    = files.Any(p => p.Sensitive),
				Attachments    = files,
				Poll           = null,
				ContentWarning = edit.Cw ?? ""
			};
			history.Add(entry);
			lastDate = edit.UpdatedAt;
		}

		return history;
	}

	private NoteEdit RenderEdit(Note note)
	{
		return new NoteEdit
		{
			Text      = note.Text,
			Cw        = note.Cw,
			FileIds   = note.FileIds,
			UpdatedAt = note.UpdatedAt ?? note.CreatedAt
		};
	}

	private static List<FilterResultEntity> GetFilterResult(
		IReadOnlyCollection<(Filter filter, string keyword)> filtered
	)
	{
		var res = new List<FilterResultEntity>();

		foreach (var entry in filtered.DistinctBy(p => p.filter.Id))
		{
			var (filter, keyword) = entry;
			res.Add(new FilterResultEntity { Filter = FilterRenderer.RenderOne(filter), KeywordMatches = [keyword] });
		}

		return res;
	}

	private async Task<List<MentionEntity>> GetMentionsAsync(List<Note> notes)
	{
		if (notes.Count == 0) return [];
		var ids = notes.SelectMany(n => n.Mentions).Distinct();
		return await db.Users.IncludeCommonProperties()
		               .Where(p => ids.Contains(p.Id))
		               .Select(u => new MentionEntity(u, config.Value.WebDomain))
		               .ToListAsync();
	}

	private async Task<List<AttachmentEntity>> GetAttachmentsAsync(List<Note> notes)
	{
		if (notes.Count == 0) return [];
		var ids = notes.SelectMany(n => n.FileIds).Distinct();
		return await db.DriveFiles.Where(p => ids.Contains(p.Id))
		               .Select(f => attachmentRenderer.Render(f, true))
		               .ToListAsync();
	}

	private async Task<List<AttachmentEntity>> GetAttachmentsAsync(IEnumerable<string> fileIds)
	{
		var ids = fileIds.Distinct().ToList();
		if (ids.Count == 0) return [];
		return await db.DriveFiles.Where(p => ids.Contains(p.Id))
		               .Select(f => attachmentRenderer.Render(f, true))
		               .ToListAsync();
	}

	internal async Task<List<AccountEntity>> GetAccountsAsync(List<User> users, User? localUser)
	{
		if (users.Count == 0) return [];
		return (await userRenderer.RenderManyAsync(users.DistinctBy(p => p.Id), localUser)).ToList();
	}

	private async Task<List<string>> GetLikedNotesAsync(List<Note> notes, User? user)
	{
		if (user == null) return [];
		if (notes.Count == 0) return [];
		return await db.NoteLikes.Where(p => p.User == user && notes.Contains(p.Note))
		               .Select(p => p.NoteId)
		               .ToListAsync();
	}

	public async Task<List<ReactionEntity>> GetReactionsAsync(List<Note> notes, User? user)
	{
		if (notes.Count == 0) return [];
		var counts = notes.ToDictionary(p => p.Id, p => p.Reactions);
		var res = await db.NoteReactions
		                  .Where(p => notes.Contains(p.Note))
		                  .GroupBy(p => new { p.NoteId, p.Reaction })
		                  .Select(p => new ReactionEntity
		                  {
			                  NoteId    = p.First().NoteId,
			                  Count     = (int)counts[p.First().NoteId].GetValueOrDefault(p.First().Reaction, 1),
			                  Name      = p.First().Reaction,
			                  Url       = null,
			                  StaticUrl = null,
			                  Me = user != null &&
			                       db.NoteReactions.Any(i => i.NoteId == p.First().NoteId &&
			                                                 i.Reaction == p.First().Reaction &&
			                                                 i.User == user),
			                  AccountIds = db.NoteReactions
			                                 .Where(i => i.NoteId == p.First().NoteId &&
			                                             p.Select(r => r.Id).Contains(i.Id))
			                                 .Select(i => i.UserId)
			                                 .ToList()
		                  })
		                  .ToListAsync();

		foreach (var item in res.Where(item => item.Name.StartsWith(':')))
		{
			var hit = await emojiSvc.ResolveEmojiAsync(item.Name);
			if (hit == null) continue;
			item.Url       = hit.GetAccessUrl(config.Value);
			item.StaticUrl = hit.GetAccessUrl(config.Value);
			item.Name      = item.Name.Trim(':');
		}

		return res;
	}

	private async Task<List<string>> GetBookmarkedNotesAsync(List<Note> notes, User? user)
	{
		if (user == null) return [];
		if (notes.Count == 0) return [];
		return await db.NoteBookmarks.Where(p => p.User == user && notes.Contains(p.Note))
		               .Select(p => p.NoteId)
		               .ToListAsync();
	}

	private async Task<List<string>> GetMutedNotesAsync(List<Note> notes, User? user)
	{
		if (user == null) return [];
		if (notes.Count == 0) return [];
		var ids = notes.Select(p => p.ThreadId).Distinct();
		return await db.NoteThreadMutings.Where(p => p.User == user && ids.Contains(p.ThreadId))
		               .Select(p => p.ThreadId)
		               .ToListAsync();
	}

	private async Task<List<string>> GetPinnedNotesAsync(List<Note> notes, User? user)
	{
		if (user == null) return [];
		if (notes.Count == 0) return [];
		return await db.UserNotePins.Where(p => p.User == user && notes.Contains(p.Note))
		               .Select(p => p.NoteId)
		               .ToListAsync();
	}

	private async Task<List<string>> GetRenotesAsync(List<Note> notes, User? user)
	{
		if (user == null) return [];
		if (notes.Count == 0) return [];
		return await db.Notes.Where(p => p.User == user && p.IsPureRenote && notes.Contains(p.Renote!))
		               .Select(p => p.RenoteId)
		               .Where(p => p != null)
		               .Distinct()
		               .Cast<string>()
		               .ToListAsync();
	}

	private async Task<List<PollEntity>> GetPollsAsync(List<Note> notes, User? user)
	{
		if (notes.Count == 0) return [];
		var polls = await db.Polls.Where(p => notes.Contains(p.Note))
		                    .ToListAsync();

		return await pollRenderer.RenderManyAsync(polls, user).ToListAsync();
	}

	private async Task<List<EmojiEntity>> GetEmojiAsync(IEnumerable<Note> notes)
	{
		var ids = notes.SelectMany(p => p.Emojis).ToList();
		if (ids.Count == 0) return [];

		return await db.Emojis
		               .Where(p => ids.Contains(p.Id))
		               .Select(p => new EmojiEntity
		               {
			               Id              = p.Id,
			               Shortcode       = p.Name.Trim(':'),
			               Url             = p.GetAccessUrl(config.Value),
			               StaticUrl       = p.GetAccessUrl(config.Value), //TODO
			               VisibleInPicker = true,
			               Category        = p.Category
		               })
		               .ToListAsync();
	}

	private async Task<List<Filter>> GetFiltersAsync(User? user, Filter.FilterContext? filterContext)
	{
		return filterContext == null
			? await db.Filters.Where(p => p.User == user).ToListAsync()
			: await db.Filters.Where(p => p.User == user && p.Contexts.Contains(filterContext.Value)).ToListAsync();
	}

	public async Task<IEnumerable<StatusEntity>> RenderManyAsync(
		IEnumerable<Note> notes, User? user, Filter.FilterContext? filterContext = null,
		List<AccountEntity>? accounts = null
	)
	{
		var noteList = notes.ToList();
		if (noteList.Count == 0) return [];

		var allNotes = noteList.SelectMany<Note, Note?>(p => [p, p.Renote, p.Renote?.Renote])
		                       .OfType<Note>()
		                       .Distinct()
		                       .ToList();

		var data = new NoteRendererDto
		{
			Accounts        = accounts ?? await GetAccountsAsync(allNotes.Select(p => p.User).ToList(), user),
			Mentions        = await GetMentionsAsync(allNotes),
			Attachments     = await GetAttachmentsAsync(allNotes),
			Polls           = await GetPollsAsync(allNotes, user),
			LikedNotes      = await GetLikedNotesAsync(allNotes, user),
			BookmarkedNotes = await GetBookmarkedNotesAsync(allNotes, user),
			MutedNotes      = await GetMutedNotesAsync(allNotes, user),
			PinnedNotes     = await GetPinnedNotesAsync(allNotes, user),
			Renotes         = await GetRenotesAsync(allNotes, user),
			Emoji           = await GetEmojiAsync(allNotes),
			Reactions       = await GetReactionsAsync(allNotes, user),
			Filters         = await GetFiltersAsync(user, filterContext)
		};

		return await noteList.Select(p => RenderAsync(p, user, filterContext, data)).AwaitAllAsync();
	}

	public class NoteRendererDto
	{
		public List<AccountEntity>?    Accounts;
		public List<AttachmentEntity>? Attachments;
		public List<string>?           BookmarkedNotes;
		public List<EmojiEntity>?      Emoji;
		public List<Filter>?           Filters;
		public List<string>?           LikedNotes;
		public List<MentionEntity>?    Mentions;
		public List<string>?           MutedNotes;
		public List<string>?           PinnedNotes;
		public List<PollEntity>?       Polls;
		public List<ReactionEntity>?   Reactions;
		public List<string>?           Renotes;

		public bool Source;
	}
}
