using System.Runtime.InteropServices;
using Newtonsoft.Json;
using OF_DL.Entities;
using OF_DL.Exceptions;
using OF_DL.Helpers;
using Serilog;

namespace OF_DL;

public class AppCommon
{
    private readonly Auth _auth;
    private readonly Config _config;
    private readonly bool _useCdrmProject;

    private readonly IAPIHelper _apiHelper;
    private readonly IDBHelper _dbHelper;
    private readonly IDownloadHelper _downloadHelper;

    private Dictionary<string, int> _activeSubscriptions = new();
    private Dictionary<string, int> _expiredSubscriptions = new();

    public AppCommon()
    {
        Log.Logger = new LoggerConfiguration()
            .MinimumLevel.Debug()
            .WriteTo.File("logs/OFDL.txt", rollingInterval: RollingInterval.Day)
            .WriteTo.Console()
            .CreateLogger();

        VerifyOperatingSystemCompatibility();
        _auth = GetAuth();
        _config = GetConfig();
        _useCdrmProject = !DetectDrmKeysPresence();
        LoadFfmpeg();

        _apiHelper = new APIHelper();
        _dbHelper = new DBHelper();
        _downloadHelper = new DownloadHelper();
    }

    private static void VerifyOperatingSystemCompatibility()
    {
        var os = Environment.OSVersion;
        if (os.Platform != PlatformID.Win32NT || os.Version.Major >= 10) return;

        var platform =
            os.Platform switch
            {
                PlatformID.Win32NT => "Windows",
                PlatformID.Unix => "Unix",
                PlatformID.MacOSX => "macOS",
                _ => "Unknown"
            };

        Log.Error($"Unsupported operating system: {platform} version {os.VersionString}");
        throw new UnsupportedOperatingSystem(platform, os.VersionString);
    }

    private static Auth GetAuth()
    {
        if (File.Exists("auth.json"))
        {
            Log.Debug("auth.json located successfully");
            var authJson = JsonConvert.DeserializeObject<Auth>(File.ReadAllText("auth.json"));
            if (authJson != null)
            {
                return authJson;
            }

            Log.Error("auth.json is invalid");
            throw new MalformedFileException("auth.json");
        }

        Log.Error("auth.json does not exist");
        throw new MissingFileException("auth.json");
    }

    private static Config GetConfig()
    {
        if (File.Exists("config.json"))
        {
            Log.Debug("config.json located successfully");
            var configJson = JsonConvert.DeserializeObject<Config>(File.ReadAllText("config.json"));
            if (configJson != null)
            {
                return configJson;
            }

            Log.Error("config.json is invalid");
            throw new MalformedFileException("config.json");
        }

        Log.Error("config.json does not exist");
        throw new MissingFileException("config.json");
    }

    private void LoadFfmpeg()
    {
        var ffmpegFound = false;
        var pathAutoDetected = false;
        if (!string.IsNullOrEmpty(_config!.FFmpegPath) && ValidateFilePath(_config.FFmpegPath))
        {
            // FFmpeg path is set in config.json and is valid
            ffmpegFound = true;
        }
        else if (!string.IsNullOrEmpty(_auth!.FFMPEG_PATH) && ValidateFilePath(_auth.FFMPEG_PATH))
        {
            // FFmpeg path is set in auth.json and is valid (config.json takes precedence and auth.json is only available for backward compatibility)
            ffmpegFound = true;
            _config.FFmpegPath = _auth.FFMPEG_PATH;
        }
        else if (string.IsNullOrEmpty(_config.FFmpegPath))
        {
            // FFmpeg path is not set in config.json, so we will try to locate it in the PATH or current directory
            var ffmpegPath = GetFullPath("ffmpeg");
            if (ffmpegPath != null)
            {
                // FFmpeg is found in the PATH or current directory
                ffmpegFound = true;
                pathAutoDetected = true;
                _config.FFmpegPath = ffmpegPath;
            }
            else
            {
                // FFmpeg is not found in the PATH or current directory, so we will try to locate the windows executable
                ffmpegPath = GetFullPath("ffmpeg.exe");
                if (ffmpegPath != null)
                {
                    // FFmpeg windows executable is found in the PATH or current directory
                    ffmpegFound = true;
                    pathAutoDetected = true;
                    _config.FFmpegPath = ffmpegPath;
                }
            }
        }

        if (ffmpegFound)
        {
            Log.Debug(
                pathAutoDetected
                    ? $"FFmpeg located successfully. Path auto-detected: {_config.FFmpegPath}"
                    : $"FFmpeg located successfully"
            );

            // Escape backslashes in the path for Windows
            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && _config.FFmpegPath!.Contains(@":\") && !_config.FFmpegPath.Contains(@":\\"))
            {
                _config.FFmpegPath = _config.FFmpegPath.Replace(@"\", @"\\");
            }
        }
        else
        {
            Log.Error($"Cannot locate FFmpeg with path: {_config.FFmpegPath}");
            throw new Exception("Cannot locate FFmpeg");
        }
    }

    private static bool DetectDrmKeysPresence()
    {
        var clientIdBlobMissing = false;
        var devicePrivateKeyMissing = false;

        if (!File.Exists("cdm/devices/chrome_1610/device_client_id_blob"))
        {
            clientIdBlobMissing = true;
        }
        else
        {
            Log.Debug($"device_client_id_blob located successfully");
        }

        if (!File.Exists("cdm/devices/chrome_1610/device_private_key"))
        {
            devicePrivateKeyMissing = true;
        }
        else
        {
            Log.Debug($"device_private_key located successfully");
        }

        if (!clientIdBlobMissing && !devicePrivateKeyMissing)
        {
            return true;
        }

        Log.Information("device_client_id_blob and/or device_private_key missing, https://cdrm-project.com/ will be used instead for DRM protected videos");
        return false;
    }

    private static bool ValidateFilePath(string path)
    {
        var invalidChars = System.IO.Path.GetInvalidPathChars();
        var foundInvalidChars = path.Where(c => invalidChars.Contains(c)).ToArray();

        if (foundInvalidChars.Length != 0)
        {
            Log.Information($"Invalid characters found in path {path}:[/] {string.Join(", ", foundInvalidChars)}");
            return false;
        }

        if (File.Exists(path)) return true;

        Log.Information(
            Directory.Exists(path)
                ? $"The provided path {path} improperly points to a directory and not a file."
                : $"The provided path {path} does not exist or is not accessible."
        );

        return false;
    }

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

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

    public async Task<User> GetUser()
    {
        var user = await _apiHelper.GetUserInfo("/users/me", _auth);

        if (user is not { id: not null })
        {
            Log.Error("Authentication failed. Please check your credentials in auth.json");
            throw new AuthenticationFailureException();
        }

        Log.Debug($"Logged in successfully as {user.name} {user.username}");
        return user;
    }

    private async Task<Dictionary<string, int>> GetActiveSubscriptions()
    {
        if (_activeSubscriptions.Count > 0)
        {
            return _activeSubscriptions;
        }

        _activeSubscriptions = await _apiHelper.GetActiveSubscriptions("/subscriptions/subscribes", _auth, _config.IncludeRestrictedSubscriptions);
        return _activeSubscriptions;
    }

    private async Task<Dictionary<string, int>> GetExpiredSubscriptions()
    {
        if (_expiredSubscriptions.Count > 0)
        {
            return _expiredSubscriptions;
        }

        _expiredSubscriptions = await _apiHelper.GetExpiredSubscriptions("/subscriptions/subscribes", _auth, _config.IncludeRestrictedSubscriptions);
        return _expiredSubscriptions;
    }

    public async Task<Dictionary<string, int>> GetSubscriptions()
    {
        var subscriptions = new Dictionary<string, int>();

        foreach (var (key, value) in  await GetActiveSubscriptions())
        {
            subscriptions.Add(key, value);
        }

        if (_config.IncludeExpiredSubscriptions)
        {
            foreach (var (key, value) in await GetExpiredSubscriptions())
            {
                subscriptions.Add(key, value);
            }
        }

        return subscriptions;
    }

    public async Task<Dictionary<string, int>> GetLists()
    {
        return await _apiHelper.GetLists("/lists", _auth);
    }

    public async Task CreateOrUpdateUsersDatabase()
    {
        var users = await GetSubscriptions();
        await _dbHelper.CreateUsersDB(users);
    }
}
