@using AliasVault.Shared.Models.Spamok
@using AliasVault.Shared.Utilities
@using AliasVault.Shared.Core;
@using AliasVault.Client.Main.Components.Layout
@using AliasVault.RazorComponents.Services
@using AliasClientDb.Models
@using Microsoft.EntityFrameworkCore
@using Microsoft.Extensions.Localization
@inject JsInteropService JsInteropService
@inject GlobalNotificationService GlobalNotificationService
@inject IHttpClientFactory HttpClientFactory
@inject EmailService EmailService
@inject HttpClient HttpClient
@inject ConfirmModalService ConfirmModalService
@inject IStringLocalizerFactory LocalizerFactory
@inject DbService DbService
@inject NavigationManager NavigationManager

<ClickOutsideHandler OnClose="OnClose" ContentId="emailModal,confirm-modal">
    <ModalWrapper OnEnter="Close">
        <div id="emailModal" class="relative bg-white dark:bg-gray-800 w-3/4 flex flex-col rounded-lg shadow-xl max-h-[90vh] border border-gray-200 dark:border-gray-700">
            <!-- Header -->
            <div class="p-4 border-b border-gray-200 dark:border-gray-700">
                <div class="flex items-center justify-between gap-3 mb-3">
                    <h2 class="text-2xl font-bold text-gray-900 dark:text-gray-100 truncate min-w-0">
                        @if (Email == null)
                        {
                            <SkeletonBase Height="28" AdditionalClasses="w-3/4">
                                <div class="w-full h-full bg-gray-300 dark:bg-gray-600 rounded"></div>
                            </SkeletonBase>
                        }
                        else if (IsSpamOk)
                        {
                            <a target="_blank" href="https://spamok.com/@(Email!.ToLocal)/@(Email!.Id)" class="hover:underline">@Email.Subject</a>
                        }
                        else
                        {
                            <span>@Email.Subject</span>
                        }
                    </h2>
                    <div class="flex items-center gap-1 flex-shrink-0">
                        @if (GetAvailableModes().Count > 1)
                        {
                            <ModalHeaderAction OnClick="CycleViewMode" Title="@SharedLocalizer["EmailFormatSwitchTitle"]">
                                @CurrentFormatLabel
                            </ModalHeaderAction>
                        }
                        @if (Email != null)
                        {
                            <ModalHeaderAction OnClick="ShowDeleteConfirmation" Title="@Localizer["DeleteButton"]" Variant="ModalHeaderAction.ActionVariant.Danger">
                                <svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
                                </svg>
                            </ModalHeaderAction>
                        }
                        <ModalHeaderAction OnClick="Close" Title="@SharedLocalizer["Close"]">
                            <svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
                            </svg>
                        </ModalHeaderAction>
                    </div>
                </div>
                <!-- 2-column on desktop; on mobile the columns stack and flow as one continuous list. The second column gets mt-1 on mobile so the gap between Date/Item and From/To matches the space-y-1 used within each block. -->
                <div class="grid grid-cols-1 sm:grid-cols-2 gap-x-4 text-sm text-gray-500 dark:text-gray-400">
                    @if (Email == null)
                    {
                        <div class="space-y-2">
                            <SkeletonBase Height="16" AdditionalClasses="w-1/2">
                                <div class="w-full h-full bg-gray-300 dark:bg-gray-600 rounded"></div>
                            </SkeletonBase>
                            <SkeletonBase Height="16" AdditionalClasses="w-1/2">
                                <div class="w-full h-full bg-gray-300 dark:bg-gray-600 rounded"></div>
                            </SkeletonBase>
                        </div>
                        <div class="space-y-2 mt-2 sm:mt-0">
                            <SkeletonBase Height="16" AdditionalClasses="w-1/3">
                                <div class="w-full h-full bg-gray-300 dark:bg-gray-600 rounded"></div>
                            </SkeletonBase>
                            <SkeletonBase Height="16" AdditionalClasses="w-1/4">
                                <div class="w-full h-full bg-gray-300 dark:bg-gray-600 rounded"></div>
                            </SkeletonBase>
                        </div>
                    }
                    else
                    {
                    <div class="space-y-1">
                        <p>@Localizer["FromLabel"] @(Email.FromLocal)@@@(Email.FromDomain)</p>
                        <p>@Localizer["ToLabel"] @(Email.ToLocal)@@@(Email.ToDomain)</p>
                    </div>
                    <div class="space-y-1 mt-1 sm:mt-0">
                        <p>@Localizer["DateLabel"] @Email.DateSystem</p>
                        @if (ShowItemLink)
                        {
                            @if (!string.IsNullOrEmpty(_credentialName) && _credentialId != Guid.Empty)
                            {
                                <p>
                                    <span class="font-medium">@SharedLocalizer["EmailItemLabel"]</span>
                                    <button @onclick="NavigateToCredential"
                                            class="text-blue-600 hover:underline dark:text-blue-400 cursor-pointer">
                                        @_credentialName
                                    </button>
                                </p>
                            }
                            else
                            {
                                <p>
                                    <span class="font-medium">@SharedLocalizer["EmailItemLabel"]</span>
                                    <span class="text-gray-400 dark:text-gray-500">@SharedLocalizer["EmailItemNone"]</span>
                                </p>
                            }
                        }
                    </div>
                    }
                </div>
            </div>

            <!-- Scrollable Content -->
            <div class="flex-1 overflow-y-auto p-4">
                <div class="text-gray-700 dark:text-gray-300">
                    @if (Email == null || _isBodyLoading)
                    {
                        <!-- Body skeleton: shown while the email loads and while the (synchronous, potentially heavy) HTML sanitize runs. -->
                        <div class="space-y-3" style="height:500px;">
                            <SkeletonBase Height="16" AdditionalClasses="w-11/12">
                                <div class="w-full h-full bg-gray-300 dark:bg-gray-600 rounded"></div>
                            </SkeletonBase>
                            <SkeletonBase Height="16" AdditionalClasses="w-full">
                                <div class="w-full h-full bg-gray-300 dark:bg-gray-600 rounded"></div>
                            </SkeletonBase>
                            <SkeletonBase Height="16" AdditionalClasses="w-10/12">
                                <div class="w-full h-full bg-gray-300 dark:bg-gray-600 rounded"></div>
                            </SkeletonBase>
                            <SkeletonBase Height="16" AdditionalClasses="w-5/6">
                                <div class="w-full h-full bg-gray-300 dark:bg-gray-600 rounded"></div>
                            </SkeletonBase>
                            <SkeletonBase Height="16" AdditionalClasses="w-2/3">
                                <div class="w-full h-full bg-gray-300 dark:bg-gray-600 rounded"></div>
                            </SkeletonBase>
                        </div>
                    }
                    else
                    {
                        <div>
                            <iframe class="w-full overscroll-y-auto bg-white rounded" style="height:500px;" srcdoc="@EmailBody" sandbox="allow-popups allow-popups-to-escape-sandbox">
                            </iframe>
                        </div>
                    }
                </div>
                <div class="mt-4">
                    @if (Email?.Attachments?.Any() == true)
                    {
                        <div class="border-t border-gray-200 dark:border-gray-600 pt-4">
                            <h3 class="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">@Localizer["AttachmentsLabel"]</h3>
                            <div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
                                @foreach (var attachment in Email.Attachments)
                                {
                                    <div class="flex items-center space-x-2">
                                        <svg class="w-4 h-4 text-gray-500 dark:text-gray-400 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
                                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.172 7l-6.586 6.586a2 2 0 102.828 2.828l6.414-6.586a4 4 0 00-5.656-5.656l-6.415 6.585a6 6 0 108.486 8.486L20.5 13"></path>
                                        </svg>
                                        <button @onclick="() => DownloadAttachment(attachment)"
                                                class="text-primary-600 hover:underline text-sm truncate dark:text-primary-400 attachment-link">
                                            (@(Math.Ceiling((double)attachment.Filesize / 1024)) KB) @attachment.Filename
                                        </button>
                                    </div>
                                }
                            </div>
                        </div>
                    }
                </div>
            </div>
        </div>
    </ModalWrapper>
</ClickOutsideHandler>

@code {
    private IStringLocalizer Localizer => LocalizerFactory.Create("Components.Main.Email.EmailModal", "AliasVault.Client");

    private IStringLocalizer SharedLocalizer => LocalizerFactory.Create("SharedResources", "AliasVault.Client");

    /// <summary>
    /// The email to show in the modal.
    /// </summary>
    [Parameter]
    public EmailApiModel? Email { get; set; }

    /// <summary>
    /// Boolean that indicates if the email is from SpamOK public API.
    /// </summary>
    [Parameter]
    public bool IsSpamOk { get; set; }

    /// <summary>
    /// Callback when the modal is closed.
    /// </summary>
    [Parameter]
    public EventCallback<bool> OnClose { get; set; }

    /// <summary>
    /// Callback when an email is deleted.
    /// </summary>
    [Parameter]
    public EventCallback<int> OnEmailDeleted { get; set; }

    /// <summary>
    /// When true, render the "Item:" row that links back to the associated credential. Defaults to false because
    /// when this modal is opened from an item-detail page the context is already clear. Callers that surface the
    /// modal from a cross-item list (e.g. the mobile emails page) should pass true; the modal then resolves and
    /// navigates to the credential itself based on the email's to-address.
    /// </summary>
    [Parameter]
    public bool ShowItemLink { get; set; }

    /// <summary>
    /// Resolved credential ID for the email's to-address (only populated when ShowItemLink is true).
    /// </summary>
    private Guid _credentialId;

    /// <summary>
    /// Resolved credential name for the email's to-address (only populated when ShowItemLink is true).
    /// </summary>
    private string _credentialName = string.Empty;

    /// <summary>
    /// The currently selected view mode.
    /// </summary>
    private EmailViewMode _viewMode = EmailViewMode.Html;

    /// <summary>
    /// The message body to display
    /// </summary>
    private string EmailBody = string.Empty;

    /// <summary>
    /// The id of the email whose body is currently rendered. Used to (re)compute the body only when the email changes.
    /// </summary>
    private int? _renderedEmailId;

    /// <summary>
    /// True while the email body is being prepared (sanitized) so the skeleton is shown instead of the iframe.
    /// </summary>
    private bool _isBodyLoading;

    /// <summary>
    /// Monotonic counter used to discard stale body-load work when the email changes mid-flight.
    /// </summary>
    private int _bodyLoadSequence;

    /// <summary>
    /// Show confirmation modal before deleting email.
    /// </summary>
    private async Task ShowDeleteConfirmation()
    {
        if (Email == null)
        {
            return;
        }

        var result = await ConfirmModalService.ShowConfirmation(
            Localizer["DeleteEmailTitle"],
            Localizer["DeleteEmailConfirmation"],
            SharedLocalizer["Confirm"],
            SharedLocalizer["Cancel"]
        );

        if (result)
        {
            await DeleteEmail();
        }
    }

    /// <summary>
    /// Close the modal.
    /// </summary>
    [JSInvokable]
    public Task Close()
    {
        return OnClose.InvokeAsync(false);
    }

    /// <summary>
    /// Delete the current email.
    /// </summary>
    private async Task DeleteEmail()
    {
        if (Email == null)
        {
            return;
        }

        if (IsSpamOk)
        {
            await DeleteEmailSpamOk();
        }
        else
        {
            await DeleteEmailAliasVault();
        }
    }

    /// <summary>
    /// Delete the current email in SpamOk.
    /// </summary>
    private async Task DeleteEmailSpamOk()
    {
        if (Email == null)
        {
            return;
        }

        try
        {
            var client = HttpClientFactory.CreateClient("EmailClient");
            var request = new HttpRequestMessage(HttpMethod.Delete, $"https://api.spamok.com/v2/Email/{Email.ToLocal}/{Email.Id}");
            request.Headers.Add("X-Asdasd-Platform-Id", "av-web");
            request.Headers.Add("X-Asdasd-Platform-Version", AppInfo.GetFullVersion());

            var response = await client.SendAsync(request);

            if (response.IsSuccessStatusCode)
            {
                await OnEmailDeleted.InvokeAsync(Email.Id);
                GlobalNotificationService.AddSuccessMessage(Localizer["EmailDeletedSuccess"], true);
                await Close();
            }
            else
            {
                var errorMessage = await response.Content.ReadAsStringAsync();
                GlobalNotificationService.AddErrorMessage($"{Localizer["EmailDeleteFailed"]}: {errorMessage}", true);
            }
        }
        catch (Exception ex)
        {
            GlobalNotificationService.AddErrorMessage($"{Localizer["GenericError"]}: {ex.Message}", true);
        }
    }

    /// <summary>
    /// Delete the current email in AliasVault.
    /// </summary>
    private async Task DeleteEmailAliasVault()
    {
        if (Email == null)
        {
            return;
        }

        try
        {
            var response = await HttpClient.DeleteAsync($"v1/Email/{Email.Id}");
            if (response.IsSuccessStatusCode)
            {
                await OnEmailDeleted.InvokeAsync(Email.Id);
                GlobalNotificationService.AddSuccessMessage(Localizer["EmailDeletedSuccess"], true);
            }
            else
            {
                GlobalNotificationService.AddErrorMessage(Localizer["EmailDeleteFailed"], true);
            }
        }
        catch (Exception ex)
        {
            GlobalNotificationService.AddErrorMessage($"{Localizer["EmailDeleteFailed"]}: {ex.Message}", true);
        }
    }

    /// <summary>
    /// Returns the list of view modes that have content for the current email,
    /// in display order (HTML, Plain, Source).
    /// </summary>
    private List<EmailViewMode> GetAvailableModes()
    {
        var modes = new List<EmailViewMode>();
        var hasHtml = !string.IsNullOrWhiteSpace(Email?.MessageHtml);
        var hasPlain = Email?.MessagePlain != null;
        var hasSource = !string.IsNullOrWhiteSpace(Email?.MessageSource);
        if (hasHtml)
        {
            modes.Add(EmailViewMode.Html);
        }

        // Text is only offered when the server actually provided a plain-text part.
        if (hasPlain)
        {
            modes.Add(EmailViewMode.Plain);
        }

        // Source is always offered whenever there's any content.
        if (hasSource)
        {
            modes.Add(EmailViewMode.Source);
        }
        return modes;
    }

    /// <summary>
    /// Label for the currently active view mode, used as the cycle button caption.
    /// </summary>
    private string CurrentFormatLabel => _viewMode switch
    {
        EmailViewMode.Html => SharedLocalizer["EmailFormatHtml"],
        EmailViewMode.Plain => SharedLocalizer["EmailFormatPlain"],
        EmailViewMode.Source => SharedLocalizer["EmailFormatSource"],
        _ => string.Empty,
    };

    /// <summary>
    /// Cycle to the next available view mode.
    /// </summary>
    private void CycleViewMode()
    {
        var modes = GetAvailableModes();
        if (modes.Count <= 1)
        {
            return;
        }

        var currentIndex = modes.IndexOf(_viewMode);
        var nextIndex = (currentIndex + 1) % modes.Count;
        SetViewMode(modes[nextIndex]);
    }

    /// <summary>
    /// Sets the email body based on the selected view mode.
    /// </summary>
    private void SetViewMode(EmailViewMode mode)
    {
        _viewMode = mode;
        if (Email == null)
        {
            return;
        }

        switch (mode)
        {
            case EmailViewMode.Html:
                if (!string.IsNullOrWhiteSpace(Email.MessageHtml))
                {
                    EmailBody = ConversionUtility.SanitizeAndPrepareEmailHtml(Email.MessageHtml);
                }
                else
                {
                    EmailBody = Localizer["NoEmailBody"];
                }

                break;
            case EmailViewMode.Plain:
                if (Email.MessagePlain != null)
                {
                    var escapedText = System.Net.WebUtility.HtmlEncode(Email.MessagePlain);
                    EmailBody = $"<pre style='font-family: system-ui, -apple-system, sans-serif; white-space: pre-wrap; word-wrap: break-word; margin: 0;'>{escapedText}</pre>";
                }
                else
                {
                    EmailBody = Localizer["NoEmailBody"];
                }

                break;
            case EmailViewMode.Source:
                if (!string.IsNullOrWhiteSpace(Email.MessageSource))
                {
                    var escapedSource = System.Net.WebUtility.HtmlEncode(Email.MessageSource);
                    EmailBody = $"<pre style='font-family: monospace; white-space: pre-wrap; word-wrap: break-word; margin: 0; font-size: 12px; line-height: 1.4;'>{escapedSource}</pre>";
                }
                else
                {
                    EmailBody = Localizer["NoEmailBody"];
                }

                break;
        }
    }

    /// <inheritdoc />
    protected override async Task OnParametersSetAsync()
    {
        await base.OnParametersSetAsync();

        // Only (re)compute the rendered body when the email actually changes. The caller renders this modal
        // immediately (with a null Email) so the shell paints right away, then supplies the decrypted email.
        if (Email?.Id == _renderedEmailId)
        {
            return;
        }

        var sequence = ++_bodyLoadSequence;
        _renderedEmailId = Email?.Id;
        EmailBody = string.Empty;
        _credentialId = Guid.Empty;
        _credentialName = string.Empty;

        if (Email == null)
        {
            return;
        }

        // Show the body skeleton and yield so the browser can paint it before the sanitize pass.
        // This improves the perceived loading time of the email body (UX).
        _isBodyLoading = true;
        StateHasChanged();
        await Task.Delay(1);

        if (sequence != _bodyLoadSequence)
        {
            return;
        }

        // Determine initial view mode and email body.
        if (!string.IsNullOrWhiteSpace(Email.MessageHtml))
        {
            SetViewMode(EmailViewMode.Html);
        }
        else if (Email.MessagePlain != null)
        {
            SetViewMode(EmailViewMode.Plain);
        }
        else
        {
            EmailBody = Localizer["NoEmailBody"];
        }

        if (sequence != _bodyLoadSequence)
        {
            return;
        }

        _isBodyLoading = false;
        StateHasChanged();

        // Resolve the credential linked to this email's to-address when the caller asks for the
        // Item row. SpamOK emails aren't backed by a local vault item so the lookup is skipped.
        if (ShowItemLink && !IsSpamOk)
        {
            await ResolveLinkedCredential();
            if (sequence == _bodyLoadSequence)
            {
                StateHasChanged();
            }
        }
    }

    /// <summary>
    /// Look up the credential in the local vault whose login email matches this email's to-address.
    /// </summary>
    private async Task ResolveLinkedCredential()
    {
        if (Email == null)
        {
            return;
        }

        var toEmail = (Email.ToLocal + "@" + Email.ToDomain).ToLower();
        var context = await DbService.GetDbContextAsync();
        var item = await context.Items
            .Where(x => !x.IsDeleted && x.DeletedAt == null)
            .Where(x => x.FieldValues.Any(fv => fv.FieldKey == FieldKey.LoginEmail && fv.Value != null && fv.Value.ToLower() == toEmail && !fv.IsDeleted))
            .Select(x => new { x.Id, x.Name })
            .FirstOrDefaultAsync();

        if (item != null)
        {
            _credentialId = item.Id;
            _credentialName = item.Name ?? string.Empty;
        }
    }

    /// <summary>
    /// Navigate to the linked credential's detail page.
    /// </summary>
    private void NavigateToCredential()
    {
        if (_credentialId == Guid.Empty)
        {
            return;
        }

        NavigationManager.NavigateTo($"/items/{_credentialId}");
    }

    /// <summary>
    /// Download an attachment.
    /// </summary>
    private async Task DownloadAttachment(AttachmentApiModel attachment)
    {
        try
        {
            if (IsSpamOk)
            {
                var client = HttpClientFactory.CreateClient("EmailClient");
                var request = new HttpRequestMessage(HttpMethod.Get, $"https://api.spamok.com/v2/Attachment/{Email!.Id}/{attachment.Id}/download");
                request.Headers.Add("X-Asdasd-Platform-Id", "av-web");
                request.Headers.Add("X-Asdasd-Platform-Version", AppInfo.GetFullVersion());

                var response = await client.SendAsync(request);
                if (response.IsSuccessStatusCode)
                {
                    var bytes = await response.Content.ReadAsByteArrayAsync();
                    await JsInteropService.DownloadFileFromStream(attachment.Filename, bytes);
                }
                else
                {
                    GlobalNotificationService.AddErrorMessage(Localizer["AttachmentDownloadFailed"], true);
                }
            }
            else
            {
                var response = await HttpClient.GetAsync($"v1/Email/{Email!.Id}/attachments/{attachment.Id}");

                if (response.IsSuccessStatusCode)
                {
                    // Get attachment bytes from API.
                    var bytes = await response.Content.ReadAsByteArrayAsync();

                    // Decrypt the attachment locally with email's encryption key.
                    var decryptedBytes = await EmailService.DecryptEmailAttachment(Email, bytes);

                    // Offer the decrypted attachment as download to the user's browser.
                    if (decryptedBytes != null)
                    {
                        await JsInteropService.DownloadFileFromStream(attachment.Filename, decryptedBytes);
                    }
                }
                else
                {
                    GlobalNotificationService.AddErrorMessage(Localizer["AttachmentDownloadFailed"], true);
                }
            }
        }
        catch (Exception ex)
        {
            GlobalNotificationService.AddErrorMessage($"{Localizer["AttachmentDownloadError"]}: {ex.Message}", true);
        }
    }
}
