@using System.ComponentModel.DataAnnotations
@using Microsoft.JSInterop
@implements IAsyncDisposable
@inject ImageService Images
@inject IDbContextFactory<ApplicationDbContext> DbFactory
@inject IJSRuntime JS

<div class="card mb-3" id="@editorId">
    <div class="card-body">
        <h5 class="card-title">@(IsNew ? "Add item" : "Edit item")</h5>
        <EditForm Model="model" OnValidSubmit="SaveAsync">
            <DataAnnotationsValidator />
            <ValidationSummary class="text-danger" />

            <div class="mb-2">
                <label class="form-label">Name</label>
                <InputText class="form-control" @bind-Value="model.Name" />
                <ValidationMessage For="() => model.Name" />
            </div>
            <div class="mb-2">
                <label class="form-label">Description</label>
                <InputTextArea class="form-control" @bind-Value="model.Description" rows="2" />
            </div>
            <div class="row">
                <div class="col-md-6 mb-2">
                    <label class="form-label">Product link</label>
                    <InputText class="form-control" @bind-Value="model.ProductUrl" placeholder="https://…" />
                </div>
                <div class="col-md-2 mb-2">
                    <label class="form-label">Price</label>
                    <InputNumber class="form-control" @bind-Value="model.Price" />
                </div>
                <div class="col-md-2 mb-2">
                    <label class="form-label">Priority</label>
                    <InputSelect class="form-select" @bind-Value="model.Priority">
                        <option value="1">1 – Must have</option>
                        <option value="2">2 – High</option>
                        <option value="3">3 – Normal</option>
                        <option value="4">4 – Low</option>
                        <option value="5">5 – Someday</option>
                    </InputSelect>
                </div>
                <div class="col-md-2 mb-2">
                    <label class="form-label">Quantity</label>
                    <InputNumber class="form-control" @bind-Value="model.Quantity" min="1" />
                </div>
            </div>

            <div class="mb-3 border rounded p-2 bg-light">
                <label class="form-label fw-bold">Image</label>
                <div class="d-flex gap-3 align-items-start flex-wrap">
                    <div>
                        @if (PreviewUrl is not null)
                        {
                            <img src="@PreviewUrl" alt="preview" style="max-width:140px;max-height:140px;" class="border rounded" />
                        }
                        else
                        {
                            <div class="text-secondary small border rounded d-flex align-items-center justify-content-center"
                                 style="width:140px;height:140px;">No image</div>
                        }
                    </div>
                    <div class="flex-grow-1">
                        <div class="mb-2">
                            <label class="form-label small mb-0">Upload a file</label>
                            <InputFile class="form-control form-control-sm" OnChange="OnFileSelected" accept="image/*" />
                        </div>
                        <div class="mb-2">
                            <label class="form-label small mb-0">…or fetch an image from a URL <span class="text-secondary">(downloaded &amp; stored locally)</span></label>
                            <div class="input-group input-group-sm">
                                <input class="form-control" @bind="externalUrlInput" placeholder="https://…/image.jpg" disabled="@downloadingUrl" />
                                <button type="button" class="btn btn-outline-secondary" @onclick="ApplyImageUrl" disabled="@downloadingUrl">
                                    @(downloadingUrl ? "Fetching…" : "Fetch")
                                </button>
                            </div>
                        </div>
                        <div class="small text-secondary">
                            …or just press <kbd>Ctrl</kbd>+<kbd>V</kbd> to paste an image from your clipboard.
                        </div>
                        @if (imageError is not null)
                        {
                            <div class="text-danger small mt-1">@imageError</div>
                        }
                        @if (model.ImageKind != ImageKind.None)
                        {
                            <button type="button" class="btn btn-link btn-sm text-danger px-0" @onclick="RemoveImage">Remove image</button>
                        }
                    </div>
                </div>
            </div>

            <button type="submit" class="btn btn-success btn-sm">Save</button>
            <button type="button" class="btn btn-link btn-sm" @onclick="CancelAsync">Cancel</button>
        </EditForm>
    </div>
</div>

@code {
    [Parameter] public int WishlistId { get; set; }
    [Parameter] public WishlistItem? Item { get; set; }
    [Parameter] public EventCallback OnSaved { get; set; }
    [Parameter] public EventCallback OnCancelled { get; set; }

    private readonly string editorId = $"item-editor-{Guid.NewGuid():N}";
    private ItemModel model = new();
    private string? externalUrlInput;
    private string? imageError;
    private string? originalUploadedFile; // existing uploaded file when editor opened
    private string? stagedUploadedFile;   // newly uploaded file not yet committed
    private DotNetObjectReference<ItemEditor>? selfRef;
    private IJSObjectReference? jsModule;
    private IJSObjectReference? pasteSub;

    private bool IsNew => Item is null;

    private string? PreviewUrl =>
        model.ImageKind == ImageKind.Uploaded && model.ImagePath is not null
            ? $"/uploads/{model.ImagePath}"
            : null;

    protected override void OnInitialized()
    {
        if (Item is not null)
        {
            model = new ItemModel
            {
                Name = Item.Name,
                Description = Item.Description,
                ProductUrl = Item.ProductUrl,
                Price = Item.Price,
                Priority = Item.Priority,
                Quantity = Item.Quantity,
                ImageKind = Item.ImageKind,
                ImagePath = Item.ImagePath,
            };
            originalUploadedFile = Item.ImageKind == ImageKind.Uploaded ? Item.ImagePath : null;
        }
    }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            jsModule = await JS.InvokeAsync<IJSObjectReference>("import", "./js/itemEditor.js");
            selfRef = DotNetObjectReference.Create(this);
            pasteSub = await jsModule.InvokeAsync<IJSObjectReference>("registerPaste", editorId, selfRef);
        }
    }

    private async Task OnFileSelected(InputFileChangeEventArgs e)
    {
        imageError = null;
        var file = e.File;
        if (!Images.IsAllowedContentType(file.ContentType))
        {
            imageError = "Unsupported image type. Use PNG, JPEG, WebP or GIF.";
            return;
        }
        try
        {
            await using var stream = file.OpenReadStream(Images.MaxBytes);
            var result = await Images.SaveAsync(stream, file.ContentType);
            ApplySaveResult(result);
        }
        catch (Exception)
        {
            imageError = $"Upload failed — the file may exceed the {Images.MaxBytes / (1024 * 1024)} MB limit.";
        }
    }

    [JSInvokable]
    public async Task OnImagePasted(string base64, string contentType)
    {
        imageError = null;
        if (!Images.IsAllowedContentType(contentType))
        {
            imageError = "Unsupported image type pasted.";
            await InvokeAsync(StateHasChanged);
            return;
        }
        var bytes = Convert.FromBase64String(base64);
        using var stream = new MemoryStream(bytes);
        var result = await Images.SaveAsync(stream, contentType);
        ApplySaveResult(result);
        await InvokeAsync(StateHasChanged);
    }

    private void ApplySaveResult(ImageSaveResult result)
    {
        if (!result.Succeeded)
        {
            imageError = result.Error;
            return;
        }
        // Discard a previously staged (uncommitted) file before replacing it.
        if (stagedUploadedFile is not null) Images.Delete(stagedUploadedFile);
        stagedUploadedFile = result.FileName;
        model.ImageKind = ImageKind.Uploaded;
        model.ImagePath = result.FileName;
        externalUrlInput = null;
    }

    private bool downloadingUrl;

    private async Task ApplyImageUrl()
    {
        imageError = null;
        if (string.IsNullOrWhiteSpace(externalUrlInput)) return;
        downloadingUrl = true;
        var result = await Images.SaveFromUrlAsync(externalUrlInput);
        ApplySaveResult(result); // stores locally as an Uploaded image
        downloadingUrl = false;
    }

    private void RemoveImage()
    {
        if (stagedUploadedFile is not null) { Images.Delete(stagedUploadedFile); stagedUploadedFile = null; }
        model.ImageKind = ImageKind.None;
        model.ImagePath = null;
        externalUrlInput = null;
    }

    private async Task SaveAsync()
    {
        await using var db = await DbFactory.CreateDbContextAsync();
        WishlistItem entity;
        if (IsNew)
        {
            entity = new WishlistItem { WishlistId = WishlistId };
            db.WishlistItems.Add(entity);
        }
        else
        {
            entity = await db.WishlistItems.FirstAsync(i => i.Id == Item!.Id);
        }

        entity.Name = model.Name.Trim();
        entity.Description = model.Description;
        entity.ProductUrl = model.ProductUrl;
        entity.Price = model.Price;
        entity.Priority = model.Priority;
        entity.Quantity = model.Quantity < 1 ? 1 : model.Quantity;
        entity.ImageKind = model.ImageKind;
        entity.ImagePath = model.ImagePath;

        await db.SaveChangesAsync();

        // The staged file is now committed; delete the previous uploaded file if it was replaced.
        if (originalUploadedFile is not null && originalUploadedFile != model.ImagePath)
            Images.Delete(originalUploadedFile);
        stagedUploadedFile = null;

        await OnSaved.InvokeAsync();
    }

    private async Task CancelAsync()
    {
        // Drop any uploaded-but-uncommitted file.
        if (stagedUploadedFile is not null && stagedUploadedFile != originalUploadedFile)
            Images.Delete(stagedUploadedFile);
        await OnCancelled.InvokeAsync();
    }

    public async ValueTask DisposeAsync()
    {
        try
        {
            if (pasteSub is not null) { await pasteSub.InvokeVoidAsync("dispose"); await pasteSub.DisposeAsync(); }
            if (jsModule is not null) await jsModule.DisposeAsync();
        }
        catch (JSDisconnectedException) { /* circuit gone */ }
        selfRef?.Dispose();
    }

    private sealed class ItemModel
    {
        [Required, MaxLength(300)]
        public string Name { get; set; } = "";
        public string? Description { get; set; }
        public string? ProductUrl { get; set; }
        public decimal? Price { get; set; }
        public int Priority { get; set; } = 3;
        public int Quantity { get; set; } = 1;
        public ImageKind ImageKind { get; set; } = ImageKind.None;
        public string? ImagePath { get; set; }
    }
}
