using System.ComponentModel.DataAnnotations;
using System.Threading.Channels;
using ErsatzTV.Application;
using ErsatzTV.Application.Channels;
using ErsatzTV.Application.Playouts;
using ErsatzTV.Application.Templates;
using ErsatzTV.Controllers.Api.Requests;
using ErsatzTV.Core;
using ErsatzTV.Core.Api.Channels;
using ErsatzTV.Core.Api.LibraryBrowse;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Locking;
using ErsatzTV.Core.Scheduling;
using ErsatzTV.Extensions;
using MediatR;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;

namespace ErsatzTV.Controllers.Api;

[ApiController]
public class ChannelController(
    ChannelWriter<IBackgroundServiceRequest> workerChannel,
    IMediator mediator,
    IEntityLocker entityLocker)
{
    [HttpGet("/api/v1/channels")]
    [EndpointGroupName("general")]
    public async Task<List<ChannelResponseModel>> GetAll() =>
        await mediator.Send(new GetAllChannelsForApi());

    [HttpGet("/api/v1/channels/state")]
    [Tags("Channels")]
    [EndpointSummary("Get channel runtime state")]
    [EndpointGroupName("general")]
    public async Task<List<ChannelStateResponseModel>> GetState(CancellationToken cancellationToken) =>
        await mediator.Send(new GetChannelStatesForApi(DateTime.UtcNow), cancellationToken);

    [HttpGet("/api/v1/guide")]
    [Tags("Channels")]
    [EndpointSummary("Get the JSON channel guide (EPG)")]
    [EndpointDescription(
        "Returns per-channel programme arrays for the EPG grid. When omitted, start defaults to now, " +
        "and end defaults to start plus the configured XmltvDaysToBuild window.")]
    [EndpointGroupName("general")]
    [ProducesResponseType(typeof(ChannelGuideResponseModel), StatusCodes.Status200OK)]
    public async Task<ChannelGuideResponseModel> GetGuide(
        [FromQuery] DateTimeOffset? start,
        [FromQuery] DateTimeOffset? end,
        CancellationToken cancellationToken) =>
        await mediator.Send(new GetChannelGuideData(start, end), cancellationToken);

    [HttpGet("/api/v1/channels/music-video-credits-templates", Name = "GetMusicVideoCreditsTemplates")]
    [Tags("Channels")]
    [EndpointSummary("Get available music video credits template names")]
    [EndpointGroupName("general")]
    [ProducesResponseType(typeof(List<string>), StatusCodes.Status200OK)]
    public async Task<List<string>> GetMusicVideoCreditsTemplates(CancellationToken cancellationToken) =>
        await mediator.Send(new GetMusicVideoCreditTemplates(), cancellationToken);

    [HttpGet("/api/v1/channels/stream-selectors", Name = "GetChannelStreamSelectors")]
    [Tags("Channels")]
    [EndpointSummary("Get available channel stream selector names")]
    [EndpointGroupName("general")]
    [ProducesResponseType(typeof(List<string>), StatusCodes.Status200OK)]
    public async Task<List<string>> GetStreamSelectors(CancellationToken cancellationToken) =>
        await mediator.Send(new GetChannelStreamSelectors(), cancellationToken);

    [HttpGet("/api/v1/channels/{id:int}", Name = "GetChannelById")]
    [Tags("Channels")]
    [EndpointSummary("Get a channel by id")]
    [EndpointGroupName("general")]
    [ProducesResponseType(typeof(ChannelDetailResponseModel), StatusCodes.Status200OK)]
    [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
    public async Task<IActionResult> GetById(int id, CancellationToken cancellationToken)
    {
        Option<ChannelDetailResponseModel> result = await mediator.Send(new GetChannelByIdForApi(id), cancellationToken);
        return result.ToGetResult();
    }

    [HttpPost("/api/v1/channels")]
    [Tags("Channels")]
    [EndpointSummary("Create a channel")]
    [EndpointGroupName("general")]
    [ProducesResponseType(typeof(ChannelDetailResponseModel), StatusCodes.Status201Created)]
    [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
    [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status422UnprocessableEntity)]
    public async Task<IActionResult> Create(
        [Required][FromBody] CreateChannelRequest request,
        CancellationToken cancellationToken)
    {
        Either<BaseError, CreateChannelResult> result = await mediator.Send(request.ToCommand(), cancellationToken);
        return await result.Match(
            Left: error => Task.FromResult(error.ToErrorResult()),
            Right: async created =>
            {
                Option<ChannelDetailResponseModel> channel =
                    await mediator.Send(new GetChannelByIdForApi(created.ChannelId), cancellationToken);
                return channel.Match(
                    Some: model => (IActionResult)new CreatedResult($"/api/v1/channels/{model.Id}", model),
                    None: () => ApiResults.NotFoundProblem());
            });
    }

    [HttpPost("/api/v1/channels/from-lineup", Name = "CreateChannelFromLineup")]
    [Tags("Channels")]
    [EndpointSummary("Create a channel from a library lineup")]
    [EndpointDescription(
        "Atomically creates the channel, program schedule, a classic playout, and (for multi-item lineups) a " +
        "generated system playlist. A single-item lineup produces one flood schedule item that references the " +
        "target directly (movie, show, season, artist, collection, smart/multi collection, rerun collection, or " +
        "playlist) and no generated playlist. A lineup with two or more items produces one generated system " +
        "playlist whose entries play in the given order (each entry played in full before the next) referenced by " +
        "one flood schedule item; only movies, shows, seasons, artists, collections, smart collections and multi " +
        "collections are allowed there (rerun collections and playlists are single-item only). playbackOrder sets " +
        "how items within each lineup entry are ordered. Template defaults are stamped at create time; advanced " +
        "overrides win.")]
    [EndpointGroupName("general")]
    [ProducesResponseType(typeof(CreateChannelFromLineupResponseModel), StatusCodes.Status201Created)]
    [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
    [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status422UnprocessableEntity)]
    public async Task<IActionResult> CreateFromLineup(
        [Required][FromBody] CreateChannelFromLineupRequest request,
        CancellationToken cancellationToken)
    {
        Either<BaseError, CreateChannelFromLineupResponseModel> result =
            await mediator.Send(request.ToCommand(), cancellationToken);
        return result.ToCreatedResult(response => $"/api/v1/channels/{response.ChannelId}", response => response);
    }

    [HttpPut("/api/v1/channels/{id:int}")]
    [Tags("Channels")]
    [EndpointSummary("Update a channel")]
    [EndpointGroupName("general")]
    [ProducesResponseType(typeof(ChannelDetailResponseModel), StatusCodes.Status200OK)]
    [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
    [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status422UnprocessableEntity)]
    public async Task<IActionResult> Update(
        int id,
        [Required][FromBody] UpdateChannelRequest request,
        CancellationToken cancellationToken)
    {
        Either<BaseError, ChannelViewModel> result =
            await mediator.Send(request.ToCommand(id), cancellationToken);
        return await result.Match(
            Left: error => Task.FromResult(error.ToErrorResult()),
            Right: async _ =>
            {
                // Re-project through the read-side query so the response carries the same
                // ChannelDetailResponseModel shape (raw editable ids the editor re-drafts) as GET.
                Option<ChannelDetailResponseModel> channel =
                    await mediator.Send(new GetChannelByIdForApi(id), cancellationToken);
                return channel.Match(
                    Some: model => (IActionResult)new OkObjectResult(model),
                    None: () => ApiResults.NotFoundProblem());
            });
    }

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

    [HttpPost("/api/v1/channels/bulk/renumber")]
    [Tags("Channels")]
    [EndpointSummary("Renumber channels")]
    [EndpointGroupName("general")]
    [ProducesResponseType(StatusCodes.Status204NoContent)]
    [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
    [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status422UnprocessableEntity)]
    public async Task<IActionResult> BulkRenumber(
        [Required][FromBody] BulkRenumberChannelsRequest request,
        CancellationToken cancellationToken)
    {
        Option<BaseError> result = await mediator.Send(request.ToCommand(), cancellationToken);
        return result.Match<IActionResult>(
            Some: error => error.ToErrorResult(),
            None: () => new NoContentResult());
    }

    [HttpPost("/api/v1/channels/bulk/group")]
    [Tags("Channels")]
    [EndpointSummary("Move channels to a group")]
    [EndpointGroupName("general")]
    [ProducesResponseType(StatusCodes.Status204NoContent)]
    [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
    [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status422UnprocessableEntity)]
    public async Task<IActionResult> BulkMoveToGroup(
        [Required][FromBody] BulkMoveChannelsToGroupRequest request,
        CancellationToken cancellationToken)
    {
        Either<BaseError, Unit> result = await mediator.Send(request.ToCommand(), cancellationToken);
        return result.ToDeletedResult();
    }

    [HttpPost("/api/v1/channels/bulk/delete")]
    [Tags("Channels")]
    [EndpointSummary("Delete channels")]
    [EndpointGroupName("general")]
    [ProducesResponseType(StatusCodes.Status204NoContent)]
    [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
    [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status422UnprocessableEntity)]
    public async Task<IActionResult> BulkDelete(
        [Required][FromBody] BulkDeleteChannelsRequest request,
        CancellationToken cancellationToken)
    {
        Either<BaseError, Unit> result = await mediator.Send(request.ToCommand(), cancellationToken);
        return result.ToDeletedResult();
    }

    [HttpPost("/api/v1/channels/auto-tune/preview", Name = "PreviewAutoTuneChannels")]
    [Tags("Channels")]
    [EndpointSummary("Preview auto-tuned channels")]
    [EndpointGroupName("general")]
    [ProducesResponseType(typeof(List<AutoTuneProposalResponseModel>), StatusCodes.Status200OK)]
    [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status422UnprocessableEntity)]
    public async Task<IActionResult> PreviewAutoTune(
        [Required][FromBody] PreviewAutoTuneChannelsRequest request,
        CancellationToken cancellationToken)
    {
        Either<BaseError, List<AutoTuneProposal>> result =
            await mediator.Send(request.ToCommand(), cancellationToken);
        return result.Match<IActionResult>(
            Left: error => error.ToErrorResult(),
            Right: proposals => new OkObjectResult(proposals.Select(ProjectToResponseModel).ToList()));
    }

    [HttpPost("/api/v1/channels/auto-tune", Name = "CreateAutoTunedChannels")]
    [Tags("Channels")]
    [EndpointSummary("Create auto-tuned channels")]
    [EndpointDescription(
        "Bulk-creates the selected auto-tuned channels. Each channel is independent — one failure never " +
        "aborts the batch (see the per-channel Created/Skipped/Failed outcomes). Per channel, the optional " +
        "templateId, advanced overrides (same set as the manual Channel Builder; advanced values win over " +
        "the template) and an uploaded logo image override the batch defaults; an omitted field keeps the " +
        "batch template, the axis-derived playback order and the on-the-fly fallback logo.")]
    [EndpointGroupName("general")]
    [ProducesResponseType(typeof(AutoTuneResultResponseModel), StatusCodes.Status200OK)]
    public async Task<IActionResult> CreateAutoTuned(
        [Required][FromBody] CreateAutoTunedChannelsRequest request,
        CancellationToken cancellationToken)
    {
        AutoTuneResult result = await mediator.Send(request.ToCommand(), cancellationToken);
        return new OkObjectResult(ProjectToResponseModel(result));
    }

    [HttpGet("/api/v1/channels/auto-tune/members", Name = "GetAutoTuneChannelMembers")]
    [Tags("Channels")]
    [EndpointSummary("List a proposed auto-tune channel's distinct content-source members")]
    [EndpointDescription(
        "Given an auto-tune axis and value, returns the distinct content sources (parent shows for the " +
        "TV axes, movies for the movie-genre axis) the server-generated SmartCollection query resolves " +
        "to, with a per-source item count. Read-only; the server owns query generation.")]
    [EndpointGroupName("general")]
    [ProducesResponseType(typeof(PagedLibraryBrowseItemsResponseModel), StatusCodes.Status200OK)]
    public async Task<PagedLibraryBrowseItemsResponseModel> GetAutoTuneChannelMembers(
        [FromQuery] AutoTuneAxis axis,
        [FromQuery] string value,
        [FromQuery] int pageNum,
        [FromQuery] int pageSize,
        CancellationToken cancellationToken)
    {
        pageNum = Math.Max(0, pageNum);
        pageSize = pageSize <= 0 ? 100 : Math.Min(pageSize, 200);
        return await mediator.Send(
            new GetAutoTuneChannelMembers(axis, value, pageNum, pageSize),
            cancellationToken);
    }

    private static AutoTuneProposalResponseModel ProjectToResponseModel(AutoTuneProposal p) =>
        new(p.Axis.ToString(), p.Value, p.Name, p.Number, p.ItemCount, p.AlreadyExists);

    private static AutoTuneResultResponseModel ProjectToResponseModel(AutoTuneResult r) =>
        new(
            r.Results.Select(o => new AutoTuneChannelResultModel(
                o.Name, o.Status.ToString(), o.ChannelId, o.Reason)).ToList(),
            r.CreatedCount,
            r.SkippedCount,
            r.FailedCount);

    [HttpPost("/api/v1/channels/{id:int}/playout/reset")]
    [Tags("Channels")]
    [EndpointSummary("Reset channel playout")]
    [EndpointDescription(
        "When mode is omitted, classic playouts use Refresh (rebuild while maintaining collection " +
        "progress) and all other playout kinds use Reset (rebuild from scratch), matching the Blazor UI. " +
        "Pass mode to force a specific PlayoutBuildMode.")]
    [EndpointGroupName("general")]
    [ProducesResponseType(StatusCodes.Status202Accepted)]
    [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
    [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status409Conflict)]
    public async Task<IActionResult> ResetPlayout(
        int id,
        [FromQuery] PlayoutBuildMode? mode,
        CancellationToken cancellationToken)
    {
        Option<int> maybePlayoutId =
            await mediator.Send(new GetPlayoutIdByChannelId(id), cancellationToken);
        foreach (int playoutId in maybePlayoutId)
        {
            // Mirror Blazor's EntityLocker gating: don't enqueue a rebuild while one is already in flight.
            if (entityLocker.IsPlayoutLocked(playoutId))
            {
                return ApiResults.ConflictProblem(
                    "Playout build in progress",
                    "A build for this playout is currently in progress; try again once it completes.");
            }

            PlayoutBuildMode buildMode = mode ?? await DefaultResetMode(playoutId, cancellationToken);
            await workerChannel.WriteAsync(new BuildPlayout(playoutId, buildMode), cancellationToken);
            return new AcceptedResult();
        }

        return ApiResults.NotFoundProblem();
    }

    // Match Blazor's Playouts.razor reset semantics: classic playouts refresh (preserve progress),
    // every other kind resets from scratch.
    private async Task<PlayoutBuildMode> DefaultResetMode(int playoutId, CancellationToken cancellationToken)
    {
        Option<PlayoutNameViewModel> maybePlayout =
            await mediator.Send(new GetPlayoutById(playoutId), cancellationToken);
        return maybePlayout.Match(
            Some: vm => vm.ScheduleKind switch
            {
                PlayoutScheduleKind.Classic => PlayoutBuildMode.Refresh,
                _ => PlayoutBuildMode.Reset
            },
            None: () => PlayoutBuildMode.Reset);
    }
}
