use core::cell::RefCell;
use core::u64;
use std::io::{self, Read};
use std::process;
use std::rc::Rc;

use anyhow::Context as _;
use anyhow::anyhow;
use boa_engine::context::ContextBuilder;
use boa_engine::job::JobExecutor;
use boa_engine::property::Attribute;
use boa_engine::vm::RuntimeLimits;
use boa_engine::{Context, Script, Source};
use boa_runtime::fetch::BlockingReqwestFetcher;
use boa_runtime::{Console, fetch};
use clap::Parser;
use clap::builder::NonEmptyStringValueParser;
use rlimit::Resource;
use serde::Deserialize;
use sqlx::postgres::PgPoolOptions;

use crate::builtins::fs::Fs;
use crate::builtins::sql::Sql;
use crate::logger::StderrLogger;
use crate::queue::Queue;

pub(crate) mod builtins;
pub(crate) mod logger;
pub(crate) mod queue;

static JS_PRELUDE: &str = include_str!("builtins/prelude.js");

#[derive(Parser, Debug, Clone)]
struct Args {
    #[clap(long, help = "Set the recursion limit")]
    recursion_limit: Option<usize>,
    #[clap(long, help = "Set the loop iteration limit")]
    loop_limit: Option<u64>,
    #[clap(long, help = "Set the virtual memory limit (in megabytes)")]
    virtual_memory_limit: Option<u64>,
    #[clap(long, help = "Enable file system access", default_value_t = false)]
    enable_fs: bool,
    #[clap(long, help = "Enable network access", default_value_t = false)]
    enable_net: bool,
    #[clap(long, help = "Enables PostgreSQL acress limited to single table")]
    enable_sql: Option<String>,
    #[clap(
        help = "Input filename. Use - to read from stdin",
        value_parser = NonEmptyStringValueParser::new(),
    )]
    filename: String,
}

#[derive(Deserialize, Debug, Clone)]
struct DatabaseConfig {
    postgres_url: String,
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let args = Args::parse();

    let script_source = read_script(&args)?;
    let (mut context, queue) = init_context(&args).await?;

    let prelude = Script::parse(
        Source::from_bytes(JS_PRELUDE.as_bytes()),
        None,
        &mut context,
    )
    .map_err(|_| anyhow!("Failed to parse prelude"))?;

    let script = Script::parse(
        Source::from_bytes(script_source.as_bytes()),
        None,
        &mut context,
    )
    .map_err(|_| anyhow!("Failed to parse input script"))?;

    prelude
        .evaluate_async(&mut context)
        .await
        .map_err(|e| anyhow!("Prelude: Uncaught {e}"))?;

    limit_additional_memory(&args)?;

    script
        .evaluate_async(&mut context)
        .await
        .map_err(|e| anyhow!("Uncaught {e}"))?;

    queue
        .run_jobs_async(&RefCell::new(&mut context))
        .await
        .map_err(|e| anyhow!("Uncaught {e}"))?;

    unlimit_memory()?;

    let serialize_code = r#"
    JSON.stringify(globalThis.res)
    "#;

    match context.eval(Source::from_bytes(serialize_code.as_bytes())) {
        Ok(result) => match result.to_string(&mut context) {
            Ok(output) => println!("{}", output.to_std_string_lossy()),

            Err(err) => {
                eprintln!("Unexpected serialization output: {err}")
            }
        },
        Err(err) => {
            eprintln!("Uncaught {err}");
            process::exit(129);
        }
    }

    Ok(())
}

fn limit_additional_memory(args: &Args) -> anyhow::Result<()> {
    let usage = memory_stats::memory_stats().context("Failed to obtain memory usage statistics")?;

    if let Some(limit_mb) = args.virtual_memory_limit {
        let limit = limit_mb * 1024 * 1024 + usage.virtual_mem as u64;

        Resource::AS
            .set(limit, limit)
            .context("Failed to enforce virtual memory limit")?;
    }

    Ok(())
}

fn unlimit_memory() -> anyhow::Result<()> {
    Resource::AS
        .set(u64::MAX, u64::MAX)
        .context("Failed to remove memory limit")
}

fn init_runtime_limits(context: &mut Context, args: &Args) {
    context.set_runtime_limits(get_runtime_limits(args));
}

fn get_runtime_limits(args: &Args) -> RuntimeLimits {
    let mut limits = RuntimeLimits::default();

    if let Some(recursion_limit) = args.recursion_limit {
        limits.set_recursion_limit(recursion_limit);
    }

    if let Some(loop_limit) = args.loop_limit {
        limits.set_loop_iteration_limit(loop_limit);
    }

    limits
}

async fn init_builtins(context: &mut Context, args: &Args) -> anyhow::Result<()> {
    let console = Console::init_with_logger(StderrLogger, context);
    context
        .register_global_property(Console::NAME, console, Attribute::all())
        .unwrap();

    if args.enable_fs {
        let fs = builtins::fs::Fs::init(context);
        context
            .register_global_property(Fs::NAME, fs, Attribute::all())
            .unwrap();
    }

    if args.enable_net {
        let fetcher = BlockingReqwestFetcher::default();
        fetch::register(fetcher, None, context).unwrap();
    }

    if let Some(ref scope) = args.enable_sql {
        let db_config = envy::from_env::<DatabaseConfig>()?;
        let pool = PgPoolOptions::default()
            .min_connections(0)
            .max_connections(1)
            .connect(&db_config.postgres_url)
            .await?;

        let sql = Sql::init(pool, scope.clone(), context);
        context
            .register_global_property(Sql::NAME, sql, Attribute::all())
            .unwrap();
    }

    Ok(())
}

async fn init_context(args: &Args) -> anyhow::Result<(Context, Rc<Queue>)> {
    let queue = Rc::new(Queue::new());
    let mut context = ContextBuilder::new()
        .job_executor(queue.clone())
        .build()
        .unwrap();

    init_runtime_limits(&mut context, args);
    init_builtins(&mut context, args).await?;

    Ok((context, queue))
}

fn read_script(args: &Args) -> anyhow::Result<String> {
    if args.filename == "-" {
        let mut buffer = String::new();
        io::stdin()
            .read_to_string(&mut buffer)
            .context("Failed to read input script from stdin")?;

        Ok(buffer)
    } else {
        std::fs::read_to_string(&args.filename).context("Failed to read the script file")
    }
}
