//! JavaScript/WASM bindings for Hypen Engine
//!
//! This module provides JavaScript-specific bindings via wasm-bindgen.
//! For WASI/non-JS runtimes, see the `wasi` module.

use serde_wasm_bindgen::from_value;
use std::collections::HashSet;
use wasm_bindgen::prelude::*;

/// Static null value to avoid cloning state when no module exists
static NULL_STATE: serde_json::Value = serde_json::Value::Null;

/// Format parser errors into a human-readable message.
fn format_parse_errors(errors: &[hypen_parser::error::Rich<char>]) -> String {
    errors
        .iter()
        .map(|e| hypen_parser::error::format_error_simple(e))
        .collect::<Vec<_>>()
        .join("; ")
}

use crate::{
    dispatch::Action,
    ir::ComponentRegistry,
    ir::{ast_to_ir, ast_to_ir_node, Element, IRNode, NodeId},
    lifecycle::{Module, ModuleInstance},
    reactive::{DependencyGraph, Scheduler},
    reconcile::{create_ir_node_tree, reconcile_ir, reconcile_ir_node, InstanceTree, Patch},
};

/// WASM-exported engine instance for JavaScript runtimes
/// Uses Rc<RefCell<>> instead of Send+Sync for WASM single-threaded environment
#[wasm_bindgen]
pub struct WasmEngine {
    component_registry: ComponentRegistry,
    module: Option<ModuleInstance>,
    tree: InstanceTree,
    dependencies: DependencyGraph,
    scheduler: Scheduler,
    revision: u64,
    patch_callback: Option<js_sys::Function>,
    action_handlers: std::collections::HashMap<String, js_sys::Function>,
    root_ir_node: Option<IRNode>,
    component_resolver: Option<js_sys::Function>,
    /// Tracks visited import paths to prevent circular imports during resolution
    import_visited: HashSet<String>,
    /// O(1) index from compact node-ID strings (e.g. "1", "42") to their
    /// actual SlotMap keys.  Populated from Create patches.
    node_id_index: std::collections::HashMap<String, NodeId>,
}

#[wasm_bindgen]
impl WasmEngine {
    /// Create a new engine instance
    #[allow(clippy::new_without_default)]
    #[wasm_bindgen(constructor)]
    pub fn new() -> Self {
        // Enable console_error_panic_hook for better debugging
        #[cfg(feature = "console_error_panic_hook")]
        console_error_panic_hook::set_once();

        Self {
            component_registry: ComponentRegistry::new(),
            module: None,
            tree: InstanceTree::new(),
            dependencies: DependencyGraph::new(),
            scheduler: Scheduler::new(),
            revision: 0,
            patch_callback: None,
            action_handlers: std::collections::HashMap::new(),
            root_ir_node: None,
            component_resolver: None,
            import_visited: HashSet::new(),
            node_id_index: std::collections::HashMap::new(),
        }
    }

    /// Parse and render Hypen DSL source code
    /// Supports documents with import statements - imports are resolved via the component resolver
    #[wasm_bindgen(js_name = renderSource)]
    pub fn render_source(&mut self, source: &str) -> Result<(), JsValue> {
        let doc = hypen_parser::parse_document(source)
            .map_err(|e| JsValue::from_str(&format!("Parse error: {}", format_parse_errors(&e))))?;

        // Pre-resolve imported components before rendering
        self.import_visited.clear();
        self.resolve_imports(&doc.imports);

        // Render the first component (entry point)
        if let Some(component) = doc.components.first() {
            let ir_node = ast_to_ir_node(component);
            self.root_ir_node = Some(ir_node.clone());
            self.render(&ir_node);
        }

        Ok(())
    }

    /// Set the render callback that receives patches
    #[wasm_bindgen(js_name = setRenderCallback)]
    pub fn set_render_callback(&mut self, callback: js_sys::Function) {
        self.patch_callback = Some(callback);
    }

    /// Set the component resolver callback
    /// The resolver receives (componentName, contextPath) and should return
    /// { source: string, path: string } or null
    ///
    /// contextPath is the path of the component that's referencing this component
    /// The returned path should be the resolved absolute path to the component file
    #[wasm_bindgen(js_name = setComponentResolver)]
    pub fn set_component_resolver(&mut self, resolver: js_sys::Function) {
        self.component_resolver = Some(resolver);
    }

    /// Register a primitive element (like Text, Button, etc.) to skip component resolution
    /// This prevents unnecessary resolver calls for built-in DOM elements
    #[wasm_bindgen(js_name = registerPrimitive)]
    pub fn register_primitive(&mut self, name: &str) {
        self.component_registry.register_primitive(name);
    }

    /// Render a component source (for lazy loading routes)
    /// This allows rendering a component on-demand
    #[wasm_bindgen(js_name = renderLazyComponent)]
    pub fn render_lazy_component(&mut self, source: &str) -> Result<(), JsValue> {
        // Parse and render the source
        self.render_source(source)
    }

    /// Render a component into a specific parent node (subtree rendering)
    /// This is used for lazy routing where components are rendered on-demand into route containers
    ///
    /// # Arguments
    /// * `source` - The Hypen DSL source to parse and render
    /// * `parent_node_id_str` - The serialized node ID string of the parent element
    /// * `state_js` - The state to use for rendering (as JsValue)
    #[wasm_bindgen(js_name = renderInto)]
    pub fn render_into(
        &mut self,
        source: &str,
        parent_node_id_str: &str,
        state_js: JsValue,
    ) -> Result<(), JsValue> {
        // Parse as document to support import statements
        let doc = hypen_parser::parse_document(source)
            .map_err(|e| JsValue::from_str(&format!("Parse error: {}", format_parse_errors(&e))))?;

        // Pre-resolve imported components
        self.import_visited.clear();
        self.resolve_imports(&doc.imports);

        let component = doc
            .components
            .first()
            .ok_or_else(|| JsValue::from_str("No component found in source"))?;

        // Convert to IRNode (supports first-class ForEach, When/If)
        let ir_node = ast_to_ir_node(component);

        // Resolve any unregistered components in the IR node
        self.resolve_ir_node_components(&ir_node);

        // Expand the IR node using the registry
        let expanded = self.component_registry.expand_ir_node(&ir_node);

        // Use the provided state instead of module state
        let state: serde_json::Value = if state_js.is_null() || state_js.is_undefined() {
            serde_json::Value::Null
        } else {
            from_value(state_js).map_err(|e| JsValue::from_str(&format!("Invalid state: {}", e)))?
        };

        // O(1) lookup via the node-ID index (populated from Create patches)
        let parent_id = self
            .node_id_index
            .get(parent_node_id_str)
            .copied()
            .ok_or_else(|| {
                JsValue::from_str(&format!("Parent node not found: {}", parent_node_id_str))
            })?;

        #[cfg(debug_assertions)]
        web_sys::console::log_1(
            &format!("[WASM] Rendering into parent node: {}", parent_node_id_str).into(),
        );

        let mut patches = Vec::new();

        // Check if parent already has children - if so, reconcile; otherwise create
        let parent_has_children = self
            .tree
            .get(parent_id)
            .map(|node| !node.children.is_empty())
            .unwrap_or(false);

        if parent_has_children {
            // Reconcile existing children with the new IR node
            #[cfg(debug_assertions)]
            web_sys::console::log_1(&"[WASM] Reconciling existing route content".into());
            if let Some(parent_node) = self.tree.get(parent_id) {
                // For now, reconcile the first child (single root element in route)
                // TODO: Handle multiple root elements if needed
                if let Some(&first_child_id) = parent_node.children.front() {
                    reconcile_ir_node(
                        &mut self.tree,
                        first_child_id,
                        &expanded,
                        &state,
                        &mut patches,
                        &mut self.dependencies,
                    );
                }
            }
        } else {
            // Create the subtree under the specified parent (first render)
            #[cfg(debug_assertions)]
            web_sys::console::log_1(&"[WASM] Creating new route content".into());
            create_ir_node_tree(
                &mut self.tree,
                &expanded,
                Some(parent_id),
                &state,
                &mut patches,
                false, // Not root
                &mut self.dependencies,
            );
        }

        // Emit patches to the renderer
        self.emit_patches(patches);

        self.revision += 1;

        Ok(())
    }

    /// Internal render method using IRNode
    fn render(&mut self, ir_node: &IRNode) {
        // Try to resolve any unregistered components in the IR node
        self.resolve_ir_node_components(ir_node);

        // Expand components in the IR node
        let expanded = self.component_registry.expand_ir_node(ir_node);

        // Get state reference (no clone needed - reconcile only borrows)
        let state: &serde_json::Value = self
            .module
            .as_ref()
            .map(|m| m.get_state())
            .unwrap_or(&NULL_STATE);

        // Clear dependencies
        self.dependencies.clear();

        // Reconcile using IRNode and generate patches
        let patches = reconcile_ir(
            &mut self.tree,
            &expanded,
            None,
            state,
            &mut self.dependencies,
        );

        // Emit patches via callback
        self.emit_patches(patches);

        self.revision += 1;
    }

    /// Pre-resolve imported components via the JS resolver callback.
    /// For each import, calls the resolver with (componentName, importSourcePath).
    /// If the resolved source itself contains imports, recurses to resolve them too.
    fn resolve_imports(&mut self, imports: &[hypen_parser::ImportStatement]) {
        let resolver = match self.component_resolver.clone() {
            Some(r) => r,
            None => return, // No resolver set — imports can't be resolved
        };

        for import in imports {
            let source_path = import.source_path();

            for name in import.imported_names() {
                // Skip if already registered
                if self
                    .component_registry
                    .get(&name, Some(source_path))
                    .is_some()
                {
                    continue;
                }

                // Prevent circular imports
                let import_key = format!("{}:{}", source_path, name);
                if self.import_visited.contains(&import_key) {
                    continue;
                }
                self.import_visited.insert(import_key);

                // Call JS resolver with (componentName, importSourcePath)
                let name_js = JsValue::from_str(&name);
                let source_path_js = JsValue::from_str(source_path);

                if let Ok(result) = resolver.call2(&JsValue::NULL, &name_js, &source_path_js) {
                    if result.is_null() || result.is_undefined() {
                        continue;
                    }

                    let source_val =
                        js_sys::Reflect::get(&result, &JsValue::from_str("source")).ok();
                    let path_val = js_sys::Reflect::get(&result, &JsValue::from_str("path")).ok();
                    let passthrough_val =
                        js_sys::Reflect::get(&result, &JsValue::from_str("passthrough")).ok();
                    let lazy_val = js_sys::Reflect::get(&result, &JsValue::from_str("lazy")).ok();

                    if let (Some(source_js), Some(path_js)) = (source_val, path_val) {
                        if let (Some(resolved_source), Some(path)) =
                            (source_js.as_string(), path_js.as_string())
                        {
                            let is_lazy = lazy_val.and_then(|v| v.as_bool()).unwrap_or(false);
                            let is_passthrough =
                                passthrough_val.and_then(|v| v.as_bool()).unwrap_or(false);

                            if is_lazy {
                                let dummy_element = Element::new(&name);
                                let component =
                                    crate::ir::Component::new(name.clone(), move |_props| {
                                        dummy_element.clone()
                                    })
                                    .with_source_path(&path)
                                    .with_lazy(true);
                                self.component_registry.register(component);
                            } else if is_passthrough {
                                let dummy_element = Element::new(&name);
                                let component =
                                    crate::ir::Component::new(name.clone(), move |_props| {
                                        dummy_element.clone()
                                    })
                                    .with_source_path(&path)
                                    .with_passthrough(true);
                                self.component_registry.register(component);
                            } else {
                                // Parse the resolved source as a document — it may have its own imports
                                if let Ok(resolved_doc) =
                                    hypen_parser::parse_document(&resolved_source)
                                {
                                    // Recursively resolve this document's imports
                                    self.resolve_imports(&resolved_doc.imports);

                                    // Register the component from the first component spec
                                    if let Some(component_spec) = resolved_doc.components.first() {
                                        let ir_element = ast_to_ir(component_spec);
                                        let children = ir_element.children.clone();

                                        let component = crate::ir::Component::new(
                                            name.clone(),
                                            move |_props| ir_element.clone(),
                                        )
                                        .with_source_path(&path);
                                        self.component_registry.register(component);

                                        // Also resolve any unregistered components referenced in children
                                        for child in &children {
                                            self.resolve_components_with_context(
                                                child,
                                                Some(&path),
                                            );
                                        }
                                    }
                                } else if let Ok(component_spec) =
                                    hypen_parser::parse_component(&resolved_source)
                                {
                                    // Fallback: try parsing as bare component (no imports)
                                    let ir_element = ast_to_ir(&component_spec);
                                    let children = ir_element.children.clone();

                                    let component =
                                        crate::ir::Component::new(name.clone(), move |_props| {
                                            ir_element.clone()
                                        })
                                        .with_source_path(&path);
                                    self.component_registry.register(component);

                                    for child in &children {
                                        self.resolve_components_with_context(child, Some(&path));
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }

    /// Recursively resolve components with path context
    fn resolve_components_with_context(&mut self, element: &Element, context_path: Option<&str>) {
        // Check if this component needs to be resolved
        if self
            .component_registry
            .get(&element.element_type, context_path)
            .is_none()
        {
            if let Some(ref resolver) = self.component_resolver.clone() {
                let name_js = JsValue::from_str(&element.element_type);
                let context_js = context_path.map(JsValue::from_str).unwrap_or(JsValue::NULL);

                if let Ok(result) = resolver.call2(&JsValue::NULL, &name_js, &context_js) {
                    if !result.is_null() && !result.is_undefined() {
                        // Expect result to be { source: string, path: string, passthrough?: boolean, lazy?: boolean }
                        let source_val =
                            js_sys::Reflect::get(&result, &JsValue::from_str("source")).ok();
                        let path_val =
                            js_sys::Reflect::get(&result, &JsValue::from_str("path")).ok();
                        let passthrough_val =
                            js_sys::Reflect::get(&result, &JsValue::from_str("passthrough")).ok();
                        let lazy_val =
                            js_sys::Reflect::get(&result, &JsValue::from_str("lazy")).ok();

                        if let (Some(source_js), Some(path_js)) = (source_val, path_val) {
                            if let (Some(source), Some(path)) =
                                (source_js.as_string(), path_js.as_string())
                            {
                                let is_lazy = lazy_val.and_then(|v| v.as_bool()).unwrap_or(false);

                                let is_passthrough =
                                    passthrough_val.and_then(|v| v.as_bool()).unwrap_or(false);

                                if is_lazy {
                                    // Lazy component: don't parse, just create a dummy component
                                    #[cfg(debug_assertions)]
                                    web_sys::console::log_1(
                                        &format!(
                                            "[WASM] Registering lazy component: {}",
                                            element.element_type
                                        )
                                        .into(),
                                    );

                                    let name = element.element_type.clone();
                                    let dummy_element = Element::new(&name);
                                    let component =
                                        crate::ir::Component::new(name.clone(), move |_props| {
                                            dummy_element.clone()
                                        })
                                        .with_source_path(&path)
                                        .with_lazy(true);

                                    self.component_registry.register(component);
                                } else if is_passthrough {
                                    // Passthrough component: don't parse, just create a dummy component
                                    #[cfg(debug_assertions)]
                                    web_sys::console::log_1(
                                        &format!(
                                            "[WASM] Registering passthrough component: {}",
                                            element.element_type
                                        )
                                        .into(),
                                    );

                                    let name = element.element_type.clone();
                                    let dummy_element = Element::new(&name);
                                    let component =
                                        crate::ir::Component::new(name.clone(), move |_props| {
                                            dummy_element.clone()
                                        })
                                        .with_source_path(&path)
                                        .with_passthrough(true);

                                    self.component_registry.register(component);
                                } else {
                                    // Regular component: parse and register
                                    if let Ok(component_spec) =
                                        hypen_parser::parse_component(&source)
                                    {
                                        let ir_element = ast_to_ir(&component_spec);
                                        let name = element.element_type.clone();
                                        let path_clone = path.clone();

                                        // Clone children before moving ir_element into closure
                                        let children = ir_element.children.clone();

                                        let component = crate::ir::Component::new(
                                            name.clone(),
                                            move |_props| ir_element.clone(),
                                        )
                                        .with_source_path(&path);

                                        self.component_registry.register(component);

                                        // Recursively resolve children of this component with its path as context
                                        for child in &children {
                                            self.resolve_components_with_context(
                                                child,
                                                Some(&path_clone),
                                            );
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }

        // Recursively resolve children
        for child in &element.children {
            self.resolve_components_with_context(child, context_path);
        }
    }

    /// Recursively resolve components in an IRNode using the JS resolver
    fn resolve_ir_node_components(&mut self, node: &IRNode) {
        self.resolve_ir_node_components_with_context(node, None);
    }

    /// Recursively resolve components in an IRNode with path context
    fn resolve_ir_node_components_with_context(
        &mut self,
        node: &IRNode,
        context_path: Option<&str>,
    ) {
        match node {
            IRNode::Element(element) => {
                // Resolve the element itself
                self.resolve_components_with_context(element, context_path);
            }
            IRNode::ForEach { template, .. } => {
                // Resolve components in template children
                for child in template {
                    self.resolve_ir_node_components_with_context(child, context_path);
                }
            }
            IRNode::Conditional {
                branches, fallback, ..
            } => {
                // Resolve components in branches
                for branch in branches {
                    for child in &branch.children {
                        self.resolve_ir_node_components_with_context(child, context_path);
                    }
                }
                // Resolve components in fallback
                if let Some(fb) = fallback {
                    for child in fb {
                        self.resolve_ir_node_components_with_context(child, context_path);
                    }
                }
            }
        }
    }

    /// Emit patches to the callback
    fn emit_patches(&mut self, patches: Vec<Patch>) {
        // Index newly created node IDs for O(1) lookup in render_into.
        // node_id_str() uses a global monotonic counter, so we can reverse-map
        // efficiently by scanning only the tree nodes that correspond to new patches.
        for patch in &patches {
            if let Patch::Create { id, .. } = patch {
                if !self.node_id_index.contains_key(id) {
                    // node_id_str() is deterministic — scan tree for the matching NodeId
                    for (node_id, _) in self.tree.iter() {
                        if crate::reconcile::node_id_str(node_id) == *id {
                            self.node_id_index.insert(id.clone(), node_id);
                            break;
                        }
                    }
                }
            }
        }

        if let Some(ref callback) = self.patch_callback {
            // Use json_compatible() to ensure IndexMap props serialize as plain JS objects
            // instead of ES2015 Map objects (which appear as empty {} in JS)
            let serializer = serde_wasm_bindgen::Serializer::json_compatible();
            if let Ok(patches_js) = serde::Serialize::serialize(&patches, &serializer) {
                let _ = callback.call1(&JsValue::NULL, &patches_js);
            }
        }
    }

    /// Update state from JavaScript
    #[wasm_bindgen(js_name = updateState)]
    pub fn update_state(&mut self, state_patch: JsValue) -> Result<(), JsValue> {
        let patch: serde_json::Value = from_value(state_patch)
            .map_err(|e| JsValue::from_str(&format!("Invalid state patch: {}", e)))?;

        // Extract changed paths from the patch
        let changed_paths = super::ffi::extract_changed_paths(&patch);

        // Update module state
        if let Some(module) = &mut self.module {
            module.update_state(patch);
        }

        // Find affected nodes and mark them dirty
        let mut affected_nodes = Vec::new();
        for path in &changed_paths {
            affected_nodes.extend(self.dependencies.get_affected_nodes(path));
        }

        for &node_id in &affected_nodes {
            self.scheduler.mark_dirty(node_id);
        }

        // Render only dirty nodes (not the entire tree)
        self.render_dirty();

        Ok(())
    }

    /// Update state using sparse path-value pairs
    /// More efficient than updateState for large state objects when only a few paths changed
    ///
    /// # Arguments
    /// * `paths_js` - Array of changed paths (e.g., ["user.name", "count"])
    /// * `values_js` - Object mapping paths to their new values (e.g., { "user.name": "Bob", "count": 42 })
    #[wasm_bindgen(js_name = updateStateSparse)]
    pub fn update_state_sparse(
        &mut self,
        paths_js: JsValue,
        values_js: JsValue,
    ) -> Result<(), JsValue> {
        // Parse paths array
        let paths: Vec<String> = from_value(paths_js)
            .map_err(|e| JsValue::from_str(&format!("Invalid paths array: {}", e)))?;

        // Parse values object
        let values: serde_json::Value = from_value(values_js)
            .map_err(|e| JsValue::from_str(&format!("Invalid values object: {}", e)))?;

        // Update module state using sparse update
        if let Some(module) = &mut self.module {
            module.update_state_sparse(&paths, &values);
        }

        // Find affected nodes and mark them dirty
        let mut affected_nodes = Vec::new();
        for path in &paths {
            affected_nodes.extend(self.dependencies.get_affected_nodes(path));
        }

        for &node_id in &affected_nodes {
            self.scheduler.mark_dirty(node_id);
        }

        // Render only dirty nodes (not the entire tree)
        self.render_dirty();

        Ok(())
    }

    /// Render only dirty nodes (optimized for state changes)
    fn render_dirty(&mut self) {
        let patches = crate::render::render_dirty_nodes(
            &mut self.scheduler,
            &mut self.tree,
            self.module.as_ref(),
        );

        if !patches.is_empty() {
            self.emit_patches(patches);
            self.revision += 1;
        }
    }

    /// Dispatch an action
    #[wasm_bindgen(js_name = dispatchAction)]
    pub fn dispatch_action(&mut self, name: &str, payload: JsValue) -> Result<(), JsValue> {
        let payload: Option<serde_json::Value> = if payload.is_undefined() || payload.is_null() {
            None
        } else {
            Some(
                from_value(payload)
                    .map_err(|e| JsValue::from_str(&format!("Invalid action payload: {}", e)))?,
            )
        };

        // Call the registered handler if exists
        if let Some(handler) = self.action_handlers.get(name) {
            let action = Action::new(name).with_payload(payload.unwrap_or(serde_json::Value::Null));
            // Use json_compatible() for consistent serialization
            let serializer = serde_wasm_bindgen::Serializer::json_compatible();
            if let Ok(action_js) = serde::Serialize::serialize(&action, &serializer) {
                let _ = handler.call1(&JsValue::NULL, &action_js);
            }
        }

        Ok(())
    }

    /// Register an action handler
    #[wasm_bindgen(js_name = onAction)]
    pub fn on_action(&mut self, action_name: &str, handler: js_sys::Function) {
        self.action_handlers
            .insert(action_name.to_string(), handler);
    }

    /// Clear the engine tree (useful when switching samples)
    #[wasm_bindgen(js_name = clearTree)]
    pub fn clear_tree(&mut self) {
        self.tree.clear();
    }

    /// Debug method to inspect parsed components
    #[wasm_bindgen(js_name = debugParseComponent)]
    pub fn debug_parse_component(&self, source: &str) -> Result<String, JsValue> {
        let component = hypen_parser::parse_component(source)
            .map_err(|e| JsValue::from_str(&format!("Parse error: {}", format_parse_errors(&e))))?;

        let _element = crate::ir::expand::ast_to_ir(&component);

        let debug_info = format!(
            "Component: {} with {} applicators\nApplicators: {:?}",
            component.name,
            component.applicators.len(),
            component
                .applicators
                .iter()
                .map(|a| &a.name)
                .collect::<Vec<_>>()
        );

        Ok(debug_info)
    }

    /// Initialize a module
    #[wasm_bindgen(js_name = setModule)]
    pub fn set_module(
        &mut self,
        name: &str,
        actions: Vec<String>,
        state_keys: Vec<String>,
        initial_state: JsValue,
    ) -> Result<(), JsValue> {
        let state: serde_json::Value = from_value(initial_state)
            .map_err(|e| JsValue::from_str(&format!("Invalid initial state: {}", e)))?;

        let module = Module::new(name)
            .with_actions(actions)
            .with_state_keys(state_keys);

        let instance = ModuleInstance::new(module, state);
        self.module = Some(instance);

        Ok(())
    }

    /// Get the current revision number
    #[wasm_bindgen(js_name = getRevision)]
    pub fn get_revision(&self) -> u64 {
        self.revision
    }
}

/// Export patches as JSON string (for debugging)
#[wasm_bindgen(js_name = patchesToJson)]
pub fn patches_to_json(patches: JsValue) -> Result<String, JsValue> {
    let patches: Vec<Patch> =
        from_value(patches).map_err(|e| JsValue::from_str(&format!("Invalid patches: {}", e)))?;

    serde_json::to_string_pretty(&patches)
        .map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
}

/// Parse Hypen DSL and return AST as JSON
#[wasm_bindgen(js_name = parseToJson)]
pub fn parse_to_json(source: &str) -> Result<String, JsValue> {
    let component = hypen_parser::parse_component(source)
        .map_err(|e| JsValue::from_str(&format!("Parse error: {}", format_parse_errors(&e))))?;

    serde_json::to_string_pretty(&component)
        .map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
}

#[wasm_bindgen(start)]
pub fn main() {}
