use indicatif::{ProgressBar, ProgressStyle};
use reqwest::StatusCode;
use crate::domain::Domain;
use regex::Regex;
use lazy_static::lazy_static;
use native_tls::{TlsConnector, Certificate, TlsStream};
use std::net::TcpStream;
use std::io::{Write, Read};
use std::str::FromStr;
use x509_parser::prelude::*;

/// Populate status, brand, and certificate expiry (TTE) for each `Domain`.
///
/// For each domain, this function establishes a TLS connection to port 443,
/// sends a minimal HTTP/1.0 GET request, and extracts:
/// - HTTP status code from the response start line
/// - Brand identifier from HTML via regex
/// - Time-to-expiry (seconds) from the server TLS certificate
///
/// It also follows simple HTTP redirects (301/302) by recursively resolving
/// the `Location` header (with a recursion limit). A progress bar is displayed
/// while processing.
///
/// Errors are logged to stderr and skipped; processing continues for the rest
/// of the list.
pub fn populate_domains(domains: &mut Vec<Domain>) {
    // Create progress bar with eta
    let bar: ProgressBar = ProgressBar::new(domains.len().try_into().unwrap())
        .with_style(ProgressStyle::with_template("{wide_bar} {pos}/{len} (eta: {eta})").unwrap());

    // Create a single client to use for all requests
    let connection = match TlsConnector::new() {
        Ok(i) => i,
        Err(_) => {
            eprintln!("Error creating TLS client");
            return
        }
    };

    for mut domain in domains {
        match populate_domain(&connection, &mut domain, &mut 3u8) {
        // match populate_domain(&client, &mut domain) {
            Ok(_) => (),
            Err(_) => eprintln!("Unable to populate {}", domain.domain.host_str().unwrap_or("???"))
        };

        // Increment bar after every domain has been populated
        bar.inc(1);
    }

    // Remove the bar once all domains have been populated
    bar.finish_and_clear();
}

/// Populate a single domain, following redirects up to `recurse_max`.
///
/// Establishes a TLS connection, issues an HTTP/1.0 GET, parses status, brand,
/// and certificate expiry. On 301/302, updates the domain URL and recurses
/// until the limit is reached. Errors are logged; `Ok(())` is returned so that
/// batch processing can continue.
fn populate_domain(connector: &TlsConnector, domain: &mut Domain, recurse_max: &mut u8) -> Result<(), native_tls::Error> {
    if recurse_max.lt(&&mut 1u8) {
        return Ok(())
    }
    *recurse_max -= 1u8;
    
    let host_str = domain.domain.host_str().unwrap();
    let path = domain.domain.path();

    let stream = match TcpStream::connect(format!("{}:443", host_str)) {
        Ok(i) => i,
        Err(_) => {
            if cfg!(debug_assertions) {
                eprintln!("Error connecting to {host_str}");
            }
            return Ok(())
        }
    };
    let mut stream = match connector.connect(host_str, stream) {
        Ok(i) => i,
        Err(_) => {
            if cfg!(debug_assertions) {
                eprintln!("Error connecting to {host_str}");
            }
            return Ok(())
        }
    };

    // Build a correctly formatted HTTP 1.0 request
    let req = format!("GET {} HTTP/1.0\r\nHost: {}\r\nUser-Agent: {}\r\n\r\n",
        path,
        host_str,
        String::from("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36")
    );

    // Send a basic request
    match stream.write_all(req.as_bytes()) {
        Ok(i) => i,
        Err(_) => {
            eprintln!("Error sending request to {host_str}");
            return Ok(())
        }
    }

    // Get response body and convert to str
    let mut res = vec![];
    match stream.read_to_end(&mut res) {
        Ok(i) => i,
        Err(_) => {
            eprintln!("Error reading response stream for {host_str}");
            return Ok(())
        }
    };
    let body = String::from_utf8_lossy(&res);

    domain.brand = get_brand(&body);
    domain.status = get_status_from_body(&body);
    domain.tte = get_tte_from_stream(domain, &stream);

    // If we have temporary or permanent a redirect, we need to follow it
    if domain.status == Some(StatusCode::MOVED_PERMANENTLY) || domain.status == Some(StatusCode::FOUND) {
        let redirect_path = match get_redirect_path_from_body(&body) {
            Some(path) => path,
            None => {
                eprintln!("Unable to find redirect path for {}", domain.domain.as_str());
                return Ok(())
            }
        };

        let redirect_domain: String ;
        if redirect_path.starts_with("http") {
            redirect_domain = redirect_path.clone();
        } else {
            redirect_domain = format!("https://{}{}", host_str, redirect_path);
        }
        let replace_domain = Domain::new(redirect_domain.as_str()).unwrap();
        domain.domain = replace_domain.domain;
        populate_domain(connector, domain, recurse_max)?;
    }

    Ok(())
}

/// Extract a brand identifier from HTML using a precompiled regex.
///
/// Normalizes common defaults (e.g., `css` or `default`) to `moneyinfo` for
/// readability.
fn get_brand(body: &str) -> Option<String> {
    // This ensures the Regex only gets compiled once and reused
    lazy_static! {
        static ref RE: Regex = Regex::new(r####"<link href="/Content(?:/styles/default/brands)?/([\w-]+)\?v=[^"]+" rel="stylesheet"/>"####).unwrap();
    }

    // Find the name of the brand from the stylesheet bundle in the body
    let mut brand = RE.captures(&body)?.get(1)?.as_str().to_owned();

    // Default moneyinfo brand bundle is called css, default MIM brand is 'default', we transform this into moneyinfo for readability
    if brand == String::from("css") ||
        brand == String::from("default") { brand = String::from("moneyinfo") }

    Some(brand)
}

/// Extract the redirect path from a `Location:` header in the HTTP response
/// text (case-insensitive match). Returns the raw path or absolute URL.
fn get_redirect_path_from_body(body: &str) -> Option<String> {
    // This ensures the Regex only gets compiled once and reused
    lazy_static! {
        static ref RedirectPathPatern: Regex = Regex::new(r####"[lL]ocation:\s(\S+)"####).unwrap();
    }

    let path = RedirectPathPatern.captures(body)?.get(1)?.as_str().to_owned();

    Some(path)
}

/// Parse the HTTP status code from the response start line and map it to
/// `reqwest::StatusCode`.
fn get_status_from_body(body: &str) -> Option<StatusCode> {
    // This ensures the Regex only gets compiled once and reused
    lazy_static! {
        static ref StatusCodePatern: Regex = Regex::new(r####"^HTTP/[\d.]+\s+(\d\d\d)"####).unwrap();
    }

    // Find the name of the brand from the stylsheet bundle in the body
    let status = StatusCodePatern.captures(&body)?.get(1)?.as_str();

    StatusCode::from_str(&status).ok()
}

/// Retrieve certificate time-to-expiry (seconds) from an established TLS
/// stream, if a peer certificate is present and parsable.
fn get_tte_from_stream(domain: &Domain, stream: &TlsStream<TcpStream>) -> Option<i64> {
    // Check we received a certificate from the server
    let cert: Option<Certificate> = match stream.peer_certificate() {
        Ok(cer) => cer,
        Err(_) => {
            eprintln!("Error retrieving cert for {}", domain.domain.as_str());
            None
        }
    };

    match cert {
        Some(cert) => get_tte_from_cert(domain, cert),
        None => None
    }
}

/// Parse DER-encoded certificate to compute time-to-expiry (seconds) using
/// `x509-parser` validity information.
fn get_tte_from_cert(domain: &Domain, cert: Certificate) -> Option<i64> {
    // Convert certificate to der so we can parse it
    let cert_der: &[u8] = &cert.to_der().unwrap();
    
    match X509Certificate::from_der(&cert_der) {
        Ok((_rem, x509)) => {
            // Access tte seconds from certificate
            match x509.validity().time_to_expiration() {
                Some(t) => Some(t.whole_seconds()),
                None => None
            }
        },
        Err(_) => {
            println!("Unable to parse Certificate for {}", domain.domain.as_str());
            None
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn it_gets_moneyinfo_brand_from_body() {
        let body = r#"
            <link href="/Content/styles/default/brands/css?v=1.0.0" rel="stylesheet"/>
        "#;
        assert_eq!(get_brand(body).unwrap(), "moneyinfo");
    }

    #[test]
    fn it_gets_alpha_brand_from_body() {
        let body = r#"
            <link href="/Content/styles/default/brands/alpha?v=1.0.0" rel="stylesheet"/>
        "#;
        assert_eq!(get_brand(body).unwrap(), "alpha");
    }

    #[test]
    fn it_gets_mim_moneyinfo_brand_from_body() {
        let body = r#"
            <link href="/Content/default?v=1.0.0" rel="stylesheet"/>
        "#;
        assert_eq!(get_brand(body).unwrap(), "moneyinfo");
    }

    #[test]
    fn it_gets_mim_alpha_brand_from_body() {
        let body = r#"
            <link href="/Content/alpha?v=1.0.0" rel="stylesheet"/>
        "#;
        assert_eq!(get_brand(body).unwrap(), "alpha");
    }

    #[test]
    fn it_gets_redirect_path_from_body() {
        let body = r#"
            Location: /redirect
        "#;
        assert_eq!(get_redirect_path_from_body(body).unwrap(), "/redirect");
    }

    #[test]
    fn it_gets_alternate_case_redirect_path_from_body() {
        let body = r#"
            location: /redirect
        "#;
        assert_eq!(get_redirect_path_from_body(body).unwrap(), "/redirect");
    }

    #[test]
    fn it_gets_ok_status_from_body() {
        let body = r#"HTTP/1.1 200 OK"#;
        assert_eq!(get_status_from_body(body).unwrap(), StatusCode::OK);
    }

    #[test]
    fn it_gets_found_status_from_body() {
        let body = r#"HTTP/1.1 302 OK"#;
        assert_eq!(get_status_from_body(body).unwrap(), StatusCode::FOUND);
    }
}