using System.ComponentModel;
using System.Diagnostics;
using System.Text;

namespace EllieBot.Modules.Music.Resolvers;

public sealed class LocalTrackResolver : ILocalTrackResolver
{
    private static readonly HashSet<string> _musicExtensions = new[]
    {
        ".MP4", ".MP3", ".FLAC", ".OGG", ".WAV", ".WMA", ".WMV", ".AAC", ".MKV", ".WEBM", ".M4A", ".AA", ".AAX",
        ".ALAC", ".AIFF", ".MOV", ".FLV", ".OGG", ".M4V"
    }.ToHashSet();

    public async Task<ITrackInfo?> ResolveByQueryAsync(string query)
    {
        if (!File.Exists(query))
            return null;

        var trackDuration = await Ffprobe.GetTrackDurationAsync(query);
        return new SimpleTrackInfo(Path.GetFileNameWithoutExtension(query),
            $"https://google.com?q={Uri.EscapeDataString(Path.GetFileNameWithoutExtension(query))}",
            "https://cdn.discordapp.com/attachments/155726317222887425/261850914783100928/1482522077_music.png",
            trackDuration,
            MusicPlatform.Local,
            $"\"{Path.GetFullPath(query)}\"");
    }

    public async IAsyncEnumerable<ITrackInfo> ResolveDirectoryAsync(string dirPath)
    {
        DirectoryInfo dir;
        try
        {
            dir = new(dirPath);
        }
        catch (Exception ex)
        {
            Log.Error(ex, "Specified directory {DirectoryPath} could not be opened", dirPath);
            yield break;
        }

        var files = dir.EnumerateFiles("*", SearchOption.AllDirectories)
                       .Where(x =>
                       {
                           if (!x.Attributes.HasFlag(FileAttributes.Hidden | FileAttributes.System)
                               && _musicExtensions.Contains(x.Extension.ToUpperInvariant()))
                               return true;
                           return false;
                       })
                       .ToList();

        var firstFile = files.FirstOrDefault()?.FullName;
        if (firstFile is null)
            yield break;

        var firstData = await ResolveByQueryAsync(firstFile);
        if (firstData is not null)
            yield return firstData;

        var fileChunks = files.Skip(1).Chunk(10);
        foreach (var chunk in fileChunks)
        {
            var part = await chunk.Select(x => ResolveByQueryAsync(x.FullName)).WhenAll();

            // nullable reference types being annoying
            foreach (var p in part)
            {
                if (p is null)
                    continue;

                yield return p;
            }
        }
    }
}

public static class Ffprobe
{
    public static async Task<TimeSpan> GetTrackDurationAsync(string query)
    {
        query = query.Replace("\"", "");

        try
        {
            using var p = Process.Start(new ProcessStartInfo
            {
                FileName = "ffprobe",
                Arguments =
                    $"-v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 -- \"{query}\"",
                UseShellExecute = false,
                RedirectStandardOutput = true,
                RedirectStandardError = true,
                StandardOutputEncoding = Encoding.UTF8,
                StandardErrorEncoding = Encoding.UTF8,
                CreateNoWindow = true
            });

            if (p is null)
                return TimeSpan.Zero;

            var data = await p.StandardOutput.ReadToEndAsync();
            if (double.TryParse(data, out var seconds))
                return TimeSpan.FromSeconds(seconds);

            var errorData = await p.StandardError.ReadToEndAsync();
            if (!string.IsNullOrWhiteSpace(errorData))
                Log.Warning("Ffprobe warning for file {FileName}: {ErrorMessage}", query, errorData);

            return TimeSpan.Zero;
        }
        catch (Win32Exception)
        {
            Log.Warning("Ffprobe was likely not installed. Local song durations will show as (?)");
        }
        catch (Exception ex)
        {
            Log.Error(ex, "Unknown exception running ffprobe; {ErrorMessage}", ex.Message);
        }

        return TimeSpan.Zero;
    }
}