use crate::api::MyceliumClient;
use crate::elevation;
use crate::error::{AppError, AppResult};
use parking_lot::Mutex;
use std::collections::VecDeque;
use std::path::PathBuf;
use std::process::Stdio;
use std::sync::Arc;
use std::time::{Duration, Instant};
use tauri::{AppHandle, Emitter, Manager};
use tokio::io::{AsyncBufReadExt, BufReader};
use tokio::process::Child;
use tracing::{info, warn};

const HEALTH_CHECK_TIMEOUT_SECS: u64 = 20;
const HEALTH_CHECK_INTERVAL_MS: u64 = 400;
const LOG_RING_CAPACITY: usize = 500;

#[derive(Debug, Clone, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SidecarConfig {
    pub peers: Vec<String>,
    pub tun_name: Option<String>,
    pub no_tun: bool,
}

impl Default for SidecarConfig {
    fn default() -> Self {
        Self {
            // A small set of well-known public peers from the mycelium README,
            // used as bootstrap when the user hasn't configured their own.
            peers: vec![
                "tcp://188.40.132.242:9651".into(),
                "quic://[2a01:4f8:212:fa6::2]:9651".into(),
            ],
            tun_name: None,
            no_tun: false,
        }
    }
}

/// Holds the running mycelium child process plus a small in-memory log
/// buffer so the Settings page can show recent stderr/stdout without
/// reading from disk.
pub struct SidecarHandle {
    child: Mutex<Option<Child>>,
    api_url: Mutex<Option<String>>,
    logs: Mutex<VecDeque<String>>,
    config_path: Mutex<Option<PathBuf>>,
    key_path: Mutex<Option<PathBuf>>,
    /// Full overlay IPv6, parsed out of mycelium's `Node overlay IP: ...`
    /// startup log line. The daemon's HTTP API only exposes the subnet,
    /// so we extract the host part from the binary's stderr.
    overlay_ip: Mutex<Option<String>>,
}

impl SidecarHandle {
    pub fn new() -> Arc<Self> {
        Arc::new(Self {
            child: Mutex::new(None),
            api_url: Mutex::new(None),
            logs: Mutex::new(VecDeque::with_capacity(LOG_RING_CAPACITY)),
            config_path: Mutex::new(None),
            key_path: Mutex::new(None),
            overlay_ip: Mutex::new(None),
        })
    }

    pub fn is_running(&self) -> bool {
        self.api_url.lock().is_some()
    }

    pub fn client(&self) -> Option<MyceliumClient> {
        self.api_url
            .lock()
            .as_ref()
            .map(|u| MyceliumClient::new(u.clone()))
    }

    pub fn logs_snapshot(&self) -> Vec<String> {
        self.logs.lock().iter().cloned().collect()
    }

    pub fn key_path(&self) -> Option<PathBuf> {
        self.key_path.lock().clone()
    }

    pub fn config_path(&self) -> Option<PathBuf> {
        self.config_path.lock().clone()
    }

    pub fn overlay_ip(&self) -> Option<String> {
        self.overlay_ip.lock().clone()
    }

    fn push_log(&self, line: String) {
        // mycelium prefixes log lines with ANSI colour escapes; strip them
        // before scanning for the overlay IP marker so the regex stays
        // resilient to cosmetic changes in upstream's log format.
        if self.overlay_ip.lock().is_none() {
            let stripped = strip_ansi(&line);
            if let Some(ip) = extract_overlay_ip(&stripped) {
                tracing::info!(overlay_ip = %ip, "node overlay IP captured");
                *self.overlay_ip.lock() = Some(ip);
            }
        }
        let mut buf = self.logs.lock();
        if buf.len() >= LOG_RING_CAPACITY {
            buf.pop_front();
        }
        buf.push_back(line);
    }

    pub async fn start(
        self: &Arc<Self>,
        app: &AppHandle,
        config: &SidecarConfig,
    ) -> AppResult<String> {
        if self.is_running() {
            return Err(AppError::DaemonAlreadyRunning);
        }

        let bin = locate_sidecar(app)?;
        // In a `.deb` install, our pre-install script ships
        // /usr/bin/mycellium-bootstrap that pkill+ip-link-del cleans
        // any orphan state before exec-ing the real binary. Polkit
        // is configured to auth_admin_keep that exact path, so
        // subsequent starts are silent.
        // Falls back to the bare binary in `tauri dev` where the
        // bootstrap script isn't installed system-wide.
        let elevation_target = match bootstrap_path() {
            Some(p) => p,
            None => bin.clone(),
        };
        // Three ports: HTTP API (loopback), TCP listen, QUIC (UDP) listen.
        // mycelium defaults to 9651 for both peer-listen ports, which
        // collides if another instance (or a leftover from a previous test)
        // is already up. Always picking ephemeral ports avoids that at the
        // cost of inbound peers needing the actual port number.
        let api_port = pick_port()?;
        let tcp_port = pick_port_skip(&[api_port])?;
        let quic_port = pick_port_skip(&[api_port, tcp_port])?;
        // mycelium also opens an internal JSON-RPC / metrics endpoint on
        // 127.0.0.1:8990 by default; if 8990 is already taken (e.g. by an
        // orphan from a previous run we couldn't SIGKILL because it ran as
        // root) the new instance dies a few seconds after start. Pin this
        // to a fresh ephemeral port too.
        let metrics_port = pick_port_skip(&[api_port, tcp_port, quic_port])?;

        let data_dir = app
            .path()
            .app_data_dir()
            .map_err(|e| AppError::TauriPath(e.to_string()))?;
        std::fs::create_dir_all(&data_dir)?;
        let key_path = data_dir.join("priv_key.bin");
        let config_path = data_dir.join("mycelium.toml");

        let mut args = vec![
            "--api-addr".to_string(),
            format!("127.0.0.1:{api_port}"),
            "--tcp-listen-port".to_string(),
            tcp_port.to_string(),
            "--quic-listen-port".to_string(),
            quic_port.to_string(),
            "--metrics-api-address".to_string(),
            format!("127.0.0.1:{metrics_port}"),
            "--key-file".to_string(),
            key_path.display().to_string(),
        ];
        if config_path.exists() {
            args.push("--config-file".to_string());
            args.push(config_path.display().to_string());
        }
        if config.no_tun {
            args.push("--no-tun".to_string());
        }
        if let Some(name) = &config.tun_name {
            args.push("--tun-name".to_string());
            args.push(name.clone());
        }
        if !config.peers.is_empty() {
            args.push("--peers".to_string());
            for p in &config.peers {
                args.push(p.clone());
            }
        }

        info!(
            ?bin,
            elevation_target = %elevation_target.display(),
            api_port, tcp_port, quic_port, metrics_port,
            "spawning mycelium sidecar via pkexec"
        );

        let mut cmd = elevation::elevated(&elevation_target, &args);
        cmd.stdout(Stdio::piped())
            .stderr(Stdio::piped())
            .kill_on_drop(true);

        let mut child = cmd.spawn()?;
        let stdout = child.stdout.take();
        let stderr = child.stderr.take();

        // Stash before we await the health check, so a slow daemon
        // doesn't leave us with a zombie process if anything panics.
        let api_url = format!("http://127.0.0.1:{api_port}");
        *self.child.lock() = Some(child);
        *self.api_url.lock() = Some(api_url.clone());
        *self.config_path.lock() = Some(config_path);
        *self.key_path.lock() = Some(key_path);

        if let Some(out) = stdout {
            let me = Arc::clone(self);
            tokio::spawn(async move {
                let mut lines = BufReader::new(out).lines();
                while let Ok(Some(line)) = lines.next_line().await {
                    me.push_log(format!("[stdout] {line}"));
                }
            });
        }
        if let Some(err) = stderr {
            let me = Arc::clone(self);
            tokio::spawn(async move {
                let mut lines = BufReader::new(err).lines();
                while let Ok(Some(line)) = lines.next_line().await {
                    me.push_log(format!("[stderr] {line}"));
                }
            });
        }

        // Background watcher: polls every 2s and emits `sidecar://exited`
        // when the child dies after the start sequence has succeeded.
        {
            let me = Arc::clone(self);
            let app = app.clone();
            tokio::spawn(async move {
                loop {
                    tokio::time::sleep(Duration::from_secs(2)).await;
                    if !me.is_running() {
                        break;
                    }
                    if let Some(code) = me.child_exit_status() {
                        me.handle_exit(&app, code);
                        break;
                    }
                }
            });
        }

        // Health-check loop. The pkexec dialog can take several seconds, so
        // give the daemon a generous window to come up before failing.
        let client = MyceliumClient::new(&api_url);
        let started = Instant::now();
        loop {
            // Bail early if the child died (auth cancel, missing TUN cap, etc.).
            if let Some(code) = self.child_exit_status() {
                self.cleanup();
                if elevation::is_auth_failure(code) {
                    return Err(AppError::ElevationCancelled);
                }
                return Err(AppError::SidecarExited(format!("exit code {code}")));
            }

            if client.is_alive().await {
                info!(api_url = %api_url, "mycelium sidecar healthy");
                let _ = app.emit("sidecar://ready", &api_url);
                return Ok(api_url);
            }

            if started.elapsed() > Duration::from_secs(HEALTH_CHECK_TIMEOUT_SECS) {
                self.stop().await;
                return Err(AppError::HealthCheckTimeout(HEALTH_CHECK_TIMEOUT_SECS));
            }

            tokio::time::sleep(Duration::from_millis(HEALTH_CHECK_INTERVAL_MS)).await;
        }
    }

    fn child_exit_status(&self) -> Option<i32> {
        let mut lock = self.child.lock();
        let child = lock.as_mut()?;
        match child.try_wait() {
            Ok(Some(s)) => Some(s.code().unwrap_or(-1)),
            _ => None,
        }
    }

    fn handle_exit(&self, app: &AppHandle, code: i32) {
        warn!(code, "mycelium sidecar exited");
        self.cleanup();
        let _ = app.emit("sidecar://exited", code);
    }

    fn cleanup(&self) {
        *self.api_url.lock() = None;
        *self.overlay_ip.lock() = None;
        let _ = self.child.lock().take();
    }

    pub async fn stop(&self) {
        // Take the child by value so the parking_lot guard isn't held across
        // the await on `wait()`. `kill_on_drop(true)` is set, but pkexec runs
        // mycelium as root, so we still ask politely first; the polkit agent
        // reaps the elevated child.
        let child_opt = self.child.lock().take();
        if let Some(mut child) = child_opt {
            let _ = child.start_kill();
            let _ = child.wait().await;
        }
        self.cleanup();
    }
}

/// Path of the privileged wrapper script shipped in our `.deb`. When
/// present, we invoke it instead of the mycelium binary directly so
/// the elevated context can clean up any orphan TUN / processes from
/// a previous crash before `exec /usr/bin/mycelium`.
fn bootstrap_path() -> Option<PathBuf> {
    let p = PathBuf::from("/usr/bin/mycellium-bootstrap");
    p.exists().then_some(p)
}

/// Mycelium logs the assigned overlay IPv6 once at startup like:
///   `INFO mycelium: Node overlay IP: 43d:956e:7877:d933:eecc:b305:21ff:77f9`
/// We don't pull a regex crate just for this — a hand-rolled parser is
/// resilient enough.
fn extract_overlay_ip(line: &str) -> Option<String> {
    const NEEDLE: &str = "Node overlay IP:";
    let idx = line.find(NEEDLE)?;
    let rest = line[idx + NEEDLE.len()..].trim();
    // The IP is the first whitespace-bounded token; rejection criteria
    // (must contain `:`, must not contain `/`) keep us from accidentally
    // capturing the subnet.
    let token = rest.split_whitespace().next()?;
    if !token.contains(':') || token.contains('/') {
        return None;
    }
    Some(token.to_string())
}

/// Strip ANSI SGR escape sequences (`ESC [ ... m`) from a log line so the
/// overlay-IP scanner doesn't choke on tracing's coloured output.
fn strip_ansi(s: &str) -> String {
    let bytes = s.as_bytes();
    let mut out = String::with_capacity(s.len());
    let mut i = 0;
    while i < bytes.len() {
        if bytes[i] == 0x1B && i + 1 < bytes.len() && bytes[i + 1] == b'[' {
            // Skip until 'm' or any letter terminator.
            i += 2;
            while i < bytes.len() && !bytes[i].is_ascii_alphabetic() {
                i += 1;
            }
            if i < bytes.len() {
                i += 1;
            }
        } else {
            out.push(bytes[i] as char);
            i += 1;
        }
    }
    out
}

fn pick_port() -> AppResult<u16> {
    portpicker::pick_unused_port().ok_or_else(|| AppError::Other("no free port available".into()))
}

fn pick_port_skip(taken: &[u16]) -> AppResult<u16> {
    for _ in 0..16 {
        let p = pick_port()?;
        if !taken.contains(&p) {
            return Ok(p);
        }
    }
    Err(AppError::Other(
        "could not find a unique free port".into(),
    ))
}

/// Resolve the bundled `mycelium` sidecar across our two build modes:
///   • `tauri dev` keeps the file under `src-tauri/binaries/` with the
///     `-<target_triple>` suffix Tauri's externalBin convention requires.
///   • `tauri build` for a `.deb` strips the suffix and places the
///     binary at `/usr/bin/mycelium` next to the app launcher.
/// We probe the bundled path first, then walk back to the dev location.
fn locate_sidecar(app: &AppHandle) -> AppResult<PathBuf> {
    let triple = std::env::var("TAURI_ENV_TARGET_TRIPLE")
        .ok()
        .or_else(|| option_env!("TARGET").map(|s| s.to_string()))
        .unwrap_or_else(|| "x86_64-unknown-linux-gnu".to_string());
    let suffixed = format!("mycelium-{triple}");
    let plain = "mycelium".to_string();
    let mut tried: Vec<PathBuf> = Vec::new();

    // Bundled .deb / AppImage: the launcher lives next to the sidecar
    // under /usr/bin/. Resolve relative to the running executable.
    if let Ok(exe) = std::env::current_exe() {
        if let Some(dir) = exe.parent() {
            for name in [&plain, &suffixed] {
                let p = dir.join(name);
                if p.exists() {
                    return Ok(p);
                }
                tried.push(p);
            }
        }
    }

    // Tauri's resource_dir() — used when externalBin is treated as a
    // resource (older bundles, or when the user moves things around).
    if let Ok(resource) = app.path().resource_dir() {
        for name in [&plain, &suffixed] {
            let p = resource.join(name);
            if p.exists() {
                return Ok(p);
            }
            tried.push(p);
        }
    }

    // Dev mode: `pnpm tauri dev` runs the binary out of target/debug/ so
    // current_exe() is far from src-tauri/binaries/. Use the manifest dir.
    let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
    for name in [&suffixed, &plain] {
        let p = manifest_dir.join("binaries").join(name);
        if p.exists() {
            return Ok(p);
        }
        tried.push(p);
    }

    // Final fallback: trust $PATH if a system-installed mycelium is around.
    if let Ok(path) = std::env::var("PATH") {
        for entry in path.split(':') {
            let p = PathBuf::from(entry).join(&plain);
            if p.exists() {
                return Ok(p);
            }
        }
    }

    Err(AppError::SidecarNotFound(tried))
}
