// Copyright (c) Subzero Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

//! # Rialo CLI Representable Derive Macro
//!
//! This crate provides a procedural macro for deriving the `Representable` and `HumanReadable` traits
//! for structs in the Rialo CLI system.
//!
//! ## Usage
//!
//! To use this macro, add the following to your struct:
//!
//! ```text
//! #[derive(Representable)]
//! #[representable(human_readable = "my_human_readable_fn")]
//! struct MyStruct {
//!     pub field1: String,
//!     pub field2: u64,
//! }
//!
//! fn my_human_readable_fn(data: &MyStruct) -> String {
//!     format!("Field1: {}, Field2: {}", data.field1, data.field2)
//! }
//! ```
//!
//! ## Attributes
//!
//! The `#[representable]` attribute supports the following options:
//!
//! - `human_readable = "function_name"`: Specifies a custom function to use for human-readable output.
//!   The function should take a reference to the struct and return a `String`.
//!
//! If no `human_readable` function is specified, the macro defaults to using `serde_json::to_string()`.
//!
//! ## Generated Code
//!
//! The macro generates implementations for:
//!
//! - `Representable`: A marker trait for CLI-representable types
//! - `HumanReadable`: Provides a `human_readable()` method that returns a user-friendly string representation
//!
//! ## Example with Custom Display Function
//!
//! ```text
//! #[derive(serde::Serialize, Clone, Representable)]
//! #[representable(human_readable = "balance_display")]
//! pub struct BalanceResult {
//!     pub amount: f64,
//!     pub currency: String,
//! }
//!
//! fn balance_display(result: &BalanceResult) -> String {
//!     format!("Balance: {} {}", result.amount, result.currency)
//! }
//! ```

use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput, Error, Ident, Meta};

/// Derives the `Representable` and `HumanReadable` traits for a struct.
///
/// This macro automatically implements:
/// - `Representable`: A marker trait indicating the type can be represented in CLI output
/// - `HumanReadable`: Provides a `human_readable()` method for user-friendly string representation
///
/// # Attributes
///
/// ## `#[representable(human_readable = "function_name")]`
///
/// Specifies a custom function to use for human-readable output. The function should:
/// - Take a reference to the struct (`&Self`)
/// - Return a `String`
/// - Be defined in the same module as the struct
///
/// # Examples
///
/// With a custom human-readable function:
///
/// ```text
/// #[derive(Representable)]
/// #[representable(human_readable = "custom_display")]
/// struct MyData {
///     value: u64,
/// }
///
/// fn custom_display(data: &MyData) -> String {
///     format!("Value: {}", data.value)
/// }
/// ```
///
/// Without specifying a function (defaults to JSON):
///
/// ```text
/// #[derive(Representable)]
/// struct MyData {
///     value: u64,
/// }
/// // Will use serde_json::to_string(self) by default
/// ```
#[proc_macro_derive(Representable, attributes(representable))]
pub fn derive_representable(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);
    let name = input.ident;

    match parse_human_readable_fn(&input.attrs, &name) {
        Ok(human_readable_fn) => {
            let expanded = quote! {
                impl ::rialo_cli_representation::HumanReadable for #name {
                    fn human_readable(&self) -> String {
                        #human_readable_fn
                    }
                }

                impl ::rialo_cli_representation::Representable for #name {}
            };

            TokenStream::from(expanded)
        }
        Err(err) => {
            let error = err.to_compile_error();
            TokenStream::from(error)
        }
    }
}

/// Validates a function name to ensure it's safe for code generation.
///
/// This function performs several security checks:
/// - Ensures the function name is a valid Rust identifier
/// - Prevents path traversal attempts
/// - Checks for potentially dangerous characters
/// - Validates length constraints
/// - Checks against reserved keywords using syn's built-in detection
///
/// # Arguments
///
/// * `function_name` - The function name to validate
/// * `span` - The span for error reporting
///
/// # Returns
///
/// Returns `Ok(())` if the function name is valid, or an error with details.
fn validate_function_name(function_name: &str, span: proc_macro2::Span) -> Result<(), Error> {
    // Check for empty or whitespace-only names
    if function_name.trim().is_empty() {
        return Err(Error::new(
            span,
            "Function name cannot be empty or whitespace-only",
        ));
    }

    // Check length constraints (reasonable limits for function names)
    if function_name.len() > 100 {
        return Err(Error::new(
            span,
            "Function name is too long (maximum 100 characters)",
        ));
    }

    // Check for path separators and other dangerous characters
    let dangerous_chars = ['/', '\\', ':', '*', '?', '"', '<', '>', '|', '\0'];
    if let Some(ch) = function_name
        .chars()
        .find(|&c| dangerous_chars.contains(&c))
    {
        return Err(Error::new(
            span,
            format!("Function name contains invalid character '{ch}'. Function names must be valid Rust identifiers.")
        ));
    }

    // Check for control characters
    if function_name.chars().any(|c| c.is_control()) {
        return Err(Error::new(
            span,
            "Function name contains control characters",
        ));
    }

    // Check for valid Rust identifier start (must start with letter or underscore)
    if let Some(first_char) = function_name.chars().next() {
        if !first_char.is_alphabetic() && first_char != '_' {
            return Err(Error::new(
                span,
                "Function name must start with a letter or underscore",
            ));
        }
    }

    // Check that all characters are valid for Rust identifiers
    if !function_name
        .chars()
        .all(|c| c.is_alphanumeric() || c == '_')
    {
        return Err(Error::new(
            span,
            "Function name must contain only letters, numbers, and underscores",
        ));
    }

    // Check for reserved keywords using syn's built-in detection
    // This is more robust than maintaining a hardcoded list
    if is_reserved_keyword(function_name) {
        return Err(Error::new(
            span,
            format!("Function name '{function_name}' is a reserved Rust keyword"),
        ));
    }

    Ok(())
}

/// Checks if a function name is a reserved Rust keyword or invalid identifier.
///
/// This approach uses a focused, minimal list of core Rust keywords that are
/// most problematic in the context of function names. Rather than trying to
/// maintain a complete language specification, we focus on keywords that would
/// cause immediate compilation issues.
///
/// This is a pragmatic approach that balances:
/// - **Robustness**: Catches the most problematic keywords
/// - **Maintainability**: Minimal, focused list that's easy to keep updated
/// - **Performance**: Simple string comparison rather than complex parsing
/// - **Reliability**: No false positives from parsing edge cases
///
/// The list is intentionally kept small and focused on keywords that would
/// cause immediate compilation failures, rather than trying to catch every
/// possible edge case.
///
/// # Arguments
///
/// * `function_name` - The function name to check
///
/// # Returns
///
/// Returns `true` if the function name is a reserved keyword or invalid, `false` otherwise.
fn is_reserved_keyword(function_name: &str) -> bool {
    // Instead of maintaining a hardcoded list, we use a minimal set of keywords
    // that are most problematic in the context of function names.
    // This is a pragmatic approach that balances robustness with simplicity.

    // Core Rust keywords that would cause immediate compilation issues
    let core_keywords = [
        "fn",
        "struct",
        "enum",
        "trait",
        "impl",
        "mod",
        "use",
        "extern",
        "crate",
        "type",
        "const",
        "static",
        "let",
        "mut",
        "ref",
        "move",
        "dyn",
        "async",
        "await",
        "if",
        "else",
        "match",
        "loop",
        "while",
        "for",
        "in",
        "return",
        "break",
        "continue",
        "pub",
        "priv",
        "unsafe",
        "where",
        "as",
        "box",
        "do",
        "final",
        "override",
        "self",
        "Self",
        "super",
        "macro",
        "macro_rules",
        "try",
        "union",
    ];

    core_keywords.contains(&function_name)
}

/// Parses the `human_readable` attribute from the struct's attributes.
///
/// This function extracts the function name specified in the `#[representable(human_readable = "fn_name")]`
/// attribute, validates it for security, and generates the appropriate function call expression.
///
/// # Arguments
///
/// * `attrs` - The struct's attributes to parse
/// * `_struct_name` - The name of the struct (currently unused but kept for future extensibility)
///
/// # Returns
///
/// A `Result<syn::Expr, Error>` containing either:
/// - A `syn::Expr` representing the function call to the human-readable function, or
/// - An `Error` if validation fails
///
/// If no function is specified, returns a default JSON serialization expression.
///
/// # Examples
///
/// For `#[representable(human_readable = "my_fn")]`, this returns `Ok(my_fn(self))`.
/// If no attribute is found, it returns `Ok(serde_json::to_string(self).unwrap_or_else(|_| "{}".to_string()))`.
fn parse_human_readable_fn(
    attrs: &[syn::Attribute],
    _struct_name: &Ident,
) -> Result<syn::Expr, Error> {
    for attr in attrs {
        if attr.path().is_ident("representable") {
            if let Meta::List(meta_list) = &attr.meta {
                for nested in meta_list
                    .parse_args_with(
                        syn::punctuated::Punctuated::<syn::Meta, syn::Token![,]>::parse_terminated,
                    )
                    .unwrap_or_default()
                {
                    if let syn::Meta::NameValue(name_value) = nested {
                        if name_value.path.is_ident("human_readable") {
                            if let syn::Expr::Lit(syn::ExprLit {
                                lit: syn::Lit::Str(lit_str),
                                ..
                            }) = &name_value.value
                            {
                                let function_name = &lit_str.value();
                                let span = lit_str.span();

                                // Validate the function name for security
                                validate_function_name(function_name, span)?;

                                let fn_name = Ident::new(function_name, span);
                                return Ok(syn::parse_quote! { #fn_name(self) });
                            }
                        }
                    }
                }
            }
        }
    }

    // Default to JSON output if no explicit function is provided
    Ok(syn::parse_quote! {
        serde_json::to_string(self).unwrap_or_else(|_| "{}".to_string())
    })
}

#[cfg(test)]
mod tests {
    use proc_macro2::Span;
    use syn::parse_quote;

    use super::*;

    #[test]
    fn test_validate_function_name_valid() {
        let valid_names = [
            "my_function",
            "myFunction",
            "my_function_123",
            "_private_function",
            "f",
            "a1b2c3",
        ];

        for name in &valid_names {
            assert!(
                validate_function_name(name, Span::call_site()).is_ok(),
                "Function name '{name}' should be valid"
            );
        }
    }

    #[test]
    fn test_validate_function_name_invalid_characters() {
        let invalid_names = [
            ("my/function", "path separator"),
            ("my\\function", "backslash"),
            ("my:function", "colon"),
            ("my*function", "asterisk"),
            ("my?function", "question mark"),
            ("my\"function", "quote"),
            ("my<function", "less than"),
            ("my>function", "greater than"),
            ("my|function", "pipe"),
            ("my\0function", "null byte"),
        ];

        for (name, description) in &invalid_names {
            let result = validate_function_name(name, Span::call_site());
            assert!(
                result.is_err(),
                "Function name '{name}' ({description}) should be invalid"
            );
        }
    }

    #[test]
    fn test_validate_function_name_invalid_start() {
        let invalid_names = ["1function", "123function", ".function", "-function"];

        for name in &invalid_names {
            let result = validate_function_name(name, Span::call_site());
            assert!(
                result.is_err(),
                "Function name '{name}' should be invalid (invalid start)"
            );
        }
    }

    #[test]
    fn test_validate_function_name_reserved_keywords() {
        let reserved_keywords = [
            "fn", "struct", "enum", "impl", "trait", "mod", "use", "pub", "priv", "let", "mut",
            "const", "static", "if", "else", "match", "loop", "while", "for", "in", "return",
            "break", "continue", "as", "where", "unsafe", "async", "await", "dyn", "move", "ref",
            "self", "Self", "super",
        ];

        for keyword in &reserved_keywords {
            let result = validate_function_name(keyword, Span::call_site());
            assert!(
                result.is_err(),
                "Reserved keyword '{keyword}' should be invalid"
            );
        }
    }

    #[test]
    fn test_validate_function_name_length_constraints() {
        // Test empty and whitespace-only names
        let empty_names = ["", "   ", "\t", "\n", " \t \n "];
        for name in &empty_names {
            let result = validate_function_name(name, Span::call_site());
            assert!(
                result.is_err(),
                "Empty/whitespace name '{name}' should be invalid"
            );
        }

        // Test very long names
        let long_name = "a".repeat(101);
        let result = validate_function_name(&long_name, Span::call_site());
        assert!(result.is_err(), "Very long name should be invalid");
    }

    #[test]
    fn test_validate_function_name_control_characters() {
        let control_chars = [
            '\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07', '\x08', '\x09', '\x0A',
            '\x0B', '\x0C', '\x0D', '\x0E', '\x0F', '\x10', '\x11', '\x12', '\x13', '\x14', '\x15',
            '\x16', '\x17', '\x18', '\x19', '\x1A', '\x1B', '\x1C', '\x1D', '\x1E', '\x1F', '\x7F',
        ];

        for &ch in &control_chars {
            let name = format!("my{ch}function");
            let result = validate_function_name(&name, Span::call_site());
            assert!(
                result.is_err(),
                "Function name with control character '{ch}' should be invalid"
            );
        }
    }

    #[test]
    fn test_parse_human_readable_fn_valid() {
        let attrs = vec![parse_quote! {
            #[representable(human_readable = "my_display_fn")]
        }];
        let struct_name = Ident::new("MyStruct", Span::call_site());

        let result = parse_human_readable_fn(&attrs, &struct_name);
        assert!(result.is_ok());

        // The result should be a function call expression
        if let Ok(expr) = result {
            // We can't easily test the exact structure, but we can verify it's not the default
            let expr_string = quote!(#expr).to_string();
            assert!(expr_string.contains("my_display_fn"));
        }
    }

    #[test]
    fn test_parse_human_readable_fn_invalid() {
        let attrs = vec![parse_quote! {
            #[representable(human_readable = "my/function")]
        }];
        let struct_name = Ident::new("MyStruct", Span::call_site());

        let result = parse_human_readable_fn(&attrs, &struct_name);
        assert!(result.is_err());
    }

    #[test]
    fn test_parse_human_readable_fn_no_attribute() {
        let attrs = vec![];
        let struct_name = Ident::new("MyStruct", Span::call_site());

        let result = parse_human_readable_fn(&attrs, &struct_name);
        assert!(result.is_ok());

        // Should return the default JSON serialization
        if let Ok(expr) = result {
            let expr_string = quote!(#expr).to_string();
            // The default expression contains serde_json::to_string and unwrap_or_else
            assert!(expr_string.contains("serde_json") || expr_string.contains("unwrap_or_else"));
        }
    }

    #[test]
    fn test_parse_human_readable_fn_reserved_keyword() {
        let attrs = vec![parse_quote! {
            #[representable(human_readable = "fn")]
        }];
        let struct_name = Ident::new("MyStruct", Span::call_site());

        let result = parse_human_readable_fn(&attrs, &struct_name);
        assert!(result.is_err());
    }
}
