using System;
using System.IO;
using Microsoft.Extensions.Logging;
using OpenQA.Selenium;
using OpenQA.Selenium.Firefox;
using OpenQA.Selenium.Chrome;
using OpenQA.Selenium.Chromium;
using OpenQA.Selenium.Edge;
using OpenQA.Selenium.Remote;

namespace Beam.Stealth {
    public enum Browser {
        Firefox = 0,
        Chrome = 1,
        Chromium = 2,
        Edge = 3,
    }

    public sealed class StealthConfig : IDisposable {
        public bool ShowBrowser { get; init; }
        public TimeSpan TimeOut { get; init; } = Timeout.InfiniteTimeSpan;
        public string DownloadsDirectory { get; }
        public ILogger? Logger { get; init; }
        public required IWebDriver Driver { get; init; }

        public string? RemoteAddress { get; init; }
        
        private StealthConfig(string downloadDir) => DownloadsDirectory = downloadDir;

        /* ---------- browser-specific option builders ---------- */

        private static FirefoxOptions GetFirefoxOptions(string downloadDir, bool headless) {
            var o = new FirefoxOptions();
            if (headless) o.AddArgument("--headless");

            o.SetPreference("browser.download.folderList", 2);              // use custom dir
            o.SetPreference("browser.download.dir", downloadDir);
            o.SetPreference("browser.download.useDownloadDir", true);
            o.SetPreference("browser.helperApps.neverAsk.saveToDisk",
                            "application/octet-stream,application/pdf,application/zip");
            o.SetPreference("pdfjs.disabled", true);                         // open PDFs externally
            o.SetPreference("browser.download.manager.showWhenStarting", false);

            return o;
        }

        private static ChromeOptions GetChromeOptions(string downloadDir, bool headless) {
            var o = new ChromeOptions();
            if (headless) o.AddArgument("--headless=new");

            // download prefs
            o.AddUserProfilePreference("download.default_directory", downloadDir);
            o.AddUserProfilePreference("download.prompt_for_download", false);
            o.AddUserProfilePreference("safebrowsing.enabled", false);

            // common stability flags
            o.AddArgument("--no-sandbox");
            o.AddArgument("--disable-dev-shm-usage");

            return o;
        }

        private static EdgeOptions GetEdgeOptions(string downloadDir, bool headless) {
            var o = new EdgeOptions();
            if (headless) o.AddArgument("--headless=new");

            o.AddUserProfilePreference("download.default_directory", downloadDir);
            o.AddUserProfilePreference("download.prompt_for_download", false);
            o.AddUserProfilePreference("safebrowsing.enabled", false);

            return o;
        }

        /* ---------- factory ---------- */
        private record DriverDefinition(Func<DriverOptions, IWebDriver> Factory, DriverOptions Options);
        public static StealthConfig Create(
            bool showBrowser = false,
            string? downloadDir = null,
            TimeSpan? timeOut = null,
            Browser preferredBrowser = Browser.Firefox,
            string? remoteAddress = null,
            Addon[]? utilityAddons = null, 
            ILogger? logger = null) {
            // pick or create a dedicated download folder
            downloadDir ??= Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
            Directory.CreateDirectory(downloadDir);

            bool headless = !showBrowser;
            IWebDriver? driver = null;
            List<Exception>? errors = [];
            
            while (preferredBrowser <= Browser.Edge) {
                try {
                    DriverDefinition def = preferredBrowser switch {
                    Browser.Chrome or Browser.Chromium
                        => new((DriverOptions opts) => new ChromeDriver(opts as ChromeOptions), GetChromeOptions(downloadDir, headless)),
                    Browser.Edge
                        => new((DriverOptions opts) => new EdgeDriver(opts as EdgeOptions), GetEdgeOptions(downloadDir, headless)),
                    Browser.Firefox or _
                        => new((DriverOptions opts) => new FirefoxDriver(opts as FirefoxOptions), GetFirefoxOptions(downloadDir, headless)),
                    };
                    
                    if (remoteAddress is not null)
                        driver = new RemoteWebDriver(new Uri(remoteAddress), def.Options);
                    else
                        driver = def.Factory(def.Options);

                    try {
                        foreach(var addon in utilityAddons)
                            if (addon.Browser == Browser.Firefox && driver is FirefoxDriver fd)
                                fd.InstallAddOnFromFile(addon.AddonPath);
                    }
                    catch {
                        logger?.LogWarning("Unable to load utility addons");
                    }

                    break;
                }
                catch (Exception e) {
                    if (preferredBrowser != Browser.Edge)
                        logger?.LogWarning(e, "Failed to create driver for browser {Browser}, falling back to {Fallback}", preferredBrowser, Enum.GetName(preferredBrowser + 1));
                    else {
                        logger?.LogCritical(e, "Failed to create driver for browser {Browser}, no more fallbacks remaining!", preferredBrowser);
                    }
                    
                    preferredBrowser++;
                    errors.Add(e);
                }
            }

            if (driver is null)
                throw new AggregateException(errors);
            
            return new StealthConfig(downloadDir) {
                ShowBrowser = showBrowser,
                TimeOut = timeOut ?? Timeout.InfiniteTimeSpan,
                RemoteAddress = remoteAddress,
                Logger = logger,
                Driver = driver
            };
        }

        public void Dispose() {
            Driver.Dispose();
        }
    }
}
