//! OpenRPC Client generation macro.
//!
//! This module provides macros for generating typed RPC clients from OpenRPC specifications.
//!
//! # Usage
//!
//! ## From file path
//! ```ignore
//! // Generate client from OpenRPC spec file
//! openrpc_client!("path/to/spec.json");
//! ```
//!
//! ## From URL (HTTP endpoint)
//! ```ignore
//! // Generate client that fetches spec from HTTP endpoint
//! openrpc_client!(http = "http://localhost:8080/openrpc.json");
//! ```
//!
//! ## From Unix socket with discover
//! ```ignore
//! // Generate client that discovers spec from Unix socket
//! openrpc_client!(socket = "/tmp/myservice.sock");
//! ```
//!
//! ## With custom client name
//! ```ignore
//! openrpc_client!("spec.json", name = "MyServiceClient");
//! ```

use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::{format_ident, quote};
use serde::Deserialize;
use std::path::PathBuf;
use syn::parse::{Parse, ParseStream};
use syn::{Ident, LitStr, Token, parse_macro_input};

/// Configuration for openrpc_client! macro.
#[derive(Default)]
struct OpenRpcClientConfig {
    /// Path to OpenRPC spec file (relative to CARGO_MANIFEST_DIR).
    file_path: Option<String>,
    /// HTTP URL to fetch OpenRPC spec from.
    http_url: Option<String>,
    /// Unix socket path for discovery.
    socket_path: Option<String>,
    /// Custom client struct name.
    client_name: Option<String>,
    /// Output directory for generated code (for inspection).
    output_dir: Option<String>,
}

impl Parse for OpenRpcClientConfig {
    fn parse(input: ParseStream) -> syn::Result<Self> {
        let mut config = OpenRpcClientConfig::default();

        // First argument can be a string literal (file path) or key=value
        if input.peek(LitStr) {
            let path: LitStr = input.parse()?;
            config.file_path = Some(path.value());
        }

        // Parse optional key=value pairs
        while !input.is_empty() {
            if input.peek(Token![,]) {
                input.parse::<Token![,]>()?;
            }

            if input.is_empty() {
                break;
            }

            let key: Ident = input.parse()?;
            input.parse::<Token![=]>()?;

            match key.to_string().as_str() {
                "http" => {
                    let value: LitStr = input.parse()?;
                    config.http_url = Some(value.value());
                }
                "socket" => {
                    let value: LitStr = input.parse()?;
                    config.socket_path = Some(value.value());
                }
                "name" => {
                    let value: LitStr = input.parse()?;
                    config.client_name = Some(value.value());
                }
                "output_dir" => {
                    let value: LitStr = input.parse()?;
                    config.output_dir = Some(value.value());
                }
                other => {
                    return Err(syn::Error::new(
                        key.span(),
                        format!(
                            "unknown option '{}'. Expected 'http', 'socket', 'name', or 'output_dir'",
                            other
                        ),
                    ));
                }
            }
        }

        // Validate that at least one source is specified
        if config.file_path.is_none() && config.http_url.is_none() && config.socket_path.is_none() {
            return Err(syn::Error::new(
                proc_macro2::Span::call_site(),
                "Must specify either a file path, http URL, or socket path",
            ));
        }

        Ok(config)
    }
}

// ============================================================================
// OpenRPC Spec Types (for parsing)
// ============================================================================

/// OpenRPC specification document.
#[derive(Debug, Clone, Deserialize)]
#[allow(dead_code)]
pub struct OpenRpcSpec {
    /// OpenRPC version.
    pub openrpc: String,
    /// API info.
    pub info: OpenRpcInfo,
    /// Server definitions.
    #[serde(default)]
    pub servers: Vec<OpenRpcServer>,
    /// Method definitions.
    #[serde(default)]
    pub methods: Vec<OpenRpcMethod>,
    /// Component definitions.
    #[serde(default)]
    pub components: Option<OpenRpcComponents>,
}

#[derive(Debug, Clone, Deserialize)]
#[allow(dead_code)]
pub struct OpenRpcInfo {
    pub title: String,
    pub version: String,
    #[serde(default)]
    pub description: Option<String>,
}

#[derive(Debug, Clone, Deserialize)]
#[allow(dead_code)]
pub struct OpenRpcServer {
    pub name: String,
    #[serde(default)]
    pub url: String,
    #[serde(default)]
    pub summary: Option<String>,
    #[serde(default)]
    pub description: Option<String>,
}

#[derive(Debug, Clone, Deserialize)]
#[allow(dead_code)]
pub struct OpenRpcMethod {
    pub name: String,
    #[serde(default)]
    pub summary: Option<String>,
    #[serde(default)]
    pub description: Option<String>,
    #[serde(default)]
    pub params: Vec<OpenRpcParam>,
    #[serde(default)]
    pub result: Option<OpenRpcResult>,
    #[serde(default)]
    pub deprecated: Option<bool>,
}

#[derive(Debug, Clone, Deserialize)]
#[allow(dead_code)]
pub struct OpenRpcParam {
    pub name: String,
    #[serde(default)]
    pub description: Option<String>,
    #[serde(default)]
    pub required: Option<bool>,
    pub schema: serde_json::Value,
}

#[derive(Debug, Clone, Deserialize)]
#[allow(dead_code)]
pub struct OpenRpcResult {
    pub name: String,
    #[serde(default)]
    pub description: Option<String>,
    pub schema: serde_json::Value,
}

#[derive(Debug, Clone, Deserialize, Default)]
#[allow(dead_code)]
pub struct OpenRpcComponents {
    #[serde(default)]
    pub schemas: Option<std::collections::HashMap<String, serde_json::Value>>,
}

// ============================================================================
// Code Generation
// ============================================================================

/// Implementation of the openrpc_client! macro.
pub fn impl_openrpc_client(input: TokenStream) -> TokenStream {
    let config = parse_macro_input!(input as OpenRpcClientConfig);

    // Load the OpenRPC spec
    let spec = match load_openrpc_spec(&config) {
        Ok(spec) => spec,
        Err(e) => {
            return syn::Error::new(proc_macro2::Span::call_site(), e)
                .to_compile_error()
                .into();
        }
    };

    // Generate the client code
    match generate_client_code(&spec, &config) {
        Ok(tokens) => tokens.into(),
        Err(e) => syn::Error::new(proc_macro2::Span::call_site(), e)
            .to_compile_error()
            .into(),
    }
}

/// Load OpenRPC spec from the configured source.
fn load_openrpc_spec(config: &OpenRpcClientConfig) -> Result<OpenRpcSpec, String> {
    if let Some(ref file_path) = config.file_path {
        load_from_file(file_path)
    } else if let Some(ref http_url) = config.http_url {
        // For HTTP, we embed the URL and generate code that fetches at runtime
        // But for compile-time generation, we try to fetch it
        load_from_http(http_url)
    } else if let Some(ref socket_path) = config.socket_path {
        // For socket, we use rpc.discover method
        load_from_socket(socket_path)
    } else {
        Err("No spec source configured".to_string())
    }
}

/// Load OpenRPC spec from a file.
fn load_from_file(file_path: &str) -> Result<OpenRpcSpec, String> {
    let manifest_dir =
        std::env::var("CARGO_MANIFEST_DIR").map_err(|_| "CARGO_MANIFEST_DIR not set")?;
    let full_path = PathBuf::from(&manifest_dir).join(file_path);

    let content = std::fs::read_to_string(&full_path)
        .map_err(|e| format!("Failed to read '{}': {}", full_path.display(), e))?;

    serde_json::from_str(&content).map_err(|e| format!("Failed to parse OpenRPC spec: {}", e))
}

/// Load OpenRPC spec from HTTP URL (blocking, for compile time).
fn load_from_http(url: &str) -> Result<OpenRpcSpec, String> {
    // Use ureq for blocking HTTP request during compilation
    // Note: This requires ureq as a build dependency
    // For now, we'll generate code that does runtime discovery
    Err(format!(
        "HTTP discovery at compile time not supported yet. URL: {}. \
         Use file-based spec or runtime discovery.",
        url
    ))
}

/// Load OpenRPC spec from Unix socket using rpc.discover.
fn load_from_socket(socket_path: &str) -> Result<OpenRpcSpec, String> {
    // For compile-time, we try to connect and discover
    // This requires the socket to be available during compilation
    use std::io::{BufRead, BufReader, Write};
    use std::os::unix::net::UnixStream;

    let stream = UnixStream::connect(socket_path)
        .map_err(|e| format!("Failed to connect to socket '{}': {}", socket_path, e))?;

    let mut stream = stream;
    stream
        .set_read_timeout(Some(std::time::Duration::from_secs(5)))
        .ok();
    stream
        .set_write_timeout(Some(std::time::Duration::from_secs(5)))
        .ok();

    // Send JSON-RPC discover request
    let request = r#"{"jsonrpc":"2.0","method":"rpc.discover","params":[],"id":1}"#;
    writeln!(stream, "{}", request)
        .map_err(|e| format!("Failed to send discover request: {}", e))?;

    // Read response
    let mut reader = BufReader::new(stream);
    let mut response = String::new();
    reader
        .read_line(&mut response)
        .map_err(|e| format!("Failed to read discover response: {}", e))?;

    // Parse JSON-RPC response
    let json: serde_json::Value = serde_json::from_str(&response)
        .map_err(|e| format!("Failed to parse JSON-RPC response: {}", e))?;

    // Extract result
    let result = json.get("result").ok_or("No result in JSON-RPC response")?;

    serde_json::from_value(result.clone())
        .map_err(|e| format!("Failed to parse OpenRPC spec from result: {}", e))
}

/// Generate the client code from OpenRPC spec.
fn generate_client_code(
    spec: &OpenRpcSpec,
    config: &OpenRpcClientConfig,
) -> Result<TokenStream2, String> {
    // Determine client name
    let client_name = config
        .client_name
        .clone()
        .unwrap_or_else(|| format!("{}Client", to_pascal_case(&spec.info.title)));
    let client_ident = format_ident!("{}", client_name);

    // Generate input/output structs for each method
    let mut structs = Vec::new();
    let mut methods = Vec::new();

    // Collect schemas from components
    let schemas = spec
        .components
        .as_ref()
        .and_then(|c| c.schemas.as_ref())
        .cloned()
        .unwrap_or_default();

    for method in &spec.methods {
        let method_name = to_snake_case(&method.name.replace('.', "_"));
        let method_ident = format_ident!("{}", method_name);

        let input_struct_name = format!("{}Input", to_pascal_case(&method_name));
        let output_struct_name = format!("{}Output", to_pascal_case(&method_name));
        let input_ident = format_ident!("{}", input_struct_name);
        let output_ident = format_ident!("{}", output_struct_name);

        // Generate input struct from params
        let input_struct = generate_input_struct(&method.params, &input_ident, &schemas);
        structs.push(input_struct);

        // Generate output struct from result
        let output_struct = generate_output_struct(&method.result, &output_ident, &schemas);
        structs.push(output_struct);

        // Generate method
        let rpc_method_name = &method.name;
        let summary = method.summary.as_deref().unwrap_or("");
        let deprecated = method.deprecated.unwrap_or(false);

        let deprecated_attr = if deprecated {
            quote! { #[deprecated] }
        } else {
            quote! {}
        };

        methods.push(quote! {
            #[doc = #summary]
            #deprecated_attr
            pub async fn #method_ident(&self, input: #input_ident) -> Result<#output_ident, OpenRpcError> {
                self.transport.call(#rpc_method_name, input).await
            }
        });
    }

    // Generate server URL constant if available
    let default_url = spec
        .servers
        .first()
        .map(|s| s.url.clone())
        .unwrap_or_default();

    // Generate doc comment
    let title = &spec.info.title;
    let version = &spec.info.version;
    let description = spec.info.description.as_deref().unwrap_or("");

    let client_doc = format!("{} v{}\n\n{}", title, version, description);

    // Determine connection type based on config
    let (connect_impl, transport_type) = if config.socket_path.is_some() {
        let socket = config.socket_path.as_ref().unwrap();
        (
            quote! {
                /// Connect to the RPC server via Unix socket.
                pub async fn connect() -> Result<Self, OpenRpcError> {
                    let transport = OpenRpcTransport::unix_socket(#socket).await?;
                    Ok(Self { transport })
                }

                /// Connect to a specific Unix socket path.
                pub async fn connect_socket(path: &str) -> Result<Self, OpenRpcError> {
                    let transport = OpenRpcTransport::unix_socket(path).await?;
                    Ok(Self { transport })
                }
            },
            quote! { OpenRpcTransport },
        )
    } else if config.http_url.is_some() {
        let url = config.http_url.as_ref().unwrap();
        (
            quote! {
                /// Connect to the RPC server via HTTP.
                pub async fn connect() -> Result<Self, OpenRpcError> {
                    let transport = OpenRpcTransport::http(#url)?;
                    Ok(Self { transport })
                }

                /// Connect to a specific HTTP URL.
                pub async fn connect_http(url: &str) -> Result<Self, OpenRpcError> {
                    let transport = OpenRpcTransport::http(url)?;
                    Ok(Self { transport })
                }
            },
            quote! { OpenRpcTransport },
        )
    } else {
        // File-based spec, generate both connection methods
        (
            quote! {
                /// Connect to the RPC server via HTTP.
                pub async fn connect_http(url: &str) -> Result<Self, OpenRpcError> {
                    let transport = OpenRpcTransport::http(url)?;
                    Ok(Self { transport })
                }

                /// Connect to the RPC server via Unix socket.
                pub async fn connect_socket(path: &str) -> Result<Self, OpenRpcError> {
                    let transport = OpenRpcTransport::unix_socket(path).await?;
                    Ok(Self { transport })
                }

                /// Connect using the default server URL from the spec.
                pub async fn connect() -> Result<Self, OpenRpcError> {
                    let url = #default_url;
                    if url.starts_with("unix://") || url.starts_with("/") {
                        Self::connect_socket(&url.replace("unix://", "")).await
                    } else {
                        Self::connect_http(url).await
                    }
                }
            },
            quote! { OpenRpcTransport },
        )
    };

    // Write generated code to file for inspection
    let tokens = quote! {
        // Input/Output structs
        #(#structs)*

        #[doc = #client_doc]
        pub struct #client_ident {
            transport: #transport_type,
        }

        impl #client_ident {
            #connect_impl

            /// Get a reference to the underlying transport.
            pub fn transport(&self) -> &#transport_type {
                &self.transport
            }

            /// Discover the OpenRPC specification from the server.
            pub async fn discover(&self) -> Result<serde_json::Value, OpenRpcError> {
                self.transport.discover().await
            }

            // Generated methods
            #(#methods)*
        }
    };

    // Write to .generated.rs file if output_dir specified or alongside spec
    if let Some(ref file_path) = config.file_path {
        let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").ok();
        if let Some(dir) = manifest_dir {
            let full_path = PathBuf::from(&dir).join(file_path);
            let generated_path = full_path.with_extension("client.generated.rs");
            let formatted = format_generated_code(&tokens);
            let content = format!(
                r#"// =============================================================================
// AUTO-GENERATED FILE - DO NOT EDIT MANUALLY
// =============================================================================
//
// Generated from: {}
// Generator: herolib_derive::openrpc_client!
//
// This file is for inspection only. The actual code is generated at compile
// time by the openrpc_client! macro.
//
// =============================================================================

#![allow(dead_code)]
#![allow(unused_imports)]

use serde::{{Deserialize, Serialize}};
use herolib_clients::openrpc::{{OpenRpcTransport, OpenRpcError}};

{}
"#,
                file_path, formatted
            );
            let _ = std::fs::write(&generated_path, &content);
        }
    }

    Ok(tokens)
}

/// Generate input struct from OpenRPC params.
fn generate_input_struct(
    params: &[OpenRpcParam],
    struct_ident: &proc_macro2::Ident,
    schemas: &std::collections::HashMap<String, serde_json::Value>,
) -> TokenStream2 {
    if params.is_empty() {
        return quote! {
            #[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
            pub struct #struct_ident {}
        };
    }

    // If single param with object schema, flatten it
    if params.len() == 1 {
        let param = &params[0];
        let schema = resolve_schema(&param.schema, schemas);

        if let Some(props) = schema.get("properties").and_then(|p| p.as_object()) {
            let required: Vec<String> = schema
                .get("required")
                .and_then(|r| r.as_array())
                .map(|arr| {
                    arr.iter()
                        .filter_map(|v| v.as_str().map(String::from))
                        .collect()
                })
                .unwrap_or_default();

            let fields = generate_struct_fields(props, &required, schemas);
            return quote! {
                #[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
                pub struct #struct_ident {
                    #fields
                }
            };
        }
    }

    // Multiple params or non-object param
    let fields: Vec<TokenStream2> = params
        .iter()
        .map(|param| {
            let field_name = format_ident!("{}", to_snake_case(&param.name));
            let field_type = schema_to_rust_type(&param.schema, schemas);
            let is_required = param.required.unwrap_or(false);

            let serde_attr = if to_snake_case(&param.name) != param.name {
                let original = &param.name;
                quote! { #[serde(rename = #original)] }
            } else {
                quote! {}
            };

            if is_required {
                quote! {
                    #serde_attr
                    pub #field_name: #field_type
                }
            } else {
                quote! {
                    #serde_attr
                    #[serde(skip_serializing_if = "Option::is_none")]
                    pub #field_name: Option<#field_type>
                }
            }
        })
        .collect();

    quote! {
        #[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
        pub struct #struct_ident {
            #(#fields),*
        }
    }
}

/// Generate output struct from OpenRPC result.
fn generate_output_struct(
    result: &Option<OpenRpcResult>,
    struct_ident: &proc_macro2::Ident,
    schemas: &std::collections::HashMap<String, serde_json::Value>,
) -> TokenStream2 {
    let Some(result) = result else {
        return quote! {
            #[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
            pub struct #struct_ident {}
        };
    };

    let schema = resolve_schema(&result.schema, schemas);

    // If object with properties, generate struct fields
    if let Some(props) = schema.get("properties").and_then(|p| p.as_object()) {
        let required: Vec<String> = schema
            .get("required")
            .and_then(|r| r.as_array())
            .map(|arr| {
                arr.iter()
                    .filter_map(|v| v.as_str().map(String::from))
                    .collect()
            })
            .unwrap_or_default();

        let fields = generate_struct_fields(props, &required, schemas);
        return quote! {
            #[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
            pub struct #struct_ident {
                #fields
            }
        };
    }

    // Otherwise, wrap in a single value field
    let value_type = schema_to_rust_type(&result.schema, schemas);
    quote! {
        #[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
        pub struct #struct_ident {
            #[serde(flatten)]
            pub value: #value_type
        }
    }
}

/// Generate struct fields from JSON Schema properties.
fn generate_struct_fields(
    props: &serde_json::Map<String, serde_json::Value>,
    required: &[String],
    schemas: &std::collections::HashMap<String, serde_json::Value>,
) -> TokenStream2 {
    let fields: Vec<TokenStream2> = props
        .iter()
        .map(|(name, schema)| {
            let field_name = to_snake_case(name);
            let field_ident = format_ident!("{}", field_name);
            let field_type = schema_to_rust_type(schema, schemas);
            let is_required = required.contains(name);

            let serde_attr = if field_name != *name {
                quote! { #[serde(rename = #name)] }
            } else {
                quote! {}
            };

            if is_required {
                quote! {
                    #serde_attr
                    pub #field_ident: #field_type
                }
            } else {
                quote! {
                    #serde_attr
                    #[serde(skip_serializing_if = "Option::is_none")]
                    pub #field_ident: Option<#field_type>
                }
            }
        })
        .collect();

    quote! { #(#fields),* }
}

/// Resolve a schema reference to actual schema.
fn resolve_schema<'a>(
    schema: &'a serde_json::Value,
    schemas: &'a std::collections::HashMap<String, serde_json::Value>,
) -> &'a serde_json::Value {
    if let Some(reference) = schema.get("$ref").and_then(|r| r.as_str()) {
        // Extract schema name from reference like "#/components/schemas/MyType"
        let schema_name = reference
            .strip_prefix("#/components/schemas/")
            .unwrap_or(reference);

        if let Some(resolved) = schemas.get(schema_name) {
            return resolved;
        }
    }
    schema
}

/// Convert JSON Schema to Rust type.
fn schema_to_rust_type(
    schema: &serde_json::Value,
    schemas: &std::collections::HashMap<String, serde_json::Value>,
) -> TokenStream2 {
    // Handle $ref
    if let Some(reference) = schema.get("$ref").and_then(|r| r.as_str()) {
        let schema_name = reference
            .strip_prefix("#/components/schemas/")
            .unwrap_or(reference);
        let type_ident = format_ident!("{}", schema_name);
        return quote! { #type_ident };
    }

    // Handle array of types (nullable)
    if let Some(types) = schema.get("type").and_then(|t| t.as_array()) {
        let has_null = types.iter().any(|t| t.as_str() == Some("null"));
        let non_null: Vec<_> = types
            .iter()
            .filter(|t| t.as_str() != Some("null"))
            .collect();

        if non_null.len() == 1 {
            let mut schema_copy = schema.clone();
            schema_copy["type"] = non_null[0].clone();
            let inner = schema_to_rust_type(&schema_copy, schemas);
            if has_null {
                return quote! { Option<#inner> };
            }
            return inner;
        }
        return quote! { serde_json::Value };
    }

    // Handle oneOf/anyOf
    if schema.get("oneOf").is_some() || schema.get("anyOf").is_some() {
        return quote! { serde_json::Value };
    }

    // Handle enum
    if schema.get("enum").is_some() {
        return quote! { String };
    }

    match schema.get("type").and_then(|t| t.as_str()) {
        Some("string") => quote! { String },
        Some("integer") => match schema.get("format").and_then(|f| f.as_str()) {
            Some("int32") => quote! { i32 },
            Some("int64") => quote! { i64 },
            Some("uint32") => quote! { u32 },
            Some("uint64") => quote! { u64 },
            _ => quote! { i64 },
        },
        Some("number") => match schema.get("format").and_then(|f| f.as_str()) {
            Some("float") => quote! { f32 },
            Some("double") => quote! { f64 },
            _ => quote! { f64 },
        },
        Some("boolean") => quote! { bool },
        Some("null") => quote! { () },
        Some("array") => {
            let default_items = serde_json::json!({});
            let items = schema.get("items").unwrap_or(&default_items);
            let item_type = schema_to_rust_type(items, schemas);
            quote! { Vec<#item_type> }
        }
        Some("object") => {
            if let Some(additional) = schema.get("additionalProperties") {
                if additional.is_object() {
                    let value_type = schema_to_rust_type(additional, schemas);
                    return quote! { std::collections::HashMap<String, #value_type> };
                }
                if additional.as_bool() == Some(true) {
                    return quote! { std::collections::HashMap<String, serde_json::Value> };
                }
            }
            quote! { serde_json::Value }
        }
        None | Some(_) => quote! { serde_json::Value },
    }
}

// ============================================================================
// Helper Functions
// ============================================================================

fn to_pascal_case(s: &str) -> String {
    s.split(['_', '-', ' ', '.'])
        .map(|word| {
            let mut chars = word.chars();
            match chars.next() {
                Some(first) => first.to_uppercase().chain(chars).collect(),
                None => String::new(),
            }
        })
        .collect()
}

fn to_snake_case(s: &str) -> String {
    let mut result = String::new();
    for (i, c) in s.chars().enumerate() {
        if c.is_uppercase() && i > 0 {
            result.push('_');
        }
        result.push(c.to_lowercase().next().unwrap());
    }
    result.replace(['-', '.', ' '], "_")
}

/// Format generated code using prettyplease.
fn format_generated_code(tokens: &TokenStream2) -> String {
    let code = tokens.to_string();
    match syn::parse_file(&code) {
        Ok(file) => prettyplease::unparse(&file),
        Err(_) => code,
    }
}
