using DeepDrftContent.Constants;
using DeepDrftContent.FileDatabase.Services;
using DeepDrftContent.FileDatabase.Models;
using DeepDrftContent.Processors;
using DeepDrftModels.Entities;

namespace DeepDrftContent;

/// <summary>
/// Service for managing tracks in both SQL and FileDatabase
/// </summary>
public class TrackContentService
{
    private readonly FileDatabase.Services.FileDatabase _fileDatabase;
    private readonly AudioProcessorRouter _audioProcessorRouter;

    public TrackContentService(FileDatabase.Services.FileDatabase fileDatabase, AudioProcessorRouter audioProcessorRouter)
    {
        _fileDatabase = fileDatabase;
        _audioProcessorRouter = audioProcessorRouter;
    }

    /// <summary>
    /// Adds a new track from a supported audio file (.wav, .mp3, .flac) to both databases. The
    /// router selects the processor by extension; original bytes are stored for mp3/flac (no
    /// transcoding), while EXTENSIBLE WAVs are normalized to standard PCM at storage time.
    /// </summary>
    /// <param name="audioFilePath">Path to the audio file</param>
    /// <param name="trackName">Name of the track</param>
    /// <param name="artist">Artist name</param>
    /// <param name="album">Optional album name</param>
    /// <param name="genre">Optional genre</param>
    /// <param name="releaseDate">Optional release date</param>
    /// <param name="originalFileName">Optional original browser filename captured at upload time</param>
    /// <returns>The track entity with generated ID and media path</returns>
    public async Task<TrackEntity?> AddTrackAsync(
        string audioFilePath,
        string trackName,
        string artist,
        string? album = null,
        string? genre = null,
        DateOnly? releaseDate = null,
        string? originalFileName = null)
    {
        try
        {
            // Process the audio file (routed by extension)
            var audioBinary = await _audioProcessorRouter.ProcessAudioFileAsync(audioFilePath);
            if (audioBinary == null)
            {
                throw new InvalidOperationException("Failed to process audio file");
            }

            // Generate a unique track ID
            var trackId = Guid.NewGuid().ToString();

            // Ensure tracks vault exists
            if (!_fileDatabase.HasVault(VaultConstants.Tracks))
            {
                await _fileDatabase.CreateVaultAsync(VaultConstants.Tracks, MediaVaultType.Audio);
            }

            // Store the audio in FileDatabase
            var success = await _fileDatabase.RegisterResourceAsync(VaultConstants.Tracks, trackId, audioBinary);
            if (!success)
            {
                throw new InvalidOperationException("Failed to store audio in FileDatabase");
            }

            // Create the track entity for SQL database. Post Phase 8 §8.0 the entity holds only
            // track-cardinal fields; release-cardinal data (artist/album/genre/releaseDate) is
            // resolved into a ReleaseEntity by the caller (UnifiedTrackService) and linked via FK.
            var trackEntity = new TrackEntity
            {
                EntryKey = trackId, // FileDatabase entry ID
                TrackName = trackName,
                OriginalFileName = originalFileName,
                // Persist the processor-extracted runtime to SQL so aggregate queries (total mix runtime)
                // need not touch the vault. Same value the high-res waveform compute reads downstream.
                DurationSeconds = audioBinary.Duration
            };

            return trackEntity;
        }
        catch (Exception ex) when (ex is not OperationCanceledException)
        {
            Console.WriteLine($"TrackContentService.AddTrackAsync failed: {ex.Message}");
            return null;
        }
    }

    /// <summary>
    /// Backward-compatible shim — delegates to <see cref="AddTrackAsync"/>. The router accepts WAV
    /// alongside MP3 and FLAC, so this carries no WAV-specific logic of its own.
    /// </summary>
    public Task<TrackEntity?> AddTrackFromWavAsync(
        string wavFilePath,
        string trackName,
        string artist,
        string? album = null,
        string? genre = null,
        DateOnly? releaseDate = null,
        string? originalFileName = null) =>
        AddTrackAsync(wavFilePath, trackName, artist, album, genre, releaseDate, originalFileName);

    /// <summary>
    /// Swaps the audio bytes for an existing track in place: processes a new audio file and
    /// re-registers it under the SAME <paramref name="entryKey"/> in the tracks vault. The track's
    /// vault key — and therefore its SQL link, release membership, position, and metadata — is
    /// untouched; only the binary changes. The new audio is written first; only on confirmed success
    /// is a stale old backing file cleaned up. A cross-format replacement (e.g. .wav → .flac) leaves
    /// the old file on disk under its former filename once the index is updated; the post-success
    /// cleanup removes it. For a same-extension overwrite the register alone suffices — the file is
    /// written in place. If the register fails the original audio is left intact and null is returned,
    /// so the track remains playable. Returns the freshly stored <see cref="AudioBinary"/> on success
    /// (so the caller can regenerate waveform data from the same bytes) — matching the FileDatabase
    /// swallow-and-return-null contract.
    /// </summary>
    public async Task<AudioBinary?> ReplaceTrackAudioAsync(string entryKey, string audioFilePath)
    {
        try
        {
            // Capture the old extension before touching the vault. After register the index
            // will point to the new extension, so we need the old value now to detect a
            // cross-format swap and clean up the stale file post-success.
            var existing = await _fileDatabase.LoadResourceAsync<AudioBinary>(VaultConstants.Tracks, entryKey);
            var oldExtension = existing?.Extension;

            var audioBinary = await _audioProcessorRouter.ProcessAudioFileAsync(audioFilePath);
            if (audioBinary == null)
            {
                Console.WriteLine($"TrackContentService.ReplaceTrackAudioAsync: processing returned null for {entryKey}");
                return null;
            }

            if (!_fileDatabase.HasVault(VaultConstants.Tracks))
            {
                await _fileDatabase.CreateVaultAsync(VaultConstants.Tracks, MediaVaultType.Audio);
            }

            // Register the new audio. This upserts the index entry (new extension recorded) and
            // writes the new file to disk. If this fails the original entry and file are untouched.
            var success = await _fileDatabase.RegisterResourceAsync(VaultConstants.Tracks, entryKey, audioBinary);
            if (!success)
            {
                Console.WriteLine($"TrackContentService.ReplaceTrackAudioAsync: vault write failed for {entryKey}; original audio preserved");
                return null;
            }

            // Post-success stale-file cleanup for cross-format swaps. The register wrote the new
            // file (e.g. .flac) and updated the index to the new extension, but the old backing
            // file (e.g. .wav) is now unreferenced on disk. Delete it directly by constructing the
            // old path — RemoveResourceAsync would now resolve to the new extension and delete the
            // wrong file. Non-fatal: an orphaned old file is a disk-hygiene concern, not a
            // playback issue (the index no longer references it).
            if (oldExtension != null && oldExtension != audioBinary.Extension)
            {
                var vault = _fileDatabase.GetVault(VaultConstants.Tracks);
                if (vault != null)
                {
                    var sanitizedKey = System.Text.RegularExpressions.Regex.Replace(entryKey, @"[^a-zA-Z0-9]", "-");
                    var staleFilePath = Path.Combine(vault.RootPath, $"{sanitizedKey}{oldExtension}");
                    try
                    {
                        if (File.Exists(staleFilePath))
                            File.Delete(staleFilePath);
                    }
                    catch (Exception ex)
                    {
                        Console.WriteLine($"TrackContentService.ReplaceTrackAudioAsync: stale backing-file removal failed for {entryKey} ({staleFilePath}): {ex.Message} — new audio is live; orphaned file may remain on disk");
                    }
                }
            }

            return audioBinary;
        }
        catch (Exception ex) when (ex is not OperationCanceledException)
        {
            Console.WriteLine($"TrackContentService.ReplaceTrackAudioAsync failed: {ex.Message}");
            return null;
        }
    }

    /// <summary>
    /// Retrieves audio binary from FileDatabase
    /// </summary>
    /// <param name="trackId">Track ID (EntryKey)</param>
    /// <returns>Audio binary or null if not found</returns>
    public async Task<AudioBinary?> GetAudioBinaryAsync(string trackId)
    {
        return await _fileDatabase.LoadResourceAsync<AudioBinary>(VaultConstants.Tracks, trackId);
    }

    /// <summary>
    /// Checks if FileDatabase is available and tracks vault exists
    /// </summary>
    public bool IsFileDatabaseReady()
    {
        return _fileDatabase.HasVault(VaultConstants.Tracks);
    }

    /// <summary>
    /// Initializes the tracks vault if it doesn't exist
    /// </summary>
    public async Task InitializeTracksVaultAsync()
    {
        if (!_fileDatabase.HasVault(VaultConstants.Tracks))
        {
            await _fileDatabase.CreateVaultAsync(VaultConstants.Tracks, MediaVaultType.Audio);
        }
    }
}
