@using Wave.Data
@using System.ComponentModel.DataAnnotations
@using System.Diagnostics.CodeAnalysis
@using System.Net
@using System.Security.Claims
@using Microsoft.AspNetCore.Identity
@using Microsoft.EntityFrameworkCore
@using Wave.Services
@using Wave.Utilities

@inject ILogger<ArticleEditor> Logger
@inject NavigationManager Navigation
@inject UserManager<ApplicationUser> UserManager
@inject IDbContextFactory<ApplicationDbContext> ContextFactory
@inject IStringLocalizer<ArticleEditor> Localizer
@inject IMessageDisplay Message
@inject ImageService Images

<div class="w-full">
	<ul class="steps w-full max-w-xs">
		<li class="step @(Article.Status >= ArticleStatus.Draft ? "step-secondary" : "")">@Localizer["Draft"]</li>
		<li class="step @(Article.Status >= ArticleStatus.InReview ? "step-secondary" : "")">@Localizer["InReview"]</li>
		<li class="step @(Article.Status >= ArticleStatus.Published ? "step-secondary" : "")">@Localizer["Published"]</li>
	</ul>
</div>

<EditForm method="post" FormName="article-editor" Model="@Model" OnValidSubmit="OnValidSubmit">
	<DataAnnotationsValidator/>
	<input type="hidden" @bind-value="@Model.Id"/>

	<div class="grid grid-cols-1 lg:grid-cols-2 gap-x-8">
		<InputLabelComponent LabelText="@Localizer["Title_Label"]" For="() => Model.Title">
			<InputText class="input input-bordered w-full" maxlength="256" required aria-required
			           @bind-Value="@Model.Title" placeholder="@Localizer["Title_Placeholder"]" autocomplete="off"/>
		</InputLabelComponent>
		
		<InputLabelComponent LabelText="@Localizer["Slug_Label"]" For="() => Model.Slug">
			@if (Article.Status is not ArticleStatus.Published || Article.PublishDate >= DateTimeOffset.UtcNow) {
				<InputText class="input input-bordered w-full" maxlength="64"
				           @bind-Value="@Model.Slug" placeholder="@Localizer["Slug_Placeholder"]" autocomplete="off"/>
			} else {
				<input class="input input-bordered w-full" readonly value="@Model.Slug"
				       placeholder="@Localizer["Slug_Placeholder"]" autocomplete="off" />
			}
		</InputLabelComponent>

		<InputLabelComponent LabelText="@Localizer["PublishDate_Label"]" For="() => Model.PublishDate">
			@if (Article.Status is not ArticleStatus.Published || Article.PublishDate >= DateTimeOffset.UtcNow) {
				<InputDate class="input input-bordered w-full" Type="InputDateType.DateTimeLocal"
				           @bind-Value="@Model.PublishDate" placeholder="@Localizer["PublishDate_Placeholder"]" autocomplete="off"/>
			} else {
				<input class="input input-bordered w-full"
				       type="datetime-local" readonly value="@Article.PublishDate.ToString("yyyy-MM-dd\\THH:mm:ss")"/>
			}
		</InputLabelComponent>
		<InputLabelComponent LabelText="@Localizer["Categories_Label"]" For="() => Model.Categories">
			<select class="select select-bordered w-full h-32" @onchange="CategorySelectionChanged" multiple>
				@foreach (var category in Categories) {
					<option value="@category.Id" selected="@Model.Categories?.Contains(category.Id)">@category.Name</option>
				}
			</select>
		</InputLabelComponent>
	</div>

	<AdvancedMarkdownEditor Title="@Model.Title" MarkdownCallback="() => Model.Body">
		<textarea id="tool-target" class="textarea textarea-bordered outline-none w-full flex-1 join-item" 
				  required aria-required placeholder="@Localizer["Body_Placeholder"]"
				  @bind="@Model.Body" @bind:event="oninput" autocomplete="off"></textarea>
	</AdvancedMarkdownEditor>

	<div class="flex gap-2 flex-wrap mt-3">
		<button type="submit" class="btn btn-primary w-full sm:btn-wide @(Saving ? "btn-loading" : "")" disabled="@Saving">
			@Localizer["EditorSubmit"]
		</button>
		@if (Article.Id != Guid.Empty) {
			<a class="btn w-full sm:btn-wide" href="@ArticleUtilities.GenerateArticleLink(Article, null)">
				@Localizer["ViewArticle_Label"]
			</a>
		}
	</div>
</EditForm>

<ImageModal Id="@ImageModal" ImageAdded="ImageAdded" />
<div class="my-3 flex flex-wrap gap-4 min-h-24">
	@foreach (var image in Article.Images) {
		<figure class="p-2 bg-base-200 relative">
			<img class="w-40" src="/images/@(image.Id)?size=400" width="400"
			     title="@image.ImageDescription" alt="@image.ImageDescription"/>
			<figcaption>
				<button type="button" class="btn btn-info w-full mt-3"
				        onclick="navigator.clipboard.writeText('![@image.ImageDescription](@(Navigation.ToAbsoluteUri("/images/" + image.Id))?size=400)')">
					@Localizer["Image_CopyLink"]
				</button>
			</figcaption>
			<button type="button" class="btn btn-square btn-sm btn-error absolute top-0 right-0" 
			        title="@Localizer["Image_Delete_Label"]"
			        @onclick="async () => await ImageDelete(image)">
				<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5">
					<path fill-rule="evenodd" d="M8.75 1A2.75 2.75 0 0 0 6 3.75v.443c-.795.077-1.584.176-2.365.298a.75.75 0 1 0 .23 1.482l.149-.022.841 10.518A2.75 2.75 0 0 0 7.596 19h4.807a2.75 2.75 0 0 0 2.742-2.53l.841-10.52.149.023a.75.75 0 0 0 .23-1.482A41.03 41.03 0 0 0 14 4.193V3.75A2.75 2.75 0 0 0 11.25 1h-2.5ZM10 4c.84 0 1.673.025 2.5.075V3.75c0-.69-.56-1.25-1.25-1.25h-2.5c-.69 0-1.25.56-1.25 1.25v.325C8.327 4.025 9.16 4 10 4ZM8.58 7.72a.75.75 0 0 0-1.5.06l.3 7.5a.75.75 0 1 0 1.5-.06l-.3-7.5Zm4.34.06a.75.75 0 1 0-1.5-.06l-.3 7.5a.75.75 0 1 0 1.5.06l.3-7.5Z" clip-rule="evenodd" />
				</svg>
			</button>
		</figure>
	}
	<button type="button" class="btn" onclick="@(ImageModal).showModal()">@Localizer["Image_Add_Label"]</button>
</div>

@code {
	private const string ImageModal = "AddImage";

	[Parameter] 
    public Guid? Id { get; set; }
	[Parameter] 
    public required ApplicationUser User { get; set; }
	[Parameter]
	public required ClaimsPrincipal ClaimsUser { get; set; }

	[SupplyParameterFromForm] 
    private InputModel Model { get; set; } = new();
	
	private Article Article { get; set; } = default!;
	private List<Category> Categories { get; set; } = [];
	private bool Saving { get; set; }

	protected override void OnInitialized() {
		// ReSharper disable once NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract
		Article ??= new Article {Author = User, Title = "", Body = ""};
	}

	protected override async Task OnInitializedAsync() {
		await using var context = await ContextFactory.CreateDbContextAsync();
		if (Categories.Count < 1)
			Categories = await context.Set<Category>().IgnoreQueryFilters().OrderBy(c => c.Color).ToListAsync();

		Article? article = null;
		if (Id is not null) {
			// Ensure we are not double-tracking User on existing articles, since 
			// a different context loaded this user originally 
			context.Entry(User).State = EntityState.Unchanged;

			article = await context.Set<Article>()
				.IgnoreQueryFilters().Where(a => !a.IsDeleted)
				.Include(a => a.Author)
				.Include(a => a.Reviewer)
				.Include(a => a.Categories)
				.Include(a => a.Images)
				.FirstAsync(a => a.Id == Id);
			if (article is null) throw new ApplicationException("Article not found.");
		}

		if (article is not null) {
			if (!article.AllowedToEdit(ClaimsUser))
				throw new ApplicationException("You are not allowed to edit this article");

			Model.Id ??= article.Id;
			Model.Title ??= article.Title;
			Model.Slug ??= article.Slug;
			Model.Body ??= article.Body;
			Model.PublishDate ??= article.PublishDate.LocalDateTime;
			Model.Categories ??= article.Categories.Select(c => c.Id).ToArray();
			Article = article;
			await InvokeAsync(StateHasChanged);
		}
	}

	private async Task OnValidSubmit() {
		if (Article.AllowedToEdit(ClaimsUser) is false) {
			Message.ShowError("Permission denied.");
			return;
		}
		try {
			Saving = true;
			
			// Double check user permissions
			await HandleRoles(Article, User);
			
			if (Model.Title is not null) Article.Title = Model.Title;
			if (Model.Body is not null) Article.Body = Model.Body;
			if (Model.PublishDate is not null && 
			    (Article.Status is not ArticleStatus.Published || Article.PublishDate >= DateTimeOffset.UtcNow))
				Article.PublishDate = Model.PublishDate.Value;
			if (Article.Status is ArticleStatus.Published && Article.PublishDate < DateTimeOffset.Now) {
				// can't change slugs when the article is public
			} else if (!string.IsNullOrWhiteSpace(Model.Slug)) {
				Article.Slug = WebUtility.UrlEncode(Model.Slug);
			} else if (string.IsNullOrWhiteSpace(Article.Slug)) {
				Article.Slug = Uri.EscapeDataString(Article.Title.ToLowerInvariant())
					.Replace("-", "+")
					.Replace("%20", "-");
				Article.Slug = Article.Slug[..Math.Min(64, Article.Slug.Length)];
				Model.Slug = Article.Slug;
			}

			Article.LastModified = DateTimeOffset.UtcNow;
			Article.BodyHtml = MarkdownUtilities.Parse(Article.Body);
			Article.BodyPlain = HtmlUtilities.GetPlainText(Article.BodyHtml);
			
			await using var context = await ContextFactory.CreateDbContextAsync();

			// Update Newsletter distribution if exists
			var newsletter = await context.Set<EmailNewsletter>()
				.IgnoreQueryFilters().IgnoreAutoIncludes()
				.FirstOrDefaultAsync(n => n.Article == Article);
			if (newsletter is not null) {
				newsletter.DistributionDateTime = Article.PublishDate;
			}
			
			// Avoid unnecessary updates 
			context.Entry(User).State = EntityState.Unchanged;
			Categories.ForEach(c => context.Entry(c).State = EntityState.Unchanged);
			await context.Set<ArticleCategory>().IgnoreQueryFilters().IgnoreAutoIncludes()
				.Where(ac => ac.Article.Id == Article.Id).LoadAsync();

			Model.Categories ??= [];
			context.Update(Article);

			var existingImages = await context.Set<Article>()
				.IgnoreQueryFilters().Where(a => a.Id == Article.Id)
				.AsNoTrackingWithIdentityResolution()
				.SelectMany(a => a.Images).ToListAsync();
			foreach (var image in Article.Images) {
				int index = existingImages.FindIndex(a => a.Id == image.Id);
				context.Entry(image).State = index > -1 ? EntityState.Modified : EntityState.Added;
				if(index > -1) existingImages.RemoveAt(index);
			}
			foreach (var image in existingImages) {
				context.Entry(image).State = EntityState.Deleted;
			}
			
			var relations = await context.Set<ArticleCategory>()
				.IgnoreQueryFilters().IgnoreAutoIncludes()
				.Where(ac => ac.Article == Article && !Model.Categories.Contains(ac.Category.Id))
				.ToListAsync();
			context.RemoveRange(relations);
			
			foreach (var category in Model.Categories) {
				if (Article.Categories.Any(c => c.Id == category) is not true) {
					context.Add(new ArticleCategory {
						Article = Article,
						Category = Categories.First(c => c.Id == category)
					});
				}
			}

			await context.SaveChangesAsync();
			foreach (var image in existingImages) {
				try {
					Images.Delete(image.Id);
				} catch (Exception ex) {
					Logger.LogWarning(ex, "Failed to delete image: {image}", image.Id);
				}
			}
			Message.ShowSuccess(Localizer["Save_Success"]);
		} catch (Exception ex) {
			Message.ShowError(Localizer["Save_Error"]);
			Logger.LogError(ex, "Failed to save article.");
		} finally {
			Saving = false;
			await InvokeAsync(StateHasChanged);
		}
	}

	[SuppressMessage("ReSharper", "ConvertIfStatementToSwitchStatement")]
	private async Task HandleRoles(Article article, ApplicationUser me) {
		// it's our draft
		if (article.Status is ArticleStatus.Draft && article.Author.Id == me.Id) return;

		var roles = await UserManager.GetRolesAsync(me);

		// reviewers and admins can review articles
		if (article.Status is ArticleStatus.InReview && roles.Any(r => r is "Admin" or "Reviewer")) {
			article.Reviewer = me;
			return;
		}

		// published articles may only be edited my admins or moderators
		if (article.Status is ArticleStatus.Published && roles.Any(r => r is "Admin" or "Reviewer")) {
			article.Reviewer = me; // TODO replace with editor or something?
			return;
		}

		throw new ApplicationException("You do not have permissions to edit this article");
	}

	private void CategorySelectionChanged(ChangeEventArgs args) {
		string[]? selected = (string[]?) args.Value;
		Model.Categories = selected?.Select(Guid.Parse).ToArray();
	}

	private async Task ImageAdded(ArticleImage image) {
		Article.Images.Add(image);
		await InvokeAsync(StateHasChanged);
	}

	private async Task ImageDelete(ArticleImage image) {
		Article.Images.Remove(image);
		await InvokeAsync(StateHasChanged);
	}

	private sealed class InputModel {
		public Guid? Id { get; set; }

		[Required(AllowEmptyStrings = false), MaxLength(256)]
		public string? Title { get; set; }
		[MaxLength(64)]
		public string? Slug { get; set; }
		[Required(AllowEmptyStrings = false)] 
		public string? Body { get; set; }

		public Guid[]? Categories { get; set; }
		public DateTimeOffset? PublishDate { get; set; }
	}

}