using DeepDrftContent.Constants;
using DeepDrftContent.FileDatabase.Models;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using FileDb = DeepDrftContent.FileDatabase.Services.FileDatabase;

namespace DeepDrftContent.Processors;

/// <summary>
/// Computes a track's waveform loudness profile from its WAV bytes and persists it as a sidecar
/// in the <see cref="VaultConstants.WaveformProfiles"/> vault, keyed by the track's EntryKey.
/// The profile is the upload-time, off-the-playback-path representation the frontend fetches to
/// render the WaveformSeeker. The loudness measure is injected (<see cref="ILoudnessAlgorithm"/>)
/// so it can be swapped without changing storage or the wire format.
/// </summary>
public class WaveformProfileService
{
    private const string ProfileExtension = ".wfp";

    private readonly FileDb _fileDatabase;
    private readonly AudioProcessor _audioProcessor;
    private readonly ILoudnessAlgorithm _loudnessAlgorithm;
    private readonly WaveformProfileOptions _options;
    private readonly ILogger<WaveformProfileService> _logger;

    public WaveformProfileService(
        FileDb fileDatabase,
        AudioProcessor audioProcessor,
        ILoudnessAlgorithm loudnessAlgorithm,
        IOptions<WaveformProfileOptions> options,
        ILogger<WaveformProfileService> logger)
    {
        _fileDatabase = fileDatabase;
        _audioProcessor = audioProcessor;
        _loudnessAlgorithm = loudnessAlgorithm;
        _options = options.Value;
        _logger = logger;
    }

    /// <summary>
    /// Computes the loudness profile from <paramref name="wavBytes"/> and stores it under
    /// <paramref name="entryKey"/> in <paramref name="vaultName"/> (defaults to
    /// <see cref="VaultConstants.WaveformProfiles"/> when null). Bucket resolution defaults to
    /// <see cref="WaveformProfileOptions.BucketCount"/> (512) when <paramref name="bucketCount"/> is null;
    /// callers pass an explicit count for higher-resolution data — e.g. the per-track high-res datum
    /// derives its count from the audio duration (≈333 samples/sec, see <c>WaveformResolution</c>) so long
    /// tracks are not under-sampled. This service is content-agnostic: it captures however many buckets it is told to and
    /// does not itself decide the count. Returns false (and logs) on any
    /// failure — a missing profile is handled gracefully downstream, so callers on the upload path
    /// log-and-continue rather than failing the upload. Does not throw for expected failure modes.
    /// </summary>
    public async Task<bool> ComputeAndStoreAsync(
        ReadOnlyMemory<byte> wavBytes,
        string entryKey,
        int? bucketCount = null,
        string? vaultName = null)
    {
        var effectiveBucketCount = bucketCount ?? _options.BucketCount;
        var effectiveVaultName = vaultName ?? VaultConstants.WaveformProfiles;

        try
        {
            var pcm = _audioProcessor.TryExtractPcm(wavBytes.Span);
            if (pcm is null)
            {
                _logger.LogWarning(
                    "Waveform profile not computed for {EntryKey}: WAV PCM could not be extracted.",
                    entryKey);
                return false;
            }

            var value = pcm.Value;
            var profile = _loudnessAlgorithm.Compute(
                value.Pcm.Span,
                value.Channels,
                value.SampleRate,
                value.BitsPerSample,
                effectiveBucketCount);

            var quantized = Quantize(profile);

            await EnsureVaultAsync(effectiveVaultName);

            var binary = new MediaBinary(new MediaBinaryParams(quantized, quantized.Length, ProfileExtension));
            var stored = await _fileDatabase.RegisterResourceAsync(effectiveVaultName, entryKey, binary);

            if (!stored)
            {
                _logger.LogWarning("Waveform profile vault write failed for {EntryKey}.", entryKey);
                return false;
            }

            return true;
        }
        catch (Exception ex) when (ex is not OperationCanceledException)
        {
            _logger.LogError(ex, "Waveform profile computation failed for {EntryKey}.", entryKey);
            return false;
        }
    }

    /// <summary>
    /// Computes a track's high-resolution loudness datum and stores it in the
    /// <see cref="VaultConstants.TrackWaveforms"/> vault keyed by <paramref name="entryKey"/>. The bucket
    /// count is duration-derived (≈333 samples/sec, clamped — see <see cref="WaveformResolution"/>) so the
    /// datum captures at a constant time resolution regardless of track length. This is the single home
    /// for "the high-res per-track datum" — the upload path, the CMS generate action, and the Mix trigger
    /// all funnel through it, so every track (Mix, Session, Cut) gets an identical datum keyed the same way.
    /// Returns false (logged) on any failure, per the content-agnostic contract above.
    /// </summary>
    public Task<bool> ComputeAndStoreHighResAsync(
        ReadOnlyMemory<byte> wavBytes,
        string entryKey,
        double durationSeconds)
    {
        var bucketCount = WaveformResolution.BucketCountForDuration(durationSeconds);
        return ComputeAndStoreAsync(wavBytes, entryKey, bucketCount, VaultConstants.TrackWaveforms);
    }

    /// <summary>
    /// Returns the stored quantized profile bytes for a track from <paramref name="vaultName"/>
    /// (defaults to <see cref="VaultConstants.WaveformProfiles"/> when null), or null if no profile
    /// is stored (existing tracks predate profiling, and computation may have failed). Each byte is
    /// a peak-normalized loudness value in [0, 255].
    /// </summary>
    public async Task<byte[]?> GetProfileAsync(string entryKey, string? vaultName = null)
    {
        var binary = await _fileDatabase.LoadResourceAsync<MediaBinary>(
            vaultName ?? VaultConstants.WaveformProfiles, entryKey);
        return binary?.Buffer;
    }

    /// <summary>
    /// Maps each [0, 1] bucket to a [0, 255] byte. 1.0 maps to 255; the multiply-by-255 with a
    /// truncating cast keeps every in-range value within a byte without a clamp branch.
    /// </summary>
    private static byte[] Quantize(double[] profile)
    {
        var bytes = new byte[profile.Length];
        for (var i = 0; i < profile.Length; i++)
        {
            bytes[i] = (byte)(profile[i] * 255);
        }
        return bytes;
    }

    private async Task EnsureVaultAsync(string vaultName)
    {
        if (!_fileDatabase.HasVault(vaultName))
        {
            await _fileDatabase.CreateVaultAsync(vaultName, MediaVaultType.Media);
        }
    }
}
