// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License.  You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied.  See the License for the
// specific language governing permissions and limitations
// under the License.

use crate::config::OS::{LINUX, MACOS, WINDOWS};
use crate::files::get_cache_folder;
use crate::{format_one_arg, run_shell_command, REQUEST_TIMEOUT_SEC, UNAME_COMMAND};
use crate::{ARCH_AMD64, ARCH_ARM64, ARCH_X86, TTL_BROWSERS_SEC, TTL_DRIVERS_SEC, WMIC_COMMAND_OS};
use std::env;
use std::env::consts::OS;
use std::error::Error;
use std::fs::read_to_string;
use toml::Table;

pub const ARM64_ARCH: &str = "arm64";
pub const CONFIG_FILE: &str = "selenium-manager-config.toml";
pub const ENV_PREFIX: &str = "SE_";
pub const VERSION_PREFIX: &str = "-version";
pub const PATH_PREFIX: &str = "-path";

pub struct ManagerConfig {
    pub browser_version: String,
    pub driver_version: String,
    pub browser_path: String,
    pub os: String,
    pub arch: String,
    pub proxy: String,
    pub timeout: u64,
    pub browser_ttl: u64,
    pub driver_ttl: u64,
}

impl ManagerConfig {
    pub fn default(browser_name: &str, driver_name: &str) -> ManagerConfig {
        let self_os = OS;
        let self_arch = if WINDOWS.is(self_os) {
            let wmic_output =
                run_shell_command(self_os, WMIC_COMMAND_OS.to_string()).unwrap_or_default();
            if wmic_output.contains("32") {
                ARCH_X86.to_string()
            } else if wmic_output.contains("ARM") {
                ARCH_ARM64.to_string()
            } else {
                ARCH_AMD64.to_string()
            }
        } else {
            let uname_a = format_one_arg(UNAME_COMMAND, "a");
            if run_shell_command(self_os, uname_a)
                .unwrap_or_default()
                .to_ascii_lowercase()
                .contains(ARCH_ARM64)
            {
                ARCH_ARM64.to_string()
            } else {
                let uname_m = format_one_arg(UNAME_COMMAND, "m");
                run_shell_command(self_os, uname_m).unwrap_or_default()
            }
        };

        let browser_version_label = concat(browser_name, VERSION_PREFIX);
        let driver_version_label = concat(driver_name, VERSION_PREFIX);
        let browser_path_label = concat(browser_name, PATH_PREFIX);

        ManagerConfig {
            browser_version: StringKey(vec!["browser-version", browser_version_label.as_str()], "")
                .get_value(),
            driver_version: StringKey(vec!["driver-version", driver_version_label.as_str()], "")
                .get_value(),
            browser_path: StringKey(vec!["browser-path", browser_path_label.as_str()], "")
                .get_value(),
            os: StringKey(vec!["os"], self_os).get_value(),
            arch: StringKey(vec!["arch"], self_arch.as_str()).get_value(),
            proxy: StringKey(vec!["proxy"], "").get_value(),
            timeout: IntegerKey("timeout", REQUEST_TIMEOUT_SEC).get_value(),
            browser_ttl: IntegerKey("browser-ttl", TTL_BROWSERS_SEC).get_value(),
            driver_ttl: IntegerKey("driver-ttl", TTL_DRIVERS_SEC).get_value(),
        }
    }
}

#[allow(dead_code)]
#[allow(clippy::upper_case_acronyms)]
#[derive(Hash, Eq, PartialEq, Debug)]
pub enum OS {
    WINDOWS,
    MACOS,
    LINUX,
}

impl OS {
    pub fn to_str(&self) -> &str {
        match self {
            WINDOWS => "windows",
            MACOS => "macos",
            LINUX => "linux",
        }
    }

    pub fn is(&self, os: &str) -> bool {
        self.to_str().eq_ignore_ascii_case(os)
    }
}

pub fn str_to_os(os: &str) -> OS {
    if WINDOWS.is(os) {
        WINDOWS
    } else if MACOS.is(os) {
        MACOS
    } else {
        LINUX
    }
}

#[allow(dead_code)]
#[allow(clippy::upper_case_acronyms)]
pub enum ARCH {
    X32,
    X64,
    ARM64,
}

impl ARCH {
    pub fn to_str_vector(&self) -> Vec<&str> {
        match self {
            ARCH::X32 => vec![ARCH_X86, "i386"],
            ARCH::X64 => vec![ARCH_AMD64, "x86_64", "x64", "i686", "ia64"],
            ARCH::ARM64 => vec![ARCH_ARM64, "aarch64", "arm"],
        }
    }

    pub fn is(&self, arch: &str) -> bool {
        self.to_str_vector()
            .contains(&arch.to_ascii_lowercase().as_str())
    }
}

pub struct StringKey<'a>(pub Vec<&'a str>, pub &'a str);

impl StringKey<'_> {
    pub fn get_value(&self) -> String {
        let config = get_config().unwrap_or_default();
        let keys = self.0.to_owned();
        let mut result;
        for key in keys {
            if config.contains_key(key) {
                result = config[key].as_str().unwrap().to_string()
            } else {
                result = env::var(get_env_name(key)).unwrap_or_default()
            }
            if !result.is_empty() {
                return result;
            }
        }
        self.1.to_owned()
    }
}

pub struct IntegerKey<'a>(pub &'a str, pub u64);

impl IntegerKey<'_> {
    pub fn get_value(&self) -> u64 {
        let config = get_config().unwrap_or_default();
        let key = self.0;
        if config.contains_key(key) {
            config[key].as_integer().unwrap() as u64
        } else {
            env::var(get_env_name(key))
                .unwrap_or_default()
                .parse::<u64>()
                .unwrap_or_else(|_| self.1.to_owned())
        }
    }
}

pub struct BooleanKey<'a>(pub &'a str, pub bool);

impl BooleanKey<'_> {
    pub fn get_value(&self) -> bool {
        let config = get_config().unwrap_or_default();
        let key = self.0;
        if config.contains_key(key) {
            config[key].as_bool().unwrap()
        } else {
            env::var(get_env_name(key))
                .unwrap_or_default()
                .parse::<bool>()
                .unwrap_or_else(|_| self.1.to_owned())
        }
    }
}

fn get_env_name(suffix: &str) -> String {
    let suffix_uppercase: String = suffix.replace('-', "_").to_uppercase();
    concat(ENV_PREFIX, suffix_uppercase.as_str())
}

fn get_config() -> Result<Table, Box<dyn Error>> {
    let config_path = get_cache_folder().join(CONFIG_FILE);
    Ok(read_to_string(config_path)?.parse()?)
}

fn concat(prefix: &str, suffix: &str) -> String {
    let mut version_label: String = prefix.to_owned();
    version_label.push_str(suffix);
    version_label
}
