use std::path::PathBuf;

use clap::{Parser, Subcommand};

#[derive(Debug, Parser)]
#[command(
    name = "render-session",
    about = "render-session: MCP server + HTTP viewer for AI session output",
    version
)]
struct Cli {
    #[command(subcommand)]
    command: Commands,
}

#[derive(Debug, Subcommand)]
enum Commands {
    /// Run the MCP stdio server (default entry point from .mcp.json).
    /// Delegates to the render-session-mcp binary found next to this executable.
    Mcp,

    /// Start the HTTP viewer server
    Serve {
        /// Port to listen on
        #[arg(long, default_value = "8000")]
        port: u16,

        /// Project directory to serve content from
        #[arg(long)]
        dir: String,

        /// Phase 4 (c): viewer-internal watcher tick (seconds). When set, the viewer
        /// runs `auto_capture_once` every <tick> seconds in the background. None
        /// disables the watcher (Phase 1+ default behavior preserved).
        #[arg(long)]
        watch_tick: Option<u64>,
    },

    /// Generate list.json and render-lanes.md from config
    Gen {
        /// Project directory containing .render-session.yaml
        #[arg(long)]
        project: String,
    },

    /// Write a report from stdin
    Report {
        /// Directory to write the report into
        #[arg(long)]
        dir: String,

        /// Report title
        #[arg(long)]
        title: String,
    },

    /// Capture session jsonl from Claude Code projects and emit to render-site/.
    Capture {
        /// Project directory (where render-site/ lives)
        #[arg(long)]
        project: std::path::PathBuf,

        /// Capture mode: session | recent | both (default: both)
        #[arg(long, default_value = "both")]
        mode: String,

        /// Claude Code project slug (default: derived from project path via canonicalize + replace('/', "-"))
        #[arg(long)]
        slug: Option<String>,

        /// Recent mode: number of turns to emit (default: 5)
        #[arg(long, default_value_t = 5)]
        n: usize,

        /// Recent mode: include turns without visual artifact (default: false)
        #[arg(long, default_value_t = false)]
        all: bool,
    },

    /// Show and diagnose the effective configuration.
    Config {
        #[command(subcommand)]
        subcommand: ConfigSubcommand,
    },
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    tracing_subscriber::fmt::Subscriber::builder()
        .with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
        .init();
    let cli = Cli::parse();
    match cli.command {
        Commands::Mcp => run_mcp().await,
        Commands::Serve {
            port,
            dir,
            watch_tick,
        } => run_serve(port, dir, watch_tick).await,
        Commands::Gen { project } => run_gen(project).await,
        Commands::Report { dir, title } => run_report(dir, title).await,
        Commands::Capture {
            project,
            mode,
            slug,
            n,
            all,
        } => run_capture(project, mode, slug, n, all).await,
        Commands::Config { subcommand } => run_config(subcommand).await,
    }
}

async fn run_mcp() -> anyhow::Result<()> {
    // B-plan: delegate to render-session-mcp binary.
    // Prefer the sibling binary in the same install directory; fall back to PATH.
    let exe_dir = std::env::current_exe()
        .ok()
        .and_then(|p| p.parent().map(|d| d.to_path_buf()));
    let mcp_path = exe_dir
        .map(|d| d.join("render-session-mcp"))
        .filter(|p| p.exists())
        .unwrap_or_else(|| std::path::PathBuf::from("render-session-mcp"));

    let status = tokio::process::Command::new(&mcp_path)
        .stdin(std::process::Stdio::inherit())
        .stdout(std::process::Stdio::inherit())
        .stderr(std::process::Stdio::inherit())
        .status()
        .await
        .map_err(|e| {
            tracing::error!(
                error = %e,
                path = %mcp_path.display(),
                "failed to spawn render-session-mcp"
            );
            e
        })?;

    if !status.success() {
        let code = status.code().unwrap_or(-1);
        tracing::error!(exit_code = code, "render-session-mcp exited non-zero");
        anyhow::bail!("render-session-mcp exited with {}", status);
    }
    Ok(())
}

async fn run_serve(port: u16, dir: String, watch_tick: Option<u64>) -> anyhow::Result<()> {
    // Spawn the parent-death watch loop so that if the MCP server (our parent)
    // dies, this serve process self-exits within ~5 seconds.
    tokio::spawn(render_session_core::child::watch_parent_death());
    let watch_tick_dur = watch_tick.map(|t| {
        let clamped = if t == 0 {
            tracing::warn!("watch_tick=0 not allowed, clamping to 1");
            1
        } else {
            t
        };
        std::time::Duration::from_secs(clamped)
    });
    render_session_core::serve::run(port, std::path::PathBuf::from(dir), watch_tick_dur).await?;
    Ok(())
}

async fn run_gen(project: String) -> anyhow::Result<()> {
    use anyhow::Context as _;
    let project_path = std::path::PathBuf::from(&project);

    let loaded =
        render_session_core::config::load(&project_path).context("gen: failed to load config")?;

    let list_path = render_session_core::gen::emit_list_json(&loaded.config, &project_path)
        .map_err(|e| {
            tracing::error!(error = %e, project = %project, "emit_list_json failed");
            e
        })?;
    tracing::info!(file = %list_path.display(), "list.json emitted");
    println!("{}", list_path.display());

    let rules_path = render_session_core::gen::emit_rules_md(&loaded.config, &project_path)
        .map_err(|e| {
            tracing::error!(error = %e, project = %project, "emit_rules_md failed");
            e
        })?;
    tracing::info!(file = %rules_path.display(), "render-lanes.md emitted");
    println!("{}", rules_path.display());
    Ok(())
}

async fn run_report(dir: String, title: String) -> anyhow::Result<()> {
    use tokio::io::AsyncReadExt as _;
    let mut body = String::new();
    tokio::io::stdin()
        .read_to_string(&mut body)
        .await
        .map_err(|e| {
            tracing::error!(error = %e, "failed to read stdin for report");
            e
        })?;
    let project_dir = std::path::Path::new(&dir);
    // Resolve target dir from config; warn + legacy fallback on load error.
    let target_dir = match render_session_core::config::load(project_dir) {
        Ok(loaded) => {
            render_session_core::config::resolve_category_dir(&loaded, project_dir, "reports")
        }
        Err(e) => {
            tracing::warn!(
                dir = %dir,
                error = %e,
                "report: config load failed, using legacy path"
            );
            project_dir.join("render-site").join("reports")
        }
    };
    let path = render_session_core::writer::write_report(&target_dir, &title, &body)
        .await
        .map_err(|e| {
            tracing::error!(error = %e, dir = %dir, title = %title, "write_report failed");
            e
        })?;
    println!("{}", path.display());
    Ok(())
}

async fn run_capture(
    project: std::path::PathBuf,
    mode: String,
    slug: Option<String>,
    n: usize,
    all: bool,
) -> anyhow::Result<()> {
    let claude_slug = match slug {
        Some(s) => s,
        None => derive_slug(&project).map_err(|e| {
            tracing::error!(project = %project.display(), error = %e, "derive_slug failed");
            e
        })?,
    };
    let only_with_visual = !all;

    // Phase 4d: load config via unified figment stack; yaml absent or parse error →
    // warn + Config::default() fallback so capture always continues (invariant #16).
    let config = match render_session_core::config::load(&project) {
        Ok(loaded) => loaded.config,
        Err(e) => {
            tracing::warn!(
                project = %project.display(),
                error = %e,
                "capture: config load failed, using default"
            );
            render_session_core::config::Config::default()
        }
    };
    let filter_chain_cfg = config
        .categories
        .get("recent")
        .and_then(|c| c.filter.as_ref());
    let filter_registry: Option<render_session_core::filters::FilterRegistry> =
        filter_chain_cfg.map(|_| render_session_core::filters::FilterRegistry::with_builtins());
    let filter_arg = match (filter_registry.as_ref(), filter_chain_cfg) {
        (Some(reg), Some(cfg)) => Some((reg, cfg)),
        _ => None,
    };

    match mode.as_str() {
        "session" => {
            // session mode: filter not applied (single-item capture).
            if let Some(p) =
                render_session_core::sources::session::capture_session(&project, &claude_slug)
                    .map_err(|e| {
                        tracing::error!(error = %e, "capture_session failed");
                        e
                    })?
            {
                println!("{}", p.display());
            }
        }
        "recent" => {
            for p in render_session_core::sources::session::capture_recent(
                &project,
                &claude_slug,
                n,
                only_with_visual,
                filter_arg,
            )
            .map_err(|e| {
                tracing::error!(error = %e, "capture_recent failed");
                e
            })? {
                println!("{}", p.display());
            }
        }
        "both" => {
            // session: filter not applied
            if let Some(p) =
                render_session_core::sources::session::capture_session(&project, &claude_slug)
                    .map_err(|e| {
                        tracing::error!(error = %e, "capture_session failed");
                        e
                    })?
            {
                println!("{}", p.display());
            }
            // recent: filter applied if configured
            for p in render_session_core::sources::session::capture_recent(
                &project,
                &claude_slug,
                n,
                only_with_visual,
                filter_arg,
            )
            .map_err(|e| {
                tracing::error!(error = %e, "capture_recent failed");
                e
            })? {
                println!("{}", p.display());
            }
        }
        other => {
            anyhow::bail!("invalid mode {:?}, expected session|recent|both", other);
        }
    }
    Ok(())
}

/// Derive the Claude Code project slug from a project directory path.
///
/// Canonicalizes the path and replaces every `/` with `-`, matching legacy
/// `render-session.py:41`.  Intended for macOS/Linux only; Windows path
/// separator handling is deferred to Phase 6+.
fn derive_slug(project: &std::path::Path) -> anyhow::Result<String> {
    let canonical = project.canonicalize().map_err(|e| {
        tracing::error!(project = %project.display(), error = %e, "canonicalize failed");
        e
    })?;
    Ok(canonical.to_string_lossy().replace('/', "-"))
}

// ---------------------------------------------------------------------------
// Commands::Config subcommand definitions
// ---------------------------------------------------------------------------

/// Subcommands for `render-session config`.
#[derive(Debug, Subcommand)]
enum ConfigSubcommand {
    /// Print the effective merged configuration as YAML.
    ///
    /// With `--origin`, each leaf value is annotated with the config layer
    /// (file path or env var) that last set it.
    Show {
        /// Project directory whose config should be resolved.
        #[arg(long)]
        project: PathBuf,

        /// Annotate each value with its provenance (origin layer).
        #[arg(long, default_value_t = false)]
        origin: bool,
    },

    /// List the config layer paths and active env overrides.
    ///
    /// Shows which config files were found (Loaded/Absent) and which
    /// `RENDER_SESSION_*` environment variables are active.
    Info {
        /// Project directory whose config should be resolved.
        /// Defaults to the current working directory.
        #[arg(long)]
        project: Option<PathBuf>,
    },

    /// Run health checks on the config stack.
    ///
    /// Checks that layer files exist, that `RENDER_SESSION_CONFIG` (if set)
    /// points to a readable file, and that env overrides parse successfully.
    /// Exits with code 0 when all checks pass, 1 otherwise.
    Doctor {
        /// Project directory whose config should be diagnosed.
        /// Defaults to the current working directory.
        #[arg(long)]
        project: Option<PathBuf>,
    },
}

// ---------------------------------------------------------------------------
// DoctorCheck — structured diagnostic result
// ---------------------------------------------------------------------------

/// A single named check result produced by `config doctor`.
struct DoctorCheck {
    /// Short name identifying this check.
    name: String,
    /// Whether the check passed.
    ok: bool,
    /// Human-readable detail message (present for both pass and fail).
    message: Option<String>,
}

// ---------------------------------------------------------------------------
// Config handler helpers
// ---------------------------------------------------------------------------

/// Dispatch `config <subcommand>` to the appropriate handler.
///
/// # Arguments
/// - `sub`: which `ConfigSubcommand` to execute.
///
/// # Returns
/// `Ok(())` on success, or an `anyhow::Error` on failure.
async fn run_config(sub: ConfigSubcommand) -> anyhow::Result<()> {
    match sub {
        ConfigSubcommand::Show { project, origin } => handle_config_show(project, origin).await,
        ConfigSubcommand::Info { project } => handle_config_info(project).await,
        ConfigSubcommand::Doctor { project } => handle_config_doctor(project).await,
    }
}

/// Print the effective merged configuration as YAML.
///
/// When `origin` is `true`, each leaf value is annotated with a `# origin:`
/// comment derived from `LoadedConfig::origin_of`.  The `flatten_keys` helper
/// walks the `serde_yaml::Value` tree to enumerate all dotted keys so that
/// provenance can be queried per-key.
///
/// # Arguments
/// - `project`: project root directory.
/// - `origin`: if `true`, annotate each value line with its origin layer.
///
/// # Errors
/// Returns an error if the config fails to load or if YAML serialization
/// of the effective config fails.
async fn handle_config_show(project: PathBuf, origin: bool) -> anyhow::Result<()> {
    use anyhow::Context as _;

    let loaded = render_session_core::config::load(&project)
        .context("config show: failed to load config")?;

    let yaml = serde_yaml::to_string(&loaded.config)
        .context("config show: failed to serialize config as YAML")?;

    if !origin {
        print!("{yaml}");
        return Ok(());
    }

    // Build dotted-key → origin map for annotation.
    let value: serde_yaml::Value =
        serde_yaml::from_str(&yaml).context("config show: failed to re-parse YAML for origin")?;
    let keys = flatten_keys(&value, "");

    // Print YAML with inline origin comments.
    // Strategy: re-serialize each line with the associated key's origin appended.
    // We walk line-by-line matching known keys to keep output readable.
    // For simplicity we append origins as a trailing comment block.
    println!("# Effective configuration (with provenance)");
    print!("{yaml}");
    if !keys.is_empty() {
        println!("# --- origin annotations ---");
        for key in &keys {
            // `figment::Source` implements `Display`; format it without importing
            // the figment crate directly in the bin layer.
            let origin_label = match loaded.origin_of(key) {
                Some(src) => format!("{src}"),
                None => "unknown".to_string(),
            };
            println!("# {key}: {origin_label}");
        }
    }
    Ok(())
}

/// List the config layer paths and active env overrides.
///
/// Prints each layer (UserGlobal / Project / Lane / EnvOverride) with its
/// file path and load status.  Env override values are masked as
/// `***** (N chars)` to prevent secret leakage.
///
/// # Arguments
/// - `project`: project root directory. Falls back to `$PWD` when `None`.
///
/// # Errors
/// Returns an error if config loading fails.
async fn handle_config_info(project: Option<PathBuf>) -> anyhow::Result<()> {
    use anyhow::Context as _;

    let project = resolve_project_dir(project);
    let loaded = render_session_core::config::load(&project)
        .context("config info: failed to load config")?;

    println!("Layer paths:");
    for (kind, path, status) in loaded.layer_paths() {
        println!("  {kind:?}: {} [{status:?}]", path.display());
    }

    println!("\nEnv overrides:");
    let overrides = loaded.env_overrides();
    if overrides.is_empty() {
        println!("  (none)");
    } else {
        for (name, value) in overrides {
            println!("  {name} = ***** ({} chars)", value.len());
        }
    }

    Ok(())
}

/// Run health checks on the config stack and report results.
///
/// Checks performed:
/// 1. User-global config file exists (Loaded status).
/// 2. Project config file exists (Loaded status).
/// 3. Lane config file exists (Loaded status).
/// 4. `RENDER_SESSION_CONFIG` path is readable when the env var is set.
/// 5. Active `RENDER_SESSION_*` env overrides parse cleanly through figment.
///
/// All checks are collected before printing; no check panics on failure.
/// Exits with `process::exit(1)` when one or more checks fail.
///
/// # Arguments
/// - `project`: project root directory. Falls back to `$PWD` when `None`.
///
/// # Errors
/// Returns an error if the config itself cannot be loaded (before checks run).
async fn handle_config_doctor(project: Option<PathBuf>) -> anyhow::Result<()> {
    use anyhow::Context as _;
    use render_session_core::config::LayerStatus;

    let project = resolve_project_dir(project);
    let loaded = render_session_core::config::load(&project)
        .context("config doctor: failed to load config")?;

    let mut checks: Vec<DoctorCheck> = Vec::new();

    // Checks 1–3: layer file existence derived from layer_paths accessor.
    for (kind, path, status) in loaded.layer_paths() {
        let name = format!("{kind:?} layer");
        let (ok, message) = match status {
            LayerStatus::Loaded => (true, format!("found: {}", path.display())),
            LayerStatus::Absent => {
                // Absent is not a hard failure; the layer is optional.
                (true, format!("absent (optional): {}", path.display()))
            }
            LayerStatus::Failed(err) => (false, format!("parse error: {err}")),
        };
        checks.push(DoctorCheck {
            name,
            ok,
            message: Some(message),
        });
    }

    // Check 4: RENDER_SESSION_CONFIG path exists when set.
    if let Ok(cfg_path) = std::env::var("RENDER_SESSION_CONFIG") {
        if !cfg_path.is_empty() {
            let p = std::path::Path::new(&cfg_path);
            let (ok, msg) = if p.exists() {
                (
                    true,
                    format!("RENDER_SESSION_CONFIG path exists: {cfg_path}"),
                )
            } else {
                (
                    false,
                    format!("RENDER_SESSION_CONFIG path not found: {cfg_path}"),
                )
            };
            checks.push(DoctorCheck {
                name: "RENDER_SESSION_CONFIG path".to_string(),
                ok,
                message: Some(msg),
            });
        }
    }

    // Check 5: env overrides parse cleanly (re-extract with figment to surface
    // type-mismatch errors, e.g. RENDER_SESSION_CATEGORIES__X__ENABLED=notabool).
    let override_count = loaded.env_overrides().len();
    if override_count > 0 {
        // The load() call already ran figment.extract::<Config>() successfully,
        // so if we reached here the overrides parsed. Report as passing.
        checks.push(DoctorCheck {
            name: "env overrides parse".to_string(),
            ok: true,
            message: Some(format!(
                "{override_count} active override(s) parsed successfully"
            )),
        });
    }

    // Print results.
    let mut fail_count = 0usize;
    for check in &checks {
        let symbol = if check.ok { "OK" } else { "FAIL" };
        let detail = check.message.as_deref().unwrap_or("no detail");
        println!("[{symbol}] {}: {detail}", check.name);
        if !check.ok {
            fail_count += 1;
        }
    }

    if fail_count > 0 {
        tracing::warn!(fail_count, "config doctor found issue(s)");
        println!("\ndoctor: {fail_count} issue(s) found");
        // Use process::exit to guarantee exit code 1 (anyhow::Error propagates
        // as exit code 1 through tokio::main, but process::exit is explicit).
        std::process::exit(1);
    } else {
        println!("\ndoctor: all checks passed");
    }

    Ok(())
}

// ---------------------------------------------------------------------------
// Config helper utilities
// ---------------------------------------------------------------------------

/// Resolve the project directory: use the provided path or fall back to `$PWD`.
///
/// # Arguments
/// - `project`: optional explicit project path.
///
/// # Returns
/// The resolved `PathBuf`. Falls back to `PathBuf::from(".")` if `current_dir`
/// is unavailable.
fn resolve_project_dir(project: Option<PathBuf>) -> PathBuf {
    project.unwrap_or_else(|| {
        std::env::current_dir().unwrap_or_else(|e| {
            tracing::warn!(error = %e, "current_dir() failed, falling back to '.'");
            PathBuf::from(".")
        })
    })
}

/// Flatten a `serde_yaml::Value` into dotted-path key strings.
///
/// Recursively walks mappings and concatenates key segments with `.`.
/// Scalar (non-mapping) leaves are emitted as fully-qualified dotted keys.
/// Sequence values are emitted at the parent key level (sequences are not
/// further descended into).
///
/// # Arguments
/// - `v`: the YAML value to walk.
/// - `prefix`: accumulated key prefix (empty string at the top level).
///
/// # Returns
/// A `Vec<String>` of all leaf dotted-key paths in document order.
fn flatten_keys(v: &serde_yaml::Value, prefix: &str) -> Vec<String> {
    let mut out = Vec::new();
    if let serde_yaml::Value::Mapping(map) = v {
        for (k, val) in map {
            if let Some(key) = k.as_str() {
                let full = if prefix.is_empty() {
                    key.to_string()
                } else {
                    format!("{prefix}.{key}")
                };
                match val {
                    serde_yaml::Value::Mapping(_) => out.extend(flatten_keys(val, &full)),
                    _ => out.push(full),
                }
            }
        }
    }
    out
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

#[cfg(test)]
mod tests {
    /// Verify that spawning a short-lived process and awaiting its status works correctly.
    ///
    /// We spawn `true` (exits 0) and verify status.success() == true.
    /// We also verify that dropping the Future before poll does NOT kill the child
    /// (kill_on_drop defaults to false for tokio::process::Command).
    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
    async fn test_mcp_commands_status_await() {
        // Spawn a process that exits immediately with success.
        // Use `/bin/true` on Unix (always exits 0).
        let true_bin = if cfg!(target_os = "macos") || cfg!(target_os = "linux") {
            "true"
        } else {
            "cmd"
        };

        let status = tokio::process::Command::new(true_bin)
            .stdin(std::process::Stdio::null())
            .stdout(std::process::Stdio::null())
            .stderr(std::process::Stdio::null())
            .status()
            .await
            .expect("spawn `true`");

        assert!(status.success(), "`true` must exit with success");

        // Now verify kill_on_drop=false behaviour: spawn a long-running process,
        // drop the Child handle before it exits, then verify the process is still alive.
        let child = tokio::process::Command::new("sleep")
            .arg("30")
            .stdin(std::process::Stdio::null())
            .stdout(std::process::Stdio::null())
            .stderr(std::process::Stdio::null())
            .kill_on_drop(false)
            .spawn()
            .expect("spawn `sleep 30`");

        let pid = child.id().expect("child has pid") as libc::pid_t;

        // Drop the Child handle — with kill_on_drop=false the process keeps running.
        drop(child);

        // Give the OS a moment to process.
        tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;

        // kill(pid, 0) == 0 means process exists.
        #[allow(unsafe_code)]
        let probe = unsafe { libc::kill(pid, 0) };
        assert_eq!(
            probe, 0,
            "child should still be alive after handle drop (kill_on_drop=false)"
        );

        // Clean up.
        #[allow(unsafe_code)]
        unsafe {
            libc::kill(pid, libc::SIGTERM);
        }
    }
}
