#[macro_use]
extern crate lazy_static;

use actix_files;
use actix_web::{App, HttpServer};
use structopt::StructOpt;
use webterm::WebTermExt;

use std::net::TcpListener;
use std::process::Command;

#[derive(StructOpt, Debug)]
#[structopt(name = "webterm-server")]
struct Opt {
    /// The port to listen on
    #[structopt(short, long, default_value = "8082")]
    port: u16,

    /// The host or IP to listen on
    #[structopt(short, long, default_value = "localhost")]
    host: String,

    /// The command to execute
    #[structopt(short, long, default_value = "/bin/sh")]
    command: String,
}

lazy_static! {
    static ref OPT: Opt = Opt::from_args();
}

fn main() {
    pretty_env_logger::init();

    // Normalize common hostnames that sometimes resolve to IPv6-only addresses
    // which can cause platform-specific bind failures. Mapping `localhost` to
    // 127.0.0.1 makes behavior predictable on systems where `::1` would otherwise
    // be selected.
    let host = if OPT.host == "localhost" {
        "127.0.0.1".to_string()
    } else {
        OPT.host.clone()
    };

    let bind_addr = format!("{}:{}", host, OPT.port);
    println!("Starting webterm server on http://{}", bind_addr);

    // Single factory closure variable that we reuse for HttpServer::new.
    // The closure does not capture any stack variables (it references the static
    // `OPT`), so it can act as a simple, repeated factory for the server.
    let factory = || {
        App::new()
            .service(actix_files::Files::new("/assets", "./static"))
            .service(actix_files::Files::new("/static", "./node_modules"))
            .webterm_socket("/websocket", |_req| {
                // Use the static OPT inside the handler; this does not make the
                // outer `factory` closure capture stack variables, so factory
                // remains a zero-capture closure (a function item/type).
                let mut cmd = Command::new(OPT.command.clone());
                cmd.env("TERM", "xterm");
                cmd
            })
            .webterm_ui("/", "/websocket", "/static")
    };

    // Bind a std::net::TcpListener ourselves and hand it to actix via `listen`.
    // This avoids actix's address parser producing EINVAL on some platforms.
    let listener = match TcpListener::bind(&bind_addr) {
        Ok(l) => l,
        Err(e) => {
            eprintln!("Failed to bind TcpListener to {}: {}", bind_addr, e);
            eprintln!("Try `--host 0.0.0.0` or `--host 127.0.0.1` to bind explicitly.");
            std::process::exit(1);
        }
    };

    let server = HttpServer::new(factory)
        .listen(listener)
        .unwrap_or_else(|e| {
            eprintln!("Failed to listen on {}: {}", bind_addr, e);
            std::process::exit(1);
        });

    println!("Listening on http://{}", bind_addr);

    if let Err(e) = server.run() {
        eprintln!("Server run failed: {}", e);
        std::process::exit(1);
    }
}
