//! Active type wrapper generation for WASM bindings.
//!
//! Active types (LWW, Yrs, etc.) need WASM wrappers because wasm-bindgen doesn't support
//! generics. This module handles:
//! - Wrapper struct generation (e.g., `LWWString`, `Connection_LWWRefSession`)
//! - Method generation with proper TypeScript types
//! - `Ref<T>` → `TRef` conversion for type-safe entity references

use proc_macro2::TokenStream;
use quote::{format_ident, quote};
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use syn::Type;

// --- Type string helpers for Ref<T> handling ---

/// Rewrite `Ref<Model>` → `<Model as Model>::RefWrapper` for proper TS typing.
fn convert_ref_to_wrapper(type_str: &str) -> String {
    Regex::new(r"Ref<(\w+)>")
        .unwrap()
        .replace_all(type_str, |caps: &regex::Captures| format!("<{} as ::ankurah::model::Model>::RefWrapper", &caps[1]))
        .to_string()
}

/// Check for `Option<Ref<T>>` pattern.
fn has_option_ref(type_str: &str) -> bool { Regex::new(r"Option<\s*Ref<\w+>").unwrap().is_match(type_str) }

/// Check for bare `Ref<T>` (not wrapped in Option).
fn has_bare_ref(type_str: &str) -> bool { type_str.contains("Ref<") && !has_option_ref(type_str) }

/// Extract model name from `Ref<Model>` (e.g., `"Ref<User>"` → `Some("User")`).
fn extract_ref_model_name(type_str: &str) -> Option<String> {
    Regex::new(r"Ref<(\w+)>").unwrap().captures(type_str).map(|caps| caps[1].to_string())
}

/// Backend configuration for code generation
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BackendConfig {
    pub backend_name: String,
    pub namespace: String,
    pub provided_wrapper_types: Vec<String>,
    pub substitutions: HashMap<String, HashMap<String, String>>,
    pub values: Vec<ValueConfig>,
}

/// Configuration for a specific value type within a backend
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ValueConfig {
    pub type_pattern: String,
    pub fully_qualified_type: String,
    pub accepts: String,
    pub generic_params: Vec<String>,
    pub materialized_pattern: String,
    pub methods: Vec<Method>,
}

/// Method configuration for code generation
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Method {
    pub name: String,
    pub args: Vec<(String, String)>, // (name, type)
    pub return_type: String,
    #[serde(default = "default_wasm_true")]
    pub wasm: bool,
}

fn default_wasm_true() -> bool { true }

/// Describes how to generate wrappers for a specific active field type
pub struct ActiveTypeDesc {
    pub(crate) backend_config: BackendConfig,
    pub(crate) value_config: ValueConfig,               // The specific ValueConfig that matched
    pub(crate) concrete_types: HashMap<String, String>, // {T} -> Complex, etc.
}

impl ActiveTypeDesc {
    /// Create an ActiveTypeDesc from config, value config, and concrete types
    pub fn new(backend_config: BackendConfig, value_config: ValueConfig, concrete_types: HashMap<String, String>) -> Self {
        Self { backend_config, value_config, concrete_types }
    }

    /// Get the wrapper type name for WASM bindings (just the identifier)
    pub fn wrapper_type_name(&self) -> String {
        let mut pattern = self.value_config.materialized_pattern.clone();

        // Substitute all generic parameters with their concrete types
        for param in &self.value_config.generic_params {
            if let Some(concrete_type) = self.concrete_types.get(param) {
                let placeholder = format!("{{{}}}", param);
                // Strip non-alphanumeric characters from each concrete type
                let sanitized_type: String = concrete_type.chars().filter(|c| c.is_alphanumeric()).collect();
                pattern = pattern.replace(&placeholder, &sanitized_type);
            }
        }

        pattern
    }

    /// Get a model-scoped wrapper type name for WASM bindings.
    /// This includes the model name to avoid collisions when multiple models use
    /// the same custom type (e.g., Ref<Artist> in both Album and Track).
    /// Format: {ModelName}_{BaseWrapperName} e.g., "Album_LWWRefArtist"
    pub fn wrapper_type_name_for_model(&self, model_name: &str) -> String { format!("{}_{}", model_name, self.wrapper_type_name()) }

    /// Get the fully qualified wrapper type path for WASM bindings
    pub fn wrapper_type_path(&self, context: &str) -> String {
        let wrapper_name = self.wrapper_type_name();

        match context {
            "local" => {
                // For provided types generated by impl_provided_wrapper_types!()
                format!("{}::wasm::{}", self.backend_config.namespace, wrapper_name)
            }
            "external" => {
                // For custom types generated by impl_wrapper_type!() in current module
                wrapper_name
            }
            _ => wrapper_name,
        }
    }

    /// Determine if this type is provided by the backend or custom
    pub fn is_provided_type(&self) -> bool {
        // Check if any of the concrete types are in the provided_wrapper_types list
        for concrete_type in self.concrete_types.values() {
            if self.backend_config.provided_wrapper_types.contains(concrete_type) {
                return true;
            }
        }
        false
    }

    /// Get the appropriate fully qualified wrapper type path (used in tests)
    #[allow(unused)]
    pub fn get_wrapper_type_path(&self) -> String {
        if self.is_provided_type() {
            self.wrapper_type_path("local")
        } else {
            self.wrapper_type_path("external")
        }
    }

    /// Get the wrapper type for WASM bindings
    #[allow(unused)]
    pub fn wrapper_type(&self) -> syn::Result<syn::Type> {
        let wrapper_name = self.wrapper_type_name();
        syn::parse_str(&wrapper_name)
    }

    #[allow(unused)]
    pub fn rust_type_name(&self) -> String {
        let rust_type = self.rust_type().unwrap();
        quote!(#rust_type).to_string()
    }

    /// Get the fully qualified Rust type for accessor methods
    pub fn rust_type(&self) -> syn::Result<syn::Type> { self.rust_type_with_context("external") }

    /// Get the fully qualified Rust type for accessor methods with explicit context
    pub fn rust_type_with_context(&self, context: &str) -> syn::Result<syn::Type> {
        let mut type_str = self.value_config.fully_qualified_type.clone();

        // Apply context-specific substitutions first
        if let Some(context_substitutions) = self.backend_config.substitutions.get(context) {
            for (token, replacement) in context_substitutions {
                let placeholder = format!("{{{}}}", token);
                type_str = type_str.replace(&placeholder, replacement);
            }
        }

        // Substitute {T} with actual concrete types
        for (param, concrete_type) in &self.concrete_types {
            let placeholder = format!("{{{}}}", param);
            type_str = type_str.replace(&placeholder, concrete_type);
        }

        syn::parse_str(&type_str)
    }

    /// WASM methods for this active type wrapper.
    ///
    /// - `get()` → Projected value (e.g., `SessionRef` for `LWW<Ref<Session>>`)
    /// - `set(value)` → Accepts `TRef | TView` via duck typing on `.id` property
    ///
    /// Returns `(methods, ts_override)` - ts_override provides union type for set().
    fn wasm_methods(&self, context: &str) -> (Vec<TokenStream>, Option<String>) {
        let value_config = &self.backend_config.values[0];
        let substitute_fn = |backend: &ActiveTypeDesc, pattern: &str| backend.do_substitutions(pattern, context);

        let mut ts_set_override: Option<String> = None;

        let methods = value_config
            .methods
            .iter()
            .filter(|method| method.wasm)
            .map(|method| {
                let method_name = format_ident!("{}", method.name);

                // Check if any arg is a Ref<T> type for set method
                let ref_model_for_set: Option<String> = if method.name == "set" {
                    method.args.iter().find_map(|(_, arg_type)| {
                        let substituted = substitute_fn(self, arg_type);
                        if has_bare_ref(&substituted) {
                            extract_ref_model_name(&substituted)
                        } else {
                            None
                        }
                    })
                } else {
                    None
                };

                // For set with Ref<T>, accept JsValue and extract EntityId via duck typing
                // Accepts: ModelRef, ModelView (have .id), EntityId, or base64 string
                if let Some(ref model_name) = ref_model_for_set {
                    let model_ident = format_ident!("{}", model_name);
                    ts_set_override = Some(format!("{}Ref | {}View | string", model_name, model_name));

                    return quote! {
                        #[wasm_bindgen(skip_typescript)]
                        pub fn set(&self, value: JsValue) -> Result<(), JsValue> {
                            // Try multiple conversion strategies:
                            // 1. String -> parse as base64
                            // 2. EntityId -> use directly
                            // 3. Object with .id -> extract EntityId
                            let id: ::ankurah::proto::EntityId = if let Some(s) = value.as_string() {
                                ::ankurah::proto::EntityId::from_base64(&s)
                                    .map_err(|e| JsValue::from_str(&format!("Invalid EntityId string: {}", e)))?
                            } else if let Ok(id) = value.clone().try_into() {
                                id
                            } else {
                                let id_value = ::ankurah::derive_deps::js_sys::Reflect::get(&value, &JsValue::from_str("id"))
                                    .map_err(|_| JsValue::from_str("Expected string, EntityId, or object with 'id' property"))?;
                                id_value.try_into()
                                    .map_err(|_| JsValue::from_str("'id' property is not an EntityId"))?
                            };

                            let r: ::ankurah::property::Ref<#model_ident> = ::ankurah::property::Ref::new(id);
                            self.0.set(&r).map_err(|e| JsValue::from(e.to_string()))
                        }
                    };
                }

                // Regular method handling
                let args: Vec<TokenStream> = method
                    .args
                    .iter()
                    .map(|(arg_name, arg_type)| {
                        let arg_name = format_ident!("{}", arg_name);
                        let substituted = substitute_fn(self, arg_type);
                        let arg_type_str = convert_ref_to_wrapper(&substituted);
                        let arg_type: Type = syn::parse_str(&arg_type_str).expect("Failed to parse arg type");
                        quote! { #arg_name: #arg_type }
                    })
                    .collect();

                let original_return_type_str = substitute_fn(self, &method.return_type);
                let return_has_ref = original_return_type_str.contains("Ref<");
                let return_has_option_ref = has_option_ref(&original_return_type_str);
                let return_type_str = convert_ref_to_wrapper(&original_return_type_str);

                let arg_names: Vec<syn::Ident> = method.args.iter().map(|(name, _)| format_ident!("{}", name)).collect();
                let converted_args: Vec<TokenStream> = method
                    .args
                    .iter()
                    .zip(arg_names.iter())
                    .map(|((_, arg_type), arg_name)| {
                        let substituted = substitute_fn(self, arg_type);
                        let is_ref_arg = has_bare_ref(&substituted);
                        let is_option_ref_arg = has_option_ref(&substituted);
                        if arg_type == "{T}" && method.name == "set" {
                            if is_option_ref_arg {
                                quote! { &#arg_name.map(Into::into) }
                            } else if is_ref_arg {
                                quote! { &(#arg_name.into()) }
                            } else {
                                quote! { &#arg_name }
                            }
                        } else if is_option_ref_arg {
                            quote! { #arg_name.map(Into::into) }
                        } else if is_ref_arg {
                            quote! { #arg_name.into() }
                        } else {
                            quote! { #arg_name }
                        }
                    })
                    .collect();

                let (wasm_return_type_str, method_call) = if return_type_str.starts_with("Result<") {
                    let inner = return_type_str.strip_prefix("Result<").unwrap().strip_suffix(">").unwrap();
                    let ok_type = inner.split(", ").next().unwrap();
                    let call = if return_has_option_ref {
                        quote! { self.0.#method_name(#(#converted_args),*).map(|opt| opt.map(Into::into)).map_err(|e| ::wasm_bindgen::JsValue::from(e.to_string())) }
                    } else if return_has_ref {
                        quote! { self.0.#method_name(#(#converted_args),*).map(Into::into).map_err(|e| ::wasm_bindgen::JsValue::from(e.to_string())) }
                    } else {
                        quote! { self.0.#method_name(#(#converted_args),*).map_err(|e| ::wasm_bindgen::JsValue::from(e.to_string())) }
                    };
                    (format!("Result<{}, ::wasm_bindgen::JsValue>", ok_type), call)
                } else if return_has_option_ref {
                    (return_type_str.clone(), quote! { self.0.#method_name(#(#converted_args),*).map(Into::into) })
                } else if return_has_ref {
                    (return_type_str.clone(), quote! { self.0.#method_name(#(#converted_args),*).into() })
                } else {
                    (return_type_str.clone(), quote! { self.0.#method_name(#(#converted_args),*) })
                };

                let wasm_return_type: Type = syn::parse_str(&wasm_return_type_str).expect("Failed to parse return type");
                quote! {
                    pub fn #method_name(&self, #(#args),*) -> #wasm_return_type {
                        #method_call
                    }
                }
            })
            .collect();

        (methods, ts_set_override)
    }

    /// WASM wrapper struct for this active type.
    ///
    /// - `model_scope: None` → Global (e.g., `LWWString`)
    /// - `model_scope: Some("Connection")` → Scoped (e.g., `Connection_LWWRefSession`)
    pub fn wasm_wrapper(&self, context: &str, model_scope: Option<&str>) -> TokenStream {
        let wrapper_name_str = match model_scope {
            Some(model) => self.wrapper_type_name_for_model(model),
            None => self.wrapper_type_name(),
        };
        let wrapper_name = format_ident!("{}", wrapper_name_str);
        let original_type: Type = self.rust_type_with_context(context).expect("Failed to parse original type");
        let (methods, ts_set_override) = self.wasm_methods(context);

        // Generate TypeScript custom section for union type if needed
        let ts_custom_section = if let Some(ref union_type) = ts_set_override {
            let ts_decl = format!("interface {} {{ set(value: {}): void; }}", wrapper_name_str, union_type);
            quote! {
                #[wasm_bindgen(typescript_custom_section)]
                const _: &'static str = #ts_decl;
            }
        } else {
            quote! {}
        };

        quote! {
            #[wasm_bindgen]
            pub struct #wrapper_name( #[wasm_bindgen(skip)] pub #original_type);

            #[wasm_bindgen]
            impl #wrapper_name {
                #(#methods)*
            }

            #ts_custom_section
        }
    }

    /// Substitute generic parameters in type/method signatures
    fn substitute_generics(&self, pattern: &str) -> String {
        let mut result = pattern.to_string();
        for (param, concrete_type) in &self.concrete_types {
            let placeholder = format!("{{{}}}", param);
            result = result.replace(&placeholder, concrete_type);
        }
        result
    }

    /// Substitute generic parameters and context-specific tokens
    fn do_substitutions(&self, pattern: &str, context: &str) -> String {
        let mut result = self.substitute_generics(pattern);

        // Apply context-specific substitutions
        if let Some(context_substitutions) = self.backend_config.substitutions.get(context) {
            for (token, replacement) in context_substitutions {
                let placeholder = format!("{{{}}}", token);
                result = result.replace(&placeholder, replacement);
            }
        }

        result
    }
}

#[cfg(test)]
mod tests {
    use maplit::hashmap;

    use super::*;

    #[test]
    fn test_materialized_name_simple() {
        let backend_config = BackendConfig {
            backend_name: "LWW".to_string(),
            namespace: "test".to_string(),
            provided_wrapper_types: vec![],
            substitutions: HashMap::new(),
            values: vec![],
        };

        let value_config = ValueConfig {
            type_pattern: "LWW(?:<(.+)>)?".to_string(),
            fully_qualified_type: "LWW<{T}>".to_string(),
            accepts: ".*".to_string(),
            generic_params: vec!["T".to_string()],
            materialized_pattern: "LWW{T}".to_string(),
            methods: vec![],
        };

        let desc = ActiveTypeDesc::new(backend_config.clone(), value_config.clone(), hashmap! { "T".into() => "String".into() });
        assert_eq!(desc.wrapper_type_name(), "LWWString");

        let desc =
            ActiveTypeDesc::new(backend_config.clone(), value_config.clone(), hashmap! { "T".into() => "HashMap<String, i32>".into() });
        assert_eq!(desc.wrapper_type_name(), "LWWHashMapStringi32");
    }
}
