//! JsonInclude derive macro — compile-time JSON embedding.
//!
//! Reads a JSON file at compile time, generates struct construction with
//! `include_str!` for `#[from_file]` fields.

use proc_macro2::TokenStream;
use quote::quote;
use syn::{DeriveInput, Fields, LitStr};

struct JsonIncludeAttr {
    path: String,
    is_array: bool,
    base: Option<String>,
}

/// A variable declaration with optional type.
/// `"host"` → VarDecl { name: "host", ty: None } → param `host: &str`
/// `"port: u16"` → VarDecl { name: "port", ty: Some("u16") } → param `port: u16`
struct VarDecl {
    name: String,
    ty: Option<String>,
}

struct FieldInfo {
    name: syn::Ident,
    is_from_file: bool,
    vars: Vec<VarDecl>,
}

pub fn derive_json_include(input: DeriveInput) -> TokenStream {
    match derive_json_include_inner(input) {
        Ok(ts) => ts,
        Err(e) => e.to_compile_error(),
    }
}

fn derive_json_include_inner(input: DeriveInput) -> syn::Result<TokenStream> {
    let struct_name = &input.ident;

    // Parse #[json_include("path", array, base = "...")] attribute
    let attr = parse_json_include_attr(&input)?;

    // Parse struct fields
    let fields = match &input.data {
        syn::Data::Struct(data) => match &data.fields {
            Fields::Named(named) => &named.named,
            _ => return Err(syn::Error::new_spanned(&input, "JsonInclude only supports named fields")),
        },
        _ => return Err(syn::Error::new_spanned(&input, "JsonInclude only supports structs")),
    };

    let field_infos: Vec<FieldInfo> = fields
        .iter()
        .map(|f| {
            let name = f.ident.clone().unwrap();
            let from_file_attr = f.attrs.iter().find(|a| a.path().is_ident("from_file"));
            let is_from_file = from_file_attr.is_some();
            let vars = if let Some(attr) = from_file_attr {
                parse_from_file_vars(attr)?
            } else {
                Vec::new()
            };
            Ok(FieldInfo { name, is_from_file, vars })
        })
        .collect::<syn::Result<Vec<_>>>()?;

    // Collect all unique vars from all fields (for function signature)
    // Dedup by name, check type consistency across fields
    let all_vars: Vec<VarDecl> = {
        let mut seen: std::collections::HashMap<String, Option<String>> = std::collections::HashMap::new();
        let mut ordered: Vec<String> = Vec::new();
        for field in &field_infos {
            for v in &field.vars {
                if let Some(existing_ty) = seen.get(&v.name) {
                    // Check type consistency
                    if existing_ty != &v.ty {
                        return Err(syn::Error::new(
                            field.name.span(),
                            format!(
                                "var '{}' declared with conflicting types: {:?} vs {:?}",
                                v.name, existing_ty, v.ty
                            ),
                        ));
                    }
                } else {
                    ordered.push(v.name.clone());
                    seen.insert(v.name.clone(), v.ty.clone());
                }
            }
        }
        ordered.iter().map(|name| VarDecl {
            name: name.clone(),
            ty: seen.get(name).cloned().flatten(),
        }).collect()
    };

    // Read JSON at compile time
    let manifest_dir = std::env::var("CARGO_MANIFEST_DIR")
        .map_err(|_| syn::Error::new_spanned(&input, "CARGO_MANIFEST_DIR not set"))?;
    let json_path = std::path::Path::new(&manifest_dir).join(&attr.path);
    let json_content = std::fs::read_to_string(&json_path)
        .map_err(|e| syn::Error::new_spanned(&input, format!("Failed to read {}: {}", json_path.display(), e)))?;
    let json_value: serde_json::Value = serde_json::from_str(&json_content)
        .map_err(|e| syn::Error::new_spanned(&input, format!("Failed to parse JSON: {}", e)))?;

    // Generate manifest tracking (for cargo rebuild detection)
    // Use absolute path so include_str! works regardless of calling file location
    let abs_manifest_path = json_path.to_string_lossy().to_string();
    // Use UPPER_SNAKE_CASE for the tracking constant
    let upper_name = struct_name.to_string()
        .chars()
        .enumerate()
        .flat_map(|(i, c)| {
            if c.is_uppercase() && i > 0 {
                vec!['_', c]
            } else {
                vec![c.to_ascii_uppercase()]
            }
        })
        .collect::<String>();
    let track_const_name = quote::format_ident!("_JSON_INCLUDE_TRACK_{}", upper_name);
    let track_token = quote! {
        #[allow(dead_code)]
        const #track_const_name: &str = include_str!(#abs_manifest_path);
    };

    // Generate construction expressions
    let base = attr.base.as_deref().unwrap_or("");

    // Validate vars against file placeholders (for from_file+vars fields)
    let var_names: Vec<String> = all_vars.iter().map(|v| v.name.clone()).collect();
    if !var_names.is_empty() {
        validate_vars_against_files(&field_infos, &json_value, base, &manifest_dir, attr.is_array)?;
    }

    // Generate function parameters for vars
    let var_params: Vec<TokenStream> = all_vars.iter().map(|v| {
        let ident = quote::format_ident!("{}", v.name);
        if let Some(ref ty_str) = v.ty {
            let ty: TokenStream = ty_str.parse().unwrap_or_else(|_| quote! { &str });
            quote! { #ident: #ty }
        } else {
            quote! { #ident: &str }
        }
    }).collect();

    // Build a lookup set for typed vars (for replace chain generation)
    let typed_var_names: std::collections::HashSet<String> = all_vars.iter()
        .filter(|v| v.ty.is_some())
        .map(|v| v.name.clone())
        .collect();

    if attr.is_array {
        let arr = json_value.as_array()
            .ok_or_else(|| syn::Error::new_spanned(&input, "JSON root is not an array (expected array mode)"))?;

        let items: Vec<TokenStream> = arr
            .iter()
            .enumerate()
            .map(|(i, obj)| generate_struct_construction(struct_name, &field_infos, obj, base, &manifest_dir, i, &typed_var_names))
            .collect::<syn::Result<Vec<_>>>()?;

        Ok(quote! {
            #track_token

            impl #struct_name {
                /// Load all items from the embedded JSON manifest.
                pub fn load_all(#(#var_params),*) -> Vec<Self> {
                    vec![
                        #(#items),*
                    ]
                }
            }
        })
    } else {
        let item = generate_struct_construction(struct_name, &field_infos, &json_value, base, &manifest_dir, 0, &typed_var_names)?;

        Ok(quote! {
            #track_token

            impl #struct_name {
                /// Load the single item from the embedded JSON manifest.
                pub fn load(#(#var_params),*) -> Self {
                    #item
                }
            }
        })
    }
}

fn parse_json_include_attr(input: &DeriveInput) -> syn::Result<JsonIncludeAttr> {
    let attr = input.attrs.iter()
        .find(|a| a.path().is_ident("json_include"))
        .ok_or_else(|| syn::Error::new_spanned(input, "Missing #[json_include(\"path\")] attribute"))?;

    let tokens: TokenStream = attr.meta.require_list()?.tokens.clone();
    let parsed = syn::parse2::<JsonIncludeArgs>(tokens)?;

    Ok(JsonIncludeAttr {
        path: parsed.path,
        is_array: parsed.is_array,
        base: parsed.base,
    })
}

/// Custom parser for json_include attribute arguments
struct JsonIncludeArgs {
    path: String,
    is_array: bool,
    base: Option<String>,
}

impl syn::parse::Parse for JsonIncludeArgs {
    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
        // First argument: path string literal
        let path_lit: LitStr = input.parse()?;
        let path = path_lit.value();

        let mut is_array = false;
        let mut base = None;

        // Remaining arguments: comma-separated
        while !input.is_empty() {
            input.parse::<syn::Token![,]>()?;
            if input.is_empty() {
                break;
            }
            let ident: syn::Ident = input.parse()?;
            match ident.to_string().as_str() {
                "array" => {
                    is_array = true;
                }
                "base" => {
                    input.parse::<syn::Token![=]>()?;
                    let lit: LitStr = input.parse()?;
                    base = Some(lit.value());
                }
                other => {
                    return Err(syn::Error::new(ident.span(), format!("unknown argument: {}", other)));
                }
            }
        }

        Ok(JsonIncludeArgs { path, is_array, base })
    }
}

/// Extract {xxx} placeholders from a template string.
fn extract_placeholders(content: &str) -> Vec<String> {
    let mut names = Vec::new();
    let mut chars = content.chars().peekable();
    while let Some(ch) = chars.next() {
        if ch == '{' {
            let name: String = chars.by_ref().take_while(|&c| c != '}').collect();
            if !name.is_empty() && name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') {
                names.push(name);
            }
        }
    }
    names.sort();
    names.dedup();
    names
}

/// Parse #[from_file] or #[from_file(vars = "a, b: u16, c")] attribute.
/// Supports optional type annotations: `"name"` → &str (default), `"name: type"` → typed.
fn parse_from_file_vars(attr: &syn::Attribute) -> syn::Result<Vec<VarDecl>> {
    // #[from_file] — no args
    if attr.meta.require_list().is_err() {
        return Ok(Vec::new());
    }

    // #[from_file(vars = "a, b: u16, c")]
    let tokens: proc_macro2::TokenStream = attr.meta.require_list()?.tokens.clone();
    let parsed = syn::parse2::<FromFileArgs>(tokens)?;
    Ok(parsed.vars)
}

struct FromFileArgs {
    vars: Vec<VarDecl>,
}

impl syn::parse::Parse for FromFileArgs {
    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
        let ident: syn::Ident = input.parse()?;
        if ident != "vars" {
            return Err(syn::Error::new(ident.span(), "expected `vars`"));
        }
        input.parse::<syn::Token![=]>()?;
        let lit: LitStr = input.parse()?;
        let vars: Vec<VarDecl> = lit.value()
            .split(',')
            .map(|s| s.trim())
            .filter(|s| !s.is_empty())
            .map(|s| {
                if let Some((name, ty)) = s.split_once(':') {
                    let name = name.trim().to_string();
                    let ty = ty.trim().to_string();
                    VarDecl { name, ty: Some(ty) }
                } else {
                    VarDecl { name: s.to_string(), ty: None }
                }
            })
            .collect();
        Ok(FromFileArgs { vars })
    }
}

/// Validate that all file placeholders are declared in vars, and all vars are used.
fn validate_vars_against_files(
    fields: &[FieldInfo],
    json_value: &serde_json::Value,
    base: &str,
    manifest_dir: &str,
    is_array: bool,
) -> syn::Result<()> {
    let objects: Vec<&serde_json::Value> = if is_array {
        json_value.as_array().map(|a| a.iter().collect()).unwrap_or_default()
    } else {
        vec![json_value]
    };

    // Track which vars are used across all files
    let mut all_used_vars: std::collections::HashSet<String> = std::collections::HashSet::new();

    for field in fields {
        if !field.is_from_file || field.vars.is_empty() {
            continue;
        }

        let declared: std::collections::HashSet<String> = field.vars.iter().map(|v| v.name.clone()).collect();

        for obj in &objects {
            if let Some(obj_map) = obj.as_object() {
                let key = field.name.to_string();
                if let Some(serde_json::Value::String(file_path)) = obj_map.get(&key) {
                    let full_path = format!("{}/{}{}", manifest_dir, base, file_path);
                    let content = std::fs::read_to_string(&full_path).map_err(|e| {
                        syn::Error::new(field.name.span(), format!("Failed to read {}: {}", full_path, e))
                    })?;

                    let placeholders = extract_placeholders(&content);

                    // Track which declared vars are actually used
                    // Note: we only check that declared vars are used, NOT that all
                    // placeholders are declared. Files may contain non-variable {xxx}
                    // patterns (e.g. URL path params like {id}) that should not be replaced.
                    for ph in &placeholders {
                        if declared.contains(ph) {
                            all_used_vars.insert(ph.clone());
                        }
                    }
                }
            }
        }
    }

    // Check: all declared vars are used by at least one file
    for field in fields {
        for v in &field.vars {
            if !all_used_vars.contains(&v.name) {
                return Err(syn::Error::new(
                    field.name.span(),
                    format!("vars declares '{}' but no file uses placeholder '{{{}}}'", v.name, v.name),
                ));
            }
        }
    }

    Ok(())
}

fn generate_struct_construction(
    struct_name: &syn::Ident,
    fields: &[FieldInfo],
    obj: &serde_json::Value,
    base: &str,
    manifest_dir: &str,
    index: usize,
    typed_var_names: &std::collections::HashSet<String>,
) -> syn::Result<TokenStream> {
    let obj = obj.as_object()
        .ok_or_else(|| syn::Error::new(
            proc_macro2::Span::call_site(),
            format!("JSON item at index {} is not an object", index),
        ))?;

    let field_tokens: Vec<TokenStream> = fields
        .iter()
        .map(|field| {
            let name = &field.name;
            let key = name.to_string();
            let value = obj.get(&key);

            if field.is_from_file {
                // #[from_file] field: use include_str! with absolute path
                let file_path_str = value
                    .and_then(|v| v.as_str())
                    .ok_or_else(|| syn::Error::new(
                        name.span(),
                        format!("Field '{}' has #[from_file] but JSON value is not a string", key),
                    ))?;
                let full_path = format!("{}/{}{}", manifest_dir, base, file_path_str);

                if field.vars.is_empty() {
                    // No vars — pure include
                    Ok(quote! { #name: String::from(include_str!(#full_path)) })
                } else {
                    // Has vars — include + .replace() chain
                    // For typed vars, use .to_string(); for &str vars, use directly
                    let replace_chain: Vec<TokenStream> = field.vars.iter().map(|v| {
                        let placeholder = format!("{{{}}}", v.name);
                        let var_ident = quote::format_ident!("{}", v.name);
                        if typed_var_names.contains(&v.name) {
                            quote! { .replace(#placeholder, &#var_ident.to_string()) }
                        } else {
                            quote! { .replace(#placeholder, #var_ident) }
                        }
                    }).collect();
                    Ok(quote! { #name: include_str!(#full_path) #(#replace_chain)* })
                }
            } else {
                // Regular field: generate from JSON value
                match value {
                    Some(serde_json::Value::String(s)) => {
                        Ok(quote! { #name: String::from(#s) })
                    }
                    Some(serde_json::Value::Number(n)) => {
                        if let Some(i) = n.as_i64() {
                            Ok(quote! { #name: #i as _ })
                        } else if let Some(f) = n.as_f64() {
                            Ok(quote! { #name: #f as _ })
                        } else {
                            Err(syn::Error::new(name.span(), format!("Unsupported number for field '{}'", key)))
                        }
                    }
                    Some(serde_json::Value::Bool(b)) => {
                        Ok(quote! { #name: #b })
                    }
                    Some(serde_json::Value::Null) | None => {
                        // Generate Default::default() for missing/null fields
                        Ok(quote! { #name: Default::default() })
                    }
                    _ => {
                        Err(syn::Error::new(name.span(), format!("Unsupported JSON type for field '{}'", key)))
                    }
                }
            }
        })
        .collect::<syn::Result<Vec<_>>>()?;

    Ok(quote! {
        #struct_name {
            #(#field_tokens),*
        }
    })
}