using System.ComponentModel.DataAnnotations;
using System.Threading.Channels;
using ErsatzTV.Application;
using ErsatzTV.Application.Jellyfin;
using ErsatzTV.Controllers.Api.Requests;
using ErsatzTV.Core;
using ErsatzTV.Core.Api.MediaSources;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Locking;
using ErsatzTV.Core.Jellyfin;
using ErsatzTV.Extensions;
using MediatR;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;

namespace ErsatzTV.Controllers.Api;

// Design #202 §A.3 (J1-J9). Jellyfin and Emby are copy-symmetric — see EmbyMediaSourcesController.
[ApiController]
public class JellyfinMediaSourcesController(
    IMediator mediator,
    IEntityLocker entityLocker,
    ChannelWriter<IScannerBackgroundServiceRequest> scannerWorkerChannel) : ControllerBase
{
    [HttpGet("/api/v1/media-sources/jellyfin", Name = "GetJellyfinState")]
    [Tags("Jellyfin")]
    [EndpointSummary("Get Jellyfin connection state and discovered servers")]
    [EndpointGroupName("general")]
    [ProducesResponseType(typeof(RemoteMediaSourceStateResponseModel), StatusCodes.Status200OK)]
    public async Task<RemoteMediaSourceStateResponseModel> GetState(CancellationToken cancellationToken)
    {
        List<JellyfinMediaSourceViewModel> sources =
            await mediator.Send(new GetAllJellyfinMediaSources(), cancellationToken);
        JellyfinSecrets secrets = await mediator.Send(new GetJellyfinSecrets(), cancellationToken);

        bool isAuthorized = !string.IsNullOrWhiteSpace(secrets.Address) && !string.IsNullOrWhiteSpace(secrets.ApiKey);
        bool isLocked = entityLocker.IsRemoteMediaSourceLocked<JellyfinMediaSource>();

        return new RemoteMediaSourceStateResponseModel(
            isAuthorized,
            isLocked,
            sources.Map(s => new RemoteMediaSourceItemResponseModel(s.Id, s.Name, s.Address)).ToList());
    }

    [HttpGet("/api/v1/media-sources/jellyfin/connection", Name = "GetJellyfinConnection")]
    [Tags("Jellyfin")]
    [EndpointSummary("Get the Jellyfin connection address")]
    [EndpointDescription(
        "Never returns the API key — only whether one is currently configured (design #202 secure connection " +
        "contract). Use the PUT to (re)connect; a blank apiKey there retains the existing key.")]
    [EndpointGroupName("general")]
    [ProducesResponseType(typeof(RemoteConnectionResponseModel), StatusCodes.Status200OK)]
    public async Task<RemoteConnectionResponseModel> GetConnection(CancellationToken cancellationToken)
    {
        JellyfinSecrets secrets = await mediator.Send(new GetJellyfinSecrets(), cancellationToken);
        return new RemoteConnectionResponseModel(
            secrets.Address ?? string.Empty,
            !string.IsNullOrWhiteSpace(secrets.ApiKey));
    }

    [HttpPut("/api/v1/media-sources/jellyfin/connection", Name = "SaveJellyfinConnection")]
    [Tags("Jellyfin")]
    [EndpointSummary("Connect, reconnect, or edit the Jellyfin connection")]
    [EndpointDescription(
        "A blank/omitted apiKey retains the existing key; a non-blank value sets a new one. The key is required " +
        "on first connect (no existing secret).")]
    [EndpointGroupName("general")]
    [ProducesResponseType(typeof(RemoteConnectionResponseModel), StatusCodes.Status200OK)]
    [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status409Conflict)]
    [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status422UnprocessableEntity)]
    public async Task<IActionResult> SaveConnection(
        [Required][FromBody] SaveRemoteConnectionRequest request,
        CancellationToken cancellationToken)
    {
        if (entityLocker.IsRemoteMediaSourceLocked<JellyfinMediaSource>())
        {
            return ApiResults.ConflictProblem(
                "Jellyfin operation in progress",
                "A Jellyfin sign-in or sync is already in progress.");
        }

        if (!Uri.TryCreate(request.Address, UriKind.Absolute, out _))
        {
            return new UnprocessableEntityObjectResult(
                CreateProblemDetails("Address must be an absolute URI."));
        }

        JellyfinSecrets existingSecrets = await mediator.Send(new GetJellyfinSecrets(), cancellationToken);
        if (string.IsNullOrWhiteSpace(request.ApiKey) && string.IsNullOrWhiteSpace(existingSecrets.ApiKey))
        {
            return new UnprocessableEntityObjectResult(CreateProblemDetails("API key is required."));
        }

        Either<BaseError, Unit> result = await mediator.Send(
            request.ToJellyfinCommand(existingSecrets.ApiKey),
            cancellationToken);

        return await result.Match<Task<IActionResult>>(
            Left: error => Task.FromResult(error.ToErrorResult()),
            Right: async _ =>
            {
                JellyfinSecrets saved = await mediator.Send(new GetJellyfinSecrets(), cancellationToken);
                return new OkObjectResult(
                    new RemoteConnectionResponseModel(
                        saved.Address ?? string.Empty,
                        !string.IsNullOrWhiteSpace(saved.ApiKey)));
            });
    }

    [HttpPost("/api/v1/media-sources/jellyfin/disconnect", Name = "DisconnectJellyfin")]
    [Tags("Jellyfin")]
    [EndpointSummary("Disconnect Jellyfin")]
    [EndpointDescription("Purges the Jellyfin connection, discovered servers, and all synced Jellyfin content.")]
    [EndpointGroupName("general")]
    [ProducesResponseType(StatusCodes.Status204NoContent)]
    [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status409Conflict)]
    public async Task<IActionResult> Disconnect(CancellationToken cancellationToken)
    {
        if (!entityLocker.LockRemoteMediaSource<JellyfinMediaSource>())
        {
            return ApiResults.ConflictProblem(
                "Jellyfin operation in progress",
                "A Jellyfin sign-in or sync is already in progress.");
        }

        Either<BaseError, Unit> result = await mediator.Send(new DisconnectJellyfin(), cancellationToken);
        return result.Match(
            Left: error => error.ToErrorResult(),
            Right: _ => (IActionResult)new NoContentResult());
    }

    [HttpGet("/api/v1/media-sources/jellyfin/{id:int}/libraries", Name = "GetJellyfinLibraries")]
    [Tags("Jellyfin")]
    [EndpointSummary("Get a Jellyfin source's libraries")]
    [EndpointGroupName("general")]
    [ProducesResponseType(typeof(List<RemoteLibraryResponseModel>), StatusCodes.Status200OK)]
    [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
    public async Task<IActionResult> GetLibraries(int id, CancellationToken cancellationToken)
    {
        Option<JellyfinMediaSourceViewModel> maybeSource =
            await mediator.Send(new GetJellyfinMediaSourceById(id), cancellationToken);
        if (maybeSource.IsNone)
        {
            return ApiResults.NotFoundProblem();
        }

        List<JellyfinLibraryViewModel> libraries =
            await mediator.Send(new GetJellyfinLibrariesBySourceId(id), cancellationToken);
        return new OkObjectResult(libraries.Map(ProjectToResponseModel).ToList());
    }

    [HttpPut("/api/v1/media-sources/jellyfin/{id:int}/libraries", Name = "ReplaceJellyfinLibraryPreferences")]
    [Tags("Jellyfin")]
    [EndpointSummary("Replace a Jellyfin source's library sync preferences")]
    [EndpointDescription(
        "The body must be the complete set of the source's libraries (design #202 §C4a) — a row absent from " +
        "the request is rejected, not silently ignored. Ids are not stable across a disable, so re-fetch this " +
        "response rather than the request body.")]
    [EndpointGroupName("general")]
    [ProducesResponseType(typeof(List<RemoteLibraryResponseModel>), StatusCodes.Status200OK)]
    [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
    [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status422UnprocessableEntity)]
    public async Task<IActionResult> ReplaceLibraryPreferences(
        int id,
        [Required][FromBody] ReplaceRemoteLibraryPreferencesRequest request,
        CancellationToken cancellationToken)
    {
        Option<JellyfinMediaSourceViewModel> maybeSource =
            await mediator.Send(new GetJellyfinMediaSourceById(id), cancellationToken);
        if (maybeSource.IsNone)
        {
            return ApiResults.NotFoundProblem();
        }

        List<JellyfinLibraryViewModel> existingLibraries =
            await mediator.Send(new GetJellyfinLibrariesBySourceId(id), cancellationToken);

        UnprocessableEntityObjectResult validationError = ValidateLibraryPreferences(request, existingLibraries);
        if (validationError is not null)
        {
            return validationError;
        }

        Either<BaseError, Unit> result =
            await mediator.Send(request.ToJellyfinCommand(), cancellationToken);

        return await result.Match<Task<IActionResult>>(
            Left: error => Task.FromResult(error.ToErrorResult()),
            Right: async _ =>
            {
                foreach (RemoteLibraryPreferenceRequest library in request.Libraries.Where(l => l.ShouldSyncItems))
                {
                    await EnqueueLibrarySync(id, library.Id, cancellationToken);
                }

                List<JellyfinLibraryViewModel> reloaded =
                    await mediator.Send(new GetJellyfinLibrariesBySourceId(id), cancellationToken);
                return new OkObjectResult(reloaded.Map(ProjectToResponseModel).ToList());
            });
    }

    [HttpGet("/api/v1/media-sources/jellyfin/{id:int}/path-replacements", Name = "GetJellyfinPathReplacements")]
    [Tags("Jellyfin")]
    [EndpointSummary("Get a Jellyfin source's path replacements")]
    [EndpointGroupName("general")]
    [ProducesResponseType(typeof(List<PathReplacementResponseModel>), StatusCodes.Status200OK)]
    [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
    public async Task<IActionResult> GetPathReplacements(int id, CancellationToken cancellationToken)
    {
        Option<JellyfinMediaSourceViewModel> maybeSource =
            await mediator.Send(new GetJellyfinMediaSourceById(id), cancellationToken);
        if (maybeSource.IsNone)
        {
            return ApiResults.NotFoundProblem();
        }

        List<JellyfinPathReplacementViewModel> replacements =
            await mediator.Send(new GetJellyfinPathReplacementsBySourceId(id), cancellationToken);
        return new OkObjectResult(replacements.Map(ProjectToResponseModel).ToList());
    }

    [HttpPut("/api/v1/media-sources/jellyfin/{id:int}/path-replacements", Name = "ReplaceJellyfinPathReplacements")]
    [Tags("Jellyfin")]
    [EndpointSummary("Replace a Jellyfin source's path replacements")]
    [EndpointGroupName("general")]
    [ProducesResponseType(typeof(List<PathReplacementResponseModel>), StatusCodes.Status200OK)]
    [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
    [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status422UnprocessableEntity)]
    public async Task<IActionResult> ReplacePathReplacements(
        int id,
        [Required][FromBody] ReplacePathReplacementsRequest request,
        CancellationToken cancellationToken)
    {
        Option<JellyfinMediaSourceViewModel> maybeSource =
            await mediator.Send(new GetJellyfinMediaSourceById(id), cancellationToken);
        if (maybeSource.IsNone)
        {
            return ApiResults.NotFoundProblem();
        }

        Either<BaseError, Unit> result =
            await mediator.Send(request.ToJellyfinCommand(id), cancellationToken);

        return await result.Match<Task<IActionResult>>(
            Left: error => Task.FromResult(error.ToErrorResult()),
            Right: async _ =>
            {
                List<JellyfinPathReplacementViewModel> reloaded =
                    await mediator.Send(new GetJellyfinPathReplacementsBySourceId(id), cancellationToken);
                return new OkObjectResult(reloaded.Map(ProjectToResponseModel).ToList());
            });
    }

    [HttpPost("/api/v1/media-sources/jellyfin/{id:int}/refresh-libraries", Name = "RefreshJellyfinLibraries")]
    [Tags("Jellyfin")]
    [EndpointSummary("Refresh a Jellyfin source's libraries")]
    [EndpointGroupName("general")]
    [ProducesResponseType(StatusCodes.Status202Accepted)]
    [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
    [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status409Conflict)]
    public async Task<IActionResult> RefreshLibraries(int id, CancellationToken cancellationToken)
    {
        Option<JellyfinMediaSourceViewModel> maybeSource =
            await mediator.Send(new GetJellyfinMediaSourceById(id), cancellationToken);
        if (maybeSource.IsNone)
        {
            return ApiResults.NotFoundProblem();
        }

        if (entityLocker.IsRemoteMediaSourceLocked<JellyfinMediaSource>())
        {
            return ApiResults.ConflictProblem(
                "Jellyfin operation in progress",
                "A Jellyfin sign-in or sync is already in progress.");
        }

        await scannerWorkerChannel.WriteAsync(new SynchronizeJellyfinLibraries(id), cancellationToken);
        return new AcceptedResult();
    }

    [HttpPost("/api/v1/media-sources/jellyfin/{id:int}/scan-collections", Name = "ScanJellyfinCollections")]
    [Tags("Jellyfin")]
    [EndpointSummary("Scan a Jellyfin source's collections")]
    [EndpointDescription(
        "Queues a synchronization of the source's collections (fire-and-forget). Pass ?deep=true for a deep " +
        "scan. Returns 409 while a Jellyfin collections scan is already in progress.")]
    [EndpointGroupName("general")]
    [ProducesResponseType(StatusCodes.Status202Accepted)]
    [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
    [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status409Conflict)]
    public async Task<IActionResult> ScanCollections(
        int id,
        [FromQuery] bool deep = false,
        CancellationToken cancellationToken = default)
    {
        Option<JellyfinMediaSourceViewModel> maybeSource =
            await mediator.Send(new GetJellyfinMediaSourceById(id), cancellationToken);
        if (maybeSource.IsNone)
        {
            return ApiResults.NotFoundProblem();
        }

        // The collections lock IS the running scan (§3b): fail to acquire = a scan is already active → 409.
        if (!entityLocker.LockJellyfinCollections())
        {
            return ApiResults.ConflictProblem(
                "Jellyfin collections scan in progress",
                "A Jellyfin collections scan is already in progress; try again once it completes.");
        }

        try
        {
            await scannerWorkerChannel.WriteAsync(
                new SynchronizeJellyfinCollections(id, true, deep),
                cancellationToken);
        }
        catch
        {
            // the scanner releases the lock when it processes the message; if the enqueue throws after we
            // acquired the lock, release it here (EnqueueWithTraktLock compensating-unlock, §3b)
            entityLocker.UnlockJellyfinCollections();
            throw;
        }

        return new AcceptedResult();
    }

    // §C7: LockLibrary then enqueue the sync pair; a throw from the enqueue compensates by unlocking
    // (EnqueueWithTraktLock pattern) — a locked library is silently skipped (Blazor parity).
    private async Task EnqueueLibrarySync(int sourceId, int libraryId, CancellationToken cancellationToken)
    {
        if (!entityLocker.LockLibrary(libraryId))
        {
            return;
        }

        try
        {
            await scannerWorkerChannel.WriteAsync(new SynchronizeJellyfinLibraries(sourceId), cancellationToken);
            await scannerWorkerChannel.WriteAsync(
                new SynchronizeJellyfinLibraryByIdIfNeeded(libraryId),
                cancellationToken);
        }
        catch
        {
            entityLocker.UnlockLibrary(libraryId);
            throw;
        }
    }

    private static UnprocessableEntityObjectResult ValidateLibraryPreferences(
        ReplaceRemoteLibraryPreferencesRequest request,
        List<JellyfinLibraryViewModel> existingLibraries)
    {
        List<RemoteLibraryPreferenceRequest> libraries = request.Libraries ?? [];

        if (libraries.Any(l => l.Id < 1))
        {
            return new UnprocessableEntityObjectResult(CreateProblemDetails("Every library id is required."));
        }

        var existingIds = existingLibraries.Map(l => l.Id).ToList();
        var foreignIds = libraries.Filter(l => !existingIds.Contains(l.Id)).Map(l => l.Id).ToList();
        if (foreignIds.Count > 0)
        {
            return new UnprocessableEntityObjectResult(
                CreateProblemDetails(
                    $"Library id(s) {string.Join(", ", foreignIds)} do not belong to this Jellyfin source."));
        }

        var incomingIds = libraries.Map(l => l.Id).ToList();
        var missingIds = existingIds.Filter(existingId => !incomingIds.Contains(existingId)).ToList();
        if (missingIds.Count > 0)
        {
            return new UnprocessableEntityObjectResult(
                CreateProblemDetails(
                    "The request must include every library for this source " +
                    $"(missing id(s) {string.Join(", ", missingIds)})."));
        }

        return null;
    }

    private static ProblemDetails CreateProblemDetails(string detail) =>
        new()
        {
            Status = StatusCodes.Status422UnprocessableEntity,
            Title = "Validation failed",
            Detail = detail
        };

    private static RemoteLibraryResponseModel ProjectToResponseModel(JellyfinLibraryViewModel vm) =>
        new(vm.Id, vm.Name, vm.MediaKind, vm.ShouldSyncItems);

    private static PathReplacementResponseModel ProjectToResponseModel(JellyfinPathReplacementViewModel vm) =>
        new(vm.Id, vm.JellyfinPath, vm.LocalPath);
}
