using System.Diagnostics;
using System.Reflection;
using System.Runtime.InteropServices;
using Newtonsoft.Json;
using OF_DL.Helpers;
using OF_DL.Models;
using OF_DL.Models.OfdlApi;
using Serilog;
using static Newtonsoft.Json.JsonConvert;
using WidevineConstants = OF_DL.Widevine.Constants;

namespace OF_DL.Services;

public class StartupService(IConfigService configService, IAuthService authService) : IStartupService
{
    /// <summary>
    /// Validates the runtime environment and returns a structured result.
    /// </summary>
    /// <returns>A result describing environment checks and detected tools.</returns>
    public async Task<StartupResult> ValidateEnvironmentAsync()
    {
        StartupResult result = new();

        // OS validation
        OperatingSystem os = Environment.OSVersion;
        result.OsVersionString = os.VersionString;
        Log.Debug($"Operating system information: {os.VersionString}");

        if (os.Platform == PlatformID.Win32NT && os.Version.Major < 10)
        {
            result.IsWindowsVersionValid = false;
            Log.Error("Windows version prior to 10.x: {0}", os.VersionString);
        }

        // FFmpeg detection
        DetectFfmpeg(result);
        // FFprobe detection
        DetectFfprobe(result);

        if (result is { FfmpegFound: true, FfmpegPath: not null })
        {
            // Escape backslashes for Windows
            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) &&
                result.FfmpegPath.Contains(@":\") &&
                !result.FfmpegPath.Contains(@":\\"))
            {
                result.FfmpegPath = result.FfmpegPath.Replace(@"\", @"\\");
                configService.CurrentConfig.FFmpegPath = result.FfmpegPath;
            }

            // Get FFmpeg version
            result.FfmpegVersion = await GetToolVersionAsync(result.FfmpegPath, "ffmpeg");
        }

        if (result is { FfprobeFound: true, FfprobePath: not null })
        {
            // Escape backslashes for Windows
            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) &&
                result.FfprobePath.Contains(@":\") &&
                !result.FfprobePath.Contains(@":\\"))
            {
                result.FfprobePath = result.FfprobePath.Replace(@"\", @"\\");
                configService.CurrentConfig.FFprobePath = result.FfprobePath;
            }

            // Get FFprobe version
            result.FfprobeVersion = await GetToolVersionAsync(result.FfprobePath, "ffprobe");
        }

        // Widevine device checks
        result.ClientIdBlobMissing = !File.Exists(Path.Join(WidevineConstants.DEVICES_FOLDER,
            WidevineConstants.DEVICE_NAME, "device_client_id_blob"));
        result.DevicePrivateKeyMissing = !File.Exists(Path.Join(WidevineConstants.DEVICES_FOLDER,
            WidevineConstants.DEVICE_NAME, "device_private_key"));

        Log.Debug("device_client_id_blob {Status}", result.ClientIdBlobMissing ? "missing" : "found");
        Log.Debug("device_private_key {Status}", result.DevicePrivateKeyMissing ? " missing" : "found");

        // rules.json validation
        if (!File.Exists("rules.json"))
        {
            return result;
        }

        result.RulesJsonExists = true;
        try
        {
            DeserializeObject<DynamicRules>(await File.ReadAllTextAsync("rules.json"));
            Log.Debug("Rules.json: ");
            Log.Debug(SerializeObject(await File.ReadAllTextAsync("rules.json"), Formatting.Indented));
            result.RulesJsonValid = true;
        }
        catch (Exception e)
        {
            result.RulesJsonError = e.Message;
            Log.Error("rules.json processing failed. {ErrorMessage}", e.Message);
        }

        return result;
    }

    /// <summary>
    /// Checks the current application version against the latest release tag.
    /// </summary>
    /// <returns>A result describing the version check status.</returns>
    public async Task<VersionCheckResult> CheckVersionAsync()
    {
        VersionCheckResult result = new();

#if !DEBUG
        try
        {
            result.LocalVersion = Assembly.GetEntryAssembly()?.GetName().Version;

            using CancellationTokenSource cts = new(TimeSpan.FromSeconds(30));
            string? latestReleaseTag;

            try
            {
                latestReleaseTag = await VersionHelper.GetLatestReleaseTag(cts.Token);
            }
            catch (OperationCanceledException)
            {
                result.TimedOut = true;
                Log.Warning("Version check timed out after 30 seconds");
                return result;
            }

            if (latestReleaseTag == null)
            {
                result.CheckFailed = true;
                Log.Error("Failed to get the latest release tag.");
                return result;
            }

            result.LatestVersion = new Version(latestReleaseTag.Replace("OFDLV", ""));
            int? versionComparison = result.LocalVersion?.CompareTo(result.LatestVersion);
            result.IsUpToDate = versionComparison >= 0;

            Log.Debug("Detected client running version " +
                      $"{result.LocalVersion?.Major}.{result.LocalVersion?.Minor}.{result.LocalVersion?.Build}");
            Log.Debug("Latest release version " +
                      $"{result.LatestVersion.Major}.{result.LatestVersion.Minor}.{result.LatestVersion.Build}");
        }
        catch (Exception e)
        {
            result.CheckFailed = true;
            Log.Error("Error checking latest release on GitHub. {Message}", e.Message);
        }
#else
        await Task.CompletedTask;
        Log.Debug("Running in Debug/Local mode. Version check skipped.");
        result.IsUpToDate = true;
#endif

        return result;
    }

    private void DetectFfmpeg(StartupResult result)
    {
        if (!string.IsNullOrEmpty(configService.CurrentConfig.FFmpegPath) &&
            ValidateFilePath(configService.CurrentConfig.FFmpegPath))
        {
            result.FfmpegFound = true;
            result.FfmpegPath = configService.CurrentConfig.FFmpegPath;
            Log.Debug($"FFmpeg found: {result.FfmpegPath}");
            Log.Debug("FFmpeg path set in config.conf");
        }
        else if (!string.IsNullOrEmpty(authService.CurrentAuth?.FfmpegPath) &&
                 ValidateFilePath(authService.CurrentAuth.FfmpegPath))
        {
            result.FfmpegFound = true;
            result.FfmpegPath = authService.CurrentAuth.FfmpegPath;
            configService.CurrentConfig.FFmpegPath = result.FfmpegPath;
            Log.Debug($"FFmpeg found: {result.FfmpegPath}");
            Log.Debug("FFmpeg path set in auth.json");
        }
        else if (string.IsNullOrEmpty(configService.CurrentConfig.FFmpegPath))
        {
            string? ffmpegPath = GetFullPath("ffmpeg") ?? GetFullPath("ffmpeg.exe");
            if (ffmpegPath != null)
            {
                result.FfmpegFound = true;
                result.FfmpegPathAutoDetected = true;
                result.FfmpegPath = ffmpegPath;
                configService.CurrentConfig.FFmpegPath = ffmpegPath;
                Log.Debug($"FFmpeg found: {ffmpegPath}");
                Log.Debug("FFmpeg path found via PATH or current directory");
            }
        }

        if (!result.FfmpegFound)
        {
            Log.Error($"Cannot locate FFmpeg with path: {configService.CurrentConfig.FFmpegPath}");
        }
    }

    private void DetectFfprobe(StartupResult result)
    {
        if (!string.IsNullOrEmpty(configService.CurrentConfig.FFprobePath) &&
            ValidateFilePath(configService.CurrentConfig.FFprobePath))
        {
            result.FfprobeFound = true;
            result.FfprobePath = configService.CurrentConfig.FFprobePath;
            Log.Debug($"FFprobe found: {result.FfprobePath}");
            Log.Debug("FFprobe path set in config.conf");
        }

        if (!result.FfprobeFound && !string.IsNullOrEmpty(result.FfmpegPath))
        {
            string? ffmpegDirectory = Path.GetDirectoryName(result.FfmpegPath);
            if (!string.IsNullOrEmpty(ffmpegDirectory))
            {
                string ffprobeFileName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
                    ? "ffprobe.exe"
                    : "ffprobe";
                string inferredFfprobePath = Path.Combine(ffmpegDirectory, ffprobeFileName);
                if (ValidateFilePath(inferredFfprobePath))
                {
                    result.FfprobeFound = true;
                    result.FfprobePathAutoDetected = true;
                    result.FfprobePath = inferredFfprobePath;
                    configService.CurrentConfig.FFprobePath = inferredFfprobePath;
                    Log.Debug($"FFprobe found: {inferredFfprobePath}");
                    Log.Debug("FFprobe path inferred from FFmpeg path");
                }
            }
        }

        if (!result.FfprobeFound && string.IsNullOrEmpty(configService.CurrentConfig.FFprobePath))
        {
            string? ffprobePath = GetFullPath("ffprobe") ?? GetFullPath("ffprobe.exe");
            if (ffprobePath != null)
            {
                result.FfprobeFound = true;
                result.FfprobePathAutoDetected = true;
                result.FfprobePath = ffprobePath;
                configService.CurrentConfig.FFprobePath = ffprobePath;
                Log.Debug($"FFprobe found: {ffprobePath}");
                Log.Debug("FFprobe path found via PATH or current directory");
            }
        }

        if (!result.FfprobeFound)
        {
            Log.Error($"Cannot locate FFprobe with path: {configService.CurrentConfig.FFprobePath}");
        }
    }

    private static async Task<string?> GetToolVersionAsync(string toolPath, string toolName)
    {
        try
        {
            ProcessStartInfo processStartInfo = new()
            {
                FileName = toolPath,
                Arguments = "-version",
                RedirectStandardOutput = true,
                RedirectStandardError = true,
                UseShellExecute = false,
                CreateNoWindow = true
            };

            using Process? process = Process.Start(processStartInfo);
            if (process != null)
            {
                string output = await process.StandardOutput.ReadToEndAsync();
                await process.WaitForExitAsync();

                Log.Information("{ToolName} version output:\n{Output}", toolName, output);

                string firstLine = output.Split('\n')[0].Trim();
                string expectedPrefix = $"{toolName} version ";
                if (firstLine.StartsWith(expectedPrefix, StringComparison.OrdinalIgnoreCase))
                {
                    int versionStart = expectedPrefix.Length;
                    int copyrightIndex = firstLine.IndexOf(" Copyright", StringComparison.Ordinal);
                    return copyrightIndex > versionStart
                        ? firstLine.Substring(versionStart, copyrightIndex - versionStart)
                        : firstLine.Substring(versionStart);
                }
            }
        }
        catch (Exception ex)
        {
            Log.Warning(ex, "Failed to get {ToolName} version", toolName);
        }

        return null;
    }

    private static bool ValidateFilePath(string path)
    {
        char[] invalidChars = Path.GetInvalidPathChars();
        return !path.Any(c => invalidChars.Contains(c)) && File.Exists(path);
    }

    private static string? GetFullPath(string filename)
    {
        if (File.Exists(filename))
        {
            return Path.GetFullPath(filename);
        }

        string pathEnv = Environment.GetEnvironmentVariable("PATH") ?? "";
        return pathEnv.Split(Path.PathSeparator).Select(path => Path.Combine(path, filename))
            .FirstOrDefault(File.Exists);
    }
}
