@page "/article/{id:guid}"
@page "/{year:int:min(1)}/{month:int:range(1,12)}/{day:int:range(1,31)}/{titleEncoded}"
@using Microsoft.EntityFrameworkCore
@using Wave.Data
@using System.Security.Claims
@using System.Globalization
@using System.Net
@using Microsoft.AspNetCore.Identity
@using Microsoft.Extensions.Options
@using Wave.Services
@using Wave.Utilities

@inject ILogger<ArticleView> Logger
@inject IDbContextFactory<ApplicationDbContext> ContextFactory
@inject NavigationManager Navigation
@inject IOptions<Customization> Customizations
@inject IOptions<Features> Features
@inject IStringLocalizer<ArticleView> Localizer
@inject IMessageDisplay Message
@inject UserManager<ApplicationUser> UserManager
@inject EmailFactory Email
@inject IEmailService EmailService

<PageTitle>@((Article?.Title ?? Localizer["NotFound_Title"]) + TitlePostfix)</PageTitle>

@if (Article is not null) {
	<HeadContent>
		<meta name="author" content="@Article.Author.Name">
		<meta name="description" content="@string.Format(Localizer["Meta_Description"], Customizations.Value.AppName, Article.BodyPlain[..Math.Min(80, Article.BodyPlain.Length)] + "... ")">
		
		<!-- Open Graph -->
		<meta property="og:title" content="@Article.Title">
		<meta property="og:description" content="@string.Format(Localizer["Meta_Description"], Customizations.Value.AppName, Article.BodyPlain[..Math.Min(80, Article.BodyPlain.Length)] + "... ")">
		<meta property="og:url" content="@Navigation.ToAbsoluteUri("/article/" + Article.Id)">
		<meta property="og:image" content="@Navigation.ToAbsoluteUri("/api/user/pfp/" + Article.Author.Id)">
		<meta property="og:type" content="article">
		<meta property="og:article:author" content="@Article.Author.Name">
		<meta property="og:article:published_time" content="@Article.PublishDate.ToString("u")">
		@if (Article.LastModified.HasValue) {
			<meta property="og:article:modified_time" content="@Article.LastModified.Value.ToString("u")">
		}
		<meta property="og:site_name" content="@Customizations.Value.AppName">
		@if (Features.Value.Rss) { 
			<link rel="alternate" type="application/rss+xml" title="RSS Feed on @Customizations.Value.AppName" href="/rss/rss.xml">
			<link rel="alternate" type="application/atom+xml" title="Atom RSS Feed on @Customizations.Value.AppName" href="/rss/atom.xml">
		}
		
		@if (Article is not null) {
			<link rel="canonical" href="@ArticleUtilities.GenerateArticleLink(Article, new Uri(Navigation.BaseUri))" />
		}
	</HeadContent>
}

<ErrorBoundary>
	<ChildContent>
		@if (GetArticle(HttpContext.User) is {} article) {
			<ArticleComponent Article="@article">
				<AdditionalContent>
					@if (Recommendations.Count > 0) {
						<h2 class="text-3xl my-6">@Localizer["Recommendations_Title"]</h2>
						<ArticleCardList Articles="Recommendations" />
					}
				</AdditionalContent>
			</ArticleComponent>
			
			
			<div class="flex gap-2 mt-3 flex-wrap">
				@if (article.AllowedToEdit(HttpContext.User)) {
					<a class="btn btn-info w-full sm:btn-wide" href="article/@Article!.Id/edit"
					   data-enhance-nav="false">@Localizer["Edit"]</a>
				}
				@if (article.AllowedToDelete(HttpContext.User)) {
					<a class="btn btn-error w-full sm:btn-wide" href="/article/@article.Id/delete">
						@Localizer["Delete_Submit"]
					</a>
				}
				@if (article.AllowedToRejectReview(HttpContext.User)) {
					<form @formname="reject-review" method="post" @onsubmit="RejectReview" class="max-sm:w-full">
						<AntiforgeryToken />
						<button type="submit" class="btn btn-error w-full sm:btn-wide">
							@Localizer["Review_Reject"]
						</button>
					</form>
				}
				@if (article.AllowedToSubmitForReview(HttpContext.User)) {
					<form @formname="submit-for-review" method="post" @onsubmit="SubmitForReview" class="max-sm:w-full">
						<AntiforgeryToken/>
						
						@if (Reviewers.Count > 1) {
							<div class="join join-vertical md:join-horizontal w-full">
								<button type="submit" class="btn btn-primary flex-1 sm:btn-wide join-item">
									@Localizer["Review_Submit"]
								</button>
								<InputSelect @bind-Value="ReviewerId" class="select select-bordered select-primary join-item">
									<option value="" selected>@Localizer["Review_Reviewer_Any"]</option>
									@foreach (var reviewer in Reviewers) {
										if (reviewer.FullName is null) continue;

										<option value="@reviewer.Id">
											@reviewer.Name
										</option>
									}
								</InputSelect>
							</div>
						} else {
							<button type="submit" class="btn btn-primary w-full sm:btn-wide">
								@Localizer["Review_Submit"]
							</button>
						}

					</form>
				}
				@if (article.AllowedToPublish(HttpContext.User)) {
					<form @formname="submit-for-publish" method="post" @onsubmit="SubmitForPublish" class="max-sm:w-full">
						<AntiforgeryToken/>
						<button type="submit" class="btn btn-primary w-full sm:btn-wide">@Localizer["Publish_Submit"]</button>
						@if (Features.Value.EmailSubscriptions && HttpContext.User.IsInRole("Admin")) {
							<div class="form-control">
								<label class="label cursor-pointer">
									<span class="label-text">@Localizer["Publish_Silent_Label"]</span>
									<InputCheckbox @bind-Value="PublishSilently" class="checkbox"/>
								</label>
							</div>
						}
					</form>
				}
			</div>
		}
	</ChildContent>
	<ErrorContent>
		<h1 class="text-3xl lg:text-5xl font-light mb-6 text-primary">@Localizer["NotFound_Title"]</h1>
		<p class="my-3">@Localizer["NotFound_Description"]</p>
		<a class="btn btn-primary" href="/">@Localizer["NotFound_BackToHome_Label"]</a>
		@if (Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT")?.ToLower() == "development") {
			<p class="mt-3">[DEBUG] EXCEPTION MESSAGE: @context.Message</p>
		}
	</ErrorContent>
</ErrorBoundary>

@code {
	[CascadingParameter(Name = "TitlePostfix")]
	private string TitlePostfix { get; set; } = default!;

	#region Route Parameters
	
	[Parameter]
	public Guid? Id { get; set; }
	[Parameter]
	public int? Year { get; set; }
	[Parameter]
	public int? Month { get; set; }
	[Parameter]
	public int? Day { get; set; }
	[Parameter]
	public string? TitleEncoded { get; set; }
	
	private DateTimeOffset? Date => 
		Year is {} y && Month is {} m && Day is {} d 
			? new DateTimeOffset(new DateTime(y, m, d)) : 
			null;
	private string? Title => TitleEncoded is null ? null : Uri.UnescapeDataString(TitleEncoded.Replace("-", "%20").Replace("+", "-"));

	#endregion
	private Article? Article { get; set; }

	[Parameter, SupplyParameterFromForm(FormName = "submit-for-publish")]
	public bool PublishSilently { get; set; }

	[Parameter, SupplyParameterFromForm(FormName = "submit-for-review")]
	public string ReviewerId { get; set; } = string.Empty;

	[CascadingParameter] 
	public HttpContext HttpContext { get; set; } = default!;

	private List<ApplicationUser> Reviewers { get; } = [];
	private List<Article> Recommendations { get; } = [];

	private Article GetArticle(ClaimsPrincipal principal) {
		if (Article.AllowedToRead(principal)) return Article!;
	
		throw new ApplicationException("Article not found or missing permissions.");
	}

	protected override void OnInitialized() {
		using var context = ContextFactory.CreateDbContext();
		var query = context.Set<Article>()
			.IgnoreQueryFilters().Where(a => !a.IsDeleted)
			.Include(a => a.Author)
			.Include(a => a.Reviewer)
			.Include(a => a.Categories);

		// We need blocking calls here, bc otherwise Blazor will execute Render in parallel,
		// running into a null pointer on the Article property and panicking
		if (Id is not null) {
			Article = query.AsSingleQuery().FirstOrDefault(a => a.Id == Id);
		} else if (Date is { } date && Title is { } title) {
			string? slug = TitleEncoded == null ? null : Uri.EscapeDataString(TitleEncoded.Replace("-", " ")).Replace("%20", "-");
			Article = query.AsSingleQuery().FirstOrDefault(a =>
				a.PublishDate.Date == date.Date 
				&& (slug != null && a.Slug == slug || a.Title.ToLower() == title));
		}

		if (Article is null) {
			HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound;
		}
	}

	protected override async Task OnInitializedAsync() {
		if (Article is null) return;
		
		if (Article.Status != ArticleStatus.Published) {
			Reviewers.AddRange([
				.. await UserManager.GetUsersInRoleAsync("Reviewer"),
				.. await UserManager.GetUsersInRoleAsync("Admin")
			]);
		} else {
			var primaryCategories = Article.Categories.Where(c => c.Color == CategoryColors.Primary).ToArray();

			await using var context = await ContextFactory.CreateDbContextAsync();
			const int featuredArticleCount = 3;
			
			// See if we can find 3 articles with the same primary category
			if (primaryCategories.Length > 0) {
				foreach (var category in primaryCategories) {
					Recommendations.AddRange(await context.Set<Article>()
						.Include(a => a.Author)
						.Include(a => a.Categories)
						.OrderByDescending(a => a.PublishDate).ThenBy(a => a.Id)
						.Where(a => a.Categories.Contains(category) && a.Id != Article.Id)
						.Take(featuredArticleCount - Recommendations.Count)
						.ToListAsync());

					if (Recommendations.Count >= featuredArticleCount) break;
				}
			}

			// Fill up with the newest articles of possible 
			if (Recommendations.Count < featuredArticleCount) {
				Recommendations.AddRange(await context.Set<Article>()
					.Include(a => a.Author)
					.Include(a => a.Categories)
					.OrderByDescending(a => a.PublishDate).ThenBy(a => a.Id)
					.Where(a => a.Id != Article.Id)
					.Take(featuredArticleCount - Recommendations.Count).ToListAsync());
			}

			Recommendations.Sort((a1, a2) => a2.PublishDate.CompareTo(a1.PublishDate));
		}
	}

	private async Task SubmitForReview() {
		if (Article.AllowedToSubmitForReview(HttpContext.User) is false) return;

		await using var context = await ContextFactory.CreateDbContextAsync();
		Article!.Status = ArticleStatus.InReview;

		if (!string.IsNullOrWhiteSpace(ReviewerId)) {
			Article.Reviewer = Reviewers.First(r => r.Id == ReviewerId);
		}

		context.Update(Article);
		await context.SaveChangesAsync();
		Message.ShowSuccess(Localizer["Submit_Review_Success"]);

		try {
			if (Reviewers.Count > 0) {
				await EmailService.ConnectAsync(CancellationToken.None);

				if (!string.IsNullOrWhiteSpace(ReviewerId) && Article.Reviewer is not null) {
					if (Article.Reviewer.Id != HttpContext.User.FindFirst("Id")!.Value) {
						var email = await Email.CreateDefaultEmail(
							Article.Reviewer.Email!,
							Article.Reviewer.Name,
							"Article submitted for Review",
							"Article submitted for Review",
							$"<p>The Article '{Article.Title}' by {Article.Author.Name} has been submitted for review.</p>",
							$"The Article '{Article.Title}' by {Article.Author.Name} has been submitted for review.");
						// TODO check if they enabled email notifications (property currently not implemented)
						await EmailService.SendEmailAsync(email);
					}
				} else {
					foreach (var reviewer in Reviewers) {
						if (reviewer.Id == HttpContext.User.FindFirst("Id")!.Value) continue;

						var email = await Email.CreateDefaultEmail(
							reviewer.Email!, 
							reviewer.Name, 
							"Article submitted for Review", 
							"Article submitted for Review", 
							$"<p>The Article '{Article.Title}' by {Article.Author.Name} has been submitted for review.</p>",
							$"The Article '{Article.Title}' by {Article.Author.Name} has been submitted for review.");
						// TODO check if they enabled email notifications (property currently not implemented)
						await EmailService.SendEmailAsync(email);
					}
				}

				await EmailService.DisconnectAsync(CancellationToken.None);
			}
		} catch (Exception ex) {
			Logger.LogError(ex, "Failed to send mail to reviewers about article '{title}'.", Article.Title);
		}

		Navigation.NavigateTo("/");
	}

	private async Task RejectReview() {
		if (Article.AllowedToRejectReview(HttpContext.User) is false) return;

		await using var context = await ContextFactory.CreateDbContextAsync();
		Article!.Status = ArticleStatus.Draft;
		string userId = HttpContext.User.FindFirst("Id")!.Value;
		if (Article.Author.Id != userId) {
			Article.Reviewer = await context.Users.FindAsync(userId);
		}

		context.Update(Article);
		await context.SaveChangesAsync();

		try {
			var author = Article.Author;

			string message = 
				$"The Article '{Article.Title}' has been rejected by a Reviewer, you will find it in your drafts.\n" +
				$"Please make appropriate changes before submitting it again.";
			if (author.Id != HttpContext.User.FindFirst("Id")!.Value) {
				await EmailService.ConnectAsync(CancellationToken.None);

				var email = await Email.CreateDefaultEmail(
					author.Email!,
					author.Name,
					"Review Rejected",
					"Your Article has been reject",
					$"<p>{message}</p>",
					message);
				// TODO check if they enabled email notifications (property currently not implemented)
				await EmailService.SendEmailAsync(email);

				await EmailService.DisconnectAsync(CancellationToken.None);
			}
		} catch (Exception ex) {
			Logger.LogError(ex, "Failed to send mail to author about article '{title}' being rejected.", Article.Title);
		}

		Navigation.NavigateTo("/");
	}

	private async Task SubmitForPublish() {
		if (Article.AllowedToPublish(HttpContext.User) is false) return;

		await using var context = await ContextFactory.CreateDbContextAsync();
		Article!.Status = ArticleStatus.Published;
		string userId = HttpContext.User.FindFirst("Id")!.Value;
		if (Article.Author.Id != userId) {
			Article.Reviewer = await context.Users.FindAsync(userId);
		}
		context.Update(Article);
		await context.SaveChangesAsync();

		if (Features.Value.EmailSubscriptions) {
			try {
				var newsletter = new EmailNewsletter {
					Article = Article,
					DistributionDateTime = Article.PublishDate,
					IsSend = PublishSilently
				};
				await context.Set<EmailNewsletter>().AddAsync(newsletter);
				await context.SaveChangesAsync();
			} catch (Exception ex) {
				Logger.LogError(ex, "Failed to schedule article {name} for E-Mail distribution.", Article.Title);
			}
		}

		try {
			var author = Article.Author;

			if (author.Id != HttpContext.User.FindFirst("Id")!.Value) {
				await EmailService.ConnectAsync(CancellationToken.None);

				string publishMessage = 
					(Article.PublishDate < DateTimeOffset.Now) ?
						"Is is now publicly available." : 
						$"It is currently scheduled for {Article.PublishDate.ToString("f", CultureInfo.GetCultureInfo("en-US"))}.";

				var email = await Email.CreateDefaultEmail(
					author.Email!, 
					author.Name,
					"Article has been approved", 
					"Your Article is going to be published", 
					$"<p>The Article '{Article.Title}' has been checked and approved by a reviewer.</p>" + 
					$"<p>{publishMessage}</p>",
					$"The Article '{Article.Title}' has been checked and approved by a reviewer. " +
					publishMessage);
				// TODO check if they enabled email notifications (property currently not implemented)
				await EmailService.SendEmailAsync(email);

				await EmailService.DisconnectAsync(CancellationToken.None);
			}
		} catch (Exception ex) {
			Logger.LogError(ex, "Failed to send mail to author about article '{title}' being published.", Article.Title);
		}

		Navigation.NavigateTo("/");
	}


}
