use ssh2::Session;
use std::process::{Command, Stdio};
use std::{
    fs::File,
    io::{Read, Write},
    net::{TcpStream, ToSocketAddrs},
    path::{Path, PathBuf},
    thread,
    time::Duration,
};
use tauri::{AppHandle, Manager};

fn delay() {
    thread::sleep(Duration::from_millis(350));
}

fn resource_path(app: &AppHandle, relative_path: &str) -> Result<PathBuf, String> {
    app.path()
        .resource_dir()
        .map_err(|error| format!("failed to resolve resource directory: {}", error))
        .map(|resource_dir| resource_dir.join("resources").join(relative_path))
}

#[tauri::command]
pub async fn detect_router(ip: String) -> Result<bool, String> {
    delay();

    if ip.trim().is_empty() {
        return Err("router IP is required".into());
    }

    Ok(true)
}

fn authenticate_router(session: &Session, password: &str) -> Result<(), String> {
    if !password.trim().is_empty() {
        if session.userauth_password("root", password).is_ok() && session.authenticated() {
            return Ok(());
        }
    }

    if session.userauth_agent("root").is_ok() && session.authenticated() {
        return Ok(());
    }

    Err("SSH authentication failed for root".into())
}

fn system_ssh_base_args(connect_timeout: &str) -> Vec<String> {
    vec![
        "-o".into(),
        "BatchMode=yes".into(),
        "-o".into(),
        format!("ConnectTimeout={}", connect_timeout),
        "-o".into(),
        "StrictHostKeyChecking=accept-new".into(),
        "-o".into(),
        "HostKeyAlgorithms=+ssh-rsa".into(),
        "-o".into(),
        "PubkeyAcceptedAlgorithms=+ssh-rsa".into(),
    ]
}

fn run_system_ssh(ip: &str, command: &str) -> Result<String, String> {
    let target = format!("root@{}", ip);
    let mut args = system_ssh_base_args("6");

    args.push(target);
    args.push(command.to_string());

    let output = Command::new("ssh")
        .args(args)
        .output()
        .map_err(|error| format!("failed to run system ssh: {}", error))?;

    let stdout = String::from_utf8_lossy(&output.stdout);
    let stderr = String::from_utf8_lossy(&output.stderr);

    if output.status.success() {
        return Ok(format!("{}{}", stdout, stderr));
    }

    Err(format!(
        "system ssh failed with status {:?}:\n{}\n{}",
        output.status.code(),
        stderr,
        stdout
    ))
}

fn run_system_ssh_with_stdin(ip: &str, command: &str, input: &[u8]) -> Result<String, String> {
    let target = format!("root@{}", ip);
    let mut args = system_ssh_base_args("10");

    args.push(target);
    args.push(command.to_string());

    let mut child = Command::new("ssh")
        .args(args)
        .stdin(Stdio::piped())
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .spawn()
        .map_err(|error| format!("failed to start system ssh: {}", error))?;

    {
        let stdin = child
            .stdin
            .as_mut()
            .ok_or_else(|| "failed to open ssh stdin".to_string())?;

        stdin
            .write_all(input)
            .map_err(|error| format!("failed to write ssh stdin: {}", error))?;
    }

    let output = child
        .wait_with_output()
        .map_err(|error| format!("failed waiting for system ssh: {}", error))?;

    let stdout = String::from_utf8_lossy(&output.stdout);
    let stderr = String::from_utf8_lossy(&output.stderr);

    if output.status.success() {
        return Ok(format!("{}{}", stdout, stderr));
    }

    Err(format!(
        "system ssh stdin command failed with status {:?}:\n{}\n{}",
        output.status.code(),
        stderr,
        stdout
    ))
}

fn shell_quote(value: &str) -> String {
    format!("'{}'", value.replace('\'', "'\"'\"'"))
}

fn upload_bytes_via_system_ssh(
    ip: &str,
    content: &[u8],
    remote_path: &str,
    mode: i32,
) -> Result<String, String> {
    let remote_path_quoted = shell_quote(remote_path);
    let command = format!(
        "cat > {} && chmod {:o} {}",
        remote_path_quoted, mode, remote_path_quoted
    );

    run_system_ssh_with_stdin(ip, &command, content)
}

fn upload_file_via_system_ssh(
    ip: &str,
    local_path: &Path,
    remote_path: &str,
    mode: i32,
) -> Result<String, String> {
    let mut local_file = File::open(local_path).map_err(|error| {
        format!(
            "failed to open local file {}: {}",
            local_path.display(),
            error
        )
    })?;

    let mut buffer = Vec::new();

    local_file
        .read_to_end(&mut buffer)
        .map_err(|error| format!("failed to read local file {}: {}", local_path.display(), error))?;

    upload_bytes_via_system_ssh(ip, &buffer, remote_path, mode)
}

fn run_router_command(ip: &str, password: &str, command: &str) -> Result<String, String> {
    if !password.trim().is_empty() {
        match open_router_session(ip, password) {
            Ok(session) => return run_ssh_command(&session, command),
            Err(error) => {
                if !is_ssh_auth_failure(&error) {
                    return Err(error);
                }

                // The OpenSSH client may have a usable agent/key even when libssh2 cannot use it
                // on Windows. Fall back to system ssh for key-based auth.
                return run_system_ssh(ip, command).map_err(|system_error| {
                    format!(
                        "password/libssh2 auth failed: {}; system ssh fallback failed: {}",
                        error, system_error
                    )
                });
            }
        }
    }

    run_system_ssh(ip, command)
}

fn upload_file_to_router_dynamic(
    ip: &str,
    password: &str,
    local_path: &Path,
    remote_path: &str,
    mode: i32,
) -> Result<String, String> {
    if !password.trim().is_empty() {
        match open_router_session(ip, password) {
            Ok(session) => {
                scp_file_from_disk(&session, local_path.to_string_lossy().as_ref(), remote_path, mode)?;
                return Ok(format!("uploaded {} to {}", local_path.display(), remote_path));
            }
            Err(error) => {
                if !is_ssh_auth_failure(&error) {
                    return Err(error);
                }

                // Fall back to OpenSSH agent/key auth and stdin upload. This also avoids Windows
                // scp parsing \\?\C:\... as a remote hostname.
                return upload_file_via_system_ssh(ip, local_path, remote_path, mode).map(|_| {
                    format!(
                        "uploaded {} to {} using system ssh fallback",
                        local_path.display(),
                        remote_path
                    )
                });
            }
        }
    }

    upload_file_via_system_ssh(ip, local_path, remote_path, mode).map(|_| {
        format!(
            "uploaded {} to {} using system ssh",
            local_path.display(),
            remote_path
        )
    })
}

fn upload_string_to_router_dynamic(
    ip: &str,
    password: &str,
    content: &str,
    remote_path: &str,
    mode: i32,
) -> Result<String, String> {
    if !password.trim().is_empty() {
        match open_router_session(ip, password) {
            Ok(session) => {
                scp_string(&session, content, remote_path, mode)?;
                return Ok(format!("uploaded string to {}", remote_path));
            }
            Err(error) => {
                if !is_ssh_auth_failure(&error) {
                    return Err(error);
                }

                return upload_bytes_via_system_ssh(ip, content.as_bytes(), remote_path, mode).map(|_| {
                    format!("uploaded string to {} using system ssh fallback", remote_path)
                });
            }
        }
    }

    upload_bytes_via_system_ssh(ip, content.as_bytes(), remote_path, mode).map(|_| {
        format!("uploaded string to {} using system ssh", remote_path)
    })
}


fn is_ssh_auth_failure(error: &str) -> bool {
    let lowered = error.to_lowercase();

    lowered.contains("permission denied")
        || lowered.contains("publickey,password")
        || lowered.contains("authentication failed")
}

fn open_router_session(ip: &str, password: &str) -> Result<Session, String> {
    let address = format!("{}:22", ip);

    let socket_address = address
        .to_socket_addrs()
        .map_err(|error| format!("failed to resolve router address: {}", error))?
        .next()
        .ok_or_else(|| format!("failed to resolve router address {}", address))?;

    let tcp = TcpStream::connect_timeout(&socket_address, Duration::from_secs(8))
        .map_err(|error| format!("failed to connect to SSH on {}: {}", ip, error))?;

    let _ = tcp.set_read_timeout(Some(Duration::from_secs(30)));
    let _ = tcp.set_write_timeout(Some(Duration::from_secs(30)));

    let mut session =
        Session::new().map_err(|error| format!("failed to create SSH session: {}", error))?;

    session.set_tcp_stream(tcp);

    session
        .handshake()
        .map_err(|error| format!("SSH handshake failed for {}: {}", ip, error))?;

    authenticate_router(&session, password).map_err(|error| format!("root@{}: {}", ip, error))?;

    Ok(session)
}

fn scp_file_from_disk(
    session: &Session,
    local_path: &str,
    remote_path: &str,
    mode: i32,
) -> Result<(), String> {
    let mut local_file =
        File::open(local_path).map_err(|error| format!("failed to open {}: {}", local_path, error))?;

    let file_size = local_file
        .metadata()
        .map_err(|error| format!("failed to read metadata for {}: {}", local_path, error))?
        .len();

    let mut buffer = Vec::new();

    local_file
        .read_to_end(&mut buffer)
        .map_err(|error| format!("failed to read {}: {}", local_path, error))?;

    let mut remote_file = session
        .scp_send(Path::new(remote_path), mode, file_size, None)
        .map_err(|error| format!("failed to start SCP upload to {}: {}", remote_path, error))?;

    remote_file
        .write_all(&buffer)
        .map_err(|error| format!("failed to write {}: {}", remote_path, error))?;

    remote_file.send_eof().ok();
    remote_file.wait_eof().ok();
    remote_file.close().ok();
    remote_file.wait_close().ok();

    Ok(())
}

fn scp_string(session: &Session, content: &str, remote_path: &str, mode: i32) -> Result<(), String> {
    let bytes = content.as_bytes();

    let mut remote_file = session
        .scp_send(Path::new(remote_path), mode, bytes.len() as u64, None)
        .map_err(|error| format!("failed to start SCP upload to {}: {}", remote_path, error))?;

    remote_file
        .write_all(bytes)
        .map_err(|error| format!("failed to write {}: {}", remote_path, error))?;

    remote_file.send_eof().ok();
    remote_file.wait_eof().ok();
    remote_file.close().ok();
    remote_file.wait_close().ok();

    Ok(())
}

fn run_ssh_command(session: &Session, command: &str) -> Result<String, String> {
    let mut channel = session
        .channel_session()
        .map_err(|error| format!("failed to open SSH channel: {}", error))?;

    channel
        .exec(command)
        .map_err(|error| format!("failed to run command `{}`: {}", command, error))?;

    let mut output = String::new();

    let _ = channel.read_to_string(&mut output);
    let _ = channel.wait_close();

    Ok(output)
}

#[tauri::command]
pub async fn upload_firmware(ip: String, firmware_path: String) -> Result<String, String> {
    delay();

    Ok(format!("uploaded {} to {}:/tmp/firmware.bin", firmware_path, ip))
}

#[tauri::command]
pub async fn flash_router(ip: String, remote_firmware_path: String) -> Result<String, String> {
    delay();

    Ok(format!("sysupgrade started on {} with {}", ip, remote_firmware_path))
}

#[tauri::command]
pub async fn wait_for_ssh(ip: String) -> Result<bool, String> {
    for _ in 0..3 {
        delay();
    }

    if ip == "198.51.100.1" || ip == "192.168.1.1" {
        Ok(true)
    } else {
        Err(format!("SSH timeout waiting for {}", ip))
    }
}

#[tauri::command]
pub async fn run_provisioning(ip: String, password: String) -> Result<String, String> {
    if ip.trim().is_empty() {
        return Err("router IP is required".into());
    }

    run_router_command(&ip, &password, "cd /tmp && ./provision.sh")
}

#[tauri::command]
pub async fn capture_wireguard_public_key(ip: String, password: String) -> Result<String, String> {
    if ip.trim().is_empty() {
        return Err("router IP is required".into());
    }

    let output = run_router_command(&ip, &password, "wg show wg0 public-key")?;
    let public_key = output.trim();

    if public_key.is_empty() {
        return Err("WireGuard public key was empty".into());
    }

    if !public_key.ends_with('=') || public_key.len() < 40 {
        return Err(format!("unexpected WireGuard public key output: {}", public_key));
    }

    Ok(public_key.to_string())
}

#[tauri::command]
pub async fn verify_router(ip: String) -> Result<bool, String> {
    delay();

    if ip == "198.51.100.1" {
        Ok(true)
    } else {
        Err("router verification failed".into())
    }
}

#[tauri::command]
pub async fn upload_firmware_to_router(
    app: AppHandle,
    ip: String,
    password: String,
) -> Result<String, String> {
    let local_firmware_path = resource_path(
        &app,
        "firmware/openwrt-23.05.5-zbt-we826-16m-litoral-golden-sysupgrade.bin",
    )?;

    let remote_firmware_path = "/tmp/firmware.bin";

    if ip.trim().is_empty() {
        return Err("router IP is required".into());
    }

    upload_file_to_router_dynamic(
        &ip,
        &password,
        &local_firmware_path,
        remote_firmware_path,
        0o644,
    )?;

    Ok(format!(
        "Firmware uploaded to {}:{}",
        ip, remote_firmware_path
    ))
}

#[tauri::command]
pub async fn flash_router_sysupgrade(ip: String, password: String) -> Result<String, String> {
    if ip.trim().is_empty() {
        return Err("router IP is required".into());
    }

    let flash_command = "test -f /tmp/firmware.bin && sysupgrade -F -n /tmp/firmware.bin";
    let output = run_router_command(&ip, &password, flash_command);

    match output {
        Ok(output) => Ok(format!(
            "sysupgrade command submitted on {}. {}",
            ip, output
        )),
        Err(error) => {
            if is_ssh_auth_failure(&error) {
                return Err(format!(
                    "AUTH_REQUIRED: Firmware was uploaded, but sysupgrade was NOT started on {} because SSH authentication failed. Enter the router password and run flash again.",
                    ip
                ));
            }

            let lowered = error.to_lowercase();

            if lowered.contains("commencing upgrade")
                || lowered.contains("closing all shell sessions")
                || lowered.contains("connection reset")
                || lowered.contains("broken pipe")
                || lowered.contains("disconnect")
                || lowered.contains("sysupgrade")
            {
                Ok(format!(
                    "sysupgrade started on {}; SSH disconnected as expected.",
                    ip
                ))
            } else {
                Err(error)
            }
        }
    }
}

#[tauri::command]
pub async fn reconnect_router_after_flash(ip: String, password: String) -> Result<String, String> {
    if ip.trim().is_empty() {
        return Err("router IP is required".into());
    }

    let max_attempts = 30;
    let wait_between_attempts = Duration::from_secs(5);

    for attempt in 1..=max_attempts {
        match run_router_command(&ip, &password, "ubus call system board") {
            Ok(output) => {
                return Ok(format!(
                    "Router reconnected after flash on attempt {}/{}.\n{}",
                    attempt, max_attempts, output
                ));
            }
            Err(error) => {
                if is_ssh_auth_failure(&error) {
                    return Err(format!(
                        "AUTH_REQUIRED: Router is reachable at {} over SSH, but authentication failed. Enter the router password before continuing.",
                        ip
                    ));
                }

                thread::sleep(wait_between_attempts);
            }
        }
    }

    Err(format!(
        "Router did not become reachable over SSH at {} after {} attempts. Replug Ethernet and retry.",
        ip, max_attempts
    ))
}

#[tauri::command]
pub async fn check_router_after_flash(ip: String, password: String) -> Result<String, String> {
    if ip.trim().is_empty() {
        return Err("router IP is required".into());
    }

    match run_router_command(&ip, &password, "ubus call system board") {
        Ok(output) => Ok(output),
        Err(error) => {
            if is_ssh_auth_failure(&error) {
                Err(format!(
                    "AUTH_REQUIRED: Router is reachable at {} over SSH, but authentication failed. Enter the router password before continuing.",
                    ip
                ))
            } else {
                Err(error)
            }
        }
    }
}

#[tauri::command]
pub async fn upload_provisioning_bundle(
    app: AppHandle,
    ip: String,
    password: String,
    env_content: String,
) -> Result<String, String> {
    if ip.trim().is_empty() {
        return Err("router IP is required".into());
    }

    let local_script_path = resource_path(&app, "provisioning/provision.sh")?;
    let remote_script_path = "/tmp/provision.sh";
    let remote_env_path = "/tmp/router.env";

    upload_file_to_router_dynamic(&ip, &password, &local_script_path, remote_script_path, 0o755)?;
    upload_string_to_router_dynamic(&ip, &password, &env_content, remote_env_path, 0o600)?;
    run_router_command(&ip, &password, "chmod +x /tmp/provision.sh")?;

    Ok(format!("uploaded provision.sh and router.env to {}", ip))
}
#[tauri::command]
pub async fn upload_udp2raw_setup_script(
    app: AppHandle,
    ip: String,
    password: String,
) -> Result<String, String> {
    if ip.trim().is_empty() {
        return Err("router IP is required".into());
    }

    let local_script_path = resource_path(&app, "udp2raw/setup_udp2raw.sh")?;
    let remote_script_path = "/tmp/setup_udp2raw.sh";

    upload_file_to_router_dynamic(&ip, &password, &local_script_path, remote_script_path, 0o755)?;
    run_router_command(&ip, &password, "chmod +x /tmp/setup_udp2raw.sh")?;

    Ok(format!("uploaded setup_udp2raw.sh to {}", ip))
}

#[tauri::command]
pub async fn run_udp2raw_setup(ip: String, password: String) -> Result<String, String> {
    if ip.trim().is_empty() {
        return Err("router IP is required".into());
    }

    run_router_command(&ip, &password, "sh /tmp/setup_udp2raw.sh")
}

#[tauri::command]
pub async fn check_udp2raw_router_status(ip: String, password: String) -> Result<String, String> {
    if ip.trim().is_empty() {
        return Err("router IP is required".into());
    }

    let command = r#"
    echo "== udp2raw binary =="
    if command -v udp2raw >/dev/null 2>&1; then
    command -v udp2raw
    else
    echo "missing"
    fi

    echo ""
    echo "== init script =="
    if [ -x /etc/init.d/udp2raw-wg ]; then
    echo "present"
    else
    echo "missing"
    fi

    echo ""
    echo "== process =="
    if pgrep -af "^/usr/bin/udp2raw" >/dev/null 2>&1; then
    pgrep -af "^/usr/bin/udp2raw"
    else
    echo "not running"
    fi

    echo ""
    echo "== service status =="
    if [ -x /etc/init.d/udp2raw-wg ]; then
    /etc/init.d/udp2raw-wg status || true
    else
    echo "service unavailable"
    fi

    echo ""
    echo "== WireGuard configured endpoint =="
    uci get network.wgserver.endpoint_host 2>/dev/null || true
    uci get network.wgserver.endpoint_port 2>/dev/null || true

    echo ""
    echo "== local listener =="
    netstat -ln 2>/dev/null | grep -E '127.0.0.1:4999|:4999' || echo "listener not confirmed"

    echo ""
    echo "== WireGuard runtime endpoint =="
    wg show wg0 2>/dev/null | grep -A8 '^peer:' || echo "wg0 unavailable"
    "#;

    run_router_command(&ip, &password, command)
}

#[tauri::command]
pub async fn test_udp2raw_tunnel(ip: String, password: String) -> Result<String, String> {
    if ip.trim().is_empty() {
        return Err("router IP is required".into());
    }

    let command = r#"
    echo "== udp2raw process =="
    if pgrep -af "^/usr/bin/udp2raw" >/dev/null 2>&1; then
    pgrep -af "^/usr/bin/udp2raw"
    else
    echo "ERROR: udp2raw is not running"
    exit 20
    fi

    echo ""
    echo "== WireGuard configured endpoint =="
    uci get network.wgserver.endpoint_host 2>/dev/null || true
    uci get network.wgserver.endpoint_port 2>/dev/null || true

    echo ""
    echo "== local listener =="
    netstat -ln 2>/dev/null | grep -E '127.0.0.1:4999|:4999' || echo "WARNING: listener not confirmed"

    echo ""
    echo "== ping VPS public IP =="
    ping -c 2 -W 2 146.59.230.190 || true

    echo ""
    echo "== WireGuard status =="
    wg show wg0 2>/dev/null || echo "wg0 not available"

    echo ""
    echo "== route check =="
    ip route || true

    echo ""
    echo "UDP2RAW tunnel test completed"
    "#;

    let mut last_error = String::new();

    for attempt in 1..=5 {
        match run_router_command(&ip, &password, command) {
            Ok(output) => return Ok(output),
            Err(error) => {
                last_error = format!("SSH attempt {}/5 failed: {}", attempt, error);
                thread::sleep(Duration::from_secs(2));
            }
        }
    }

    Err(last_error)
}

#[tauri::command]
pub async fn upload_udp2raw_binary(
    app: AppHandle,
    ip: String,
    password: String,
) -> Result<String, String> {
    if ip.trim().is_empty() {
        return Err("router IP is required".into());
    }

    let local_binary_path = resource_path(&app, "udp2raw/udp2raw")?;
    let remote_binary_path = "/usr/bin/udp2raw";

    upload_file_to_router_dynamic(&ip, &password, &local_binary_path, remote_binary_path, 0o755)?;
    run_router_command(
        &ip,
        &password,
        "chmod +x /usr/bin/udp2raw && ls -l /usr/bin/udp2raw",
    )?;

    Ok("uploaded udp2raw binary to /usr/bin/udp2raw".into())
}