use std::fs;
use std::path::{Path, PathBuf};
use std::process::ExitCode;

use anyhow::{anyhow, bail, Context, Result};
use serde::Serialize;

use crate::output::CommandReport;
use crate::paths::write as path_write;
use crate::shipped_skills::{embedded_skill_files, embedded_skill_names};

#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;

const CLI_SHIM_RELATIVE_PATH: &str = ".ccd/bin/ccd";

#[derive(Debug, Clone, Copy)]
pub(crate) struct HarnessSkillTarget {
    id: &'static str,
    global_dir: &'static str,
    local_dir: &'static str,
    active_env: &'static [&'static str],
}

pub(crate) const HARNESS_SKILL_TARGETS: &[HarnessSkillTarget] = &[
    HarnessSkillTarget {
        id: "codex",
        global_dir: ".codex/skills",
        local_dir: ".codex/skills",
        active_env: &[
            "CODEX_THREAD_ID",
            "CODEX_HOME",
            "CODEX_SHELL",
            "CODEX_INTERNAL_ORIGINATOR_OVERRIDE",
        ],
    },
    HarnessSkillTarget {
        id: "claude",
        global_dir: ".claude/skills",
        local_dir: ".claude/skills",
        active_env: &[
            "CLAUDECODE",
            "CLAUDE_CODE",
            "CLAUDE_PROJECT_DIR",
            "CLAUDE_SESSION_ID",
        ],
    },
    HarnessSkillTarget {
        id: "gemini",
        global_dir: ".gemini/skills",
        local_dir: ".gemini/skills",
        active_env: &["GEMINI_CLI", "GEMINI_SESSION_ID"],
    },
];

struct UnsupportedHarness {
    id: &'static str,
    aliases: &'static [&'static str],
    active_env: &'static [&'static str],
    reason: &'static str,
}

const UNSUPPORTED_HARNESSES: &[UnsupportedHarness] = &[
    UnsupportedHarness {
        id: "hermes",
        aliases: &[],
        active_env: &["HERMES_SESSION_ID", "HERMES_HOME"],
        reason: "Hermes currently has a Lifeloop reference-adapter surface, not a confirmed SKILL.md-compatible native skill layout",
    },
    UnsupportedHarness {
        id: "openclaw",
        aliases: &["openclawx", "openclauderx"],
        active_env: &["OPENCLAW_SESSION_ID", "OPENCLAW_HOME", "OPENCLAUDERX_SESSION_ID"],
        reason: "OpenClaw currently has a Lifeloop reference-adapter surface, and the local OpenClaw extension format is still unconfirmed",
    },
    UnsupportedHarness {
        id: "opencode",
        aliases: &["open_code"],
        active_env: &["OPENCODE_SESSION_ID", "OPENCODE_HOME"],
        reason: "OpenCode is modeled by Lifeloop as telemetry-only in the current contract and does not publish a native skill layout",
    },
    UnsupportedHarness {
        id: "pi",
        aliases: &["pi-harness"],
        active_env: &["PI_SESSION_ID", "PI_HOME"],
        reason: "Pi is not in Lifeloop's shipped manifest registry yet, and its native extension format is still unconfirmed",
    },
];

#[derive(Serialize)]
pub struct SkillsInstallReport {
    command: &'static str,
    ok: bool,
    mode: &'static str,
    harness: String,
    target_dir: String,
    installed: Vec<InstalledSkill>,
    #[serde(skip_serializing_if = "Option::is_none")]
    cli_shim: Option<CliShim>,
    #[serde(skip_serializing_if = "Vec::is_empty")]
    warnings: Vec<String>,
}

#[derive(Serialize)]
struct InstalledSkill {
    harness: String,
    path: String,
    skill: String,
}

#[derive(Serialize)]
struct CliShim {
    path: String,
    target: String,
}

impl CommandReport for SkillsInstallReport {
    fn exit_code(&self) -> ExitCode {
        ExitCode::SUCCESS
    }

    fn render_text(&self) {
        if self.installed.is_empty() {
            println!("No skills installed.");
        } else {
            for entry in &self.installed {
                println!(
                    "Installed {}/{} -> {}",
                    entry.harness, entry.skill, entry.path
                );
            }
        }

        if let Some(cli_shim) = &self.cli_shim {
            println!(
                "Registered CLI shim -> {} (target: {})",
                cli_shim.path, cli_shim.target
            );
        }

        for warning in &self.warnings {
            println!("Warning: {warning}");
        }
    }
}

pub fn run(
    repo_root: &Path,
    global: bool,
    requested_harness: Option<&str>,
) -> Result<SkillsInstallReport> {
    let mut installed = Vec::new();
    let mut cli_shim = None;
    let mut warnings = Vec::new();
    let home = if global {
        Some(home_dir()?)
    } else {
        non_empty_home_dir()
    };
    let harness = resolve_harness_target(requested_harness)?;
    let target_base = if global {
        home.as_ref()
            .expect("global install requires HOME")
            .join(harness.global_dir)
    } else {
        repo_root.join(harness.local_dir)
    };

    if global {
        installed.extend(write_skills(&target_base, harness.id)?);
        warn_stale_skills(&target_base, harness.id, &mut warnings);

        match register_cli_shim(home.as_ref().expect("global install requires HOME")) {
            Ok(shim) => cli_shim = Some(shim),
            Err(error) => warnings.push(format!("failed to register CLI shim: {error}")),
        }
    } else {
        installed.extend(write_skills(&target_base, harness.id)?);
        warn_stale_skills(&target_base, harness.id, &mut warnings);
    }

    Ok(SkillsInstallReport {
        command: "skills-install",
        ok: true,
        mode: if global { "global" } else { "local" },
        harness: harness.id.to_owned(),
        target_dir: target_base.display().to_string(),
        installed,
        cli_shim,
        warnings,
    })
}

fn write_skills(target_base: &Path, harness_id: &str) -> Result<Vec<InstalledSkill>> {
    let mut installed = Vec::new();

    for skill_file in embedded_skill_files() {
        let dest = target_base.join(skill_file.relative_path);

        if let Some(parent) = dest.parent() {
            fs::create_dir_all(parent)
                .with_context(|| format!("failed to create directory {}", parent.display()))?;
        }

        path_write::replace_text(&dest, skill_file.contents, None)
            .with_context(|| format!("failed to write {}", dest.display()))?;

        installed.push(InstalledSkill {
            harness: harness_id.to_owned(),
            path: dest.display().to_string(),
            skill: skill_file.skill.to_owned(),
        });
    }

    Ok(installed)
}

/// Warn about skill directories under `target_base` that start with `ccd-`
/// but are not in the current embedded set.  These may be leftover from a
/// previous CCD version or installed by an extension — install only warns,
/// it does not delete them.
fn warn_stale_skills(target_base: &Path, harness_id: &str, warnings: &mut Vec<String>) {
    let current_names = embedded_skill_names();

    let entries = match fs::read_dir(target_base) {
        Ok(entries) => entries,
        Err(_) => return,
    };

    for entry in entries.flatten() {
        let path = entry.path();
        if !path.is_dir() {
            continue;
        }
        let dir_name = match path.file_name().and_then(|n| n.to_str()) {
            Some(name) => name,
            None => continue,
        };
        if !dir_name.starts_with("ccd-") {
            continue;
        }
        if current_names.contains(&dir_name) {
            continue;
        }
        warnings.push(format!(
            "stale skill `{dir_name}` found for {harness_id} at {}; remove it manually if it is no longer needed",
            path.display()
        ));
    }
}

pub(crate) fn active_harness_skill_dirs(repo_root: &Path) -> Vec<(PathBuf, String)> {
    let home = non_empty_home_dir();
    let Ok(harness) = resolve_harness_target(None) else {
        return Vec::new();
    };

    let local_dir = repo_root.join(harness.local_dir);
    let mut dirs = Vec::new();
    if let Some(home) = home {
        let global_dir = home.join(harness.global_dir);
        dirs.push((global_dir, format!("global {}", harness.id)));
    }
    if !dirs.iter().any(|(path, _)| path == &local_dir) {
        dirs.push((local_dir, format!("local {}", harness.id)));
    }
    dirs
}

fn resolve_harness_target(requested_harness: Option<&str>) -> Result<&'static HarnessSkillTarget> {
    let requested_harness = requested_harness
        .map(str::trim)
        .filter(|value| !value.is_empty());
    if let Some(requested) = requested_harness {
        if let Some(unsupported) = lookup_unsupported_harness(requested) {
            return Err(unsupported_harness_error(unsupported));
        }
        return lookup_harness_target(requested)
            .with_context(|| format!("unknown or unsupported harness `{requested}`"));
    }

    if let Some(from_env) = explicit_harness_env() {
        if let Some(unsupported) = lookup_unsupported_harness(&from_env) {
            return Err(unsupported_harness_error(unsupported));
        }
        return lookup_harness_target(&from_env)
            .with_context(|| format!("unknown or unsupported harness `{from_env}`"));
    }

    let env_matches: Vec<&HarnessSkillTarget> = HARNESS_SKILL_TARGETS
        .iter()
        .filter(|target| target.active_env.iter().any(|name| env_os_nonempty(name)))
        .collect();
    let unsupported_env_matches: Vec<&UnsupportedHarness> = UNSUPPORTED_HARNESSES
        .iter()
        .filter(|target| target.active_env.iter().any(|name| env_os_nonempty(name)))
        .collect();

    if env_matches.len() == 1 && unsupported_env_matches.is_empty() {
        return Ok(env_matches[0]);
    }
    if env_matches.is_empty() && unsupported_env_matches.len() == 1 {
        return Err(unsupported_harness_error(unsupported_env_matches[0]));
    }
    if !env_matches.is_empty() || !unsupported_env_matches.is_empty() {
        let mut ids = env_matches
            .iter()
            .map(|target| target.id)
            .collect::<Vec<_>>();
        ids.extend(unsupported_env_matches.iter().map(|target| target.id));
        bail!(
            "multiple active harnesses detected ({}); rerun with `--harness <id>`",
            ids.join(", ")
        );
    }

    bail!(
        "could not detect an active harness; rerun with `--harness codex`, `--harness claude`, or `--harness gemini`"
    )
}

fn explicit_harness_env() -> Option<String> {
    ["CCD_ACTIVE_HARNESS", "CCD_SKILLS_HARNESS"]
        .iter()
        .find_map(|name| {
            let value = std::env::var(name).ok()?;
            let trimmed = value.trim();
            if trimmed.is_empty() {
                None
            } else {
                Some(trimmed.to_owned())
            }
        })
}

fn env_os_nonempty(name: &str) -> bool {
    std::env::var_os(name)
        .map(|value| !value.as_os_str().is_empty())
        .unwrap_or(false)
}

fn non_empty_home_dir() -> Option<PathBuf> {
    std::env::var_os("HOME").and_then(|home| {
        if home.as_os_str().is_empty() {
            None
        } else {
            Some(PathBuf::from(home))
        }
    })
}

fn lookup_harness_target(id: &str) -> Option<&'static HarnessSkillTarget> {
    let normalized = id.trim().to_ascii_lowercase().replace(['_', '-'], "");
    HARNESS_SKILL_TARGETS.iter().find(|target| {
        target.id.replace(['_', '-'], "") == normalized
            || matches!(
                (target.id, normalized.as_str()),
                ("codex", "codexcli") | ("claude", "claudecode") | ("gemini", "geminicli")
            )
    })
}

fn lookup_unsupported_harness(id: &str) -> Option<&'static UnsupportedHarness> {
    let normalized = id.trim().to_ascii_lowercase().replace(['_', '-'], "");
    UNSUPPORTED_HARNESSES.iter().find(|target| {
        target.id.replace(['_', '-'], "") == normalized
            || target
                .aliases
                .iter()
                .any(|alias| alias.replace(['_', '-'], "") == normalized)
    })
}

fn unsupported_harness_error(target: &UnsupportedHarness) -> anyhow::Error {
    anyhow!(
        "harness `{}` is known, but `ccd skills install` has no confirmed native skill layout for it yet: {}. Use `ccd start --activate --path .`, the bootstrap prompt, or a Lifeloop/runtime adapter until that layout is published.",
        target.id,
        target.reason
    )
}

fn home_dir() -> Result<PathBuf> {
    match non_empty_home_dir() {
        Some(home) => Ok(home),
        None => bail!("HOME environment variable is not set"),
    }
}

fn register_cli_shim(home: &Path) -> Result<CliShim> {
    let target = std::env::current_exe()
        .context("failed to resolve current executable")?
        .canonicalize()
        .context("failed to canonicalize current executable path")?;
    let target_text = utf8_executable_path(&target)?;

    let shim = home.join(CLI_SHIM_RELATIVE_PATH);
    if let Some(parent) = shim.parent() {
        fs::create_dir_all(parent)
            .with_context(|| format!("failed to create directory {}", parent.display()))?;
    }

    if shim.is_symlink() || shim.exists() {
        fs::remove_file(&shim)
            .with_context(|| format!("failed to remove existing {}", shim.display()))?;
    }

    let script = format!("#!/bin/sh\nexec {} \"$@\"\n", shell_quote(target_text));
    fs::write(&shim, script).with_context(|| format!("failed to write {}", shim.display()))?;

    #[cfg(unix)]
    fs::set_permissions(&shim, fs::Permissions::from_mode(0o755))
        .with_context(|| format!("failed to chmod {}", shim.display()))?;

    Ok(CliShim {
        path: shim.display().to_string(),
        target: target_text.to_owned(),
    })
}

fn utf8_executable_path(path: &Path) -> Result<&str> {
    path.to_str().ok_or_else(|| {
        anyhow!(
            "refusing to register CLI shim for non-UTF-8 executable path {}; install ccd at a UTF-8 path and rerun `ccd skills install`",
            path.display()
        )
    })
}

fn shell_quote(path: &str) -> String {
    format!("'{}'", path.replace('\'', "'\"'\"'"))
}

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

    #[cfg(unix)]
    #[test]
    fn utf8_executable_path_rejects_non_utf8_paths() {
        use std::ffi::OsString;
        use std::os::unix::ffi::OsStringExt;

        let path = PathBuf::from(OsString::from_vec(b"/tmp/ccd-\xFF".to_vec()));
        let error = utf8_executable_path(&path).expect_err("non-UTF-8 path should fail");

        assert!(error.to_string().contains("non-UTF-8 executable path"));
        assert!(error.to_string().contains("UTF-8 path"));
    }

    #[test]
    fn shell_quote_escapes_single_quotes() {
        assert_eq!(shell_quote("/tmp/ccd's/bin"), "'/tmp/ccd'\"'\"'s/bin'");
    }
}
