use std::{
    env,
    net::TcpStream,
    path::{Path, PathBuf},
    process::{Child, Command},
    thread::sleep,
    time::Duration,
};

use rayon::iter::{IntoParallelIterator, ParallelIterator};

type DynError = Box<dyn std::error::Error>;

fn main() {
    if let Err(e) = try_main() {
        eprintln!("{e}");
        std::process::exit(-1)
    }
}

fn try_main() -> Result<(), DynError> {
    let task = env::args().nth(1);
    match task.as_deref() {
        Some("local") => local()?,
        Some("local2") => local2()?,
        Some("vs_production" | "vs_prod") => vs_production()?,
        Some("regression") => regression()?,
        Some("docker") => docker()?,
        _ => print_help(),
    }
    Ok(())
}

fn print_help() {
    eprintln!(
        "Tasks:

local                    runs the snake on a local game
local2                   runs the snake twice on a local game
vs_production | vs_prod  runs the snake against the active server
regression               runs the snake against the active server multiple times

docker      package docker image and upload to docker.mkaenner.de/snake
"
    )
}

fn local() -> Result<(), DynError> {
    let mut snake = run_snake(8000, None)?;

    let game = Command::new("./battlesnake-cli")
        .current_dir(project_root())
        .args([
            "play",
            "-W",
            "11",
            "-H",
            "11",
            "--name",
            "local test",
            "--url",
            "http://localhost:8000",
            "-g",
            "solo",
            "--browser",
        ])
        .status();

    game.and(snake.kill())?;
    Ok(())
}

fn local2() -> Result<(), DynError> {
    let mut snake = run_snake(8000, None)?;

    let game = Command::new("./battlesnake-cli")
        .current_dir(project_root())
        .args([
            "play",
            "-W",
            "11",
            "-H",
            "11",
            "--name",
            "local test1",
            "--url",
            "http://localhost:8000",
            "--name",
            "local test2",
            "--url",
            "http://localhost:8000",
            "-g",
            "duel",
            "--browser",
        ])
        .status();

    game.and(snake.kill())?;
    Ok(())
}

fn vs_production() -> Result<(), DynError> {
    let mut snake = run_snake(8000, None)?;

    let game = Command::new("./battlesnake-cli")
        .current_dir(project_root())
        .args([
            "play",
            "-W",
            "11",
            "-H",
            "11",
            "--name",
            "local",
            "--url",
            "http://localhost:8000",
            "--name",
            "production",
            "--url",
            "https://snake.mkaenner.de",
            "-g",
            "duel",
            "--browser",
        ])
        .output();

    let game = snake.kill().and(game)?;
    if !game.status.success() {
        eprintln!("game output: {}", String::from_utf8(game.stderr)?);
        eprintln!("game status: {}", game.status);
        Err("failed to play the game".into())
    } else {
        const WIN_PHRASE: &[u8] = b"local was the winner.\n";
        let won =
            &game.stderr[(game.stderr.len() - WIN_PHRASE.len())..game.stderr.len()] == WIN_PHRASE;
        if won {
            println!("The local snake has won");
        } else {
            println!("The production snake has won");
        }
        Ok(())
    }
}

fn regression() -> Result<(), DynError> {
    let mut snake = run_snake(8000, Some("error"))?;
    let mut prod = match run_production(8001) {
        Ok(prod) => prod,
        err => {
            snake.kill()?;
            err?;
            unreachable!()
        }
    };

    let res = try_regression();

    snake.kill().and(prod.kill())?;
    let (won, games) = res?;
    println!(
        "\nThe local snake has won {won}/{games} games ({}%)",
        won * 100 / games
    );
    Ok(())
}

fn try_regression() -> Result<(usize, usize), DynError> {
    const GAMES: usize = 100;
    // limit the parallelism
    rayon::ThreadPoolBuilder::new()
        .num_threads(std::thread::available_parallelism()?.get() / 3)
        .build_global()
        .unwrap();

    let won_count = (0..GAMES)
        .into_par_iter()
        .map(|_| -> Option<usize> {
            eprint!(".");
            let game = Command::new("./battlesnake-cli")
                .current_dir(project_root())
                .args([
                    "play",
                    "-W",
                    "11",
                    "-H",
                    "11",
                    "--name",
                    "local",
                    "--url",
                    "http://localhost:8000",
                    "--name",
                    "production",
                    "--url",
                    "http://localhost:8001",
                    "-g",
                    "duel",
                ])
                .output()
                .ok()?;
            if !game.status.success() {
                eprintln!("game output: {}", String::from_utf8(game.stderr).ok()?);
                eprintln!("game status: {}", game.status);
                None
            } else {
                const WIN_PHRASE: &[u8] = b"local was the winner.\n";
                let won = &game.stderr[(game.stderr.len() - WIN_PHRASE.len())..game.stderr.len()]
                    == WIN_PHRASE;
                Some(usize::from(won))
            }
        })
        .flatten()
        .sum();

    Ok((won_count, GAMES))
}

fn run_snake(port: u16, log: Option<&str>) -> Result<Child, DynError> {
    let cargo = env::var("CARGO").unwrap_or_else(|_| "cargo".to_string());
    let mut snake = Command::new(cargo);
    snake
        .current_dir(project_root())
        .env("PORT", port.to_string());
    if let Some(log) = log {
        snake.env("RUST_LOG", log);
    }
    let mut snake = snake
        .args(
            ["run", "--bin", "battlesnake"]
                .map(str::to_string)
                .into_iter()
                .chain(env::args().skip(2)),
        )
        .spawn()?;
    loop {
        if let Some(status) = snake.try_wait()? {
            Err(format!("{status}"))?;
        }
        if TcpStream::connect(("127.0.0.1", port)).is_ok() {
            break Ok(snake);
        }
        sleep(Duration::from_secs(1));
    }
}

fn run_production(port: u16) -> Result<Child, DynError> {
    let mut snake = Command::new("docker")
        .args([
            "run",
            "--env",
            "RUST_LOG=error",
            "--env",
            format!("ROCKET_PORT={}", port).as_str(),
            "--network=host",
            "-i",
            "--rm",
            "docker.mkaenner.de/snake:latest",
        ])
        .spawn()?;
    loop {
        if let Some(status) = snake.try_wait()? {
            Err(format!("{status}"))?;
        }
        if TcpStream::connect(("127.0.0.1", port)).is_ok() {
            break Ok(snake);
        }
        sleep(Duration::from_secs(1));
    }
}

fn docker() -> Result<(), DynError> {
    if !Command::new("docker")
        .current_dir(project_root())
        .args(["build", ".", "-t", "docker.mkaenner.de/snake"])
        .status()?
        .success()
    {
        Err("Build of docker image failed")?;
    }
    Command::new("docker")
        .args(["push", "docker.mkaenner.de/snake"])
        .status()?;
    Ok(())
}

fn project_root() -> PathBuf {
    Path::new(&env!("CARGO_MANIFEST_DIR"))
        .ancestors()
        .nth(1)
        .unwrap()
        .to_path_buf()
}
