using DeepDrftContent;
using DeepDrftContent.Constants;
using DeepDrftContent.FileDatabase.Models;
using DeepDrftContent.Processors;
using DeepDrftData;
using DeepDrftModels.Enums;
using NetBlocks.Models;
using FileDb = DeepDrftContent.FileDatabase.Services.FileDatabase;

namespace DeepDrftAPI.Services;

/// <summary>
/// Host-internal orchestrator for the two release metadata write paths. Mirrors
/// <see cref="UnifiedTrackService"/>: it makes DeepDrftAPI the single authority over both the vault
/// (FileDatabase) and SQL satellite rows, so the controller stays a thin HTTP boundary and no caller
/// coordinates the two stores.
/// </summary>
public class UnifiedReleaseService
{
    /// <summary>Error message returned when the Mix release has no linked track.</summary>
    public const string MixHasNoTrackMessage = "Mix release has no track.";

    /// <summary>Error message returned when the Mix track has no audio stored in the vault.</summary>
    public const string MixTrackNoAudioMessage = "No audio stored for the Mix track.";

    private readonly IReleaseService _releaseService;
    private readonly FileDb _fileDatabase;
    private readonly ImageProcessor _imageProcessor;
    private readonly TrackContentService _trackContentService;
    private readonly WaveformProfileService _waveformProfileService;
    private readonly ILogger<UnifiedReleaseService> _logger;

    public UnifiedReleaseService(
        IReleaseService releaseService,
        FileDb fileDatabase,
        ImageProcessor imageProcessor,
        TrackContentService trackContentService,
        WaveformProfileService waveformProfileService,
        ILogger<UnifiedReleaseService> logger)
    {
        _releaseService = releaseService;
        _fileDatabase = fileDatabase;
        _imageProcessor = imageProcessor;
        _trackContentService = trackContentService;
        _waveformProfileService = waveformProfileService;
        _logger = logger;
    }

    /// <summary>
    /// Process a hero image into the Images vault, then point the release's Session satellite at it.
    /// The medium check lives in <see cref="IReleaseService.SetSessionHeroImageAsync"/>: if the release
    /// is not a Session, the satellite is not written and the image is orphaned (logged, recoverable).
    /// </summary>
    public async Task<Result> SetHeroImageAsync(long releaseId, IFormFile imageFile, CancellationToken ct)
    {
        if (MimeTypeExtensions.GetExtension(imageFile.ContentType) == ".bin")
        {
            _logger.LogWarning(
                "SetHeroImage rejected: unsupported content type '{ContentType}' for release {ReleaseId}",
                imageFile.ContentType, releaseId);
            return Result.CreateFailResult($"Unsupported image content type: {imageFile.ContentType}");
        }

        byte[] buffer;
        await using (var stream = imageFile.OpenReadStream())
        using (var memory = new MemoryStream())
        {
            await stream.CopyToAsync(memory, ct);
            buffer = memory.ToArray();
        }

        var imageBinary = _imageProcessor.Process(buffer, imageFile.ContentType);
        if (imageBinary is null)
        {
            _logger.LogWarning("SetHeroImage: ImageProcessor rejected content type '{ContentType}'", imageFile.ContentType);
            return Result.CreateFailResult($"Unsupported image content type: {imageFile.ContentType}");
        }

        var entryKey = Guid.NewGuid().ToString("N");
        var stored = await _fileDatabase.RegisterResourceAsync(VaultConstants.Images, entryKey, imageBinary);
        if (!stored)
        {
            _logger.LogError("SetHeroImage: vault write failed for release {ReleaseId}, entryKey={EntryKey}", releaseId, entryKey);
            return Result.CreateFailResult("Failed to store hero image.");
        }

        var linked = await _releaseService.SetSessionHeroImageAsync(releaseId, entryKey, ct);
        if (!linked.Success)
        {
            // Vault write succeeded, SQL link failed — image is orphaned in the Images vault under
            // entryKey. Log loudly (include entryKey) so it is recoverable manually.
            var error = linked.Messages.FirstOrDefault()?.Message ?? "Unknown error";
            _logger.LogError(
                "Hero image stored in vault but Session link failed. Orphaned entry: {EntryKey}. Release: {ReleaseId}. Error: {Error}",
                entryKey, releaseId, error);
            return linked;
        }

        return Result.CreatePassResult();
    }

    /// <summary>
    /// Fetch the Mix's track audio from the vault, compute a high-res waveform datum at a constant time
    /// resolution (≈333 samples/sec derived from the track's duration; see
    /// <see cref="WaveformResolution"/>), store it in the TrackWaveforms vault under the track's
    /// EntryKey, then point the release's Mix satellite at that same key. The datum key equals the
    /// track's EntryKey — the Mix is single-track. Under the per-track model (phase-12 §5) this is the
    /// same datum every track now carries. The visualizer fetches it via the track-cardinal
    /// <c>GET api/track/{trackEntryKey}/waveform/high-res</c> (12.B2); the Mix satellite link and the
    /// legacy release-addressed read path are retained transitionally and no longer feed the visualizer.
    /// </summary>
    public async Task<Result> TriggerMixWaveformAsync(long releaseId, CancellationToken ct)
    {
        var lookup = await _releaseService.GetByIdAsync(releaseId, ct);
        if (!lookup.Success)
        {
            var error = lookup.Messages.FirstOrDefault()?.Message ?? "Unknown error";
            _logger.LogError("TriggerMixWaveform: release lookup failed for {ReleaseId}: {Error}", releaseId, error);
            return Result.CreateFailResult("Failed to load release.");
        }

        if (lookup.Value is null)
            return Result.CreateFailResult(ReleaseManager.ReleaseNotFoundMessage);

        // Pre-check medium here (before fetching audio) to avoid expensive waveform compute on a
        // non-Mix release. ReleaseManager.SetMixWaveformAsync enforces this too, so the double-check
        // is intentional — the orchestrator's guard is the cheap early-exit.
        if (lookup.Value.Medium != ReleaseMedium.Mix)
            return Result.CreateFailResult($"Release {releaseId} is not a Mix medium.");

        var keysResult = await _releaseService.GetTrackEntryKeysAsync(releaseId, ct);
        if (!keysResult.Success || keysResult.Value is null)
        {
            var error = keysResult.Messages.FirstOrDefault()?.Message ?? "Unknown error";
            _logger.LogError("TriggerMixWaveform: entry-key lookup failed for release {ReleaseId}: {Error}", releaseId, error);
            return Result.CreateFailResult("Failed to load release tracks.");
        }

        var entryKey = keysResult.Value.FirstOrDefault();
        if (entryKey is null)
        {
            _logger.LogWarning("TriggerMixWaveform: no track on Mix release {ReleaseId}", releaseId);
            return Result.CreateFailResult(MixHasNoTrackMessage);
        }

        var audio = await _trackContentService.GetAudioBinaryAsync(entryKey);
        if (audio is null)
        {
            _logger.LogWarning("TriggerMixWaveform: no audio in vault for {EntryKey} (release {ReleaseId})", entryKey, releaseId);
            return Result.CreateFailResult(MixTrackNoAudioMessage);
        }

        // Duration-derived, constant-time-resolution capture (≈333 samples/sec) so long mixes are not
        // under-sampled by a fixed bucket count — see WaveformResolution / spec §F. Same per-track
        // high-res datum every track now carries (phase-12 §5).
        var computed = await _waveformProfileService.ComputeAndStoreHighResAsync(
            audio.Buffer, entryKey, audio.Duration);
        if (!computed)
        {
            _logger.LogError("TriggerMixWaveform: waveform computation/storage failed for {EntryKey}", entryKey);
            return Result.CreateFailResult("Failed to compute the Mix waveform.");
        }

        var linked = await _releaseService.SetMixWaveformAsync(releaseId, entryKey, ct);
        if (!linked.Success)
        {
            var error = linked.Messages.FirstOrDefault()?.Message ?? "Unknown error";
            _logger.LogError(
                "Mix waveform stored in vault but Mix link failed. Entry: {EntryKey}. Release: {ReleaseId}. Error: {Error}",
                entryKey, releaseId, error);
            return linked;
        }

        return Result.CreatePassResult();
    }
}
