using Butter.Dtos;
using Butter.Dtos.Asset;
using Butter.Types;
using Lactose.Mapper;
using Lactose.Models;
using Lactose.Repositories;
using Lactose.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Infrastructure;

namespace Lactose.Controllers;

[ApiController]
[Route("api/[controller]")]
public class AssetController(
    ILogger<AssetController> logger,
    LactoseAuthService authService,
    IAssetRepository assetRepository,
    IUserRepository userRepository
) : ControllerBase {
    [HttpGet("{id}")]
    public ActionResult<AssetDto> Get(Guid id) {
        var          uid         = authService.GetUserData(User)?.Id;
        EAccessLevel accessLevel = authService.GetUserData(User)?.AccessLevel ?? EAccessLevel.User;

        logger.LogTrace(
            $"""
              Requested asset: {id}
               by user:        {uid} 
               access level:   {accessLevel}
             """
        );

        var asset = assetRepository.Find(id);

        if (asset == null) {
            logger.LogWarning($"Request asset {id} not found in the database!");
            return NotFound();
        }

        switch (accessLevel) {
            case EAccessLevel.User: {
                //Not publicly shared && Not shared with the user
                if (!asset.IsPubliclyShared && asset.SharedWith?.Find(x => x.Id == uid) == null) {
                    logger.LogTrace($"{asset.Id} is not shared with user {uid}");
                    return Unauthorized();
                }

                //Deleted asset
                if (asset.DeletedAt != null) {
                    logger.LogTrace($"{asset.Id} is deleted and thus not exists for this user");
                    return NotFound();
                }

                break;
            }
            case EAccessLevel.Curator: {
                //Deleted asset owned by the user
                if (asset.DeletedAt != null)
                    if (asset.Owner?.Id != uid) {
                        logger.LogTrace(
                            $"{asset.Id} is deleted and it's not owned by this user so it does not exists for this user"
                        );

                        return NotFound();
                    } else { logger.LogTrace($"{asset.Id} is deleted but it's owned by this user so it exists for this user"); }

                break;
            }
            case EAccessLevel.Admin: {
                //Admins will see this regardless
                logger.LogTrace($"{asset.Id} is being requested by an admin, sending it regardless of the visibility state");
                break;
            }
        }

        AssetDto dto = asset.ToFullAssetsDto(accessLevel);

        logger.LogTrace(
            $"""
              Outbound DTO:
                 dto.Id:        {dto.Id}
                 dto.FileName:  {dto.FileName}
                 dto.AssetType: {dto.AssetType}
                 dto.IsPublic:  {dto.IsPublic}
                 dto.CreatedAt: {dto.CreatedAt}
                 dto.UpdatedAt: {dto.UpdatedAt}
                 dto.DeletedAt: {dto.DeletedAt}
                 dto.Owner:     {dto.Owner.Id} {dto.Owner.Username}             
             """
        );

        return Ok(dto);
    }

    [HttpGet]
    public ActionResult<List<AssetPreviewDto>> GetAll([FromQuery] AssetSearchOptionsDto searchOptionsDto) {
        var          uid         = authService.GetUserData(User)?.Id;
        EAccessLevel accessLevel = authService.GetUserData(User)?.AccessLevel ?? EAccessLevel.User;
        DateTime     from        = DateTime.Parse(searchOptionsDto.StartDate ?? string.Empty);
        DateTime     to          = DateTime.Parse(searchOptionsDto.EndDate ?? string.Empty);

        if (from > to) {
            logger.LogWarning("Invalid date range provided");
            return BadRequest();
        }

        if (searchOptionsDto.Page < 1 || searchOptionsDto.PageSize < 1) {
            logger.LogWarning("Invalid pagination parameters provided");
            return BadRequest();
        }

        var assets = assetRepository.FindByDateRange(from, to, searchOptionsDto.Page, searchOptionsDto.PageSize);

        logger.LogTrace(
            $"""
              Requested assets
               by user:        {uid} 
               access level:   {accessLevel}
               date range:     {from} - {to}
             """
        );

        switch (accessLevel) {
            case EAccessLevel.User: {
                //Only publicly shared assets and assets shared with the user that are not deleted
                assets = assets.Where(
                    a => a.IsPubliclyShared || a.SharedWith?.Find(x => x.Id == uid) != null && a.DeletedAt != null
                );

                break;
            }
            case EAccessLevel.Curator:
            case EAccessLevel.Admin: {
                //Admins will see this regardless
                break;
            }
        }

        return Ok(assets.ToAssetPreviewDto());
    }

    [HttpPost("{id}")]
    [Authorize(Roles = "Curator,Admin")]
    public IStatusCodeActionResult Update([FromRoute] Guid id, [FromBody] AssetUpdateDto dto) {
        var uid         = authService.GetUserData(User)?.Id;
        var accesslevel = authService.GetUserData(User)!.AccessLevel;
        var asset       = assetRepository.Find(id);

        if (asset == null) {
            logger.LogWarning($"Request asset {id} not found in the database!");
            return NotFound();
        }

        if (asset.Owner?.Id != uid && accesslevel != EAccessLevel.Admin) {
            logger.LogWarning($"User {authService.GetUserData(User)?.Id} is trying to update asset {id} that they do not own");
            return Unauthorized();
        }

        string log = "";

        asset.IsPubliclyShared =  dto.IsPublic ?? asset.IsPubliclyShared;
        log                    += $"PubliclyShared: {asset.IsPubliclyShared} -> {dto.IsPublic ?? asset.IsPubliclyShared}\n";

        asset.DeletedAt =  dto.DeletedAt ?? asset.DeletedAt;
        log             += $"DeletedAt: {asset.DeletedAt} -> {dto.DeletedAt ?? asset.DeletedAt}\n";

        asset.Owner =  dto.Owner != null ? userRepository.Find(dto.Owner.Value) : asset.Owner;
        log         += $"Owner: {asset.Owner?.Id} -> {dto.Owner ?? asset.Owner?.Id}\n";

        logger.LogTrace($"Updating asset {id}\n{log}");

        assetRepository.Update(asset);
        assetRepository.Save();

        return Ok();
    }

    [HttpPost]
    [Authorize(Roles = "Curator,Admin")]
    public IStatusCodeActionResult BulkUpdate([FromBody] BulkDto<AssetUpdateDto> bulkDto) {
        var uid         = authService.GetUserData(User)?.Id;
        var accesslevel = authService.GetUserData(User)!.AccessLevel;
        var assets      = assetRepository.FindBulk(bulkDto.Ids);

        var enumerable = assets as List<Asset> ?? assets.ToList();

        if (enumerable.Count() != bulkDto.Ids.Count) {
            logger.LogWarning(
                $"Some assets were not found in the database" +
                $"\nrequested assets: {bulkDto.Ids.Count}, found assets: {enumerable.Count()}"
            );

            var missingIds = bulkDto.Ids.Except(enumerable.Select(a => a.Id)).ToList();
            logger.LogTrace($"Missing IDs: {string.Join(", ", missingIds)}");
        }

        //If the user is not an admin, they can only update their own assets
        if (accesslevel != EAccessLevel.Admin) enumerable = enumerable.Where(x => x.OwnerId == uid).ToList();

        enumerable.ForEach(
            x => {
                x.IsPubliclyShared = bulkDto.Data.IsPublic ?? x.IsPubliclyShared;
                x.DeletedAt        = bulkDto.Data.DeletedAt ?? x.DeletedAt;
                x.Owner            = bulkDto.Data.Owner != null ? userRepository.Find(bulkDto.Data.Owner.Value) : x.Owner;
            }
        );

        assetRepository.UpdateBulk(enumerable);
        return Ok();
    }

    [HttpDelete("{id}")]
    [Authorize(Roles = "Curator,Admin")]
    public IStatusCodeActionResult Delete([FromRoute] Guid id) {
        var uid         = authService.GetUserData(User)?.Id;
        var accesslevel = authService.GetUserData(User)!.AccessLevel;
        var asset       = assetRepository.Find(id);

        if (asset == null) {
            logger.LogWarning($"Request asset {id} not found in the database!");
            return NotFound();
        }

        if (asset.Owner?.Id != uid && accesslevel != EAccessLevel.Admin) {
            logger.LogWarning($"User {authService.GetUserData(User)?.Id} is trying to delete asset {id} that they do not own");
            return Unauthorized();
        }

        asset.DeletedAt = DateTime.Now;
        assetRepository.Update(asset);
        assetRepository.Save();

        return Ok();
    }

    [HttpDelete]
    [Authorize(Roles = "Curator,Admin")]
    public IStatusCodeActionResult BulkDelete([FromBody] List<Guid> ids) {
        var uid         = authService.GetUserData(User)?.Id;
        var accesslevel = authService.GetUserData(User)!.AccessLevel;
        var assets      = assetRepository.FindBulk(ids);

        var enumerable = assets as List<Asset> ?? assets.ToList();

        if (enumerable.Count() != ids.Count) {
            logger.LogWarning(
                $"Some assets were not found in the database" +
                $"\nrequested assets: {ids.Count}, found assets: {enumerable.Count()}"
            );

            var missingIds = ids.Except(enumerable.Select(a => a.Id)).ToList();
            logger.LogTrace($"Missing IDs: {string.Join(", ", missingIds)}");
        }

        //If the user is not an admin, they can only delete their own assets
        if (accesslevel != EAccessLevel.Admin) enumerable = enumerable.Where(x => x.OwnerId == uid).ToList();

        enumerable.ForEach(x => x.DeletedAt = DateTime.Now);

        assetRepository.UpdateBulk(enumerable);
        return Ok();
    }

    [NonAction]
    [HttpPut]
    [Authorize(Roles = "Curator,Admin")]
    public ActionResult<Guid> Create([FromBody] AssetCreateDto dto) {
        var uid         = authService.GetUserData(User)?.Id;
        var accesslevel = authService.GetUserData(User)!.AccessLevel;

        if (userRepository.Find(dto.Owner) == null) {
            logger.LogWarning($"User {dto.Owner} does not exist in the database");
            return BadRequest();
        }

        if (accesslevel != EAccessLevel.Admin && dto.Owner != uid) {
            logger.LogWarning($"User {uid} is trying to create an asset for user {dto.Owner}");
            return Unauthorized();
        }

        //TODO: This makes exactly zero sense. How are we supposed to create an asset externally here?
        //    Does this even need to exist?
        var asset = new Asset {
            Id = Guid.NewGuid(),
            Type = dto.AssetType,
            IsPubliclyShared = dto.IsPublic,
            OwnerId = dto.Owner,
            CreatedAt = dto.CreatedAt,
            MimeType = dto.MimeType,
            ResolutionWidth = dto.ResolutionWidth,
            ResolutionHeight = dto.ResolutionHeight,
            FileSize = dto.FileSize,
            Duration = dto.Duration ?? 0,
            FrameRate = dto.FrameRate ?? 0,
            OriginalPath = String.Empty,
            OriginalFilename = String.Empty
        };

        assetRepository.Insert(asset);
        assetRepository.Save();

        return Ok(asset.Id);
    }
}
