using Data.Errors;
using Data.Managers;
using DeepDrftData.Repositories;
using DeepDrftModels.DTOs;
using DeepDrftModels.Entities;
using DeepDrftModels.Enums;
using Microsoft.Extensions.Logging;
using Models.Common;
using NetBlocks.Models;

namespace DeepDrftData;

/// <summary>
/// SQL-side track service built on the BlazorBlocks Manager base. The layer boundary:
/// TrackRepository outputs entities; this service outputs DTOs via TrackConverter — the
/// single authoritative entity↔DTO conversion path. The ITrackService surface is DTO-typed
/// throughout; the entity never escapes the service layer.
///
/// The base Manager&lt;&gt; surface does not line up with ITrackService by signature (base
/// Add vs Create, base Update→Result vs Update→DTO, base Get/GetPage vs GetAll/GetPaged,
/// base GetById→TDto vs GetById→TDto?), so the query and mutation methods are implemented
/// here over Repository + TrackConverter. Only Delete(long)→Result is inherited unchanged.
/// </summary>
public class TrackManager
    : Manager<TrackEntity, TrackDto, TrackRepository, TrackConverter>, ITrackService
{
    public TrackManager(
        TrackRepository repository,
        ILogger<Manager<TrackEntity, TrackDto, TrackRepository, TrackConverter>> logger)
        : base(repository, logger)
    {
    }

    // Explicit impl: base GetById returns ResultContainer<TrackDto> (fails on miss); the
    // service contract is ResultContainer<TrackDto?> (pass with null on miss). Return types
    // differ, so this cannot be a public overload of the inherited member.
    async Task<ResultContainer<TrackDto?>> ITrackService.GetById(long id)
    {
        try
        {
            var entity = await Repository.GetByIdAsync(id);
            return ResultContainer<TrackDto?>.CreatePassResult(
                entity is null ? null : TrackConverter.Convert(entity));
        }
        catch (Exception e)
        {
            return ResultContainer<TrackDto?>.CreateFailResult(e.Message);
        }
    }

    // Lookup by vault entry key. No base-name conflict (unlike GetById), so this is a plain
    // public method. Mirrors the nullable-on-miss shape of ITrackService.GetById.
    public async Task<ResultContainer<TrackDto?>> GetByEntryKey(string entryKey)
    {
        try
        {
            var entity = await Repository.GetByEntryKeyAsync(entryKey);
            return ResultContainer<TrackDto?>.CreatePassResult(
                entity is null ? null : TrackConverter.Convert(entity));
        }
        catch (Exception e)
        {
            return ResultContainer<TrackDto?>.CreateFailResult(e.Message);
        }
    }

    // No base-name conflict, so this is a plain public method. Mirrors the nullable-on-empty
    // shape of GetById: pass with null when the library has no tracks.
    public async Task<ResultContainer<TrackDto?>> GetRandom(CancellationToken cancellationToken = default)
    {
        try
        {
            var entity = await Repository.GetRandomAsync(cancellationToken);
            return ResultContainer<TrackDto?>.CreatePassResult(
                entity is null ? null : TrackConverter.Convert(entity));
        }
        catch (Exception e)
        {
            return ResultContainer<TrackDto?>.CreateFailResult(e.Message);
        }
    }

    public async Task<ResultContainer<List<TrackDto>>> GetAll()
    {
        try
        {
            var entities = await Repository.GetAllAsync();
            return ResultContainer<List<TrackDto>>.CreatePassResult(
                entities.Select(TrackConverter.Convert).ToList());
        }
        catch (Exception e)
        {
            return ResultContainer<List<TrackDto>>.CreateFailResult(e.Message);
        }
    }

    public async Task<ResultContainer<PagedResult<TrackDto>>> GetPaged(
        int pageNumber,
        int pageSize,
        string? sortColumn,
        bool sortDescending,
        TrackFilter? filter = null,
        CancellationToken cancellationToken = default)
    {
        try
        {
            var parameters = new PagingParameters<TrackEntity>
            {
                Page = pageNumber,
                PageSize = pageSize,
                IsDescending = sortDescending,
                // Sorts navigate through the nullable Release relation; the null-coalescing
                // sentinels push loose tracks (no release) to the end, matching the prior
                // nulls-last behaviour on the flat columns.
                OrderBy = sortColumn switch
                {
                    "TrackName"   => e => e.TrackName,
                    "Artist"      => e => (object)(e.Release == null ? string.Empty : e.Release.Artist),
                    "Album"       => e => (object)(e.Release == null ? string.Empty : e.Release.Title),
                    "Genre"       => e => (object)(e.Release == null ? string.Empty : (e.Release.Genre ?? string.Empty)),
                    "ReleaseDate"  => e => (object)(e.Release == null ? DateOnly.MaxValue : (e.Release.ReleaseDate ?? DateOnly.MaxValue)),
                    "TrackNumber"  => e => e.TrackNumber,
                    _              => e => e.Id
                }
            };

            // Always route through GetPagedFilteredAsync — it handles a null filter by skipping
            // all Where predicates, and it always includes Release. This removes the base-class
            // GetPagedAsync path, which has no .Include and would return entities with null Release.
            var effectiveFilter = filter is null || filter.IsEmpty ? null : filter;

            var page = await Repository.GetPagedFilteredAsync(parameters, effectiveFilter, cancellationToken);

            var dtoPage = PagedResult<TrackDto>.From(page, page.Items.Select(TrackConverter.Convert));
            return ResultContainer<PagedResult<TrackDto>>.CreatePassResult(dtoPage);
        }
        catch (Exception e)
        {
            return ResultContainer<PagedResult<TrackDto>>.CreateFailResult(e.Message);
        }
    }

    public async Task<ResultContainer<List<ReleaseDto>>> GetReleases(CancellationToken cancellationToken = default)
    {
        try
        {
            var releases = await Repository.GetReleasesAsync(cancellationToken);
            var counts = await Repository.GetTrackCountsByReleaseAsync(cancellationToken);

            var dtos = releases
                .Select(r =>
                {
                    var dto = TrackConverter.Convert(r);
                    dto.TrackCount = counts.GetValueOrDefault(r.Id);
                    return dto;
                })
                .ToList();

            return ResultContainer<List<ReleaseDto>>.CreatePassResult(dtos);
        }
        catch (Exception e)
        {
            return ResultContainer<List<ReleaseDto>>.CreateFailResult(e.Message);
        }
    }

    public async Task<ResultContainer<(ReleaseDto Release, bool WasCreated)>> FindOrCreateRelease(
        string title, string artist, ReleaseDto releaseData, CancellationToken cancellationToken = default)
    {
        try
        {
            var existing = await Repository.GetReleaseByTitleAndArtistAsync(title, artist, cancellationToken);
            if (existing is not null)
                return ResultContainer<(ReleaseDto, bool)>.CreatePassResult((TrackConverter.Convert(existing), false));

            // The natural key (title + artist) is authoritative — override whatever the caller put
            // in releaseData so a typo upstream cannot create a release that won't be found again.
            var entity = TrackConverter.Convert(releaseData);
            entity.Id = 0;
            // Mint the public EntryKey app-side at creation — the identical call tracks make in
            // TrackContentService (Phase 11 §3e.4). The incoming DTO carries no key on the create path.
            entity.EntryKey = Guid.NewGuid().ToString();
            entity.Title = title;
            entity.Artist = artist;

            try
            {
                var added = await Repository.AddReleaseAsync(entity, cancellationToken);
                return ResultContainer<(ReleaseDto, bool)>.CreatePassResult((TrackConverter.Convert(added), true));
            }
            catch (ClassifiedDbException ex) when (ex.Error.Category == DbErrorCategory.UniqueViolation)
            {
                // Concurrent upload inserted the same (title, artist) between our read and write.
                // Re-query and return the winning row as WasCreated=false so the caller (UploadAsync
                // CREATE path) treats the lost race as a duplicate rather than silently attaching.
                var race = await Repository.GetReleaseByTitleAndArtistAsync(title, artist, cancellationToken);
                if (race is null) throw;
                return ResultContainer<(ReleaseDto, bool)>.CreatePassResult((TrackConverter.Convert(race), false));
            }
        }
        catch (Exception e)
        {
            return ResultContainer<(ReleaseDto, bool)>.CreateFailResult(e.Message);
        }
    }

    public async Task<ResultContainer<ReleaseDto?>> GetReleaseByTitleAndArtist(
        string title, string artist, CancellationToken cancellationToken = default)
    {
        try
        {
            var existing = await Repository.GetReleaseByTitleAndArtistAsync(title, artist, cancellationToken);
            if (existing is null)
                return ResultContainer<ReleaseDto?>.CreatePassResult(null);

            var dto = TrackConverter.Convert(existing);
            dto.TrackCount = await Repository.CountLiveTracksByReleaseAsync(existing.Id, cancellationToken);
            return ResultContainer<ReleaseDto?>.CreatePassResult(dto);
        }
        catch (Exception e)
        {
            return ResultContainer<ReleaseDto?>.CreateFailResult(e.Message);
        }
    }

    public async Task<ResultContainer<List<GenreSummaryDto>>> GetDistinctGenres(CancellationToken cancellationToken = default)
    {
        try
        {
            var genres = await Repository.GetDistinctGenresAsync(cancellationToken);
            return ResultContainer<List<GenreSummaryDto>>.CreatePassResult(genres);
        }
        catch (Exception e)
        {
            return ResultContainer<List<GenreSummaryDto>>.CreateFailResult(e.Message);
        }
    }

    public async Task<ResultContainer<HomeStatsDto>> GetHomeStats(CancellationToken cancellationToken = default)
    {
        try
        {
            var stats = await Repository.GetHomeStatsAsync(cancellationToken);
            return ResultContainer<HomeStatsDto>.CreatePassResult(stats);
        }
        catch (Exception e)
        {
            return ResultContainer<HomeStatsDto>.CreateFailResult(e.Message);
        }
    }

    public async Task<ResultContainer<List<TrackDto>>> GetTracksMissingDuration(CancellationToken cancellationToken = default)
    {
        try
        {
            var entities = await Repository.GetTracksMissingDurationAsync(cancellationToken);
            return ResultContainer<List<TrackDto>>.CreatePassResult(
                entities.Select(TrackConverter.Convert).ToList());
        }
        catch (Exception e)
        {
            return ResultContainer<List<TrackDto>>.CreateFailResult(e.Message);
        }
    }

    public async Task<ResultContainer<int>> UpdateDuration(long id, double durationSeconds, CancellationToken cancellationToken = default)
    {
        try
        {
            var updated = await Repository.UpdateDurationAsync(id, durationSeconds, cancellationToken);
            return ResultContainer<int>.CreatePassResult(updated);
        }
        catch (Exception e)
        {
            return ResultContainer<int>.CreateFailResult(e.Message);
        }
    }

    public async Task<ResultContainer<int>> SetDuration(long id, double durationSeconds, CancellationToken cancellationToken = default)
    {
        try
        {
            var affected = await Repository.SetDurationAsync(id, durationSeconds, cancellationToken);
            if (affected == 0)
                return ResultContainer<int>.CreateFailResult($"Duration write matched no rows for track {id}.");
            return ResultContainer<int>.CreatePassResult(affected);
        }
        catch (Exception e)
        {
            return ResultContainer<int>.CreateFailResult(e.Message);
        }
    }

    public async Task<ResultContainer<TrackDto>> Create(TrackDto newTrack)
    {
        try
        {
            // A track with release context resolves (or creates) the shared release first so the FK
            // is set before insert. A standalone track (Release null) stays a loose track, ReleaseId
            // null. Callers that already resolved the FK (UnifiedTrackService) pass Release null and
            // a populated ReleaseId, which falls straight through.
            if (newTrack.Release is { } release && !string.IsNullOrWhiteSpace(release.Title))
            {
                var resolved = await FindOrCreateRelease(release.Title, release.Artist, release);
                if (!resolved.Success)
                {
                    var error = resolved.Messages.FirstOrDefault()?.Message ?? "Failed to resolve release.";
                    return ResultContainer<TrackDto>.CreateFailResult(error);
                }

                newTrack.ReleaseId = resolved.Value.Release.Id;
            }

            var added = await Repository.AddAsync(TrackConverter.Convert(newTrack));
            return ResultContainer<TrackDto>.CreatePassResult(TrackConverter.Convert(added));
        }
        catch (Exception e)
        {
            return ResultContainer<TrackDto>.CreateFailResult(e.Message);
        }
    }

    // Explicit impl: base Update returns Result; the service contract returns the persisted
    // DTO so the CMS edit flow reads back DB-authoritative values.
    async Task<ResultContainer<TrackDto>> ITrackService.Update(TrackDto track)
    {
        try
        {
            await Repository.UpdateAsync(TrackConverter.Convert(track));

            // Release-cardinal edits flow through the linked release row, not the track. When the
            // track carries a Release payload and a resolved FK, load the tracked release, apply the
            // edited fields, and save. EntryKey/track fields are already persisted above.
            if (track.Release is { } release && track.ReleaseId is { } releaseId)
            {
                var releaseEntity = await Repository.GetReleaseByIdAsync(releaseId);
                if (releaseEntity is not null)
                {
                    releaseEntity.Title = release.Title;
                    releaseEntity.Artist = release.Artist;
                    releaseEntity.Genre = release.Genre;
                    releaseEntity.Description = release.Description;
                    releaseEntity.ReleaseDate = release.ReleaseDate;
                    releaseEntity.ImagePath = release.ImagePath;
                    releaseEntity.Medium = release.Medium;
                    // DTO ReleaseType is nullable (meaningful only for Cut); the entity field is not.
                    // Default to Single when null, matching TrackConverter.Convert(ReleaseDto).
                    releaseEntity.ReleaseType = release.ReleaseType ?? ReleaseType.Single;
                    releaseEntity.CreatedByUserId = release.CreatedByUserId;
                    await Repository.UpdateReleaseAsync(releaseEntity);
                }
            }

            var updated = await Repository.GetByIdAsync(track.Id);
            return updated is not null
                ? ResultContainer<TrackDto>.CreatePassResult(TrackConverter.Convert(updated))
                : ResultContainer<TrackDto>.CreateFailResult("Track not found after update.");
        }
        catch (Exception e)
        {
            return ResultContainer<TrackDto>.CreateFailResult(e.Message);
        }
    }

    // Delete(long) → Result is inherited from Manager<> and satisfies ITrackService.Delete
    // by signature. No override.

    public async Task<Result> DeleteRelease(long id, CancellationToken cancellationToken = default)
    {
        try
        {
            await Repository.SoftDeleteReleaseAsync(id, cancellationToken);
            return Result.CreatePassResult();
        }
        catch (Exception e)
        {
            return Result.CreateFailResult(e.Message);
        }
    }

    public async Task<ResultContainer<int>> CountLiveTracksByRelease(long releaseId, CancellationToken cancellationToken = default)
    {
        try
        {
            var count = await Repository.CountLiveTracksByReleaseAsync(releaseId, cancellationToken);
            return ResultContainer<int>.CreatePassResult(count);
        }
        catch (Exception e)
        {
            return ResultContainer<int>.CreateFailResult(e.Message);
        }
    }
}
