using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;
using PlotLine.Services;
using PlotLine.ViewModels;

namespace PlotLine.Controllers;

[Authorize]
public sealed class ScenesController(
    ISceneService scenes,
    IPlotService plots,
    IAssetService assets,
    ICharacterService characters,
    ILocationService locations,
    IWriterWorkspaceService writerWorkspace,
    IStorageService storage,
    IUploadStorageService uploadStorage) : Controller
{
    private const long MaxAttachmentBytes = 25 * 1024 * 1024;

    public async Task<IActionResult> Create(int chapterId)
    {
        var model = await scenes.GetCreateAsync(chapterId);
        return model is null ? NotFound() : View("Edit", model);
    }

    public async Task<IActionResult> Edit(int id)
    {
        var model = await scenes.GetEditAsync(id);
        return model is null ? NotFound() : View(model);
    }

    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> Save(SceneEditViewModel model)
    {
        if (!ModelState.IsValid)
        {
            var hydratedModel = model.SceneID > 0
                ? await scenes.GetEditAsync(model.SceneID)
                : await scenes.GetCreateAsync(model.ChapterID);
            if (hydratedModel is null)
            {
                return NotFound();
            }

            CopyPostedSceneFields(model, hydratedModel);
            return View("Edit", hydratedModel);
        }

        int sceneId;
        try
        {
            sceneId = await scenes.SaveAsync(model);
        }
        catch (SubscriptionLimitException ex) when (model.SceneID == 0)
        {
            ModelState.AddModelError(string.Empty, ex.Message);
            var createModel = await scenes.GetCreateAsync(model.ChapterID);
            if (createModel is null)
            {
                return NotFound();
            }

            CopyPostedSceneFields(model, createModel);
            return View("Edit", createModel);
        }

        if (TryGetLocalReturnUrl(out var returnUrl))
        {
            return LocalRedirect(returnUrl);
        }

        if (model.ReturnToTimeline && model.ReturnProjectID.HasValue)
        {
            return RedirectToAction("Index", "Timeline", new
            {
                projectId = model.ReturnProjectID.Value,
                bookId = model.ReturnBookID,
                selectedSceneId = sceneId
            });
        }

        return RedirectToAction(nameof(Edit), new { id = sceneId });
    }

    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> Archive(int id, int chapterId)
    {
        await scenes.ArchiveAsync(id);
        TempData["ArchiveMessage"] = "Scene archived. You can restore it from Archived Items.";
        if (TryGetLocalReturnUrl(out var returnUrl))
        {
            return LocalRedirect(RemoveSelectedScene(returnUrl));
        }

        return RedirectToAction("Details", "Chapters", new { id = chapterId });
    }

    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> MoveUp(int id, int chapterId)
    {
        await scenes.MoveAsync(id, "Up");
        return RedirectToAction("Details", "Chapters", new { id = chapterId });
    }

    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> MoveDown(int id, int chapterId)
    {
        await scenes.MoveAsync(id, "Down");
        return RedirectToAction("Details", "Chapters", new { id = chapterId });
    }

    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> AddThreadEvent(ThreadEventCreateViewModel model)
    {
        try
        {
            await plots.AddThreadEventAsync(model);
        }
        catch (InvalidOperationException ex)
        {
            TempData["ThreadEventAddError"] = ex.Message;
        }

        return RedirectForScene(model.SceneID, model.ReturnProjectID, model.ReturnBookID);
    }

    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> UpdateThreadEvent(ThreadEventEditViewModel model)
    {
        try
        {
            await plots.UpdateThreadEventAsync(model);
        }
        catch (InvalidOperationException ex)
        {
            TempData[$"ThreadEventEditError:{model.ThreadEventID}"] = ex.Message;
        }

        return RedirectForScene(model.SceneID, model.ReturnProjectID, model.ReturnBookID);
    }

    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> DeleteThreadEvent(int id, int sceneId, int? projectId, int? bookId)
    {
        await plots.DeleteThreadEventAsync(id);
        return RedirectForScene(sceneId, projectId, bookId);
    }

    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> AddAssetEvent(AssetEventCreateViewModel model)
    {
        await assets.AddAssetEventAsync(model);
        return RedirectForScene(model.SceneID, model.ReturnProjectID, model.ReturnBookID);
    }

    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> DeleteAssetEvent(int id, int sceneId, int? projectId, int? bookId)
    {
        await assets.DeleteAssetEventAsync(id);
        return RedirectForScene(sceneId, projectId, bookId);
    }

    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> AddAssetCustodyEvent(AssetCustodyCreateViewModel model)
    {
        await assets.AddCustodyEventAsync(model);
        return RedirectForScene(model.SceneID, model.ReturnProjectID, model.ReturnBookID);
    }

    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> AddSceneAssetLocation(SceneAssetLocationCreateViewModel model)
    {
        await locations.SaveSceneAssetLocationAsync(model);
        return RedirectForScene(model.SceneID, model.ReturnProjectID, model.ReturnBookID);
    }

    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> DeleteSceneAssetLocation(int id, int sceneId, int? projectId, int? bookId)
    {
        await locations.DeleteSceneAssetLocationAsync(id);
        return RedirectForScene(sceneId, projectId, bookId);
    }

    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> DeleteAssetCustodyEvent(int id, int sceneId, int? projectId, int? bookId)
    {
        await assets.DeleteCustodyEventAsync(id);
        return RedirectForScene(sceneId, projectId, bookId);
    }

    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> AddSceneCharacter(SceneCharacterCreateViewModel model)
    {
        try
        {
            await characters.AddSceneCharacterAsync(model);
        }
        catch (InvalidOperationException ex)
        {
            TempData["SceneCharacterAddError"] = ex.Message;
        }

        return RedirectForScene(model.SceneID, model.ReturnProjectID, model.ReturnBookID);
    }

    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> UpdateSceneCharacter(SceneCharacterEditViewModel model)
    {
        await characters.UpdateSceneCharacterAsync(model);
        return RedirectForScene(model.SceneID, model.ReturnProjectID, model.ReturnBookID);
    }

    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> DeleteSceneCharacter(int id, int sceneId, int? projectId, int? bookId)
    {
        await characters.DeleteSceneCharacterAsync(id);
        return RedirectForScene(sceneId, projectId, bookId);
    }

    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> AddSceneDependency(SceneDependencyCreateViewModel model)
    {
        await scenes.AddDependencyAsync(model);
        return RedirectForScene(model.CurrentSceneID, model.ReturnProjectID, model.ReturnBookID);
    }

    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> DeleteSceneDependency(int id, int sceneId, int? projectId, int? bookId)
    {
        await scenes.DeleteDependencyAsync(id);
        return RedirectForScene(sceneId, projectId, bookId);
    }

    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> PreviewMove(SceneMoveRequestViewModel model)
    {
        model.ReturnUrl = GetLocalReturnUrl();
        var preview = await scenes.GetMovePreviewAsync(model);
        return preview is null ? NotFound() : View("MovePreview", preview);
    }

    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> ConfirmMove(SceneMoveRequestViewModel model)
    {
        await scenes.MoveToChapterAsync(model);
        if (TryGetLocalReturnUrl(model.ReturnUrl, out var returnUrl))
        {
            return LocalRedirect(returnUrl);
        }

        return RedirectForScene(model.SceneID, model.ReturnProjectID, model.ReturnBookID);
    }

    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> RenumberChapter(int chapterId, int? projectId, int? bookId, int? sceneId)
    {
        await scenes.RenumberChapterAsync(chapterId);
        return sceneId.HasValue
            ? RedirectForScene(sceneId.Value, projectId, bookId)
            : RedirectToAction("Details", "Chapters", new { id = chapterId });
    }

    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> AddCharacterAttributeEvent(CharacterAttributeEventCreateViewModel model)
    {
        if (model.SceneID <= 0)
        {
            return BadRequest();
        }

        if (model.CharacterID <= 0)
        {
            TempData["CharacterAttributeAddError"] = "Choose a character before adding an attribute.";
            return RedirectForScene(model.SceneID, model.ReturnProjectID, model.ReturnBookID);
        }

        if (model.CharacterAttributeTypeID <= 0)
        {
            TempData["CharacterAttributeAddError"] = "Choose an attribute type before saving.";
            return RedirectForScene(model.SceneID, model.ReturnProjectID, model.ReturnBookID);
        }

        if (string.IsNullOrWhiteSpace(model.AttributeValue))
        {
            TempData["CharacterAttributeAddError"] = "Enter an attribute value before saving.";
            return RedirectForScene(model.SceneID, model.ReturnProjectID, model.ReturnBookID);
        }

        await characters.AddAttributeEventAsync(model);
        return RedirectForScene(model.SceneID, model.ReturnProjectID, model.ReturnBookID);
    }

    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> AddCharacterKnowledge(CharacterKnowledgeCreateViewModel model)
    {
        await characters.AddKnowledgeAsync(model);
        return RedirectForScene(model.SceneID, model.ReturnProjectID, model.ReturnBookID);
    }

    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> DeleteCharacterKnowledge(int id, int sceneId, int? projectId, int? bookId)
    {
        await characters.DeleteKnowledgeAsync(id);
        return RedirectForScene(sceneId, projectId, bookId);
    }

    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> AddRelationshipEvent(RelationshipEventCreateViewModel model)
    {
        await characters.AddRelationshipEventAsync(model);
        return RedirectForScene(model.SceneID, model.ReturnProjectID, model.ReturnBookID);
    }

    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> DeleteRelationshipEvent(int id, int sceneId, int? projectId, int? bookId)
    {
        await characters.DeleteRelationshipEventAsync(id);
        return RedirectForScene(sceneId, projectId, bookId);
    }

    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> SaveWorkflow(SceneWorkflowEditViewModel model)
    {
        await writerWorkspace.SaveSceneWorkflowAsync(model);
        return RedirectForScene(model.SceneID, model.ReturnProjectID, model.ReturnBookID);
    }

    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> SaveNote(SceneNoteEditViewModel model)
    {
        if (string.IsNullOrWhiteSpace(model.NoteText))
        {
            TempData["WriterWorkspaceError"] = "Enter note text before saving.";
            return RedirectForScene(model.SceneID, model.ReturnProjectID, model.ReturnBookID);
        }

        await writerWorkspace.SaveNoteAsync(model);
        return RedirectForScene(model.SceneID, model.ReturnProjectID, model.ReturnBookID);
    }

    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> DeleteNote(int id, int sceneId, int? projectId, int? bookId)
    {
        await writerWorkspace.DeleteNoteAsync(id);
        return RedirectForScene(sceneId, projectId, bookId);
    }

    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> SaveChecklistItem(SceneChecklistItemEditViewModel model)
    {
        if (string.IsNullOrWhiteSpace(model.ItemText))
        {
            TempData["WriterWorkspaceError"] = "Enter checklist text before saving.";
            return RedirectForScene(model.SceneID, model.ReturnProjectID, model.ReturnBookID);
        }

        await writerWorkspace.SaveChecklistItemAsync(model);
        return RedirectForScene(model.SceneID, model.ReturnProjectID, model.ReturnBookID);
    }

    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> DeleteChecklistItem(int id, int sceneId, int? projectId, int? bookId)
    {
        await writerWorkspace.DeleteChecklistItemAsync(id);
        return RedirectForScene(sceneId, projectId, bookId);
    }

    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> SaveAttachment(SceneAttachmentEditViewModel model)
    {
        if (string.IsNullOrWhiteSpace(model.Title))
        {
            TempData["WriterWorkspaceError"] = "Enter an attachment title before saving.";
            return RedirectForScene(model.SceneID, model.ReturnProjectID, model.ReturnBookID);
        }

        if (model.UploadedFile is not null && model.UploadedFile.Length > MaxAttachmentBytes)
        {
            TempData["WriterWorkspaceError"] = "Attachment files must be 25 MB or smaller.";
            return RedirectForScene(model.SceneID, model.ReturnProjectID, model.ReturnBookID);
        }

        var uploadedFile = model.UploadedFile;
        var originalFilePath = model.FilePath;
        string? originalFileName = null;
        string? contentType = null;
        long fileSizeBytes = 0;

        if (uploadedFile is not null && uploadedFile.Length > 0)
        {
            var storageLimit = await storage.CanUploadSceneAttachmentAsync(model.SceneID, uploadedFile.Length);
            if (storageLimit is null)
            {
                return NotFound();
            }

            if (!storageLimit.Allowed)
            {
                TempData["WriterWorkspaceError"] = storageLimit.Message;
                return RedirectForScene(model.SceneID, model.ReturnProjectID, model.ReturnBookID);
            }

            originalFileName = Path.GetFileName(uploadedFile.FileName);
            contentType = string.IsNullOrWhiteSpace(uploadedFile.ContentType) ? null : uploadedFile.ContentType;
            fileSizeBytes = uploadedFile.Length;
            model.FilePath = await SaveUploadedAttachmentAsync(model.SceneID, uploadedFile);
        }

        if (string.IsNullOrWhiteSpace(model.FilePath) && string.IsNullOrWhiteSpace(model.ExternalUrl))
        {
            TempData["WriterWorkspaceError"] = "Upload a file or enter an external link.";
            return RedirectForScene(model.SceneID, model.ReturnProjectID, model.ReturnBookID);
        }

        var attachmentId = await writerWorkspace.SaveAttachmentAsync(model);
        if (fileSizeBytes > 0 && !string.IsNullOrWhiteSpace(model.FilePath))
        {
            await storage.TrackSceneAttachmentAsync(
                model.SceneID,
                attachmentId,
                model.FilePath,
                originalFileName ?? Path.GetFileName(model.FilePath),
                contentType,
                fileSizeBytes);

            if (!string.IsNullOrWhiteSpace(originalFilePath)
                && !string.Equals(originalFilePath, model.FilePath, StringComparison.OrdinalIgnoreCase))
            {
                await storage.MarkDeletedByStoragePathAsync(originalFilePath);
                uploadStorage.TryDeleteFile(originalFilePath, "uploads/scenes");
            }
        }

        return RedirectForScene(model.SceneID, model.ReturnProjectID, model.ReturnBookID);
    }

    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> DeleteAttachment(int id, int sceneId, int? projectId, int? bookId, string? filePath)
    {
        await writerWorkspace.DeleteAttachmentAsync(id);
        await storage.MarkDeletedByStoragePathAsync(filePath);
        uploadStorage.TryDeleteFile(filePath, "uploads/scenes");
        return RedirectForScene(sceneId, projectId, bookId);
    }

    private IActionResult RedirectForScene(int sceneId, int? projectId, int? bookId)
    {
        if (TryGetLocalReturnUrl(out var returnUrl))
        {
            return LocalRedirect(returnUrl);
        }

        if (projectId.HasValue)
        {
            return RedirectToAction("Index", "Timeline", new { projectId = projectId.Value, bookId, selectedSceneId = sceneId });
        }

        return RedirectToAction(nameof(Edit), new { id = sceneId });
    }

    [HttpGet]
    public async Task<IActionResult> OccupancyEditor(int sceneId, int? floorPlanId)
    {
        var model = await scenes.GetOccupancyEditorAsync(sceneId, floorPlanId);
        return model is null ? NotFound() : PartialView("_SceneOccupancy", model);
    }

    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> SaveOccupancy(SceneOccupancySaveViewModel model)
    {
        try
        {
            await scenes.SaveOccupancyAsync(model);
            var refreshed = await scenes.GetOccupancyEditorAsync(model.SceneID, model.FloorPlanID);
            return refreshed is null
                ? NotFound()
                : PartialView("_SceneOccupancy", refreshed);
        }
        catch (InvalidOperationException ex)
        {
            Response.StatusCode = StatusCodes.Status400BadRequest;
            return Json(new { ok = false, message = ex.Message });
        }
    }

    private string? GetLocalReturnUrl(string? returnUrl = null)
    {
        if (string.IsNullOrWhiteSpace(returnUrl)
            && Request.HasFormContentType
            && Request.Form.TryGetValue("ReturnUrl", out var postedReturnUrl))
        {
            returnUrl = postedReturnUrl.ToString();
        }

        return !string.IsNullOrWhiteSpace(returnUrl) && Url.IsLocalUrl(returnUrl)
            ? returnUrl
            : null;
    }

    private bool TryGetLocalReturnUrl(out string returnUrl) =>
        TryGetLocalReturnUrl(null, out returnUrl);

    private bool TryGetLocalReturnUrl(string? candidate, out string returnUrl)
    {
        var localReturnUrl = GetLocalReturnUrl(candidate);
        if (!string.IsNullOrWhiteSpace(localReturnUrl))
        {
            returnUrl = localReturnUrl;
            return true;
        }

        returnUrl = string.Empty;
        return false;
    }

    private static string RemoveSelectedScene(string returnUrl)
    {
        var hashIndex = returnUrl.IndexOf('#');
        var hash = hashIndex >= 0 ? returnUrl[hashIndex..] : string.Empty;
        var withoutHash = hashIndex >= 0 ? returnUrl[..hashIndex] : returnUrl;
        var queryIndex = withoutHash.IndexOf('?');
        if (queryIndex < 0)
        {
            return returnUrl;
        }

        var path = withoutHash[..queryIndex];
        var query = withoutHash[(queryIndex + 1)..]
            .Split('&', StringSplitOptions.RemoveEmptyEntries)
            .Where(part =>
            {
                var equalsIndex = part.IndexOf('=');
                var key = equalsIndex >= 0 ? part[..equalsIndex] : part;
                return !string.Equals(Uri.UnescapeDataString(key), "selectedSceneId", StringComparison.OrdinalIgnoreCase);
            })
            .ToList();

        return query.Count == 0 ? path + hash : $"{path}?{string.Join("&", query)}{hash}";
    }

    private static void CopyPostedSceneFields(SceneEditViewModel source, SceneEditViewModel target)
    {
        target.SceneTitle = source.SceneTitle;
        target.SceneNumber = source.SceneNumber;
        target.Summary = source.Summary;
        target.TimeModeID = source.TimeModeID;
        target.StartDateTime = source.StartDateTime;
        target.EndDateTime = source.EndDateTime;
        target.DurationAmount = source.DurationAmount;
        target.DurationUnitID = source.DurationUnitID;
        target.RelativeTimeText = source.RelativeTimeText;
        target.TimeConfidenceID = source.TimeConfidenceID;
        target.ScenePurposeNotes = source.ScenePurposeNotes;
        target.SceneOutcomeNotes = source.SceneOutcomeNotes;
        target.RevisionStatusID = source.RevisionStatusID;
        target.PrimaryLocationID = source.PrimaryLocationID;
        target.FloorPlanID = source.FloorPlanID;
        target.InitialFloorPlanFloorID = source.InitialFloorPlanFloorID;
        target.SelectedPurposeTypeIds = source.SelectedPurposeTypeIds;
        target.ReturnToTimeline = source.ReturnToTimeline;
        target.ReturnProjectID = source.ReturnProjectID;
        target.ReturnBookID = source.ReturnBookID;
        target.Metrics = target.Metrics.Select(metric =>
        {
            var postedMetric = source.Metrics.FirstOrDefault(x => x.MetricTypeID == metric.MetricTypeID);
            if (postedMetric is not null)
            {
                metric.Value = postedMetric.Value;
                metric.Notes = postedMetric.Notes;
            }

            return metric;
        }).ToList();
    }

    private async Task<string> SaveUploadedAttachmentAsync(int sceneId, IFormFile uploadedFile)
    {
        var uploadRoot = uploadStorage.GetDirectory("scenes", sceneId.ToString());

        var originalName = Path.GetFileName(uploadedFile.FileName);
        var safeName = MakeSafeFileName(originalName);
        var storedName = $"{Guid.NewGuid():N}_{safeName}";
        var absolutePath = Path.Combine(uploadRoot, storedName);

        await using var stream = System.IO.File.Create(absolutePath);
        await uploadedFile.CopyToAsync(stream);

        return uploadStorage.GetPublicPath("scenes", sceneId.ToString(), storedName);
    }

    private static string MakeSafeFileName(string fileName)
    {
        var name = string.IsNullOrWhiteSpace(fileName) ? "attachment" : fileName;
        foreach (var invalidChar in Path.GetInvalidFileNameChars())
        {
            name = name.Replace(invalidChar, '-');
        }

        name = string.Join("-", name.Split([' ', '\t', '\r', '\n'], StringSplitOptions.RemoveEmptyEntries));
        return name.Length > 120 ? name[^120..] : name;
    }

}
