using System.ComponentModel.DataAnnotations;
using System.Linq;
using ErsatzTV.Application.ProgramSchedules;
using ErsatzTV.Controllers.Api.Requests;
using ErsatzTV.Core;
using ErsatzTV.Core.Api.Scheduling;
using ErsatzTV.Extensions;
using MediatR;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;

namespace ErsatzTV.Controllers.Api;

[ApiController]
public class ScheduleController(IMediator mediator) : ControllerBase
{
    [HttpGet("/api/v1/schedules")]
    [Tags("Schedules")]
    [EndpointSummary("Get all schedules")]
    [EndpointGroupName("general")]
    [ProducesResponseType(typeof(List<ProgramScheduleResponseModel>), StatusCodes.Status200OK)]
    public async Task<List<ProgramScheduleResponseModel>> GetAll(CancellationToken cancellationToken)
    {
        List<ProgramScheduleViewModel> schedules = await mediator.Send(new GetAllProgramSchedules(), cancellationToken);
        return schedules.Map(ProjectToResponseModel).ToList();
    }

    [HttpGet("/api/v1/schedules/{id:int}", Name = "GetScheduleById")]
    [Tags("Schedules")]
    [EndpointSummary("Get a schedule by id")]
    [EndpointGroupName("general")]
    [ProducesResponseType(typeof(ProgramScheduleResponseModel), StatusCodes.Status200OK)]
    [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
    public async Task<IActionResult> GetById(int id, CancellationToken cancellationToken)
    {
        Option<ProgramScheduleViewModel> result = await mediator.Send(new GetProgramScheduleById(id), cancellationToken);
        return result.Map(ProjectToResponseModel).ToGetResult();
    }

    [HttpPost("/api/v1/schedules")]
    [Tags("Schedules")]
    [EndpointSummary("Create a schedule")]
    [EndpointGroupName("general")]
    [ProducesResponseType(typeof(ProgramScheduleResponseModel), StatusCodes.Status201Created)]
    [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
    [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status422UnprocessableEntity)]
    public async Task<IActionResult> Create(
        [Required][FromBody] CreateScheduleRequest request,
        CancellationToken cancellationToken)
    {
        Either<BaseError, CreateProgramScheduleResult> result =
            await mediator.Send(request.ToCreateCommand(), cancellationToken);
        return await result.Match(
            Left: error => Task.FromResult(error.ToErrorResult()),
            Right: async created =>
            {
                Option<ProgramScheduleViewModel> schedule =
                    await mediator.Send(new GetProgramScheduleById(created.ProgramScheduleId), cancellationToken);
                return schedule.Match(
                    Some: vm => (IActionResult)new CreatedResult(
                        $"/api/v1/schedules/{vm.Id}",
                        ProjectToResponseModel(vm)),
                    None: () => ApiResults.NotFoundProblem());
            });
    }

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

    [HttpDelete("/api/v1/schedules/{id:int}")]
    [Tags("Schedules")]
    [EndpointSummary("Delete a schedule")]
    [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 DeleteProgramSchedule(id), cancellationToken);
        return result.ToDeletedResult();
    }

    [HttpGet("/api/v1/schedules/{id:int}/items")]
    [Tags("Schedules")]
    [EndpointSummary("Get schedule items")]
    [EndpointDescription(
        "Returns the schedule's items plus a computed, best-effort runtime estimate: each item carries a " +
        "nullable durationEstimate and the envelope carries a nullable totalDurationEstimate. Estimates are " +
        "derived from referenced collection/media runtimes and are null when unbounded or unknown. The " +
        "response also carries a strong ETag of the schedule's version; pass that ETag back as If-Match on " +
        "the replace (PUT) to detect a concurrent edit (issue #253).")]
    [EndpointGroupName("general")]
    [ProducesResponseType(typeof(ScheduleItemsResponseModel), StatusCodes.Status200OK)]
    [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
    public async Task<IActionResult> GetItems(int id, CancellationToken cancellationToken)
    {
        Option<ProgramScheduleViewModel> schedule = await mediator.Send(new GetProgramScheduleById(id), cancellationToken);
        if (schedule.IsNone)
        {
            return ApiResults.NotFoundProblem();
        }

        // The items GET returns children, not the root, so read the schedule's version for the ETag.
        ConcurrencyHeaders.SetETag(Response, schedule.Map(s => s.Version).IfNone(0));

        ProgramScheduleItemsWithDurationViewModel items =
            await mediator.Send(new GetProgramScheduleItemsWithDurations(id), cancellationToken);
        return new OkObjectResult(ScheduleItemResponseMapper.ProjectToResponseModel(items));
    }

    [HttpPost("/api/v1/schedules/{id:int}/items")]
    [Tags("Schedules")]
    [EndpointSummary("Add a schedule item")]
    [EndpointGroupName("general")]
    [ProducesResponseType(typeof(ScheduleItemResponseModel), StatusCodes.Status201Created)]
    [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
    [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status422UnprocessableEntity)]
    public async Task<IActionResult> AddItem(
        int id,
        [Required][FromBody] ScheduleItemRequest request,
        CancellationToken cancellationToken)
    {
        Either<BaseError, ProgramScheduleItemViewModel> result =
            await mediator.Send(request.ToAddCommand(id), cancellationToken);
        return result.ToCreatedResult(
            item => $"/api/v1/schedules/{id}/items/{item.Id}",
            item => ScheduleItemResponseMapper.ProjectToResponseModel(item));
    }

    [HttpPut("/api/v1/schedules/{id:int}/items")]
    [Tags("Schedules")]
    [EndpointSummary("Replace schedule items")]
    [EndpointDescription(
        "Replaces the schedule's full item list; item indexes are assigned from the array order. Send the " +
        "ETag from the items GET as If-Match to reject a stale overwrite with 412 (issue #253); a successful " +
        "response carries the new ETag.")]
    [EndpointGroupName("general")]
    [ProducesResponseType(typeof(List<ScheduleItemResponseModel>), StatusCodes.Status200OK)]
    [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
    [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
    [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status412PreconditionFailed)]
    [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status422UnprocessableEntity)]
    public async Task<IActionResult> ReplaceItems(
        int id,
        [Required][FromBody] ReplaceScheduleItemsRequest request,
        CancellationToken cancellationToken)
    {
        IfMatchCondition ifMatch = ConcurrencyHeaders.ParseIfMatch(Request);
        if (ifMatch.Kind is IfMatchKind.Malformed)
        {
            return new BadRequestObjectResult(
                new ProblemDetails
                {
                    Status = StatusCodes.Status400BadRequest,
                    Title = "Invalid If-Match header",
                    Detail = "If-Match must be a strong ETag of the resource version (e.g. \"3\") or \"*\"."
                });
        }

        Either<BaseError, IEnumerable<ProgramScheduleItemViewModel>> result =
            await mediator.Send(request.ToCommand(id, ifMatch.ExpectedVersions), cancellationToken);

        return await result.Match(
            Left: error => Task.FromResult(error.ToErrorResult()),
            Right: async _ =>
            {
                // Reload the root (version) FIRST, then the items, so the emitted ETag is never newer than
                // the returned items (issue #253 fail-safe ordering; matches BlockController). A None root
                // (deleted between commit and reload) is a 404, never a 200 without an ETag.
                Option<ProgramScheduleViewModel> refreshed =
                    await mediator.Send(new GetProgramScheduleById(id), cancellationToken);
                List<ProgramScheduleItemViewModel> items =
                    await mediator.Send(new GetProgramScheduleItems(id), cancellationToken);
                return refreshed.Match(
                    Some: vm =>
                    {
                        // Return the new ETag so a same-tab second save doesn't 412 against its own write.
                        ConcurrencyHeaders.SetETag(Response, vm.Version);
                        return (IActionResult)new OkObjectResult(
                            items.Select(ScheduleItemResponseMapper.ProjectToResponseModel).ToList());
                    },
                    None: () => ApiResults.NotFoundProblem());
            });
    }

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

    private static ProgramScheduleResponseModel ProjectToResponseModel(ProgramScheduleViewModel vm) =>
        new(
            vm.Id,
            vm.Name,
            vm.KeepMultiPartEpisodesTogether,
            vm.TreatCollectionsAsShows,
            vm.ShuffleScheduleItems,
            vm.RandomStartPoint,
            vm.FixedStartTimeBehavior,
            vm.PadToNearestMinute);
}
