use std::{
    cell::RefCell,
    rc::Rc,
    sync::{Arc, Weak},
};

use boa_engine::{
    Context, JsNativeError, JsResult, JsValue, Module, NativeFunction, Source,
    builtins::promise::PromiseState, js_string,
};

use crate::{AsArc, Sandbox, ToolError, ToolResult};

mod console;
mod executor;
mod fetch;
mod files;
mod loader;

/// Output from a JavaScript evaluation: the expression value plus any captured console lines.
#[derive(Debug)]
pub(crate) struct JsOutput {
    /// String representation of the last evaluated expression.
    pub value: String,
    /// Lines emitted via `console.log`, `console.warn`, etc., in order.
    pub console: Vec<String>,
}

/// A JavaScript execution context with a persistent session.
///
/// Wraps a Boa [`Context`] and exposes a simple [`eval`](JsContext::eval) method.
/// All state — variables, functions, loaded modules — is preserved across calls,
/// giving tools a REPL-like experience.
///
/// `JsContext` is the single place that imports `boa_engine`; callers never
/// interact with Boa types directly.
pub struct JsContext {
    ctx: Context,
    console_output: Rc<RefCell<Vec<String>>>,
    loader: Rc<loader::SpadeboxModuleLoader>,
}

impl JsContext {
    /// Creates a new `JsContext` with all runtime APIs registered.
    ///
    /// The context holds only weak references to the sandbox (see
    /// [`SandboxCaptures`]); the caller is responsible for keeping the sandbox
    /// alive while evaluating.
    pub fn new(sandbox: impl AsArc<Sandbox>) -> Self {
        let sandbox = sandbox.as_arc();
        let loader = Rc::new(loader::SpadeboxModuleLoader::new(&sandbox));
        let mut ctx = Context::builder()
            .module_loader(Rc::clone(&loader))
            .job_executor(Rc::new(executor::SpadeboxJobExecutor::new()))
            .build()
            .expect("failed to build JS context");

        // Inject runtime functions and objects
        files::register(&mut ctx, &sandbox);
        fetch::register(&mut ctx, &sandbox);
        loader::register_require(&mut ctx, Rc::clone(&loader));
        let console_output = console::register(&mut ctx);

        Self {
            ctx,
            console_output,
            loader,
        }
    }

    /// Registers `func` as a synchronous JavaScript global named `name`.
    ///
    /// JS positional arguments are mapped to a JSON object `{ paramName: value, ... }`
    /// using `params` as the ordered key list, then passed to `func`.
    /// The return value is converted back to a `JsValue`, or a JS `Error` is thrown
    /// if `func` returns `Err`.
    pub(crate) fn register_func(
        &mut self,
        name: &str,
        params: &[String],
        func: crate::sandbox::JsFunc,
    ) -> ToolResult<()> {
        let native = NativeFunction::from_copy_closure_with_captures(
            |_this, args, captures, ctx| {
                let mut map = serde_json::Map::new();
                for (i, param) in captures.params.iter().enumerate() {
                    let js_val = args.get(i).cloned().unwrap_or_else(JsValue::undefined);
                    let json_val = js_val
                        .to_json(ctx)
                        .ok()
                        .flatten()
                        .unwrap_or(serde_json::Value::Null);
                    map.insert(param.clone(), json_val);
                }
                let json_args = serde_json::Value::Object(map);
                let result = (captures.func)(json_args)
                    .map_err(|e| JsNativeError::error().with_message(e))?;
                Ok(JsValue::from_json(&result, ctx).unwrap_or_else(|_| JsValue::null()))
            },
            UserFuncCaptures {
                params: params.to_vec(),
                func,
            },
        );
        self.ctx
            .register_global_callable(js_string!(name), params.len(), native)
            .map_err(|e| ToolError::JsError(e.to_string()))
    }

    /// Registers all `(name, params, func)` entries from `funcs` in order.
    pub(crate) fn register_funcs(
        &mut self,
        funcs: &[(String, Vec<String>, crate::sandbox::JsFunc)],
    ) -> ToolResult<()> {
        for (name, params, func) in funcs {
            self.register_func(name, params, Arc::clone(func))?;
        }
        Ok(())
    }

    /// Evaluates `code` as an ES module and returns any captured console output.
    ///
    /// Unlike [`eval`](Self::eval), this runs in module mode: `import`/`export`
    /// declarations and top-level `await` are supported. The module is fully
    /// loaded and evaluated before returning; the job queue is drained so that
    /// all async work settles. There is no "last expression" value — only
    /// console output is captured.
    ///
    /// `path` sets the module's own location so that relative imports inside
    /// the script (e.g. `import './utils.js'`) resolve correctly.
    pub fn eval_module(&mut self, code: &str, path: &std::path::Path) -> ToolResult<JsOutput> {
        let module = Module::parse(
            Source::from_bytes(code.as_bytes()).with_path(path),
            None,
            &mut self.ctx,
        )
        .map_err(|e| ToolError::JsError(e.to_string()))?;

        // Seed the loader cache so that circular imports back to the entry module
        // return this instance rather than loading a second fresh copy.
        self.loader.insert_in_cache(path, module.clone());

        let promise = module.load_link_evaluate(&mut self.ctx);
        let job_result = self.ctx.run_jobs();
        let console = self.console_output.borrow_mut().drain(..).collect();

        job_result.map_err(|e| ToolError::JsError(e.to_string()))?;

        match promise.state() {
            PromiseState::Fulfilled(_) => Ok(JsOutput {
                value: String::new(),
                console,
            }),
            PromiseState::Rejected(reason) => Err(ToolError::JsError(reason.display().to_string())),
            PromiseState::Pending => Err(ToolError::JsError(
                "module did not finish evaluating".to_string(),
            )),
        }
    }

    /// Evaluates `code` and returns the result along with any captured console output.
    ///
    /// After evaluation, the job queue is drained so that Promises and `async/await`
    /// settle before returning. Eval errors take priority over job errors.
    pub fn eval(&mut self, code: &str) -> ToolResult<JsOutput> {
        let eval_result = self.ctx.eval(Source::from_bytes(code.as_bytes()));
        // Always drain the job queue — even on eval error, pending microtasks
        // should be flushed to keep the context in a consistent state.
        let job_result = self.ctx.run_jobs();
        let console = self.console_output.borrow_mut().drain(..).collect();

        let value = eval_result.map_err(|e| ToolError::JsError(e.to_string()))?;
        job_result.map_err(|e| ToolError::JsError(e.to_string()))?;

        Ok(JsOutput {
            value: value.display().to_string(),
            console,
        })
    }
}

/// Captures for native JS functions — wraps a weak sandbox reference in a
/// GC-traceable struct.
///
/// The reference is weak, not strong, to avoid a reference cycle: a `JsContext`
/// can be owned by the long-lived REPL task, whose receiving channel is kept
/// open by a sender stored inside the `Sandbox` itself. A strong reference here
/// (task → context → sandbox → sender → task) would keep both the sandbox and
/// the task alive forever after the host drops its last handle.
struct SandboxCaptures {
    sandbox: Weak<Sandbox>,
}

impl SandboxCaptures {
    fn new(sandbox: impl AsArc<Sandbox>) -> Self {
        Self {
            sandbox: sandbox.as_weak(),
        }
    }

    /// Upgrades to a strong reference, or throws a JS error if the sandbox was
    /// dropped. Every evaluation entry point holds a strong reference for its
    /// duration, so the upgrade cannot fail while user code is running.
    fn sandbox(&self) -> JsResult<Arc<Sandbox>> {
        self.sandbox.upgrade().ok_or_else(|| {
            JsNativeError::error()
                .with_message("sandbox has been dropped")
                .into()
        })
    }
}

impl boa_engine::gc::Finalize for SandboxCaptures {}

// SAFETY: `Weak<Sandbox>` holds no GC-managed objects; nothing to trace.
unsafe impl boa_engine::gc::Trace for SandboxCaptures {
    boa_engine::gc::empty_trace!();
}

/// Captures for user-provided native functions exposed via `expose_js_func`.
///
/// The Arc closure contains no GC-managed values, so trace is a no-op.
struct UserFuncCaptures {
    params: Vec<String>,
    func: crate::sandbox::JsFunc,
}

impl boa_engine::gc::Finalize for UserFuncCaptures {}

// SAFETY: The closure holds no GC-managed objects; nothing to trace.
unsafe impl boa_engine::gc::Trace for UserFuncCaptures {
    boa_engine::gc::empty_trace!();
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

#[cfg(test)]
mod tests {
    use super::JsContext;
    use crate::Sandbox;
    use std::sync::Arc;

    // The sandbox must be returned: the context only holds weak references.
    fn ctx() -> (JsContext, Arc<Sandbox>) {
        let sandbox = Arc::new(Sandbox::new());
        (JsContext::new(&sandbox), sandbox)
    }

    #[test]
    fn register_func_callable_from_js() {
        let (mut ctx, _sandbox) = ctx();
        ctx.register_func(
            "double",
            &["n".to_string()],
            Arc::new(|args: serde_json::Value| {
                let n: i64 = args.get("n").and_then(|v| v.as_i64()).unwrap_or(0);
                Ok(serde_json::Value::Number((n * 2).into()))
            }),
        )
        .unwrap();
        assert_eq!(ctx.eval("double(21)").unwrap().value, "42");
    }

    #[test]
    fn register_func_error_becomes_js_error() {
        let (mut ctx, _sandbox) = ctx();
        ctx.register_func(
            "fail",
            &[],
            Arc::new(|_| Err("something went wrong".to_string())),
        )
        .unwrap();
        let err = ctx.eval("fail()").unwrap_err().to_string();
        assert!(err.contains("something went wrong"), "got: {err}");
    }

    #[test]
    fn jobs() {
        let (mut ctx, _sandbox) = ctx();

        // Promise .then() callbacks are settled before eval() returns.
        ctx.eval("let x = 0; Promise.resolve(1).then(v => { x = v; });")
            .unwrap();
        assert_eq!(ctx.eval("x").unwrap().value, "1", ".then callback ran");

        // async/await inside an IIFE resolves through the job queue.
        ctx.eval(
            r#"
            let y;
            (async () => { y = await Promise.resolve("done"); })();
        "#,
        )
        .unwrap();
        assert_eq!(
            ctx.eval("y").unwrap().value,
            r#""done""#,
            "async/await settled"
        );

        // console.log inside an async callback is captured.
        let out = ctx
            .eval(r#"(async () => { console.log("async log"); })();"#)
            .unwrap();
        assert_eq!(
            out.console,
            vec!["async log"],
            "console captured from async"
        );
    }
}
