package com.clanstop.healing.config;

import java.io.InputStream;
import java.util.Properties;

/**
 * Central configuration. Resolution order for every key: environment variable,
 * then {@code healing.properties} on the classpath, then a hard-coded default.
 * A single instance is created lazily and shared process-wide.
 */
public final class FrameworkConfig {

    private static volatile FrameworkConfig instance;

    private final Properties props = new Properties();

    private FrameworkConfig() {
        try (InputStream in = Thread.currentThread().getContextClassLoader()
                .getResourceAsStream("healing.properties")) {
            if (in != null) {
                props.load(in);
            }
        } catch (Exception e) {
            // Missing file is fine — env vars and defaults still apply.
        }
    }

    public static FrameworkConfig get() {
        if (instance == null) {
            synchronized (FrameworkConfig.class) {
                if (instance == null) {
                    instance = new FrameworkConfig();
                }
            }
        }
        return instance;
    }

    /** Resolve a key from env var (given name) then properties (propKey) then default. */
    private String resolve(String envVar, String propKey, String def) {
        String env = System.getenv(envVar);
        if (env != null && !env.isBlank()) {
            return env.trim();
        }
        return props.getProperty(propKey, def);
    }

    public String dbUrl()      { return resolve("HEALING_DB_URL", "healing.db.url", "jdbc:postgresql://localhost:5432/healing"); }
    public String dbUser()     { return resolve("HEALING_DB_USER", "healing.db.user", "postgres"); }
    public String dbPassword() { return resolve("HEALING_DB_PASSWORD", "healing.db.password", "postgres"); }

    public String baseUrl()    { return resolve("BASE_URL", "healing.base.url", "http://localhost:8080"); }
    public String browser()    { return resolve("HEALING_BROWSER", "healing.browser", "chrome").toLowerCase(); }
    public boolean headless()  { return Boolean.parseBoolean(resolve("HEALING_HEADLESS", "healing.headless", "true")); }

    /** Selenium implicit wait applied to every element lookup (mirrors the recorder's "Element wait"). 0 = off. */
    public long implicitWaitMs() { return parseLong(resolve("HEALING_IMPLICIT_WAIT_MS", "healing.implicit.wait.ms", "0"), 0); }
    /** Pause after every step (mirrors the recorder's "Slow-mo"). 0 = off. */
    public long stepPauseMs()    { return parseLong(resolve("HEALING_STEP_PAUSE_MS", "healing.step.pause.ms", "0"), 0); }

    /** Selenium Grid / Selenoid / cloud hub URL (e.g. http://grid:4444/wd/hub). Blank = local browser. */
    public String remoteUrl()     { return blankToNull(resolve("HEALING_REMOTE_URL", "healing.remote.url", "")); }

    // ---- Cross-browser cloud grid (Selenium Grid / BrowserStack / Sauce Labs / LambdaTest) --------
    /** Turn on the cloud grid path. When true, grid.url routes every UI test at the hub. */
    public boolean gridEnabled()  { return Boolean.parseBoolean(resolve("GRID_ENABLED", "grid.enabled", "false")); }
    /** Grid / cloud hub URL. Takes precedence over healing.remote.url when grid.enabled. */
    public String gridUrl()       { return blankToNull(resolve("GRID_URL", "grid.url", "")); }
    /** Browser to request on the grid (chrome/firefox/edge/safari). Blank = fall back to healing.browser. */
    public String gridBrowser()   { return blankToNull(resolve("GRID_BROWSER", "grid.browser", "")); }
    /** Browser version to request on the grid (e.g. "latest", "121"). Blank = the grid's default. */
    public String gridBrowserVersion() { return blankToNull(resolve("GRID_BROWSER_VERSION", "grid.browser.version", "")); }
    /** Platform to request (e.g. "Windows 11", "macOS 14"). Blank = the grid's default. */
    public String gridPlatform()  { return blankToNull(resolve("GRID_PLATFORM", "grid.platform", "")); }
    /** Cloud vendor: generic | browserstack | saucelabs | lambdatest. Shapes the caps + auth block. */
    public String gridVendor()    { return resolve("GRID_VENDOR", "grid.vendor", "generic").toLowerCase(); }
    /** Cloud vendor user / username. Prefer the env var so it stays out of committed files. */
    public String gridUser()      { return blankToNull(resolve("GRID_USER", "grid.user", "")); }
    /** Cloud vendor access key. Prefer the env var so it stays out of committed files. */
    public String gridKey()       { return blankToNull(resolve("GRID_KEY", "grid.key", "")); }
    /** Build / run name shown on the cloud dashboard. Blank = "L3git". */
    public String gridBuild()     { return blankToNull(resolve("GRID_BUILD", "grid.build", "")); }
    /**
     * Path to a driver executable. Resolution order:
     *   1) HEALING_DRIVER_PATH env / healing.driver.path property - explicit override, any OS (CI);
     *   2) a project-local ./drivers/&lt;name&gt; shipped alongside the project - runs fully OFFLINE,
     *      no Selenium-Manager download (this is what L3git bundles on Generate);
     *   3) null -&gt; Selenium Manager downloads a matching driver (needs internet).
     */
    public String driverPath() {
        String explicit = blankToNull(resolve("HEALING_DRIVER_PATH", "healing.driver.path", ""));
        if (explicit != null) return explicit;
        return bundledDriver(browser());
    }

    /**
     * Look for a driver binary bundled in ./drivers next to the project (offline runs). OS-safe: a
     * Windows .exe is only used on Windows and vice-versa, so a committed Windows driver is simply
     * ignored on a Linux CI agent (which falls back to the env var or Selenium Manager). Returns an
     * absolute path, or null when nothing suitable is present.
     */
    private static String bundledDriver(String browser) {
        boolean windows = System.getProperty("os.name", "").toLowerCase().contains("win");
        String base = switch (browser == null ? "" : browser.toLowerCase()) {
            case "firefox" -> "geckodriver";
            case "edge"    -> "msedgedriver";
            default        -> "chromedriver";
        };
        java.io.File f = new java.io.File("drivers", windows ? base + ".exe" : base);
        return f.isFile() ? f.getAbsolutePath() : null;
    }
    /** Path to the browser binary itself, if it isn't in a standard location. Blank = auto. */
    public String browserBinary() { return blankToNull(resolve("HEALING_BROWSER_BINARY", "healing.browser.binary", "")); }

    public double threshold()  { return parseDouble(resolve("HEALING_THRESHOLD", "healing.threshold", "0.72"), 0.72); }
    public int domScanLimit()  { return parseInt(resolve("HEALING_DOMSCAN_LIMIT", "healing.domscan.limit", "250"), 250); }
    public String volatileIdRegex() { return resolve("HEALING_VOLATILE_ID_REGEX", "healing.volatile.id.regex", ".*[0-9a-f]{6,}.*"); }

    public String env()        { return resolve("HEALING_ENV", "healing.env", "local"); }
    public String gitSha()     { return resolve("GIT_SHA", "healing.git.sha", null); }

    public boolean screenshotOnFailure() { return Boolean.parseBoolean(resolve("HEALING_SCREENSHOT_ON_FAILURE", "healing.screenshot.on.failure", "true")); }
    public boolean screenshotOnHeal()    { return Boolean.parseBoolean(resolve("HEALING_SCREENSHOT_ON_HEAL", "healing.screenshot.on.heal", "true")); }
    public String screenshotDir()        { return resolve("HEALING_SCREENSHOT_DIR", "healing.screenshot.dir", "target/healing-screenshots"); }

    private static String blankToNull(String v) {
        return (v == null || v.isBlank()) ? null : v.trim();
    }

    private static double parseDouble(String v, double def) {
        try { return Double.parseDouble(v); } catch (Exception e) { return def; }
    }

    private static int parseInt(String v, int def) {
        try { return Integer.parseInt(v); } catch (Exception e) { return def; }
    }

    private static long parseLong(String v, long def) {
        try { return Long.parseLong(v.trim()); } catch (Exception e) { return def; }
    }
}
