use std::collections::VecDeque;
use std::io::{BufRead, BufReader};
use std::path::{Path, PathBuf};
use std::process::{Child, Command, Stdio};
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::Instant;

use regex::Regex;
use thiserror::Error;

use crate::paths::{home_dir, is_flatpak};
use crate::updater::{configure_npm_registry, update_dsh_bin};
use crate::{ENV_BIN_OVERRIDE, ENV_CWD_OVERRIDE};

pub const DSH_DEFAULT_HOST: &str = "127.0.0.1";
pub const URL_TIMEOUT_SECONDS: u64 = 120;

#[derive(Debug, Error)]
pub enum DshNotFound {
    #[error("{0}")]
    Message(String),
}

impl DshNotFound {
    fn new(msg: impl Into<String>) -> Self {
        Self::Message(msg.into())
    }
}

pub fn strip_ansi(text: &str) -> String {
    let re = Regex::new(r"\x1b\[[0-9;?]*[ -/]*[@-~]").expect("ansi regex");
    re.replace_all(text, "").into_owned()
}

pub fn parse_dsh_url(line: &str) -> Option<String> {
    let re = Regex::new(r"https?://(?:\[[0-9a-fA-F:]+\]|[^/\s:]+)(?::\d+)?(?:/[^\s]*)?")
        .expect("url regex");
    re.find(&strip_ansi(line)).map(|m| m.as_str().to_string())
}

pub fn default_workspace() -> PathBuf {
    if let Ok(raw) = std::env::var(ENV_CWD_OVERRIDE) {
        if !raw.is_empty() {
            return PathBuf::from(&raw)
                .canonicalize()
                .unwrap_or_else(|_| PathBuf::from(raw));
        }
    }
    if is_flatpak() {
        return home_dir();
    }
    std::env::current_dir().unwrap_or_else(|_| home_dir())
}

pub fn find_npx_dsh_bins() -> Vec<PathBuf> {
    let pattern = home_dir().join(".npm/_npx/*/node_modules/.bin/dsh");
    let mut bins = Vec::new();
    if let Some(globbed) = pattern.to_str() {
        if let Ok(paths) = glob_simple(globbed) {
            for path in paths {
                if is_executable(&path) {
                    bins.push(path);
                }
            }
        }
    }
    bins.sort_by_key(|p| std::fs::metadata(p).and_then(|m| m.modified()).ok());
    bins.reverse();
    bins
}

fn glob_simple(pattern: &str) -> std::io::Result<Vec<PathBuf>> {
    // Only the `_npx/*/` segment is a wildcard.
    let Some((prefix, rest)) = pattern.split_once('*') else {
        return Ok(vec![PathBuf::from(pattern)]);
    };
    let suffix = rest.trim_start_matches('/');
    let parent = Path::new(prefix);
    let mut out = Vec::new();
    if parent.is_dir() {
        for entry in std::fs::read_dir(parent)? {
            let entry = entry?;
            let candidate = entry.path().join(suffix);
            if candidate.is_file() {
                out.push(candidate);
            }
        }
    }
    Ok(out)
}

pub struct DshLauncher {
    dsh_bin_override: Option<String>,
    in_flatpak: bool,
    pub workspace: PathBuf,
}

impl DshLauncher {
    pub fn new(dsh_bin: Option<String>, workspace: Option<PathBuf>) -> Self {
        Self {
            dsh_bin_override: dsh_bin,
            in_flatpak: is_flatpak(),
            workspace: workspace.unwrap_or_else(default_workspace),
        }
    }

    pub fn resolve(&self) -> Result<Vec<String>, DshNotFound> {
        let mut candidates: Vec<Vec<String>> = Vec::new();

        if let Ok(env_override) = std::env::var(ENV_BIN_OVERRIDE) {
            if !env_override.is_empty() {
                let parts = shlex::split(&env_override).ok_or_else(|| {
                    DshNotFound::new(format!("{ENV_BIN_OVERRIDE} 不是合法的命令"))
                })?;
                candidates.push(parts);
            }
        }
        if let Some(cli) = &self.dsh_bin_override {
            let parts =
                shlex::split(cli).ok_or_else(|| DshNotFound::new("dsh 命令不是合法的命令"))?;
            candidates.push(parts);
        }

        let updated = update_dsh_bin();
        if is_executable(&updated) {
            candidates.push(vec![updated.to_string_lossy().into_owned()]);
        }

        if self.in_flatpak {
            if let Some(bundled) = which::which("dsh").ok() {
                candidates.push(vec![bundled.to_string_lossy().into_owned()]);
            }
            for path in crate::updater::BUNDLED_DSH {
                let path = PathBuf::from(path);
                if is_executable(&path)
                    && candidates
                        .first()
                        .and_then(|c| c.first())
                        .map(|s| s.as_str())
                        != Some(path.to_string_lossy().as_ref())
                {
                    candidates.push(vec![path.to_string_lossy().into_owned()]);
                }
            }
        } else {
            let local_bin = dsh_bin_name(&home_dir().join(".local/bin"));
            if is_executable(&local_bin) {
                candidates.push(vec![local_bin.to_string_lossy().into_owned()]);
            }
            if let Some(cached) = find_npx_dsh_bins().into_iter().next() {
                candidates.push(vec![cached.to_string_lossy().into_owned()]);
            }
            if let Ok(host) = which::which("dsh") {
                candidates.push(vec![host.to_string_lossy().into_owned()]);
            }
            if which::which("npx").is_ok() {
                candidates.push(vec![
                    "npx".into(),
                    "--yes".into(),
                    "@deepseek-ai/dsh".into(),
                ]);
            }
        }

        let chosen = candidates.into_iter().next().ok_or_else(|| {
            if self.in_flatpak {
                DshNotFound::new(
                    "此 Flatpak 没有内置 dsh 可执行文件。\n请重新构建/安装 io.github.tommyfang.DshDesktop。",
                )
            } else {
                DshNotFound::new(
                    "找不到 DeepSeek Harness (dsh)，也没有可用的 npx。\n请先安装：npm install -g @deepseek-ai/dsh\n或用 DSH_DESKTOP_DSH_BIN 指定 dsh 的完整路径。",
                )
            }
        })?;
        Ok(chosen)
    }

    pub fn web_argv(&self) -> Result<Vec<String>, DshNotFound> {
        let mut argv = self.resolve()?;
        argv.extend([
            "web".into(),
            "--host".into(),
            DSH_DEFAULT_HOST.into(),
            "--port".into(),
            "0".into(),
        ]);
        Ok(argv)
    }

    pub fn start(&self) -> Result<DshProcess, DshNotFound> {
        let argv = self.web_argv()?;
        let mut cmd = Command::new(&argv[0]);
        cmd.args(&argv[1..])
            .current_dir(&self.workspace)
            .stdout(Stdio::piped())
            .stderr(Stdio::inherit())
            .stdin(Stdio::null());
        configure_npm_registry(&mut cmd);
        #[cfg(unix)]
        {
            use std::os::unix::process::CommandExt;
            cmd.process_group(0);
        }
        #[cfg(windows)]
        {
            use std::os::windows::process::CommandExt;
            const CREATE_NEW_PROCESS_GROUP: u32 = 0x00000200;
            cmd.creation_flags(CREATE_NEW_PROCESS_GROUP);
        }
        let mut child = cmd
            .spawn()
            .map_err(|exc| DshNotFound::new(format!("无法启动 dsh web: {exc}")))?;
        let stdout = child.stdout.take();
        Ok(DshProcess::new(child, stdout, self.in_flatpak))
    }
}

fn dsh_bin_name(dir: &Path) -> PathBuf {
    #[cfg(windows)]
    {
        let cmd = dir.join("dsh.cmd");
        if cmd.is_file() {
            return cmd;
        }
    }
    dir.join("dsh")
}

pub fn is_executable(path: &Path) -> bool {
    if !path.is_file() {
        return false;
    }
    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;
        std::fs::metadata(path)
            .map(|m| m.permissions().mode() & 0o111 != 0)
            .unwrap_or(false)
    }
    #[cfg(windows)]
    {
        true
    }
}

pub struct DshProcess {
    child: Arc<Mutex<Child>>,
    pub in_flatpak: bool,
    pub url: Arc<Mutex<Option<String>>>,
    pub lines: Arc<Mutex<VecDeque<String>>>,
    pub started_at: Instant,
}

impl DshProcess {
    fn new(child: Child, stdout: Option<std::process::ChildStdout>, in_flatpak: bool) -> Self {
        let proc = Self {
            child: Arc::new(Mutex::new(child)),
            in_flatpak,
            url: Arc::new(Mutex::new(None)),
            lines: Arc::new(Mutex::new(VecDeque::with_capacity(400))),
            started_at: Instant::now(),
        };
        if let Some(stdout) = stdout {
            let lines = Arc::clone(&proc.lines);
            let url = Arc::clone(&proc.url);
            thread::spawn(move || {
                let reader = BufReader::new(stdout);
                for line in reader.lines() {
                    let Ok(line) = line else { break };
                    let clean = strip_ansi(&line);
                    let trimmed = clean.trim_end();
                    if !trimmed.is_empty() {
                        if let Ok(mut buf) = lines.lock() {
                            if buf.len() == 400 {
                                buf.pop_front();
                            }
                            buf.push_back(trimmed.to_string());
                        }
                    }
                    if let Some(found) = parse_dsh_url(&line) {
                        if let Ok(mut slot) = url.lock() {
                            if slot.is_none() {
                                *slot = Some(found);
                            }
                        }
                    }
                }
            });
        }
        proc
    }

    pub fn poll(&self) -> Option<i32> {
        self.child
            .lock()
            .ok()
            .and_then(|mut child| child.try_wait().ok().flatten().and_then(|s| s.code()))
    }

    pub fn take_url(&self) -> Option<String> {
        self.url.lock().ok().and_then(|g| g.clone())
    }

    pub fn snapshot_lines(&self) -> Vec<String> {
        self.lines
            .lock()
            .map(|g| g.iter().cloned().collect())
            .unwrap_or_default()
    }

    pub fn stop(&self) {
        let Ok(mut child) = self.child.lock() else {
            return;
        };
        if child.try_wait().ok().flatten().is_some() {
            return;
        }
        let pid = child.id();
        #[cfg(unix)]
        unsafe {
            libc::killpg(pid as i32, libc::SIGTERM);
        }
        #[cfg(windows)]
        {
            let _ = child.kill();
        }
        let start = Instant::now();
        while start.elapsed() < std::time::Duration::from_secs(3) {
            if child.try_wait().ok().flatten().is_some() {
                return;
            }
            thread::sleep(std::time::Duration::from_millis(50));
        }
        #[cfg(unix)]
        unsafe {
            libc::killpg(pid as i32, libc::SIGKILL);
        }
        let _ = child.kill();
        let _ = child.wait();
    }
}

impl Drop for DshProcess {
    fn drop(&mut self) {
        self.stop();
    }
}

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

    static ENV_LOCK: Mutex<()> = Mutex::new(());

    #[test]
    fn parse_ipv4_url() {
        assert_eq!(
            parse_dsh_url("dsh web: http://127.0.0.1:39095"),
            Some("http://127.0.0.1:39095".into())
        );
    }

    #[test]
    fn parse_ipv6_url() {
        assert_eq!(
            parse_dsh_url("dsh web: http://[::1]:39096"),
            Some("http://[::1]:39096".into())
        );
    }

    #[test]
    fn parse_url_with_trailing_text() {
        assert_eq!(
            parse_dsh_url("booted profile web on http://127.0.0.1:39095 (ready)"),
            Some("http://127.0.0.1:39095".into())
        );
    }

    #[test]
    fn parse_no_url() {
        assert_eq!(parse_dsh_url("waiting for network…"), None);
    }

    #[test]
    fn strip_ansi_codes() {
        assert_eq!(strip_ansi("\x1b[36mhttp://x\x1b[0m"), "http://x");
    }

    #[test]
    fn workspace_uses_env_override() {
        let _guard = ENV_LOCK.lock().unwrap();
        std::env::set_var(ENV_CWD_OVERRIDE, "/tmp/some-workspace");
        let ws = default_workspace();
        std::env::remove_var(ENV_CWD_OVERRIDE);
        assert!(ws.ends_with("some-workspace") || ws == PathBuf::from("/tmp/some-workspace"));
    }

    #[test]
    fn env_override_wins() {
        let _guard = ENV_LOCK.lock().unwrap();
        std::env::set_var(ENV_BIN_OVERRIDE, "echo");
        let argv = DshLauncher::new(None, None).resolve().unwrap();
        std::env::remove_var(ENV_BIN_OVERRIDE);
        assert_eq!(argv, vec!["echo"]);
    }

    #[test]
    fn invalid_env_override_raises() {
        let _guard = ENV_LOCK.lock().unwrap();
        std::env::set_var(ENV_BIN_OVERRIDE, "'unterminated");
        let err = DshLauncher::new(None, None).resolve();
        std::env::remove_var(ENV_BIN_OVERRIDE);
        assert!(err.is_err());
    }

    #[test]
    fn cli_override() {
        let _guard = ENV_LOCK.lock().unwrap();
        std::env::remove_var(ENV_BIN_OVERRIDE);
        let argv = DshLauncher::new(Some("/opt/dsh/bin/dsh".into()), None)
            .resolve()
            .unwrap();
        assert_eq!(argv, vec!["/opt/dsh/bin/dsh"]);
    }

    #[test]
    fn web_argv_shape() {
        let _guard = ENV_LOCK.lock().unwrap();
        std::env::set_var(ENV_BIN_OVERRIDE, "dsh-test");
        let argv = DshLauncher::new(None, None).web_argv().unwrap();
        std::env::remove_var(ENV_BIN_OVERRIDE);
        assert_eq!(&argv[0..4], ["dsh-test", "web", "--host", "127.0.0.1"]);
        assert!(argv.contains(&"--port".into()));
        assert_eq!(argv.last().map(String::as_str), Some("0"));
    }
}
