use crate::{
    browser_module,
    convert::{self, PyResultExt},
    js_module, wasm_builtins,
};
use alloc::rc::{Rc, Weak};
use core::cell::RefCell;
use js_sys::{Object, TypeError};
use ruff_text_size::Ranged;
use rustpython_vm::{
    Interpreter, PyObjectRef, PyRef, PyResult, Settings, VirtualMachine,
    builtins::PyWeak,
    compiler::{self, Mode},
    function::ArgMapping,
    scope::Scope,
    vm::VmCompileError,
};
use std::collections::HashMap;
use wasm_bindgen::prelude::*;

pub(crate) struct StoredVirtualMachine {
    pub interp: Interpreter,
    pub scope: Scope,
    /// you can put a Rc in here, keep it as a Weak, and it'll be held only for
    /// as long as the StoredVM is alive
    held_objects: RefCell<Vec<PyObjectRef>>,
}

fn compile_err_to_js(vm: &VirtualMachine, err: VmCompileError) -> JsValue {
    match err {
        VmCompileError::Compile(err) => convert::syntax_err(err).into(),
        err => convert::py_err_to_js_err(vm, &err.into_pyexception(vm, None)),
    }
}

fn statement_chunks(source: &str) -> Option<Vec<&str>> {
    let module = compiler::parser::parse_module(source).ok()?.into_syntax();
    module
        .body
        .iter()
        .map(|stmt| {
            let range = stmt.range();
            source.get(range.start().to_usize()..range.end().to_usize())
        })
        .collect()
}

#[pymodule]
mod _window {
    use super::{js_module, wasm_builtins};
    use rustpython_vm::{Py, PyPayload, PyResult, VirtualMachine, builtins::PyModule};

    #[expect(clippy::unnecessary_wraps, reason = "Needs to comply with a signature")]
    pub(crate) fn module_exec(vm: &VirtualMachine, module: &Py<PyModule>) -> PyResult<()> {
        __module_exec(vm, module);
        extend_module!(vm, module, {
            "window" => js_module::PyJsValue::new(wasm_builtins::window()).into_ref(&vm.ctx),
        });
        Ok(())
    }
}

impl StoredVirtualMachine {
    fn new(id: String, inject_browser_module: bool) -> Self {
        let mut settings = Settings::default();
        settings.allow_external_library = false;

        let mut builder = Interpreter::builder(settings);

        #[cfg(feature = "freeze-stdlib")]
        {
            let defs = rustpython_stdlib::stdlib_module_defs(&builder.ctx);
            builder = builder
                .add_native_modules(&defs)
                .add_frozen_modules(rustpython_pylib::FROZEN_STDLIB);
        }

        // Add wasm-specific modules
        let js_def = js_module::module_def(&builder.ctx);
        builder = builder.add_native_module(js_def);

        if inject_browser_module {
            let window_def = _window::module_def(&builder.ctx);
            let browser_def = browser_module::module_def(&builder.ctx);
            builder = builder
                .add_native_modules(&[window_def, browser_def])
                .add_frozen_modules(rustpython_vm::py_freeze!(dir = "../Lib"));
        }

        let interp = builder
            .init_hook(move |vm| {
                vm.wasm_id = Some(id);
            })
            .build();

        let scope = interp.enter(|vm| vm.new_scope_with_builtins());

        Self {
            interp,
            scope,
            held_objects: RefCell::new(Vec::new()),
        }
    }
}

// It's fine that it's thread local, since WASM doesn't even have threads yet. thread_local!
// probably gets compiled down to a normal-ish static variable, like Atomic* types do:
// https://rustwasm.github.io/2018/10/24/multithreading-rust-and-wasm.html#atomic-instructions
thread_local! {
    static STORED_VMS: RefCell<HashMap<String, Rc<StoredVirtualMachine>>> = RefCell::default();
}

pub fn get_vm_id(vm: &VirtualMachine) -> &str {
    vm.wasm_id
        .as_ref()
        .expect("VirtualMachine inside of WASM crate should have wasm_id set")
}
pub(crate) fn stored_vm_from_wasm(wasm_vm: &WASMVirtualMachine) -> Rc<StoredVirtualMachine> {
    STORED_VMS.with_borrow(|vms| {
        vms.get(&wasm_vm.id)
            .expect("VirtualMachine is not valid")
            .clone()
    })
}
pub(crate) fn weak_vm(vm: &VirtualMachine) -> Weak<StoredVirtualMachine> {
    let id = get_vm_id(vm);
    STORED_VMS.with_borrow(|vms| Rc::downgrade(vms.get(id).expect("VirtualMachine is not valid")))
}

#[derive(Clone, Copy)]
#[wasm_bindgen(js_name = vmStore)]
pub struct VMStore;

#[wasm_bindgen(js_class = vmStore)]
impl VMStore {
    #[must_use]
    pub fn init(id: String, inject_browser_module: Option<bool>) -> WASMVirtualMachine {
        STORED_VMS.with_borrow_mut(|vms| {
            if !vms.contains_key(&id) {
                let stored_vm =
                    StoredVirtualMachine::new(id.clone(), inject_browser_module.unwrap_or(true));
                vms.insert(id.clone(), Rc::new(stored_vm));
            }
        });
        WASMVirtualMachine { id }
    }

    pub(crate) fn _get(id: String) -> Option<WASMVirtualMachine> {
        STORED_VMS.with_borrow(|vms| vms.contains_key(&id).then_some(WASMVirtualMachine { id }))
    }

    #[must_use]
    pub fn get(id: String) -> JsValue {
        match Self::_get(id) {
            Some(wasm_vm) => wasm_vm.into(),
            None => JsValue::UNDEFINED,
        }
    }

    pub fn destroy(id: String) {
        STORED_VMS.with_borrow_mut(|vms| {
            if let Some(stored_vm) = vms.remove(&id) {
                // for f in stored_vm.drop_handlers.iter() {
                //     f();
                // }
                // deallocate the VM
                drop(stored_vm);
            }
        });
    }

    #[must_use]
    pub fn ids() -> Vec<JsValue> {
        STORED_VMS.with_borrow(|vms| vms.keys().map(|k| k.into()).collect())
    }
}

#[wasm_bindgen(js_name = VirtualMachine)]
#[derive(Clone)]
pub struct WASMVirtualMachine {
    pub(crate) id: String,
}

#[wasm_bindgen(js_class = VirtualMachine)]
impl WASMVirtualMachine {
    pub(crate) fn with_unchecked<F, R>(&self, f: F) -> R
    where
        F: FnOnce(&StoredVirtualMachine) -> R,
    {
        let stored_vm = STORED_VMS.with_borrow_mut(|vms| vms.get_mut(&self.id).unwrap().clone());
        f(&stored_vm)
    }

    pub(crate) fn with<F, R>(&self, f: F) -> Result<R, JsValue>
    where
        F: FnOnce(&StoredVirtualMachine) -> R,
    {
        self.assert_valid()?;
        Ok(self.with_unchecked(f))
    }

    pub(crate) fn with_vm<F, R>(&self, f: F) -> Result<R, JsValue>
    where
        F: FnOnce(&VirtualMachine, &StoredVirtualMachine) -> R,
    {
        self.with(|stored| stored.interp.enter(|vm| f(vm, stored)))
    }

    #[must_use]
    pub fn valid(&self) -> bool {
        STORED_VMS.with_borrow(|vms| vms.contains_key(&self.id))
    }

    pub(crate) fn push_held_rc(
        &self,
        obj: PyObjectRef,
    ) -> Result<PyResult<PyRef<PyWeak>>, JsValue> {
        self.with_vm(|vm, stored_vm| {
            let weak = obj.downgrade(None, vm)?;
            stored_vm.held_objects.borrow_mut().push(obj);
            Ok(weak)
        })
    }

    pub fn assert_valid(&self) -> Result<(), JsValue> {
        if self.valid() {
            Ok(())
        } else {
            Err(TypeError::new(
                "Invalid VirtualMachine, this VM was destroyed while this reference was still held",
            )
            .into())
        }
    }

    pub fn destroy(&self) -> Result<(), JsValue> {
        self.assert_valid()?;
        VMStore::destroy(self.id.clone());
        Ok(())
    }

    #[wasm_bindgen(js_name = addToScope)]
    pub fn add_to_scope(&self, name: String, value: JsValue) -> Result<(), JsValue> {
        self.with_vm(move |vm, StoredVirtualMachine { scope, .. }| {
            let value = convert::js_to_py(vm, value);
            scope.globals.set_item(&name, value, vm).into_js(vm)
        })?
    }

    #[wasm_bindgen(js_name = setStdout)]
    pub fn set_stdout(&self, stdout: JsValue) -> Result<(), JsValue> {
        self.set_stdstream(
            "stdout",
            "JSStdout",
            stdout,
            wasm_builtins::sys_stdout_write_console,
        )
    }

    #[wasm_bindgen(js_name = setStderr)]
    pub fn set_stderr(&self, stderr: JsValue) -> Result<(), JsValue> {
        self.set_stdstream(
            "stderr",
            "JSStderr",
            stderr,
            wasm_builtins::sys_stderr_write_console,
        )
    }

    fn set_stdstream(
        &self,
        attr: &'static str,
        class_name: &'static str,
        stream: JsValue,
        console_write: fn(&str, &VirtualMachine) -> PyResult<()>,
    ) -> Result<(), JsValue> {
        self.with_vm(|vm, _| {
            fn error(attr: &str) -> JsValue {
                TypeError::new(&format!(
                    "Unknown {attr} option, please pass a function or 'console'"
                ))
                .into()
            }
            use wasm_builtins::make_stdstream_object;
            let stream: PyObjectRef = if let Some(s) = stream.as_string() {
                match s.as_str() {
                    "console" => make_stdstream_object(vm, class_name, console_write),
                    _ => return Err(error(attr)),
                }
            } else if stream.is_function() {
                let func = js_sys::Function::from(stream);
                make_stdstream_object(vm, class_name, move |data, vm| {
                    func.call1(&JsValue::UNDEFINED, &data.into())
                        .map_err(|err| convert::js_py_typeerror(vm, err))?;
                    Ok(())
                })
            } else if stream.is_null() {
                make_stdstream_object(vm, class_name, |_, _| Ok(()))
            } else if stream.is_undefined() {
                make_stdstream_object(vm, class_name, console_write)
            } else {
                return Err(error(attr));
            };
            vm.sys_module.set_attr(attr, stream, vm).unwrap();
            Ok(())
        })?
    }

    #[wasm_bindgen(js_name = injectModule)]
    pub fn inject_module(
        &self,
        name: String,
        source: &str,
        imports: Option<Object>,
    ) -> Result<(), JsValue> {
        self.with_vm(|vm, _| {
            let code = vm
                .compile(source, Mode::Exec, name.as_str())
                .map_err(|err| compile_err_to_js(vm, err))?;
            let attrs = vm.ctx.new_dict();
            attrs
                .set_item("__name__", vm.new_pyobj(name.as_str()), vm)
                .into_js(vm)?;

            if let Some(imports) = imports {
                for entry in convert::object_entries(&imports) {
                    let (key, value) = entry?;
                    let key: String = Object::from(key).to_string().into();
                    attrs
                        .set_item(key.as_str(), convert::js_to_py(vm, value), vm)
                        .into_js(vm)?;
                }
            }

            vm.run_code_obj(
                code,
                Scope::new(
                    Some(ArgMapping::from_dict_exact(attrs.clone())),
                    attrs.clone(),
                ),
            )
            .into_js(vm)?;

            let module = vm.new_module(&name, attrs, None);

            let sys_modules = vm.sys_module.get_attr("modules", vm).into_js(vm)?;
            sys_modules.set_item(&name, module.into(), vm).into_js(vm)?;

            Ok(())
        })?
    }

    #[wasm_bindgen(js_name = injectJSModule)]
    pub fn inject_js_module(&self, name: String, module: Object) -> Result<(), JsValue> {
        self.with_vm(|vm, _| {
            let py_module = vm.new_module(&name, vm.ctx.new_dict(), None);
            for entry in convert::object_entries(&module) {
                let (key, value) = entry?;
                let key = Object::from(key).to_string();
                extend_module!(vm, &py_module, {
                    String::from(key) => convert::js_to_py(vm, value),
                });
            }

            let sys_modules = vm.sys_module.get_attr("modules", vm).into_js(vm)?;
            sys_modules
                .set_item(&name, py_module.into(), vm)
                .into_js(vm)?;

            Ok(())
        })?
    }

    pub(crate) fn run(
        &self,
        source: &str,
        mode: Mode,
        source_path: Option<String>,
    ) -> Result<JsValue, JsValue> {
        self.with_vm(|vm, StoredVirtualMachine { scope, .. }| {
            let source_path = source_path.unwrap_or_else(|| "<wasm>".to_owned());
            let code = vm.compile(source, mode, source_path.as_str());
            let code = code.map_err(|err| compile_err_to_js(vm, err))?;
            let result = vm.run_code_obj(code, scope.clone());
            convert::pyresult_to_js_result(vm, result)
        })?
    }

    pub(crate) fn run_single(
        &self,
        source: &str,
        source_path: Option<String>,
    ) -> Result<JsValue, JsValue> {
        self.with_vm(|vm, StoredVirtualMachine { scope, .. }| {
            let source_path = source_path.unwrap_or_else(|| "<wasm>".to_owned());
            let Some(chunks) = statement_chunks(source) else {
                let code = vm.compile(source, Mode::Single, source_path.as_str());
                let code = code.map_err(|err| compile_err_to_js(vm, err))?;
                let result = vm.run_code_obj(code, scope.clone());
                return convert::pyresult_to_js_result(vm, result);
            };

            if chunks.is_empty() {
                return Ok(convert::py_to_js(vm, vm.ctx.none()));
            }

            let displayhook = vm
                .sys_module
                .get_attr("displayhook", vm)
                .map_err(|_| TypeError::new("lost sys.displayhook"))?;
            let mut result = vm.ctx.none();
            for chunk in chunks {
                let code = vm.compile(chunk, Mode::BlockExpr, source_path.as_str());
                let code = code.map_err(|err| compile_err_to_js(vm, err))?;
                result = vm.run_code_obj(code, scope.clone()).into_js(vm)?;
                displayhook.call((result.clone(),), vm).into_js(vm)?;
            }
            Ok(convert::py_to_js(vm, result))
        })?
    }

    pub fn exec(&self, source: &str, source_path: Option<String>) -> Result<JsValue, JsValue> {
        self.run(source, Mode::Exec, source_path)
    }

    pub fn eval(&self, source: &str, source_path: Option<String>) -> Result<JsValue, JsValue> {
        self.run(source, Mode::Eval, source_path)
    }

    #[wasm_bindgen(js_name = execSingle)]
    pub fn exec_single(
        &self,
        source: &str,
        source_path: Option<String>,
    ) -> Result<JsValue, JsValue> {
        self.run_single(source, source_path)
    }
}
