//! Plexus RPC Method Macro
//!
//! Proc macro for defining Plexus RPC methods where the function signature IS the schema.
//!
//! # Example
//!
//! ```ignore
//! use hub_macro::{hub_methods, hub_method};
//!
//! #[hub_methods(namespace = "bash", version = "1.0.0")]
//! impl Bash {
//!     /// Execute a bash command
//!     #[hub_method]
//!     async fn execute(&self, command: String) -> impl Stream<Item = BashEvent> {
//!         // implementation
//!     }
//! }
//! ```
//!
//! The macro extracts:
//! - Method name from function name
//! - Description from doc comments
//! - Input schema from parameter types
//! - Return type schema from Stream Item type

mod codegen;
mod handle_enum;
mod parse;
mod request;
mod stream_event;


use codegen::generate_all;
use parse::HubMethodsAttrs;
use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::{format_ident, quote};
use syn::{
    parse::{Parse, ParseStream},
    parse_macro_input, punctuated::Punctuated, Expr, ExprLit, FnArg, ItemFn, ItemImpl, Lit, Meta,
    MetaNameValue, Pat, ReturnType, Token, Type,
};

/// Parsed attributes for hub_method (standalone version)
struct HubMethodAttrs {
    name: Option<String>,
    /// Explicit description from `description = "..."`. When `Some`, this wins over
    /// `///` doc comments. When `None`, doc comments are used as the default.
    description: Option<String>,
    /// Base crate path for imports (default: "crate")
    crate_path: String,
}

impl Parse for HubMethodAttrs {
    fn parse(input: ParseStream) -> syn::Result<Self> {
        let mut name = None;
        let mut description: Option<String> = None;
        let mut crate_path = "crate".to_string();

        if !input.is_empty() {
            let metas = Punctuated::<Meta, Token![,]>::parse_terminated(input)?;

            for meta in metas {
                if let Meta::NameValue(MetaNameValue { path, value, .. }) = meta {
                    if path.is_ident("name") {
                        if let Expr::Lit(ExprLit {
                            lit: Lit::Str(s), ..
                        }) = value
                        {
                            name = Some(s.value());
                        }
                    } else if path.is_ident("description") {
                        if let Expr::Lit(ExprLit {
                            lit: Lit::Str(s), ..
                        }) = value
                        {
                            description = Some(s.value());
                        }
                    } else if path.is_ident("crate_path") {
                        if let Expr::Lit(ExprLit {
                            lit: Lit::Str(s), ..
                        }) = value
                        {
                            crate_path = s.value();
                        }
                    }
                }
            }
        }

        Ok(HubMethodAttrs { name, description, crate_path })
    }
}

/// Attribute macro for individual methods within a `#[plexus::activation]` impl block.
///
/// # Example
///
/// ```ignore
/// #[plexus::activation(namespace = "bash")]
/// impl Bash {
///     /// Execute a bash command
///     #[plexus::method]
///     async fn execute(&self, command: String) -> impl Stream<Item = BashEvent> {
///         // ...
///     }
/// }
/// ```
#[proc_macro_attribute]
pub fn method(attr: TokenStream, item: TokenStream) -> TokenStream {
    let args = parse_macro_input!(attr as HubMethodAttrs);
    let input_fn = parse_macro_input!(item as ItemFn);

    match hub_method_impl(args, input_fn) {
        Ok(tokens) => tokens.into(),
        Err(e) => e.to_compile_error().into(),
    }
}

/// Deprecated: use `plexus::method` instead.
#[deprecated(since = "0.5.0", note = "Use `plexus::method` instead")]
#[proc_macro_attribute]
pub fn hub_method(attr: TokenStream, item: TokenStream) -> TokenStream {
    method(attr, item)
}

/// Attribute macro for individual **child** methods within a
/// `#[plexus_macros::activation]` impl block.
///
/// Two method shapes are accepted:
///
/// | Shape | Role | Example |
/// |---|---|---|
/// | `fn NAME(&self) -> Child` (no args) | Static child — routing name is the method name | `fn mercury(&self) -> Mercury` |
/// | `fn NAME(&self, name: &str) -> Option<Child>` (sync or async) | Dynamic fallback dispatcher | `fn planet(&self, name: &str) -> Option<Planet>` |
///
/// The enclosing `#[plexus_macros::activation]` macro collects all `#[child]`
/// methods and generates a `ChildRouter` impl whose `get_child(name)`
/// dispatches over them. `#[child]` methods are **not** exposed as Plexus RPC
/// methods — they contribute only to `ChildRouter` routing.
///
/// # Example
///
/// ```ignore
/// #[plexus_macros::activation(namespace = "solar")]
/// impl Solar {
///     #[plexus_macros::child]
///     fn mercury(&self) -> Mercury { self.mercury.clone() }
///
///     #[plexus_macros::child]
///     async fn planet(&self, name: &str) -> Option<Planet> {
///         self.lookup_planet(name).await
///     }
/// }
/// ```
///
/// Used standalone (outside an `#[activation]` block) this attribute is a
/// no-op that returns the function unchanged.
#[proc_macro_attribute]
pub fn child(_attr: TokenStream, item: TokenStream) -> TokenStream {
    // Outside of #[plexus_macros::activation] this attribute does nothing;
    // the activation macro strips it and collects the method into the
    // generated ChildRouter impl.
    item
}

/// Companion attribute to Rust's built-in `#[deprecated]` for carrying a
/// `removed_in = "VERSION"` hint that rustc doesn't recognize.
///
/// Rust's `#[deprecated(since = "X", note = "Y")]` supports only `since` and
/// `note`. This companion attribute supplies the removal version that the
/// `plexus-macros` codegen folds into the method's `DeprecationInfo.removed_in`
/// field on the emitted `MethodSchema`.
///
/// # Example
///
/// ```ignore
/// #[deprecated(since = "0.5", note = "use `new_method` instead")]
/// #[plexus_macros::removed_in("0.6")]
/// #[plexus_macros::method]
/// async fn legacy(&self) -> impl Stream<Item = String> + Send + 'static {
///     // ...
/// }
/// ```
///
/// # Requirements
///
/// `#[plexus_macros::removed_in]` is only meaningful when paired with
/// `#[deprecated]`. The `#[plexus_macros::activation]` macro emits a compile
/// error when it encounters `#[removed_in]` on a method that doesn't also
/// carry `#[deprecated]`.
///
/// As a standalone attribute (outside an `#[activation]` block) this is a
/// no-op that returns the item unchanged — the activation macro parses and
/// strips it during codegen.
#[proc_macro_attribute]
pub fn removed_in(attr: TokenStream, item: TokenStream) -> TokenStream {
    // When `#[plexus_macros::removed_in("X")]` is placed OUTSIDE
    // `#[plexus_macros::activation(...)]` on the same impl block, the
    // `removed_in` proc macro runs BEFORE the activation macro (outer
    // attrs expand first), so a naive no-op implementation would consume
    // the attribute before the activation macro ever sees it.
    //
    // Workaround: emit a synthetic `#[doc(hidden)]` marker attribute on
    // the item's attr list that encodes the removed_in value. The
    // activation / method codegen scans for this marker as an equivalent
    // signal to the companion attribute and uses it to populate
    // `DeprecationInfo.removed_in`.
    //
    // Stable rustc treats `#[doc(hidden)]` as a regular doc attribute and
    // the sentinel payload rides along in the same literal string.
    let attr_str: String = match syn::parse::<syn::LitStr>(attr.clone()) {
        Ok(s) => s.value(),
        Err(_) => {
            // Not a bare string literal — just let the item pass through
            // and let the activation/method layer produce the proper error.
            return item;
        }
    };
    let marker = format!("__plexus_removed_in:{}", attr_str);
    let item_ts: proc_macro2::TokenStream = item.into();
    let marker_lit = proc_macro2::Literal::string(&marker);
    let out = quote::quote! {
        #[doc(hidden)]
        #[doc = #marker_lit]
        #item_ts
    };
    out.into()
}

fn hub_method_impl(args: HubMethodAttrs, input_fn: ItemFn) -> syn::Result<TokenStream2> {
    // Extract method name (from attr or function name)
    let method_name = args
        .name
        .unwrap_or_else(|| input_fn.sig.ident.to_string());

    // Resolve description: explicit `description = "..."` wins over `///` doc
    // comments; if neither is present, description is the empty string.
    let description = match args.description {
        Some(explicit) => explicit,
        None => extract_doc_comment(&input_fn),
    };

    // Extract input type from first parameter (if any)
    let input_type = extract_input_type(&input_fn)?;

    // Extract return type
    let return_type = extract_return_type(&input_fn)?;

    // Function name for the schema function
    let fn_name = &input_fn.sig.ident;
    let schema_fn_name = format_ident!("{}_schema", fn_name);

    // Parse crate path
    let crate_path: syn::Path = syn::parse_str(&args.crate_path)
        .map_err(|e| syn::Error::new_spanned(&input_fn.sig, format!("Invalid crate_path: {}", e)))?;

    // Generate the schema function
    let schema_fn = generate_schema_fn(
        &schema_fn_name,
        &method_name,
        &description,
        input_type.as_ref(),
        &return_type,
        &crate_path,
    );

    // Return the original function plus the schema function
    Ok(quote! {
        #input_fn

        #schema_fn
    })
}

fn extract_doc_comment(input_fn: &ItemFn) -> String {
    // Delegate to the shared helper so the standalone `#[method]` macro path
    // follows the same doc-comment extraction rule as the in-activation path:
    // lines joined with '\n', common leading whitespace stripped.
    parse::extract_doc_description(&input_fn.attrs).unwrap_or_default()
}

fn extract_input_type(input_fn: &ItemFn) -> syn::Result<Option<Type>> {
    // Skip self parameter, get first real parameter
    for arg in &input_fn.sig.inputs {
        match arg {
            FnArg::Receiver(_) => continue, // Skip &self
            FnArg::Typed(pat_type) => {
                // Skip context-like parameters
                if let Pat::Ident(ident) = &*pat_type.pat {
                    let name = ident.ident.to_string();
                    if name == "ctx" || name == "context" || name == "self_" {
                        continue;
                    }
                }
                return Ok(Some((*pat_type.ty).clone()));
            }
        }
    }

    Ok(None)
}

fn extract_return_type(input_fn: &ItemFn) -> syn::Result<Type> {
    match &input_fn.sig.output {
        ReturnType::Default => Err(syn::Error::new_spanned(
            &input_fn.sig,
            "hub_method requires a return type",
        )),
        ReturnType::Type(_, ty) => Ok((*ty.clone()).clone()),
    }
}

fn generate_schema_fn(
    fn_name: &syn::Ident,
    method_name: &str,
    description: &str,
    input_type: Option<&Type>,
    return_type: &Type,
    crate_path: &syn::Path,
) -> TokenStream2 {
    let input_schema = if let Some(input_ty) = input_type {
        quote! {
            Some(serde_json::to_value(schemars::schema_for!(#input_ty)).unwrap())
        }
    } else {
        quote! { None }
    };

    let _ = return_type; // Will be used for protocol schema in future
    let _ = crate_path;

    quote! {
        /// Generated schema function for this hub method
        #[allow(dead_code)]
        pub fn #fn_name() -> serde_json::Value {
            serde_json::json!({
                "name": #method_name,
                "description": #description,
                "input": #input_schema,
            })
        }
    }
}

/// Attribute macro for impl blocks defining a Plexus activation.
///
/// Generates:
/// - Method enum for schema extraction
/// - Activation trait implementation
/// - RPC server trait and implementation
///
/// # Attributes
///
/// - `namespace = "..."` (required) - The activation namespace
/// - `version = "..."` (optional, default: "1.0.0") - Version string
/// - `description = "..."` (optional) - Activation description
/// - `crate_path = "..."` (optional, default: "crate") - Path to substrate crate
///
/// # Example
///
/// ```ignore
/// #[plexus::activation(namespace = "bash", version = "1.0.0", description = "Execute bash commands")]
/// impl Bash {
///     /// Execute a bash command and stream output
///     #[plexus::method]
///     async fn execute(&self, command: String) -> impl Stream<Item = BashEvent> + Send + 'static {
///         self.executor.execute(&command).await
///     }
/// }
/// ```
#[proc_macro_attribute]
pub fn activation(attr: TokenStream, item: TokenStream) -> TokenStream {
    let args = parse_macro_input!(attr as HubMethodsAttrs);
    let input_impl = parse_macro_input!(item as ItemImpl);

    match generate_all(args, input_impl) {
        Ok(tokens) => tokens.into(),
        Err(e) => e.to_compile_error().into(),
    }
}

/// Deprecated: use `plexus::activation` instead.
#[deprecated(since = "0.5.0", note = "Use `plexus::activation` instead")]
#[proc_macro_attribute]
pub fn hub_methods(attr: TokenStream, item: TokenStream) -> TokenStream {
    activation(attr, item)
}

/// **DEPRECATED**: This derive macro is no longer needed.
///
/// With the caller-wraps streaming architecture, event types no longer need to
/// implement `ActivationStreamItem`. Just use plain domain types with standard
/// derives:
///
/// ```ignore
/// #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
/// #[serde(tag = "event", rename_all = "snake_case")]
/// pub enum MyEvent {
///     Data { value: String },
///     Complete { result: i32 },
/// }
/// ```
///
/// The wrapping happens at the call site via `wrap_stream()`.
#[deprecated(
    since = "0.2.0",
    note = "No longer needed - use plain domain types with Serialize/Deserialize"
)]
#[proc_macro_derive(StreamEvent, attributes(stream_event, terminal))]
pub fn stream_event_derive(input: TokenStream) -> TokenStream {
    stream_event::derive(input)
}

/// Derive macro for type-safe handle creation and parsing.
///
/// Generates:
/// - `to_handle(&self) -> Handle` - converts enum variant to Handle
/// - `impl TryFrom<&Handle>` - parses Handle back to enum variant
/// - `impl From<EnumName> for Handle` - convenience conversion
///
/// # Attributes
///
/// Enum-level (required):
/// - `plugin_id = "CONSTANT_NAME"` - Name of the constant holding the plugin UUID
/// - `version = "1.0.0"` - Semantic version for handles
/// - `crate_path = "..."` (optional, default: "plexus_core") - Path to plexus_core crate
/// - `plugin_id_type = "Type<Args>"` (optional) - Concrete type whose associated
///   constant `plugin_id` names. Use this when the owning activation is generic
///   (e.g. `Cone<P: HubContext = NoParent>`) to pin the instantiation and avoid
///   E0283 "cannot infer type" errors. Pairs with `plugin_id` — e.g.
///   `plugin_id = "Cone::PLUGIN_ID", plugin_id_type = "Cone<NoParent>"` emits
///   `<Cone<NoParent>>::PLUGIN_ID`.
///
/// Variant-level:
/// - `method = "..."` (required) - The handle.method value
/// - `table = "..."` (optional) - SQLite table name (for future resolution)
/// - `key = "..."` (optional) - Primary key column (for future resolution)
///
/// # Example
///
/// ```ignore
/// use hub_macro::HandleEnum;
/// use uuid::Uuid;
///
/// pub const MY_PLUGIN_ID: Uuid = uuid::uuid!("550e8400-e29b-41d4-a716-446655440000");
///
/// #[derive(HandleEnum)]
/// #[handle(plugin_id = "MY_PLUGIN_ID", version = "1.0.0")]
/// pub enum MyPluginHandle {
///     #[handle(method = "event", table = "events", key = "id")]
///     Event { event_id: String },
///
///     #[handle(method = "message")]
///     Message { message_id: String, role: String },
/// }
///
/// // Usage:
/// let handle_enum = MyPluginHandle::Event { event_id: "evt-123".into() };
/// let handle: Handle = handle_enum.to_handle();
/// // handle.method == "event"
/// // handle.meta == ["evt-123"]
///
/// // Parsing back:
/// let parsed = MyPluginHandle::try_from(&handle)?;
/// ```
#[proc_macro_derive(HandleEnum, attributes(handle))]
pub fn handle_enum_derive(input: TokenStream) -> TokenStream {
    handle_enum::derive(input)
}

/// Derive macro for typed HTTP/WebSocket request extraction.
///
/// Generates `impl PlexusRequest` (extraction) and `impl schemars::JsonSchema`
/// (with `x-plexus-source` extensions) for the annotated struct.
///
/// # Field annotations
///
/// - `#[from_cookie("name")]` — extract from named cookie
/// - `#[from_header("name")]` — extract from named HTTP header
/// - `#[from_query("name")]` — extract from named URI query parameter
/// - `#[from_peer]` — copy `ctx.peer` (peer socket address)
/// - `#[from_auth_context]` — copy `ctx.auth` (auth context)
/// - (no annotation) — `Default::default()`
///
/// # Example
///
/// ```ignore
/// use plexus_macros::PlexusRequest;
///
/// #[derive(PlexusRequest)]
/// struct MyRequest {
///     #[from_cookie("access_token")]
///     auth_token: String,
///
///     #[from_header("origin")]
///     origin: Option<String>,
///
///     #[from_peer]
///     peer_addr: Option<std::net::SocketAddr>,
/// }
/// ```
#[proc_macro_derive(
    PlexusRequest,
    attributes(from_cookie, from_header, from_query, from_peer, from_auth_context)
)]
pub fn plexus_request_derive(input: TokenStream) -> TokenStream {
    request::derive(input)
}

/// No-op `JsonSchema` derive — used when plexus-macros is aliased as `schemars`
/// in dev-dependencies to prevent duplicate `impl JsonSchema` conflicts with
/// `#[derive(PlexusRequest)]` (which generates its own `impl JsonSchema`).
///
/// When plexus-macros is aliased as schemars in dev-deps:
/// ```toml
/// [dev-dependencies]
/// schemars = { path = "../plexus-macros", package = "plexus-macros", features = ["schemars-compat"] }
/// ```
/// then `#[derive(schemars::JsonSchema)]` invokes THIS derive instead of the real schemars derive,
/// producing no output, so only `PlexusRequest`'s `impl JsonSchema` exists.
#[cfg(feature = "schemars-compat")]
#[proc_macro_derive(JsonSchema, attributes(schemars, serde))]
pub fn json_schema_noop_derive(_input: TokenStream) -> TokenStream {
    // Intentionally produces no output.
    // PlexusRequest already generates impl JsonSchema with x-plexus-source extensions.
    TokenStream::new()
}
