use super::ProtonVPN;
use super::{ConfigurationChoice, OpenVpnProvider};
use crate::config::providers::{Input, Password, UiClient};
use crate::config::vpn::OpenVpnProtocol;
use crate::util::delete_all_files_in_dir;
use anyhow::anyhow;
use log::{debug, info};
use regex::Regex;
use reqwest::Url;
use reqwest::header::{COOKIE, HeaderMap, HeaderName, HeaderValue};
use std::fmt::Display;
use std::fs::File;
use std::fs::create_dir_all;
use std::io::{Cursor, Read, Write};
use std::net::IpAddr;
use std::path::PathBuf;
use strum::IntoEnumIterator;
use strum_macros::EnumIter;
use zip::ZipArchive;

impl ProtonVPN {
    fn build_url(
        &self,
        category: &ConfigType,
        tier: &Tier,
        protocol: &OpenVpnProtocol,
    ) -> anyhow::Result<Url> {
        let cat = if tier == &Tier::Free {
            "Server".to_string()
        } else {
            category.url_part()
        };
        Ok(Url::parse(&format!(
            "https://account.protonvpn.com/api/vpn/config?Category={}&Tier={}&Platform=Linux&Protocol={}",
            cat,
            tier.url_part(),
            protocol
        ))?)
    }

    pub fn parse_auth_cookie(
        uiclient: &dyn UiClient,
    ) -> anyhow::Result<(&'static str, &'static str)> {
        let auth_pattern = Box::leak(Box::new(Regex::new(
            r"^[;\s\t]*AUTH-([^=\t:;\s]+)[\s\t=: ]+([^\s\t;=]+)[;\s\t]*$",
        )?));

        let raw_auth_cookie  = Box::leak(uiclient.get_input(Input {
            prompt: "Please log-in at https://account.protonvpn.com/dashboard and then visit https://account.protonvpn.com/account and copy the value of the cookie of the form \"AUTH-xxx=yyy\" where xxx is equal to the value of the \"x-pm-uid\" request header, in the request from your browser's network request inspector (check the request it makes to https://account.protonvpn.com/api/vpn for example). Note there may be multiple AUTH-xxx=yyy request headers, copy the one where xxx is equal to the value of the x-pm-uid header.".to_owned(),
             validator: Some(Box::new(|s: &String| if auth_pattern.is_match(s) {
                Ok(())
            } else {
                Err("The authorization code must be in the format AUTH-xxx=yyy, AUTH-xxx: yyy, or AUTH-xxx\tyyy, with an optional semicolon at the end".to_owned())
            }
                ))
             })?.into_boxed_str());

        debug!("Using AUTH cookie: {}", &raw_auth_cookie);

        // Extract the key and value parts and standardize to AUTH-xxx=yyy format
        let maybe_captures = auth_pattern.captures(raw_auth_cookie);
        let uid = maybe_captures
            .as_ref()
            .and_then(|c| c.get(1))
            .ok_or(anyhow!("Failed to parse uid from auth cookie"))?;

        info!(
            "x-pm-uid should be {} according to AUTH cookie: {}",
            uid.as_str(),
            raw_auth_cookie
        );
        let value = maybe_captures
            .and_then(|c| c.get(2))
            .ok_or(anyhow!("Failed to parse cookie value from auth cookie"))?;

        let leaked_uid = Box::leak(uid.as_str().to_owned().into_boxed_str());

        // Create the standardized form
        let auth_cookie =
            Box::leak(format!("AUTH-{}={}", uid.as_str(), value.as_str()).into_boxed_str());
        debug!("Parsed AUTH cookie: {}", &auth_cookie);
        Ok((auth_cookie, leaked_uid))
    }
}
impl OpenVpnProvider for ProtonVPN {
    fn provider_dns(&self) -> Option<Vec<IpAddr>> {
        // None will use DNS from OpenVPN headers if present
        None
        // TODO: ProtonVPN DNS servers do not respond
        // let path = self.openvpn_dir().ok()?.join("dns.txt");
        // let ip_str = match std::fs::read_to_string(&path) {
        //     Err(x) => {
        //         error!("Failed to read DNS file: {}: {:?}", path.display(), x);
        //         return None;
        //     }
        //     Ok(x) => x,
        // };

        // let ip = match Ipv4Addr::from_str(&ip_str.trim()) {
        //     Err(x) => {
        //         error!("Failed to convert IP string: {}: {:?}", ip_str, x);
        //         return None;
        //     }
        //     Ok(x) => x,
        // };

        // Some(vec![IpAddr::V4(ip)])
    }

    fn prompt_for_auth(&self, uiclient: &dyn UiClient) -> anyhow::Result<(String, String)> {
        let username = uiclient.get_input(Input {
            prompt:
                "ProtonVPN OpenVPN username (see: https://account.protonvpn.com/account#openvpn ) - add +pmp suffix if using --port-forwarding - note not all servers support this feature"
                    .to_string(),
            validator: None,
        })?;

        let password = uiclient.get_password(Password {
            prompt: "OpenVPN Password".to_string(),
            confirm: true,
        })?;
        Ok((username.trim().to_string(), password.trim().to_string()))
    }

    fn auth_file_path(&self) -> anyhow::Result<Option<PathBuf>> {
        Ok(Some(self.openvpn_dir()?.join("auth.txt")))
    }

    fn create_openvpn_config(&self, uiclient: &dyn UiClient) -> anyhow::Result<()> {
        let openvpn_dir = self.openvpn_dir()?;
        let code_map = crate::util::country_map::code_to_country_map();
        create_dir_all(&openvpn_dir)?;
        delete_all_files_in_dir(&openvpn_dir)?;
        let tier = Tier::index_to_variant(uiclient.get_configuration_choice(&Tier::default())?);
        let config_choice = if tier != Tier::Free {
            ConfigType::index_to_variant(uiclient.get_configuration_choice(&ConfigType::default())?)
        } else {
            // Dummy as not used for Free
            ConfigType::Standard
        };
        let protocol = OpenVpnProtocol::index_to_variant(
            uiclient.get_configuration_choice(&OpenVpnProtocol::default())?,
        );

        // Create the standardized form
        let (auth_cookie, uid) = Self::parse_auth_cookie(uiclient)?;

        let url = self.build_url(&config_choice, &tier, &protocol)?;

        let mut headers = HeaderMap::new();
        headers.insert(COOKIE, HeaderValue::from_static(auth_cookie));

        headers.insert(
            HeaderName::from_static("x-pm-uid"),
            HeaderValue::from_static(uid),
        );
        let client = reqwest::blocking::Client::new();

        let zipfile = client.get(url).headers(headers).send()?;

        let mut zip = ZipArchive::new(Cursor::new(zipfile.bytes()?))?;
        let openvpn_dir = self.openvpn_dir()?;
        create_dir_all(&openvpn_dir)?;
        for i in 0..zip.len() {
            // Modify auth line for config
            // Write to config dir
            let mut file_contents: Vec<u8> = Vec::with_capacity(2048);
            let mut file = zip.by_index(i).unwrap();
            file.read_to_end(&mut file_contents)?;

            let file_contents = std::str::from_utf8(&file_contents)?;
            let file_contents = file_contents
                .split('\n')
                .filter(|&x| !(x.starts_with("up ") || x.starts_with("down ")))
                .collect::<Vec<&str>>()
                .join("\n");

            // TODO: sanitized_name is now deprecated but there is not a simple alternative
            #[allow(deprecated)]
            let filename = if let Some("ovpn") = file
                .sanitized_name()
                .extension()
                .map(|x| x.to_str().expect("Could not convert OsStr"))
            {
                // Also handle server case from free servers
                let mut hostname: Option<String> = None;
                let mut code = file.name().split('.').next().unwrap();
                if code.contains("free") {
                    // Free case
                    let mut iter_split = code.split('-');
                    let fcode = iter_split.next().unwrap();
                    hostname = Some(iter_split.next().unwrap().to_owned());
                    code = fcode;
                } else if code.contains('-') {
                    // SecureCore
                    let mut iter_split = code.split('-');
                    let start = iter_split.next().unwrap();
                    let end = iter_split.next().unwrap();
                    let number = iter_split.next().unwrap();
                    hostname = Some(format!("{start}_{number}"));
                    code = end;
                }
                let country = code_map
                    .get(code)
                    .unwrap_or_else(|| panic!("Could not find code in map: {code}"));
                let host_str = if let Some(host) = hostname {
                    format!("-{host}")
                } else {
                    String::new()
                };
                format!("{}-{}{}.ovpn", country, code, &host_str)
            } else {
                file.name().to_string()
            };

            debug!("Reading file: {}", file.name());
            let mut outfile =
                File::create(openvpn_dir.join(filename.to_lowercase().replace(' ', "_")))?;
            write!(outfile, "{file_contents}")?;
        }

        // TODO: ProtonVPN DNS servers do not connect
        // Write DNS file (protocol dependent)
        // let dns_string = match protocol {
        //     OpenVpnProtocol::TCP => "10.7.7.1",
        //     OpenVpnProtocol::UDP => "10.8.8.1",
        // };

        // let mut dns_file = File::create(self.openvpn_dir()?.join("dns.txt"))?;
        // write!(dns_file, "{}", dns_string)?;

        // Write OpenVPN credentials file
        let (user, pass) = self.prompt_for_auth(uiclient)?;
        let auth_file = self.auth_file_path()?;
        if auth_file.is_some() {
            let mut outfile = File::create(auth_file.unwrap())?;
            write!(outfile, "{user}\n{pass}")?;
            info!(
                "ProtonVPN OpenVPN config written to {}",
                openvpn_dir.display()
            );
        }
        Ok(())
    }
}

#[derive(EnumIter, PartialEq)]
enum Tier {
    Plus,
    Free,
}

impl Tier {
    fn url_part(&self) -> String {
        match self {
            Self::Plus => "2".to_string(),
            Self::Free => "0".to_string(),
        }
    }
    fn index_to_variant(index: usize) -> Self {
        Self::iter().nth(index).expect("Invalid index")
    }
}

impl Display for Tier {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let s = match self {
            Self::Plus => "Plus",
            Self::Free => "Free",
        };
        write!(f, "{s}")
    }
}

impl Default for Tier {
    fn default() -> Self {
        Self::Free
    }
}

impl ConfigurationChoice for Tier {
    fn prompt(&self) -> String {
        "Choose your ProtonVPN account tier".to_string()
    }

    fn all_names(&self) -> Vec<String> {
        Self::iter().map(|x| format!("{x}")).collect()
    }
    fn all_descriptions(&self) -> Option<Vec<String>> {
        Some(Self::iter().map(|x| x.description().unwrap()).collect())
    }

    fn description(&self) -> Option<String> {
        Some(
            match self {
                Self::Plus => "Plus Account provides more VPN servers and SecureCore configuration",
                Self::Free => "Free VPN servers only",
            }
            .to_string(),
        )
    }
}

#[derive(EnumIter, PartialEq)]
enum ConfigType {
    SecureCore,
    Standard,
}

impl ConfigType {
    fn url_part(&self) -> String {
        match self {
            Self::SecureCore => "SecureCore".to_string(),
            Self::Standard => "Country".to_string(),
        }
    }
    fn index_to_variant(index: usize) -> Self {
        Self::iter().nth(index).expect("Invalid index")
    }
}

impl Display for ConfigType {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let s = match self {
            Self::SecureCore => "SecureCore",
            Self::Standard => "Standard",
        };
        write!(f, "{s}")
    }
}

impl Default for ConfigType {
    fn default() -> Self {
        Self::Standard
    }
}

impl ConfigurationChoice for ConfigType {
    fn prompt(&self) -> String {
        "Please choose the set of OpenVPN configuration files you wish to install".to_string()
    }

    fn all_names(&self) -> Vec<String> {
        Self::iter().map(|x| format!("{x}")).collect()
    }
    fn all_descriptions(&self) -> Option<Vec<String>> {
        Some(Self::iter().map(|x| x.description().unwrap()).collect())
    }
    fn description(&self) -> Option<String> {
        Some(
            match self {
                Self::SecureCore => {
                    "Connect via SecureCore bridge for additional security (Plus accounts only)"
                }
                Self::Standard => {
                    "Standard OpenVPN connection (available servers depend on account tier)"
                }
            }
            .to_string(),
        )
    }
}

#[cfg(test)]
mod tests {
    use super::*; // Import items from parent module

    #[derive(Debug)]
    struct MockUiClient {
        input_to_provide: String,
        // We can add flags here to simulate errors from get_input if needed,
        // but for these tests, we primarily care about the validation and parsing
        // based on the input_to_provide.
    }

    impl UiClient for MockUiClient {
        fn get_input(&self, input: Input) -> anyhow::Result<String> {
            // Check if a validator is provided, as it is in parse_auth_cookie
            if let Some(validator) = &input.validator {
                // Run the validator provided by the function under test
                match validator(&self.input_to_provide) {
                    Ok(_) => Ok(self.input_to_provide.clone()),
                    Err(msg) => Err(anyhow!("Input validation failed: {}", msg)),
                }
            } else {
                // No validator provided, just return the input
                Ok(self.input_to_provide.clone())
            }
        }

        // Implement other methods as unimplemented! or return dummy values
        // as they are not called by the function under test.
        fn get_configuration_choice(
            &self,
            _conf_choice: &dyn ConfigurationChoice,
        ) -> anyhow::Result<usize> {
            unimplemented!("get_configuration_choice not needed for this test")
        }

        fn get_bool_choice(
            &self,
            _bool_choice: crate::config::providers::BoolChoice,
        ) -> anyhow::Result<bool> {
            unimplemented!("get_bool_choice not needed for this test")
        }

        fn get_input_numeric_u16(
            &self,
            _input: crate::config::providers::InputNumericu16,
        ) -> anyhow::Result<u16> {
            unimplemented!("get_input_numeric_u16 not needed for this test")
        }

        fn get_password(&self, _password: Password) -> anyhow::Result<String> {
            unimplemented!("get_password not needed for this test")
        }
    }

    // Helper function to run a test case
    fn run_test(input: &str) -> anyhow::Result<(&'static str, &'static str)> {
        let mock_client = MockUiClient {
            input_to_provide: input.to_string(),
        };
        ProtonVPN::parse_auth_cookie(&mock_client)
    }

    // --- Positive Test Cases ---

    #[test]
    fn test_basic_equal_separator() {
        let result = run_test("AUTH-abc=123");
        assert!(result.is_ok());
        let (cookie, uid) = result.unwrap();
        assert_eq!(cookie, "AUTH-abc=123");
        assert_eq!(uid, "abc");
    }

    #[test]
    fn test_basic_tab_separator() {
        let result = run_test("AUTH-abc\t123");
        assert!(result.is_ok());
        let (cookie, uid) = result.unwrap();
        assert_eq!(cookie, "AUTH-abc=123");
        assert_eq!(uid, "abc");
    }

    #[test]
    fn test_basic_colon_separator() {
        let result = run_test("AUTH-abc: 123");
        assert!(result.is_ok());
        let (cookie, uid) = result.unwrap();
        assert_eq!(cookie, "AUTH-abc=123");
        assert_eq!(uid, "abc");
    }

    #[test]
    fn test_basic_equal_separator_semicolon() {
        let result = run_test("AUTH-def=456;");
        assert!(result.is_ok());
        let (cookie, uid) = result.unwrap();
        assert_eq!(cookie, "AUTH-def=456");
        assert_eq!(uid, "def");
    }

    #[test]
    fn test_basic_tab_separator_semicolon() {
        let result = run_test("AUTH-def\t456;");
        assert!(result.is_ok());
        let (cookie, uid) = result.unwrap();
        assert_eq!(cookie, "AUTH-def=456");
        assert_eq!(uid, "def");
    }

    #[test]
    fn test_basic_colon_separator_semicolon() {
        let result = run_test("AUTH-def: 456;");
        assert!(result.is_ok());
        let (cookie, uid) = result.unwrap();
        assert_eq!(cookie, "AUTH-def=456");
        assert_eq!(uid, "def");
    }

    #[test]
    fn test_no_space_after_colon() {
        let result = run_test("AUTH-id:content");
        assert!(result.is_ok());
        let (cookie, uid) = result.unwrap();
        assert_eq!(cookie, "AUTH-id=content");
        assert_eq!(uid, "id");
    }

    #[test]
    fn test_space_around_equal() {
        let result = run_test("AUTH-name = data");
        assert!(result.is_ok());
        let (cookie, uid) = result.unwrap();
        assert_eq!(cookie, "AUTH-name=data");
        assert_eq!(uid, "name");
    }

    #[test]
    fn test_multiple_spaces_as_separator() {
        // The regex [\s\t=: ]+ allows multiple spaces/tabs/=/
        let result = run_test("AUTH-token   secret");
        assert!(result.is_ok());
        let (cookie, uid) = result.unwrap();
        assert_eq!(cookie, "AUTH-token=secret");
        assert_eq!(uid, "token");
    }

    #[test]
    fn test_multiple_spaces_around_equal() {
        let result = run_test("AUTH-profile  =  info");
        assert!(result.is_ok());
        let (cookie, uid) = result.unwrap();
        assert_eq!(cookie, "AUTH-profile=info");
        assert_eq!(uid, "profile");
    }

    #[test]
    fn test_multiple_spaces_after_colon() {
        let result = run_test("AUTH-session:  data");
        assert!(result.is_ok());
        let (cookie, uid) = result.unwrap();
        assert_eq!(cookie, "AUTH-session=data");
        assert_eq!(uid, "session");
    }

    #[test]
    fn test_uid_with_hyphens_and_underscores() {
        let result = run_test("AUTH-user_id-123=some_value-with-hyphens");
        assert!(result.is_ok());
        let (cookie, uid) = result.unwrap();
        assert_eq!(cookie, "AUTH-user_id-123=some_value-with-hyphens");
        assert_eq!(uid, "user_id-123");
    }

    #[test]
    fn test_value_with_special_chars_allowed() {
        // Value can contain chars like :, +, etc. as long as not space, tab, semicolon
        let result = run_test("AUTH-key=value:and:colons+plus");
        assert!(result.is_ok());
        let (cookie, uid) = result.unwrap();
        assert_eq!(cookie, "AUTH-key=value:and:colons+plus");
        assert_eq!(uid, "key");
    }

    // --- Negative Test Cases (Should fail validation in MockUiClient) ---

    #[test]
    fn test_negative_missing_hyphen() {
        let result = run_test("AUTHxyz=123");
        assert!(result.is_err());
        // Optionally check the error message if needed
        // assert!(result.unwrap_err().to_string().contains("validation failed"));
    }

    #[test]
    fn test_negative_incomplete_prefix() {
        let result = run_test("AUTH-");
        assert!(result.is_err());
    }

    #[test]
    fn test_negative_empty_uid() {
        // Regex `([^=\t:;\s]+)` requires at least one char for UID
        let result = run_test("AUTH- =123");
        assert!(result.is_err());
    }

    #[test]
    fn test_negative_lowercase_auth() {
        let result = run_test("auth-lower=123");
        assert!(result.is_err());
    }

    #[test]
    fn test_negative_wrong_prefix() {
        let result = run_test("PREFIX-abc=123");
        assert!(result.is_err());
    }

    #[test]
    fn test_negative_missing_separator_and_value() {
        let result = run_test("AUTH-abc");
        assert!(result.is_err());
    }

    #[test]
    fn test_negative_empty_value_equal() {
        // Regex `([^\s\t;]+)` requires at least one char for value
        let result = run_test("AUTH-user=");
        assert!(result.is_err());
    }

    #[test]
    fn test_negative_empty_value_colon() {
        // Regex `([^\s\t;]+)` requires at least one char for value
        let result = run_test("AUTH-access: "); // Note space after colon is part of separator
        assert!(result.is_err());
    }

    #[test]
    fn test_negative_newline_in_value() {
        let result = run_test("AUTH-key=\n789");
        let (cookie, uid) = result.unwrap();
        assert_eq!(cookie, "AUTH-key=789");
        assert_eq!(uid, "key");
    }

    #[test]
    fn test_crlf_in_value() {
        let result = run_test("AUTH-account=\r\ndetails");
        let (cookie, uid) = result.unwrap();
        assert_eq!(cookie, "AUTH-account=details");
        assert_eq!(uid, "account");
    }

    #[test]
    fn test_negative_space_in_uid() {
        let result = run_test("AUTH-user id=value");
        assert!(result.is_err());
    }

    #[test]
    fn test_negative_space_in_value() {
        // Value group `([^\s\t;]+)` does not allow space
        let result = run_test("AUTH-uid=value part");
        assert!(result.is_err());
    }

    #[test]
    fn test_negative_tab_in_value() {
        // Value group `([^\s\t;]+)` does not allow tab
        let result = run_test("AUTH-uid=value\tpart");
        assert!(result.is_err());
    }

    #[test]
    fn test_negative_semicolon_in_value() {
        // Value group `([^\s\t;]+)` does not allow semicolon
        let result = run_test("AUTH-uid=value;part");
        assert!(result.is_err());
    }

    #[test]
    fn test_trailing_space_after_semicolon() {
        // Regex requires `$` immediately after optional `;`
        let result = run_test("AUTH-uid=value; ");
        let (cookie, uid) = result.unwrap();
        assert_eq!(cookie, "AUTH-uid=value");
        assert_eq!(uid, "uid");
    }

    #[test]
    fn test_negative_no_separator() {
        let result = run_test("AUTH-uidvalue");
        assert!(result.is_err());
    }
}
