//! Phase execution with hooks and state management
//!
//! This module provides the core execution engine for lifecycle phases, including
//! hook execution (before/after), state persistence, and deterministic caching.
//!
//! ## Features
//!
//! - **Phase execution**: Run lifecycle phases with command execution
//! - **Hook support**: Execute before/after hooks for phases
//! - **State persistence**: Track phase history and generated files
//! - **Deterministic caching**: Cache key generation for reproducible builds
//! - **Thread-safe**: Support for parallel workspace execution
//!
//! ## Examples
//!
//! ### Running a Phase
//!
//! ```rust,no_run
//! use ggen_core::lifecycle::exec::Context;
//! use ggen_core::lifecycle::exec::run_phase;
//! use ggen_core::lifecycle::Result;
//! use std::path::PathBuf;
//! use std::sync::Arc;
//!
//! # fn main() -> Result<()> {
//! // Create execution context
//! let root = PathBuf::from(".");
//! let make = Arc::new(ggen_core::lifecycle::loader::load_make("make.toml")?);
//! let state_path = root.join(".ggen/state.json");
//! let ctx = Context::new(root, make, state_path, vec![]);
//!
//! // Run a phase
//! run_phase(&ctx, "build")?;
//! # Ok(())
//! # }
//! ```
//!
//! ### Running Multiple Phases
//!
//! ```rust,no_run
//! use ggen_core::lifecycle::exec::run_pipeline;
//! use ggen_core::lifecycle::Result;
//! use std::path::PathBuf;
//! use std::sync::Arc;
//!
//! # fn main() -> Result<()> {
//! // Create context (same as above)
//! let root = PathBuf::from(".");
//! let make = Arc::new(ggen_core::lifecycle::loader::load_make("make.toml")?);
//! let state_path = root.join(".ggen/state.json");
//! let ctx = ggen_core::lifecycle::exec::Context::new(root, make, state_path, vec![]);
//!
//! // Run multiple phases in sequence
//! run_pipeline(&ctx, &vec!["test".to_string(), "lint".to_string(), "build".to_string()])?;
//! # Ok(())
//! # }
//! ```

use super::{cache::cache_key, error::*, loader::load_make, model::*, state::*};
use std::path::{Path, PathBuf};
use std::process::Command;
use std::time::Instant;

use std::collections::HashSet;
use std::sync::{Arc, Mutex};

/// Execution context for lifecycle phases (thread-safe)
pub struct Context {
    pub root: PathBuf,
    pub make: Arc<Make>,
    pub state_path: PathBuf,
    pub env: Vec<(String, String)>,
    /// Hook recursion guard (thread-safe for parallel execution)
    hook_guard: Arc<Mutex<HashSet<String>>>,
    /// Call chain tracking for better error messages (thread-safe)
    /// **DfLSS Fix**: Tracks full call chain to show complete cycle path in error messages
    call_chain: Arc<Mutex<Vec<String>>>,
}

impl Context {
    /// Create new context with empty hook guard and call chain
    pub fn new(
        root: PathBuf, make: Arc<Make>, state_path: PathBuf, env: Vec<(String, String)>,
    ) -> Self {
        Self {
            root,
            make,
            state_path,
            env,
            hook_guard: Arc::new(Mutex::new(HashSet::new())),
            call_chain: Arc::new(Mutex::new(Vec::new())),
        }
    }

    /// Check for hook recursion and add to guard
    ///
    /// **DfLSS Fix**: Now tracks full call chain for better error messages
    fn enter_phase(&self, phase: &str) -> Result<()> {
        let mut guard = self
            .hook_guard
            .lock()
            .map_err(|_| LifecycleError::MutexPoisoned {
                phase: phase.to_string(),
            })?;

        let mut chain = self
            .call_chain
            .lock()
            .map_err(|_| LifecycleError::MutexPoisoned {
                phase: phase.to_string(),
            })?;

        if guard.contains(phase) {
            // Build cycle chain: find where cycle starts and build full path
            let cycle_start = chain.iter().position(|p| p == phase).unwrap_or(0);
            let mut cycle = chain[cycle_start..].to_vec();
            cycle.push(phase.to_string());

            return Err(LifecycleError::hook_recursion_with_chain(
                phase.to_string(),
                cycle,
            ));
        }

        guard.insert(phase.to_string());
        chain.push(phase.to_string());
        Ok(())
    }

    /// Remove phase from guard after completion
    ///
    /// **DfLSS Fix**: Also removes phase from call chain
    fn exit_phase(&self, phase: &str) {
        match self.hook_guard.lock() {
            Ok(mut guard) => {
                guard.remove(phase);
            }
            Err(e) => {
                // Log mutex poisoning for debugging, but don't fail
                // If mutex is poisoned, we're in a panic scenario anyway
                tracing::error!(
                    phase = %phase,
                    error = %e,
                    "CRITICAL: Hook guard mutex poisoned - system may be in inconsistent state"
                );
            }
        }

        // Remove from call chain
        match self.call_chain.lock() {
            Ok(mut chain) => {
                if chain.last().map(|s| s.as_str()) == Some(phase) {
                    chain.pop();
                }
            }
            Err(e) => {
                tracing::error!(
                    phase = %phase,
                    error = %e,
                    "CRITICAL: Call chain mutex poisoned - system may be in inconsistent state"
                );
            }
        }
    }
}

/// Run a single lifecycle phase with hooks
#[tracing::instrument(name = "ggen.lifecycle.phase", skip(ctx), fields(phase = phase_name, duration_ms, status))]
pub fn run_phase(ctx: &Context, phase_name: &str) -> Result<()> {
    tracing::info!(phase = phase_name, "lifecycle phase starting");

    // Check for hook recursion FIRST
    ctx.enter_phase(phase_name)?;

    // Ensure we exit phase even on error
    let result = run_phase_internal(ctx, phase_name);
    ctx.exit_phase(phase_name);

    // Record status in span
    match &result {
        Ok(_) => {
            tracing::Span::current().record("status", "success");
            tracing::info!(phase = phase_name, "lifecycle phase completed");
        }
        Err(e) => {
            tracing::Span::current().record("status", "error");
            tracing::error!(phase = phase_name, error = %e, "lifecycle phase failed");
        }
    }

    result
}

/// Internal phase execution (called after recursion check)
fn run_phase_internal(ctx: &Context, phase_name: &str) -> Result<()> {
    let phase = ctx
        .make
        .lifecycle
        .get(phase_name)
        .ok_or_else(|| LifecycleError::phase_not_found(phase_name))?;

    // Run before hooks first
    run_before_hooks(ctx, phase_name)?;

    // Print phase start message for CLI output (after hooks)
    ggen_utils::alert_info!(&format!("Running phase: {}", phase_name));

    // Get commands for this phase using new Phase::commands() method
    let cmds = phase.commands();
    if cmds.is_empty() {
        tracing::warn!(phase = %phase_name, "Phase has no commands");
        return Ok(());
    }

    // Generate cache key
    let key = cache_key(phase_name, &cmds, &ctx.env, &[]);

    // Execute phase commands
    let started = current_time_ms()?;
    let timer = Instant::now();

    tracing::info!(phase = %phase_name, "Starting phase execution");
    for cmd in &cmds {
        tracing::debug!(phase = %phase_name, command = %cmd, "Executing command");
        execute_command(cmd, &ctx.root, &ctx.env)?;
    }

    let duration = timer.elapsed().as_millis();

    // Record duration in parent span
    tracing::Span::current().record("duration_ms", duration);

    tracing::info!(
        phase = %phase_name,
        duration_ms = duration,
        "Phase completed successfully"
    );

    // Update state with validation (poka-yoke)
    let mut state = load_state(&ctx.state_path)?;
    state.record_run(phase_name.to_string(), started, duration, true);
    state.add_cache_key(phase_name.to_string(), key);

    // Validate state before saving (poka-yoke)
    use super::state_validation::ValidatedLifecycleState;
    let validated_state = ValidatedLifecycleState::new(state.clone())
        .map_err(|e| LifecycleError::Other(format!("State validation failed: {}", e)))?;

    save_state(&ctx.state_path, validated_state.state())?;

    // Run after hooks
    run_after_hooks(ctx, phase_name)?;

    Ok(())
}

/// Run a pipeline of phases sequentially
#[tracing::instrument(name = "ggen.lifecycle.pipeline", skip(ctx), fields(phases = ?phases, phase_count = phases.len()))]
pub fn run_pipeline(ctx: &Context, phases: &[String]) -> Result<()> {
    tracing::info!(phases = ?phases, "starting lifecycle pipeline");
    if let Some(workspaces) = &ctx.make.workspace {
        // Check if parallel execution is requested
        let parallel = phases
            .first()
            .and_then(|p| ctx.make.lifecycle.get(p))
            .and_then(|ph| ph.parallel)
            .unwrap_or(false);

        if parallel {
            // PRODUCTION FIX: Bounded thread pool to prevent resource exhaustion
            use rayon::prelude::*;
            use rayon::ThreadPoolBuilder;

            // Limit to max 8 threads to prevent fork bomb with many workspaces
            let max_threads = 8.min(num_cpus::get());
            let pool = ThreadPoolBuilder::new()
                .num_threads(max_threads)
                .build()
                .map_err(|e| {
                    LifecycleError::Other(format!("Failed to create thread pool: {}", e))
                })?;

            let results: Vec<Result<()>> = pool.install(|| {
                workspaces
                    .par_iter()
                    .map(|(ws_name, workspace)| {
                        tracing::info!(workspace = %ws_name, "Processing workspace");
                        let ws_ctx = create_workspace_context(
                            &ctx.root, ws_name, workspace, &ctx.make, &ctx.env,
                        )?;

                        for phase in phases {
                            run_phase(&ws_ctx, phase)?;
                        }
                        Ok(())
                    })
                    .collect()
            });

            // Check for errors
            for result in results {
                result?;
            }
        } else {
            // Sequential execution
            for (ws_name, workspace) in workspaces {
                tracing::info!(workspace = %ws_name, "Processing workspace");
                let ws_ctx =
                    create_workspace_context(&ctx.root, ws_name, workspace, &ctx.make, &ctx.env)?;

                for phase in phases {
                    run_phase(&ws_ctx, phase)?;
                }
            }
        }
    } else {
        // No workspaces, run directly
        for phase in phases {
            run_phase(ctx, phase)?;
        }
    }

    Ok(())
}

/// Create workspace context with security validation
///
/// SECURITY: Validates workspace paths to prevent directory traversal attacks
fn create_workspace_context(
    root: &Path, ws_name: &str, workspace: &super::model::Workspace, root_make: &Arc<Make>,
    env: &[(String, String)],
) -> Result<Context> {
    let ws_path = root.join(&workspace.path);

    // SECURITY: Canonicalize and validate paths to prevent traversal
    let canonical_root = root
        .canonicalize()
        .map_err(|e| LifecycleError::Other(format!("Failed to canonicalize root path: {}", e)))?;

    let canonical_ws = ws_path.canonicalize().map_err(|e| {
        LifecycleError::Other(format!(
            "Failed to canonicalize workspace '{}' path: {}",
            ws_name, e
        ))
    })?;

    // SECURITY: Ensure workspace path is within project root
    if !canonical_ws.starts_with(&canonical_root) {
        return Err(LifecycleError::Other(format!(
            "Security violation: workspace '{}' path '{}' is outside project root",
            ws_name, workspace.path
        )));
    }

    let ws_make_path = canonical_ws.join("make.toml");

    // Load workspace-specific make.toml if it exists, otherwise use root
    let ws_make = if ws_make_path.exists() {
        Arc::new(load_make(&ws_make_path)?)
    } else {
        Arc::clone(root_make)
    };

    let ws_state_path = canonical_ws.join(".ggen/state.json");

    Ok(Context::new(
        canonical_ws,
        ws_make,
        ws_state_path,
        env.to_vec(),
    ))
}

/// Run before hooks for a phase
///
/// **DfLSS Fix**: Now supports both predefined phases (backward compatibility)
/// and custom phases via dynamic HashMap lookup.
fn run_before_hooks(ctx: &Context, phase_name: &str) -> Result<()> {
    if let Some(hooks) = &ctx.make.hooks {
        // Global before_all
        if let Some(before_all) = &hooks.before_all {
            for hook_phase in before_all {
                run_phase(ctx, hook_phase)?;
            }
        }

        // Phase-specific before hooks
        // First check explicit fields (backward compatibility)
        let before_hooks = match phase_name {
            "init" => hooks.before_init.as_ref(),
            "setup" => hooks.before_setup.as_ref(),
            "build" => hooks.before_build.as_ref(),
            "test" => hooks.before_test.as_ref(),
            "deploy" => hooks.before_deploy.as_ref(),
            _ => None,
        };

        // If not found in explicit fields, try dynamic lookup (supports custom phases)
        let hooks_list = before_hooks.or_else(|| {
            let hook_key = format!("before_{}", phase_name);
            hooks.phase_hooks.get(&hook_key)
        });

        if let Some(hooks_list) = hooks_list {
            for hook_phase in hooks_list {
                run_phase(ctx, hook_phase)?;
            }
        }
    }

    Ok(())
}

/// Run after hooks for a phase
///
/// **DfLSS Fix**: Now supports both predefined phases (backward compatibility)
/// and custom phases via dynamic HashMap lookup.
fn run_after_hooks(ctx: &Context, phase_name: &str) -> Result<()> {
    if let Some(hooks) = &ctx.make.hooks {
        // Phase-specific after hooks
        // First check explicit fields (backward compatibility)
        let after_hooks = match phase_name {
            "init" => hooks.after_init.as_ref(),
            "setup" => hooks.after_setup.as_ref(),
            "build" => hooks.after_build.as_ref(),
            "test" => hooks.after_test.as_ref(),
            "deploy" => hooks.after_deploy.as_ref(),
            _ => None,
        };

        // If not found in explicit fields, try dynamic lookup (supports custom phases)
        let hooks_list = after_hooks.or_else(|| {
            let hook_key = format!("after_{}", phase_name);
            hooks.phase_hooks.get(&hook_key)
        });

        if let Some(hooks_list) = hooks_list {
            for hook_phase in hooks_list {
                run_phase(ctx, hook_phase)?;
            }
        }

        // Global after_all
        if let Some(after_all) = &hooks.after_all {
            for hook_phase in after_all {
                run_phase(ctx, hook_phase)?;
            }
        }
    }

    Ok(())
}

/// Execute a shell command with streaming output and timeout
///
/// PRODUCTION FIX: Commands now have 5-minute timeout to prevent hung processes
fn execute_command(cmd: &str, cwd: &Path, env: &[(String, String)]) -> Result<()> {
    use std::time::Duration;

    let mut command = if cfg!(target_os = "windows") {
        let mut c = Command::new("cmd");
        c.arg("/C");
        c
    } else {
        let mut c = Command::new("sh");
        c.arg("-c"); // Changed from -lc to -c (don't load profile - faster)
        c
    };

    command.current_dir(cwd).arg(cmd);

    for (key, value) in env {
        command.env(key, value);
    }

    // PRODUCTION FIX: Stream output to user (80/20 - visibility over capture)
    let mut child = command
        .stdout(std::process::Stdio::inherit()) // Show stdout in real-time
        .stderr(std::process::Stdio::inherit()) // Show stderr in real-time
        .spawn()
        .map_err(|e| LifecycleError::command_spawn("unknown", cmd, e))?;

    // PRODUCTION FIX: Implement timeout to prevent hung processes
    /// Default command execution timeout: 5 minutes
    const DEFAULT_COMMAND_TIMEOUT_SECS: u64 = 300;

    let timeout = Duration::from_secs(DEFAULT_COMMAND_TIMEOUT_SECS);
    let start = Instant::now();

    loop {
        match child
            .try_wait()
            .map_err(|e| LifecycleError::command_spawn("unknown", cmd, e))?
        {
            Some(status) => {
                if !status.success() {
                    let exit_code = status.code().unwrap_or(-1);
                    return Err(LifecycleError::command_failed(
                        "unknown",
                        cmd,
                        exit_code,
                        "Command output shown above".to_string(),
                    ));
                }
                return Ok(());
            }
            None if start.elapsed() > timeout => {
                // Timeout exceeded - kill the process
                let _ = child.kill(); // Ignore kill errors
                return Err(LifecycleError::Other(format!(
                    "Command timeout after {}s: {}",
                    timeout.as_secs(),
                    cmd
                )));
            }
            None => {
                // Still running, sleep briefly and check again
                std::thread::sleep(Duration::from_millis(100));
            }
        }
    }
}

/// Get current time in milliseconds since epoch
fn current_time_ms() -> Result<u128> {
    std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| d.as_millis())
        .map_err(|_| LifecycleError::Other("System clock error: time is before UNIX epoch".into()))
}

// Unit tests removed - covered by integration_test.rs:
// - test_phase_commands_extraction (tests both single and multiple commands)
// This provides better coverage with actual make.toml parsing
