using System.ComponentModel.DataAnnotations;
using ErsatzTV.Application.MediaCollections;
using ErsatzTV.Controllers.Api.Requests;
using ErsatzTV.Core;
using ErsatzTV.Core.Api.LibraryBrowse;
using ErsatzTV.Core.Api.MediaCollections;
using ErsatzTV.Extensions;
using MediatR;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;

namespace ErsatzTV.Controllers.Api;

[ApiController]
public class CollectionController(IMediator mediator) : ControllerBase
{
    [HttpGet("/api/v1/collections")]
    [Tags("Collections")]
    [EndpointSummary("Get all collections")]
    [EndpointGroupName("general")]
    [ProducesResponseType(typeof(List<MediaCollectionResponseModel>), StatusCodes.Status200OK)]
    public async Task<List<MediaCollectionResponseModel>> GetAll(CancellationToken cancellationToken)
    {
        List<MediaCollectionViewModel> collections = await mediator.Send(new GetAllCollections(), cancellationToken);
        return collections.Map(ProjectToResponseModel).ToList();
    }

    [HttpGet("/api/v1/collections/{id:int}", Name = "GetCollectionById")]
    [Tags("Collections")]
    [EndpointSummary("Get a collection by id")]
    [EndpointGroupName("general")]
    [ProducesResponseType(typeof(MediaCollectionResponseModel), StatusCodes.Status200OK)]
    [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
    public async Task<IActionResult> GetById(int id, CancellationToken cancellationToken)
    {
        Option<MediaCollectionViewModel> result = await mediator.Send(new GetCollectionById(id), cancellationToken);
        return result.Map(ProjectToResponseModel).ToGetResult();
    }

    [HttpGet("/api/v1/collections/{id:int}/items", Name = "GetCollectionItems")]
    [Tags("Collections")]
    [EndpointSummary("Get the items in a manual collection")]
    [EndpointDescription("Returns a manual collection's full contents (all media kinds), paged.")]
    [EndpointGroupName("general")]
    [ProducesResponseType(typeof(PagedLibraryBrowseItemsResponseModel), StatusCodes.Status200OK)]
    [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
    public async Task<IActionResult> GetItems(
        int id,
        [FromQuery] int pageNum = 0,
        [FromQuery] int pageSize = 100,
        CancellationToken cancellationToken = default)
    {
        int clampedPageNum = Math.Max(0, pageNum);
        int clampedPageSize = Math.Clamp(pageSize, 1, 100);

        // The items GET is the reorder editor's load endpoint: emit the collection's version as the
        // concurrency ETag (issue #253). The response carries children, so read the root separately.
        Option<MediaCollectionViewModel> maybeCollection =
            await mediator.Send(new GetCollectionById(id), cancellationToken);
        foreach (MediaCollectionViewModel collection in maybeCollection)
        {
            ConcurrencyHeaders.SetETag(Response, collection.Version);
        }

        Either<BaseError, PagedLibraryBrowseItemsResponseModel> result = await mediator.Send(
            new GetCollectionItems(id, clampedPageNum, clampedPageSize),
            cancellationToken);
        return result.ToUpdatedResult();
    }

    [HttpPost("/api/v1/collections")]
    [Tags("Collections")]
    [EndpointSummary("Create a collection")]
    [EndpointGroupName("general")]
    [ProducesResponseType(typeof(MediaCollectionResponseModel), StatusCodes.Status201Created)]
    [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
    [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status422UnprocessableEntity)]
    public async Task<IActionResult> Create(
        [Required][FromBody] CreateCollectionRequest request,
        CancellationToken cancellationToken)
    {
        Either<BaseError, MediaCollectionViewModel> result =
            await mediator.Send(request.ToCommand(), cancellationToken);
        return result.ToCreatedResult(vm => $"/api/v1/collections/{vm.Id}", ProjectToResponseModel);
    }

    [HttpPut("/api/v1/collections/{id:int}")]
    [Tags("Collections")]
    [EndpointSummary("Update a collection")]
    [EndpointGroupName("general")]
    [ProducesResponseType(typeof(MediaCollectionResponseModel), StatusCodes.Status200OK)]
    [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
    [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status422UnprocessableEntity)]
    public async Task<IActionResult> Update(
        int id,
        [Required][FromBody] UpdateCollectionRequest request,
        CancellationToken cancellationToken)
    {
        Either<BaseError, Unit> result = await mediator.Send(request.ToCommand(id), cancellationToken);
        return await result.Match(
            Left: error => Task.FromResult(error.ToErrorResult()),
            Right: async _ =>
            {
                Option<MediaCollectionViewModel> collection =
                    await mediator.Send(new GetCollectionById(id), cancellationToken);
                return collection.Match(
                    Some: vm => (IActionResult)new OkObjectResult(ProjectToResponseModel(vm)),
                    None: () => ApiResults.NotFoundProblem());
            });
    }

    [HttpPut("/api/v1/collections/{id:int}/custom-order")]
    [Tags("Collections")]
    [EndpointSummary("Set a collection's custom playback order")]
    [EndpointDescription(
        "Replaces the custom playback order of a manual collection. CustomIndex is assigned from array position " +
        "(the request body has no index field); ids that are not members of the collection are ignored. Set " +
        "UseCustomPlaybackOrder on the collection for this order to take effect.")]
    [EndpointGroupName("general")]
    [ProducesResponseType(StatusCodes.Status204NoContent)]
    [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
    [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
    [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status412PreconditionFailed)]
    [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status422UnprocessableEntity)]
    public async Task<IActionResult> UpdateCustomOrder(
        int id,
        [Required][FromBody] UpdateCollectionCustomOrderRequest request,
        CancellationToken cancellationToken)
    {
        IfMatchCondition ifMatch = ConcurrencyHeaders.ParseIfMatch(Request);
        if (ifMatch.Kind is IfMatchKind.Malformed)
        {
            return ConcurrencyHeaders.MalformedIfMatchProblem();
        }

        Option<MediaCollectionViewModel> maybeCollection =
            await mediator.Send(new GetCollectionById(id), cancellationToken);
        if (maybeCollection.IsNone)
        {
            return ApiResults.NotFoundProblem();
        }

        Either<BaseError, Unit> result =
            await mediator.Send(request.ToCommand(id, ifMatch.ExpectedVersions), cancellationToken);
        return await result.Match(
            Left: error => Task.FromResult(error.ToErrorResult()),
            Right: async _ =>
            {
                // Emit the new ETag on the bodyless 204 so a same-tab second save doesn't 412 (#253).
                Option<MediaCollectionViewModel> refreshed =
                    await mediator.Send(new GetCollectionById(id), cancellationToken);
                ConcurrencyHeaders.SetETag(Response, refreshed.Map(c => c.Version).IfNone(0));
                return (IActionResult)new NoContentResult();
            });
    }

    [HttpDelete("/api/v1/collections/{id:int}")]
    [Tags("Collections")]
    [EndpointSummary("Delete a collection")]
    [EndpointGroupName("general")]
    [ProducesResponseType(StatusCodes.Status204NoContent)]
    [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
    [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status422UnprocessableEntity)]
    public async Task<IActionResult> Delete(int id, CancellationToken cancellationToken)
    {
        Either<BaseError, Unit> result = await mediator.Send(new DeleteCollection(id), cancellationToken);
        return result.ToDeletedResult();
    }

    [HttpPost("/api/v1/collections/{id:int}/items")]
    [Tags("Collections")]
    [EndpointSummary("Add items to a collection")]
    [EndpointGroupName("general")]
    [ProducesResponseType(StatusCodes.Status204NoContent)]
    [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
    [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status422UnprocessableEntity)]
    public async Task<IActionResult> AddItems(
        int id,
        [Required][FromBody] AddItemsToCollectionRequest request,
        CancellationToken cancellationToken)
    {
        Either<BaseError, Unit> result = await mediator.Send(request.ToCommand(id), cancellationToken);
        return result.ToDeletedResult();
    }

    [HttpDelete("/api/v1/collections/{id:int}/items/{mediaItemId:int}")]
    [Tags("Collections")]
    [EndpointSummary("Remove an item from a collection")]
    [EndpointGroupName("general")]
    [ProducesResponseType(StatusCodes.Status204NoContent)]
    [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
    [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status422UnprocessableEntity)]
    public async Task<IActionResult> RemoveItem(int id, int mediaItemId, CancellationToken cancellationToken)
    {
        Either<BaseError, Unit> result = await mediator.Send(
            new RemoveItemsFromCollection(id)
            {
                MediaItemIds = [mediaItemId]
            },
            cancellationToken);
        return result.ToDeletedResult();
    }

    private static MediaCollectionResponseModel ProjectToResponseModel(MediaCollectionViewModel vm) =>
        new(vm.Id, vm.Name, vm.CollectionType, vm.UseCustomPlaybackOrder);
}
