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

use rayon::iter::{IntoParallelIterator, ParallelIterator};
use rich_progress_bar::{Colors, RichProgressBar};

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("local4") => local4()?,
        Some("constrictor4") => constrictor4()?,
        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
local4                   runs the snake on a local standard game
constrictor4             runs the snake on a local constrictor 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 local4() -> 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",
            "--name",
            "local test3",
            "--url",
            "http://localhost:8000",
            "--name",
            "local test4",
            "--url",
            "http://localhost:8000",
            "-g",
            "standard",
            "--browser",
        ])
        .status();

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

fn constrictor4() -> 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",
            "--name",
            "local test3",
            "--url",
            "http://localhost:8000",
            "--name",
            "local test4",
            "--url",
            "http://localhost:8000",
            "-g",
            "constrictor",
            "--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_local_docker(53511)?;
    let mut prod = match run_production(64576) {
        Ok(prod) => prod,
        err => {
            snake.kill()?;
            err?;
            unreachable!()
        }
    };

    sleep(Duration::from_secs(1));

    let res = try_regression();

    snake.kill().and(prod.kill())?;
    stop_local_docker()?;
    stop_production()?;
    let (won, draw, loose) = res?;
    let games = won + draw + loose;
    println!(
        "\nThe local snake has won {won}/{games} games ({}%)",
        (won + games / 200) * 100 / games
    );
    println!(
        "The local snake has drawn {draw}/{games} games ({}%)",
        (draw + games / 200) * 100 / games
    );
    println!(
        "The local snake has lost {loose}/{games} games ({}%)",
        (loose + games / 200) * 100 / games
    );
    match won.cmp(&loose) {
        Ordering::Less => println!("The local nake is worse than the production"),
        Ordering::Equal => println!("The local snake is equivalent to the production"),
        Ordering::Greater => println!("The local snake is better than the production"),
    }
    Ok(())
}

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

    let mut progress = RichProgressBar::new();
    progress
        .set_progress_character('=')
        .set_color(Colors::BrightCyan)
        .set_total(GAMES as u64);
    let progress = Mutex::new(&mut progress);
    let stats = (0..GAMES)
        .into_par_iter()
        .flat_map(|_| {
            let game = Command::new("./battlesnake-cli")
                .current_dir(project_root())
                .args([
                    "play",
                    "--width",
                    "11",
                    "--height",
                    "11",
                    "--timeout",
                    "100",
                    "--name",
                    "local",
                    "--url",
                    "http://localhost:53511",
                    "--name",
                    "production",
                    "--url",
                    "http://localhost:64576",
                    "-g",
                    "duel",
                ])
                .output()
                .ok()?;
            let res = 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";
                const LOOSE_PHRASE: &[u8] = b"production was the winner.\n";
                if &game.stderr[(game.stderr.len() - WIN_PHRASE.len())..game.stderr.len()]
                    == WIN_PHRASE
                {
                    Some((1, 0, 0))
                } else if &game.stderr[(game.stderr.len() - LOOSE_PHRASE.len())..game.stderr.len()]
                    == LOOSE_PHRASE
                {
                    Some((0, 0, 1))
                } else {
                    Some((0, 1, 0))
                }
            };
            if let Ok(mut progress) = progress.lock() {
                let _ = progress.inc();
            }
            res
        })
        .reduce(
            || (0, 0, 0),
            |(sum_win, sum_draw, sum_loose), (win, draw, loose)| {
                (sum_win + win, sum_draw + draw, sum_loose + loose)
            },
        );

    Ok(stats)
}

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", "--release"]
                .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_local_docker(port: u16) -> Result<Child, DynError> {
    if !Command::new("docker")
        .current_dir(project_root())
        .args(["build", ".", "-t", "localhost/local_snake:latest"])
        .status()?
        .success()
    {
        Err("Build of docker image failed")?;
    }
    let mut snake = Command::new("docker")
        .args([
            "run",
            "--name",
            "battlesnake-regression-local",
            "--replace",
            "--env",
            "RUST_LOG=error",
            "--env",
            format!("PORT={}", port).as_str(),
            "--network=host",
            "--rm",
            "local_snake",
        ])
        .stdout(Stdio::inherit())
        .stderr(Stdio::inherit())
        .stdin(Stdio::inherit())
        .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 stop_local_docker() -> Result<(), DynError> {
    Command::new("docker")
        .args(["stop", "battlesnake-regression-local"])
        .status()?;
    Ok(())
}

fn run_production(port: u16) -> Result<Child, DynError> {
    let mut snake = Command::new("docker")
        .args([
            "run",
            "--name",
            "battlesnake-regression-production",
            "--replace",
            "--env",
            "RUST_LOG=error",
            "--env",
            format!("PORT={}", port).as_str(),
            "--network=host",
            "--rm",
            "docker.mkaenner.de/snake:latest",
        ])
        .stdout(Stdio::inherit())
        .stderr(Stdio::inherit())
        .stdin(Stdio::inherit())
        .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 stop_production() -> Result<(), DynError> {
    Command::new("docker")
        .args(["stop", "battlesnake-regression-production"])
        .status()?;
    Ok(())
}

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()
}
