//! Help text generation for command-line interfaces.
//!
//! This module provides utilities to generate help text from Schema,
//! including doc comments, field names, and attribute information.

use crate::driver::HelpListMode;
use crate::missing::normalize_program_name;
use crate::schema::{ArgLevelSchema, ArgSchema, Schema, Subcommand};
use facet_core::Facet;
use owo_colors::OwoColorize;
use owo_colors::Stream::Stdout;
use std::fmt;
use std::string::String;
use std::sync::Arc;
use std::vec::Vec;

/// Generate help text for a Facet type.
///
/// This is a convenience function that builds a Schema internally.
/// If you already have a Schema, use `generate_help_for_subcommand` instead.
pub fn generate_help<T: Facet<'static>>(config: &HelpConfig) -> String {
    generate_help_for_shape(T::SHAPE, config)
}

/// Generate help text from a Shape.
///
/// This is a convenience function that builds a Schema internally.
/// If you already have a Schema, use `generate_help_for_subcommand` instead.
pub fn generate_help_for_shape(shape: &'static facet_core::Shape, config: &HelpConfig) -> String {
    let schema = match Schema::from_shape(shape) {
        Ok(s) => s,
        Err(_) => {
            // Fall back to a minimal help message
            let program_name = config
                .program_name
                .clone()
                .or_else(|| {
                    std::env::args()
                        .next()
                        .map(|path| normalize_program_name(&path))
                })
                .unwrap_or_else(|| "program".to_string());
            return format!(
                "{}\n\n(Schema could not be built for this type)\n",
                program_name
            );
        }
    };

    generate_help_for_subcommand(&schema, &[], config)
}

/// Configuration for help text generation.
#[derive(Clone)]
pub struct HelpConfig {
    /// Program name (defaults to executable name)
    pub program_name: Option<String>,
    /// Program version
    pub version: Option<String>,
    /// Additional description to show after the auto-generated one
    pub description: Option<String>,
    /// Width for wrapping text (0 = no wrapping)
    pub width: usize,
    /// Whether to include implementation source file information in help output.
    pub include_implementation_source_file: bool,
    /// Optional callback to render an implementation URL from a source file path.
    pub implementation_url: Option<Arc<dyn Fn(&str) -> String + Send + Sync>>,
}

impl fmt::Debug for HelpConfig {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("HelpConfig")
            .field("program_name", &self.program_name)
            .field("version", &self.version)
            .field("description", &self.description)
            .field("width", &self.width)
            .field(
                "include_implementation_source_file",
                &self.include_implementation_source_file,
            )
            .field(
                "implementation_url",
                &self.implementation_url.as_ref().map(|_| "<fn>"),
            )
            .finish()
    }
}

impl Default for HelpConfig {
    fn default() -> Self {
        Self {
            program_name: None,
            version: None,
            description: None,
            width: 80,
            include_implementation_source_file: false,
            implementation_url: None,
        }
    }
}

/// Resolve implementation source file for a subcommand path from a root shape.
///
/// The `subcommand_path` should contain effective subcommand names (as emitted by
/// `ConfigValue::extract_subcommand_path`). An empty path resolves to the root shape.
pub(crate) fn implementation_source_for_subcommand_path(
    root_shape: &'static facet_core::Shape,
    subcommand_path: &[String],
) -> Option<&'static str> {
    let mut current_shape = root_shape;

    if subcommand_path.is_empty() {
        return current_shape.source_file;
    }

    for segment in subcommand_path {
        let next_shape = next_subcommand_shape(current_shape, segment)?;
        current_shape = next_shape;
    }

    current_shape.source_file
}

fn next_subcommand_shape(
    shape: &'static facet_core::Shape,
    target_effective_name: &str,
) -> Option<&'static facet_core::Shape> {
    let fields = match shape.ty {
        facet_core::Type::User(facet_core::UserType::Struct(s)) => s.fields,
        _ => return None,
    };

    let subcommand_field = fields
        .iter()
        .find(|field| field.has_attr(Some("args"), "subcommand"))?;

    let enum_shape = unwrap_option_shape(subcommand_field.shape());
    let variants = match enum_shape.ty {
        facet_core::Type::User(facet_core::UserType::Enum(e)) => e.variants,
        _ => return None,
    };

    let variant = variants
        .iter()
        .find(|variant| variant.effective_name() == target_effective_name)?;

    if variant.data.fields.is_empty() {
        return Some(enum_shape);
    }

    let has_direct_subcommand = variant
        .data
        .fields
        .iter()
        .any(|field| field.has_attr(Some("args"), "subcommand"));

    if has_direct_subcommand {
        return Some(enum_shape);
    }

    if variant.data.fields.len() == 1 {
        return Some(unwrap_option_shape(variant.data.fields[0].shape()));
    }

    Some(enum_shape)
}

fn unwrap_option_shape(mut shape: &'static facet_core::Shape) -> &'static facet_core::Shape {
    while let facet_core::Def::Option(option_def) = shape.def {
        shape = option_def.t;
    }
    shape
}

/// Generate help text for a specific subcommand path from a Schema.
///
/// `subcommand_path` is a list of subcommand names (e.g., `["repo", "clone"]` for `myapp repo clone --help`).
/// This navigates through the schema to find the target subcommand and generates help for it.
pub fn generate_help_for_subcommand(
    schema: &Schema,
    subcommand_path: &[String],
    config: &HelpConfig,
) -> String {
    let program_name = config
        .program_name
        .clone()
        .or_else(|| {
            std::env::args()
                .next()
                .map(|path| normalize_program_name(&path))
        })
        .unwrap_or_else(|| "program".to_string());

    if subcommand_path.is_empty() {
        return generate_help_from_schema(schema, &program_name, config);
    }

    // Navigate to the subcommand
    let mut current_args = schema.args();
    let mut command_path = vec![program_name.clone()];

    for name in subcommand_path {
        // The path contains effective names (e.g., "Clone", "rm") from ConfigValue.
        // Look up by effective_name since that's what's stored in the path.
        let sub = current_args
            .subcommands()
            .values()
            .find(|s| s.effective_name() == name);

        if let Some(sub) = sub {
            command_path.push(sub.cli_name().to_string());
            current_args = sub.args();
        } else {
            // Subcommand not found, fall back to root help
            return generate_help_from_schema(schema, &program_name, config);
        }
    }

    // Find the final subcommand to get its docs
    let mut final_sub: Option<&Subcommand> = None;
    let mut args = schema.args();

    for name in subcommand_path {
        let sub = args
            .subcommands()
            .values()
            .find(|s| s.effective_name() == name);
        if let Some(sub) = sub {
            final_sub = Some(sub);
            args = sub.args();
        }
    }

    generate_help_for_subcommand_level(current_args, final_sub, &command_path.join(" "), config)
}

/// Generate help-list output for subcommands at the current command level.
///
/// In [`HelpListMode::Short`], this returns one full CLI command path per line,
/// recursively listing all reachable leaf commands.
/// In [`HelpListMode::Full`], this returns concatenated help output for each
/// reachable leaf subcommand under the current command path.
pub(crate) fn generate_help_list_for_subcommand(
    schema: &Schema,
    subcommand_path: &[String],
    config: &HelpConfig,
    mode: HelpListMode,
) -> String {
    let program_name = config
        .program_name
        .clone()
        .or_else(|| {
            std::env::args()
                .next()
                .map(|path| normalize_program_name(&path))
        })
        .unwrap_or_else(|| "program".to_string());

    let mut current_args = schema.args();
    let mut resolved_path = Vec::new();

    for name in subcommand_path {
        let sub = current_args
            .subcommands()
            .values()
            .find(|s| s.effective_name() == name);

        let Some(sub) = sub else {
            // Fall back to regular root help if the path cannot be resolved.
            return generate_help_for_subcommand(schema, &[], config);
        };

        resolved_path.push(sub.effective_name().to_string());
        current_args = sub.args();
    }

    if !current_args.has_subcommands() {
        let command_display = if resolved_path.is_empty() {
            program_name
        } else {
            let cli_chain = resolve_cli_chain(schema, &resolved_path);
            if cli_chain.is_empty() {
                program_name
            } else {
                format!("{} {}", program_name, cli_chain.join(" "))
            }
        };
        return format!("No subcommands available for {command_display}.");
    }

    match mode {
        HelpListMode::Short => {
            let mut cli_chain = if resolved_path.is_empty() {
                Vec::new()
            } else {
                resolve_cli_chain(schema, &resolved_path)
            };
            let mut commands = Vec::new();
            collect_short_help_commands(
                &mut commands,
                program_name.as_str(),
                &mut cli_chain,
                current_args,
            );
            commands.join("\n")
        }
        HelpListMode::Full => {
            let mut sections = Vec::new();
            let mut leaf_paths = Vec::new();
            let mut working_path = resolved_path.clone();
            collect_leaf_subcommand_paths(&mut leaf_paths, &mut working_path, current_args);

            for child_path in leaf_paths {
                sections.push(generate_help_for_subcommand(schema, &child_path, config));
            }
            sections.join("\n\n")
        }
    }
}

fn collect_leaf_subcommand_paths(
    leaf_paths: &mut Vec<Vec<String>>,
    current_path: &mut Vec<String>,
    args: &ArgLevelSchema,
) {
    if !args.has_subcommands() {
        if !current_path.is_empty() {
            leaf_paths.push(current_path.clone());
        }
        return;
    }

    for sub in args.subcommands().values() {
        current_path.push(sub.effective_name().to_string());
        collect_leaf_subcommand_paths(leaf_paths, current_path, sub.args());
        current_path.pop();
    }
}

fn collect_short_help_commands(
    commands: &mut Vec<String>,
    program_name: &str,
    cli_chain: &mut Vec<String>,
    args: &ArgLevelSchema,
) {
    if args.subcommand_optional() {
        if cli_chain.is_empty() {
            commands.push(program_name.to_string());
        } else {
            commands.push(format!("{} {}", program_name, cli_chain.join(" ")));
        }
    }

    if !args.has_subcommands() {
        if !cli_chain.is_empty() {
            commands.push(format!("{} {}", program_name, cli_chain.join(" ")));
        }
        return;
    }

    for sub in args.subcommands().values() {
        cli_chain.push(sub.cli_name().to_string());
        collect_short_help_commands(commands, program_name, cli_chain, sub.args());
        cli_chain.pop();
    }
}

fn resolve_cli_chain(schema: &Schema, subcommand_path: &[String]) -> Vec<String> {
    let mut current_args = schema.args();
    let mut cli_path = Vec::new();

    for name in subcommand_path {
        let sub = current_args
            .subcommands()
            .values()
            .find(|s| s.effective_name() == name);
        let Some(sub) = sub else {
            break;
        };
        cli_path.push(sub.cli_name().to_string());
        current_args = sub.args();
    }

    cli_path
}

/// Generate help from a built Schema.
fn generate_help_from_schema(schema: &Schema, program_name: &str, config: &HelpConfig) -> String {
    let mut out = String::new();

    // Program name and version
    if let Some(version) = &config.version {
        out.push_str(&format!("{program_name} {version}\n"));
    } else {
        out.push_str(&format!("{program_name}\n"));
    }

    // Type doc comment from schema
    if let Some(summary) = schema.docs().summary() {
        out.push('\n');
        out.push_str(summary.trim());
        out.push('\n');
    }
    if let Some(details) = schema.docs().details() {
        for line in details.lines() {
            out.push_str(line.trim());
            out.push('\n');
        }
    }

    // Additional description
    if let Some(desc) = &config.description {
        out.push('\n');
        out.push_str(desc);
        out.push('\n');
    }

    out.push('\n');

    generate_arg_level_help(&mut out, schema.args(), program_name);

    out
}

/// Generate help for a subcommand level.
fn generate_help_for_subcommand_level(
    args: &ArgLevelSchema,
    subcommand: Option<&Subcommand>,
    full_command: &str,
    config: &HelpConfig,
) -> String {
    let mut out = String::new();

    // Header with full command
    out.push_str(&format!("{full_command}\n"));

    // Doc comment for the subcommand
    if let Some(sub) = subcommand {
        if let Some(summary) = sub.docs().summary() {
            out.push('\n');
            out.push_str(summary.trim());
            out.push('\n');
        }
        if let Some(details) = sub.docs().details() {
            for line in details.lines() {
                out.push_str(line.trim());
                out.push('\n');
            }
        }
    }

    // Additional description from config
    if let Some(desc) = &config.description {
        out.push('\n');
        out.push_str(desc);
        out.push('\n');
    }

    out.push('\n');

    generate_arg_level_help(&mut out, args, full_command);

    out
}

/// Generate help output for an argument level (args + subcommands).
fn generate_arg_level_help(out: &mut String, args: &ArgLevelSchema, program_name: &str) {
    // Separate positionals and named flags
    let mut positionals: Vec<&ArgSchema> = Vec::new();
    let mut flags: Vec<&ArgSchema> = Vec::new();

    for (_name, arg) in args.args().iter() {
        if arg.kind().is_positional() {
            positionals.push(arg);
        } else {
            flags.push(arg);
        }
    }

    // Usage line
    out.push_str(&format!("{}:\n    ", "USAGE".yellow().bold()));
    out.push_str(program_name);

    if !flags.is_empty() {
        out.push_str(" [OPTIONS]");
    }

    for pos in &positionals {
        let name = pos.name().to_uppercase();
        if pos.required() {
            out.push_str(&format!(" <{name}>"));
        } else {
            out.push_str(&format!(" [{name}]"));
        }
    }

    if args.has_subcommands() {
        if args.subcommand_optional() {
            out.push_str(" [COMMAND]");
        } else {
            out.push_str(" <COMMAND>");
        }
    }

    out.push_str("\n\n");

    // Positional arguments
    if !positionals.is_empty() {
        out.push_str(&format!("{}:\n", "ARGUMENTS".yellow().bold()));
        for arg in &positionals {
            write_arg_help(out, arg);
        }
        out.push('\n');
    }

    // Options
    if !flags.is_empty() {
        out.push_str(&format!("{}:\n", "OPTIONS".yellow().bold()));
        for arg in &flags {
            write_arg_help(out, arg);
        }
        out.push('\n');
    }

    // Subcommands
    if args.has_subcommands() {
        out.push_str(&format!("{}:\n", "COMMANDS".yellow().bold()));
        for sub in args.subcommands().values() {
            write_subcommand_help(out, sub);
        }
        out.push('\n');
    }
}

/// Write help for a single argument.
fn write_arg_help(out: &mut String, arg: &ArgSchema) {
    out.push_str("    ");

    let is_positional = arg.kind().is_positional();

    // Short flag (or spacing for alignment)
    if let Some(c) = arg.kind().short() {
        out.push_str(&format!(
            "{}, ",
            format!("-{c}").if_supports_color(Stdout, |text| text.green())
        ));
    } else {
        // Add spacing to align with flags that have short options
        out.push_str("    ");
    }

    // Long flag or positional name
    let name = arg.name();
    let is_counted = arg.kind().is_counted();

    if is_positional {
        out.push_str(&format!(
            "{}",
            format!("<{}>", name.to_uppercase()).if_supports_color(Stdout, |text| text.green())
        ));
    } else {
        out.push_str(&format!(
            "{}",
            format!("--{name}").if_supports_color(Stdout, |text| text.green())
        ));

        // Show value placeholder for non-bool, non-counted types
        if !is_counted && !arg.value().is_bool() {
            let placeholder = if let Some(desc) = arg.label() {
                desc.to_uppercase()
            } else if let Some(variants) = arg.value().inner_if_option().enum_variants() {
                variants.join(",")
            } else {
                arg.value().type_identifier().to_uppercase()
            };
            out.push_str(&format!(" <{}>", placeholder));
        }
    }

    // Doc comment
    if let Some(summary) = arg.docs().summary() {
        out.push_str("\n            ");
        out.push_str(summary.trim());
    }

    if is_counted {
        out.push_str("\n            ");
        out.push_str("[can be repeated]");
    }

    out.push('\n');
}

/// Write help for a subcommand.
fn write_subcommand_help(out: &mut String, sub: &Subcommand) {
    out.push_str("    ");

    out.push_str(&format!(
        "{}",
        sub.cli_name()
            .if_supports_color(Stdout, |text| text.green())
    ));

    // Doc comment
    if let Some(summary) = sub.docs().summary() {
        out.push_str("\n            ");
        out.push_str(summary.trim());
    }

    out.push('\n');
}

#[cfg(test)]
mod tests {
    use super::*;
    use facet::Facet;
    use figue_attrs as args;

    /// Common arguments that can be flattened into other structs
    #[derive(Facet)]
    struct CommonArgs {
        /// Enable verbose output
        #[facet(args::named, crate::short = 'v')]
        verbose: bool,

        /// Enable quiet mode
        #[facet(args::named, crate::short = 'q')]
        quiet: bool,
    }

    /// Args struct with flattened common args
    #[derive(Facet)]
    struct ArgsWithFlatten {
        /// Input file
        #[facet(args::positional)]
        input: String,

        /// Common options
        #[facet(flatten)]
        common: CommonArgs,
    }

    #[test]
    fn test_flatten_args_appear_in_help() {
        let schema = Schema::from_shape(ArgsWithFlatten::SHAPE).unwrap();
        let help = generate_help_for_subcommand(&schema, &[], &HelpConfig::default());

        // Flattened fields should appear at top level
        assert!(
            help.contains("--verbose"),
            "help should contain --verbose from flattened CommonArgs"
        );
        assert!(help.contains("-v"), "help should contain -v short flag");
        assert!(
            help.contains("--quiet"),
            "help should contain --quiet from flattened CommonArgs"
        );
        assert!(help.contains("-q"), "help should contain -q short flag");

        // The flattened field name 'common' should NOT appear as a flag
        assert!(
            !help.contains("--common"),
            "help should not show --common as a flag"
        );
    }

    #[test]
    fn test_flatten_docs_preserved() {
        let schema = Schema::from_shape(ArgsWithFlatten::SHAPE).unwrap();
        let help = generate_help_for_subcommand(&schema, &[], &HelpConfig::default());

        // Doc comments from flattened fields should be present
        assert!(
            help.contains("verbose output"),
            "help should contain verbose field doc"
        );
        assert!(
            help.contains("quiet mode"),
            "help should contain quiet field doc"
        );
    }

    /// Arguments for the serve subcommand
    #[derive(Facet)]
    struct ServeArgs {
        /// Port to serve on
        #[facet(args::named)]
        port: u16,

        /// Host to bind to
        #[facet(args::named)]
        host: String,
    }

    /// Top-level command with tuple variant subcommand
    #[derive(Facet)]
    struct TupleVariantArgs {
        /// Subcommand to run
        #[facet(args::subcommand)]
        command: Option<TupleVariantCommand>,
    }

    /// Command enum with tuple variant
    #[derive(Facet)]
    #[repr(u8)]
    #[allow(dead_code)]
    enum TupleVariantCommand {
        /// Start the server
        Serve(ServeArgs),
    }

    #[test]
    fn test_label_overrides_placeholder() {
        #[derive(Facet)]
        struct TDArgs {
            /// Input path
            #[facet(args::named, args::label = "PATH")]
            input: std::path::PathBuf,
        }
        let schema = Schema::from_shape(TDArgs::SHAPE).unwrap();
        let help = generate_help_for_subcommand(&schema, &[], &HelpConfig::default());
        // Only assert on the placeholder to avoid issues with ANSI color codes around the flag name
        assert!(
            help.contains("<PATH>"),
            "help should use custom label placeholder"
        );
    }

    #[test]
    fn test_tuple_variant_fields_not_shown_as_option() {
        let schema = Schema::from_shape(TupleVariantArgs::SHAPE).unwrap();
        // Path contains effective names (e.g., "Serve" not "serve")
        let help =
            generate_help_for_subcommand(&schema, &["Serve".to_string()], &HelpConfig::default());

        // The inner struct's fields should appear
        assert!(
            help.contains("--port"),
            "help should contain --port from ServeArgs"
        );
        assert!(
            help.contains("--host"),
            "help should contain --host from ServeArgs"
        );

        // The tuple field "0" should NOT appear as --0
        assert!(
            !help.contains("--0"),
            "help should NOT show --0 for tuple variant wrapper field"
        );
        assert!(
            !help.contains("SERVEARGS"),
            "help should NOT show SERVEARGS as an option value"
        );
    }

    #[derive(Facet)]
    struct NestedRootArgs {
        #[facet(args::subcommand)]
        command: NestedRootCommand,
    }

    #[derive(Facet)]
    #[repr(u8)]
    #[allow(dead_code)]
    enum NestedRootCommand {
        Home(NestedHomeArgs),
        Cache(NestedCacheArgs),
    }

    #[derive(Facet)]
    struct NestedHomeArgs {
        #[facet(args::subcommand)]
        command: NestedHomeCommand,
    }

    #[derive(Facet)]
    #[repr(u8)]
    #[allow(dead_code)]
    enum NestedHomeCommand {
        Open,
        Show,
    }

    #[derive(Facet)]
    struct NestedCacheArgs {
        #[facet(args::subcommand)]
        command: NestedCacheCommand,
    }

    #[derive(Facet)]
    #[repr(u8)]
    #[allow(dead_code)]
    enum NestedCacheCommand {
        Open,
        Show,
    }

    #[test]
    fn test_help_list_short_is_recursive_with_full_command_paths() {
        let schema = Schema::from_shape(NestedRootArgs::SHAPE).unwrap();
        let output = generate_help_list_for_subcommand(
            &schema,
            &[],
            &HelpConfig {
                program_name: Some("myapp".to_string()),
                ..HelpConfig::default()
            },
            HelpListMode::Short,
        );

        let lines: Vec<&str> = output.lines().collect();
        assert_eq!(
            lines,
            vec![
                "myapp home open",
                "myapp home show",
                "myapp cache open",
                "myapp cache show"
            ]
        );
    }

    #[test]
    fn test_help_list_full_is_recursive_for_leaf_subcommands() {
        let schema = Schema::from_shape(NestedRootArgs::SHAPE).unwrap();
        let output = generate_help_list_for_subcommand(
            &schema,
            &[],
            &HelpConfig {
                program_name: Some("myapp".to_string()),
                ..HelpConfig::default()
            },
            HelpListMode::Full,
        );

        assert!(output.contains("myapp home open"));
        assert!(output.contains("myapp home show"));
        assert!(output.contains("myapp cache open"));
        assert!(output.contains("myapp cache show"));
        assert!(!output.contains("myapp home\n\n"));
        assert!(!output.contains("myapp cache\n\n"));
    }
}
