use serde::Serialize;
use std::path::PathBuf;

use crate::component::{self, Component};
use crate::config::{is_json_input, parse_bulk_ids};
use crate::error::{Error, Result};
use crate::module;
use crate::output::{BulkResult, BulkSummary, ItemOutcome};
use crate::paths;
use crate::permissions;
use crate::ssh::execute_local_command_in_dir;
use crate::utils::command::CapturedOutput;
use crate::utils::shell;

// === Build Command Resolution ===

#[derive(Debug, Clone)]
pub enum ResolvedBuildCommand {
    ComponentDefined(String),
    ModuleProvided {
        command: String,
        source: String,
    },
    LocalScript {
        command: String,
        script_name: String,
    },
}

impl ResolvedBuildCommand {
    pub fn command(&self) -> &str {
        match self {
            ResolvedBuildCommand::ComponentDefined(cmd) => cmd,
            ResolvedBuildCommand::ModuleProvided { command, .. } => command,
            ResolvedBuildCommand::LocalScript { command, .. } => command,
        }
    }
}

/// Resolve build command for a component using the following priority:
/// 1. Explicit component.build_command (always wins)
/// 2. Module's bundled script (module.build.module_script)
/// 3. Local script matching module's script_names pattern
pub fn resolve_build_command(component: &Component) -> Result<ResolvedBuildCommand> {
    // 1. Explicit component override takes precedence
    if let Some(cmd) = &component.build_command {
        return Ok(ResolvedBuildCommand::ComponentDefined(cmd.clone()));
    }

    // 2. Check module for bundled script or local script patterns
    if let Some(modules) = &component.modules {
        for module_id in modules.keys() {
            if let Ok(module) = module::load_module(module_id) {
                if let Some(build) = &module.build {
                    // Check for module's bundled script
                    let bundled = build.module_script.as_ref().and_then(|module_script| {
                        paths::module(module_id).ok().and_then(|module_dir| {
                            let script_path = module_dir.join(module_script);
                            script_path.exists().then(|| {
                                let quoted_path =
                                    shell::quote_path(&script_path.to_string_lossy());
                                let command = build
                                    .command_template
                                    .as_ref()
                                    .map(|t| t.replace("{{script}}", &quoted_path))
                                    .unwrap_or_else(|| format!("sh {}", quoted_path));
                                ResolvedBuildCommand::ModuleProvided {
                                    command,
                                    source: format!("{}:{}", module_id, module_script),
                                }
                            })
                        })
                    });
                    if let Some(result) = bundled {
                        return Ok(result);
                    }

                    // Check for local script matching module's script_names
                    let local_path = PathBuf::from(&component.local_path);
                    for script_name in &build.script_names {
                        let local_script = local_path.join(script_name);
                        if local_script.exists() {
                            let command = build
                                .command_template
                                .as_ref()
                                .map(|t| t.replace("{{script}}", script_name))
                                .unwrap_or_else(|| format!("sh {}", script_name));
                            return Ok(ResolvedBuildCommand::LocalScript {
                                command,
                                script_name: script_name.clone(),
                            });
                        }
                    }
                }
            }
        }
    }

    // Check if any module provides build (makes build_command optional)
    if module::module_provides_build(component) {
        // Module provides build config but no matching scripts found
        Err(Error::other(format!(
            "Component '{}' links a module with build support, but no build script was found.\n\
             Expected: module's bundled script OR local script matching module pattern.\n\
             Check module installation or add a local build.sh to the component directory.",
            component.id
        )))
    } else {
        // No modules with build support - explicit buildCommand required
        Err(Error::other(format!(
            "Component '{}' has no build configuration. Either:\n\
             - Configure buildCommand: homeboy component set {} --json '{{\"buildCommand\": \"<command>\"}}'\n\
             - Link a module with build support: homeboy component set {} --json '{{\"modules\": {{\"wordpress\": {{}}}}}}'",
            component.id, component.id, component.id
        )))
    }
}

// === Public API ===

#[derive(Debug, Clone, Serialize)]
pub struct BuildOutput {
    pub command: String,
    pub component_id: String,
    pub build_command: String,
    #[serde(flatten)]
    pub output: CapturedOutput,
    pub success: bool,
}

#[derive(Debug, Serialize)]
#[serde(untagged)]
pub enum BuildResult {
    Single(BuildOutput),
    Bulk(BulkResult<BuildOutput>),
}

/// Run build for one or more components.
///
/// Accepts either:
/// - A single component ID: "extrachill-api"
/// - A JSON spec: {"componentIds": ["api", "users"]}
pub fn run(input: &str) -> Result<(BuildResult, i32)> {
    if is_json_input(input) {
        run_bulk(input)
    } else {
        run_single(input)
    }
}

/// Build a component for deploy context.
/// Returns (exit_code, error_message) - None error means success.
///
/// Shell execution is required for build commands by design:
/// - Build commands execute shell scripts (bash, sh, npm, composer, etc.)
/// - Scripts use shell features (pipes, redirects, environment variables)
/// - Examples: "bash {{script}}", "sh build.sh", "npm run build"
/// - Build processes often require chaining with &&, ||, ;
/// - Direct execution cannot handle shell scripts or shell features
///
/// See executor.rs for detailed execution strategy decision tree
pub fn build_component(component: &component::Component) -> (Option<i32>, Option<String>) {
    // Validate local_path before attempting build
    let validated_path = match component::validate_local_path(component) {
        Ok(p) => p,
        Err(e) => return (Some(1), Some(format_path_validation_error(component, &e))),
    };

    let resolved = match resolve_build_command(component) {
        Ok(r) => r,
        Err(e) => return (Some(1), Some(e.to_string())),
    };

    let build_cmd = resolved.command().to_string();

    // Fix local permissions before build to ensure zip has correct permissions
    let local_path_str = validated_path.to_string_lossy().to_string();
    permissions::fix_local_permissions(&local_path_str);

    // Get module path env vars for build command (matches pre-build script behavior)
    let env_vars = get_build_env_vars(component);
    let env_refs: Vec<(&str, &str)> = env_vars
        .iter()
        .map(|(k, v)| (k.as_str(), v.as_str()))
        .collect();

    let output = execute_local_command_in_dir(
        &build_cmd,
        Some(&local_path_str),
        if env_refs.is_empty() { None } else { Some(&env_refs) },
    );

    if output.success {
        (Some(output.exit_code), None)
    } else {
        (
            Some(output.exit_code),
            Some(format_build_error(&component.id, &build_cmd, &local_path_str, output.exit_code, &output.stderr, &output.stdout)),
        )
    }
}

/// Format a path validation error with build context.
fn format_path_validation_error(component: &component::Component, error: &Error) -> String {
    format!(
        "Build failed for component '{}':\n  {}\n\nHint: Update local_path with:\n  homeboy component set {} --local-path \"/path/to/component\"",
        component.id,
        error.message,
        component.id
    )
}

/// Format a build error message with context from stderr/stdout.
/// Only includes universal POSIX exit code hints - Homeboy is technology-agnostic.
fn format_build_error(
    component_id: &str,
    build_cmd: &str,
    working_dir: &str,
    exit_code: i32,
    stderr: &str,
    stdout: &str,
) -> String {
    // Get useful output (prefer stderr, fall back to stdout)
    let output_text = if stderr.trim().is_empty() { stdout } else { stderr };

    // Get last 15 lines for context
    let tail: Vec<&str> = output_text.lines().rev().take(15).collect();
    let output_tail: String = tail.into_iter().rev().collect::<Vec<_>>().join("\n");

    // Translate universal POSIX exit codes only (no tool-specific hints)
    let hint = match exit_code {
        127 => "\nHint: Command not found. Check that the build command and its dependencies are installed and in PATH.",
        126 => "\nHint: Permission denied. Check file permissions on the build script.",
        _ => "",
    };

    let mut msg = format!(
        "Build failed for '{}' (exit code {}).\n  Command: {}\n  Working directory: {}",
        component_id, exit_code, build_cmd, working_dir
    );

    if !output_tail.is_empty() {
        msg.push_str("\n\n--- Build output (last 15 lines) ---\n");
        msg.push_str(&output_tail);
        msg.push_str("\n--- End of output ---");
    }

    if !hint.is_empty() {
        msg.push_str(hint);
    }

    msg
}

// === Internal implementation ===

fn run_single(component_id: &str) -> Result<(BuildResult, i32)> {
    let (output, exit_code) = execute_build(component_id)?;
    Ok((BuildResult::Single(output), exit_code))
}

fn run_bulk(json_spec: &str) -> Result<(BuildResult, i32)> {
    let input = parse_bulk_ids(json_spec)?;

    let mut results = Vec::with_capacity(input.component_ids.len());
    let mut succeeded = 0usize;
    let mut failed = 0usize;

    for id in &input.component_ids {
        match execute_build(id) {
            Ok((output, _)) => {
                if output.success {
                    succeeded += 1;
                } else {
                    failed += 1;
                }
                results.push(ItemOutcome {
                    id: id.clone(),
                    result: Some(output),
                    error: None,
                });
            }
            Err(e) => {
                failed += 1;
                results.push(ItemOutcome {
                    id: id.clone(),
                    result: None,
                    error: Some(e.to_string()),
                });
            }
        }
    }

    let exit_code = if failed > 0 { 1 } else { 0 };

    Ok((
        BuildResult::Bulk(BulkResult {
            action: "build".to_string(),
            results,
            summary: BulkSummary {
                total: succeeded + failed,
                succeeded,
                failed,
            },
        }),
        exit_code,
    ))
}

fn execute_build(component_id: &str) -> Result<(BuildOutput, i32)> {
    let comp = component::load(component_id)?;

    // Validate local_path before attempting build
    let validated_path = component::validate_local_path(&comp)?;
    let local_path_str = validated_path.to_string_lossy().to_string();

    let resolved = resolve_build_command(&comp)?;
    let build_cmd = resolved.command().to_string();

    // Run pre-build script if module provides one
    if let Some((exit_code, stderr)) = run_pre_build_scripts(&comp)? {
        if exit_code != 0 {
            return Ok((
                BuildOutput {
                    command: "build.run".to_string(),
                    component_id: component_id.to_string(),
                    build_command: build_cmd,
                    output: CapturedOutput::new(String::new(), stderr),
                    success: false,
                },
                exit_code,
            ));
        }
    }

    // Fix local permissions before build to ensure zip has correct permissions
    permissions::fix_local_permissions(&local_path_str);

    // Get module path env vars for build command (matches pre-build script behavior)
    let env_vars = get_build_env_vars(&comp);
    let env_refs: Vec<(&str, &str)> = env_vars
        .iter()
        .map(|(k, v)| (k.as_str(), v.as_str()))
        .collect();

    let cmd_output = execute_local_command_in_dir(
        &build_cmd,
        Some(&local_path_str),
        if env_refs.is_empty() { None } else { Some(&env_refs) },
    );

    Ok((
        BuildOutput {
            command: "build.run".to_string(),
            component_id: component_id.to_string(),
            build_command: build_cmd,
            output: CapturedOutput::new(cmd_output.stdout, cmd_output.stderr),
            success: cmd_output.success,
        },
        cmd_output.exit_code,
    ))
}

/// Run pre-build scripts from all configured modules.
/// Returns Some((exit_code, stderr)) if any script fails, None if all pass or no scripts.
fn run_pre_build_scripts(comp: &Component) -> Result<Option<(i32, String)>> {
    let modules = match &comp.modules {
        Some(m) => m,
        None => return Ok(None),
    };

    for module_id in modules.keys() {
        let module = match module::load_module(module_id) {
            Ok(m) => m,
            Err(_) => continue,
        };

        let build_config = match &module.build {
            Some(b) => b,
            None => continue,
        };

        let pre_build_script = match &build_config.pre_build_script {
            Some(s) => s,
            None => continue,
        };

        let module_path = paths::module(module_id)?;
        let script_path = module_path.join(pre_build_script);

        if !script_path.exists() {
            continue;
        }

        let env: [(&str, &str); 3] = [
            ("HOMEBOY_MODULE_PATH", &module_path.to_string_lossy()),
            ("HOMEBOY_COMPONENT_PATH", &comp.local_path),
            ("HOMEBOY_PLUGIN_PATH", &comp.local_path),
        ];

        let output = execute_local_command_in_dir(
            &script_path.to_string_lossy(),
            None,
            Some(&env),
        );

        if !output.success {
            let combined = if output.stderr.is_empty() {
                output.stdout
            } else {
                output.stderr
            };
            return Ok(Some((output.exit_code, combined)));
        }
    }

    Ok(None)
}

/// Get environment variables for build commands (module path, component path).
/// Matches the env vars passed to pre-build scripts for consistency.
fn get_build_env_vars(comp: &Component) -> Vec<(String, String)> {
    let mut env = Vec::new();

    if let Some(modules) = &comp.modules {
        for module_id in modules.keys() {
            if let Ok(module) = module::load_module(module_id) {
                if module.build.is_some() {
                    if let Ok(module_path) = paths::module(module_id) {
                        let module_path_str = module_path.to_string_lossy().to_string();
                        env.push(("HOMEBOY_MODULE_PATH".to_string(), module_path_str));
                        env.push(("HOMEBOY_COMPONENT_PATH".to_string(), comp.local_path.clone()));
                        env.push(("HOMEBOY_PLUGIN_PATH".to_string(), comp.local_path.clone()));
                        break; // Use first module with build config
                    }
                }
            }
        }
    }

    env
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn is_json_input_detects_json() {
        assert!(is_json_input(r#"{"componentIds": ["a"]}"#));
        assert!(is_json_input(r#"  {"componentIds": ["a"]}"#));
        assert!(!is_json_input("extrachill-api"));
        assert!(!is_json_input("some-component-id"));
    }
}
