// SPDX-License-Identifier: MIT
// Copyright (c) 2025-2026 SenWeaverCoding
// Licensed under the MIT License.

pub mod retry;

use std::ffi::{OsStr, OsString};
use std::path::{Path, PathBuf};
use std::sync::{Arc, OnceLock};

use dashmap::DashMap;
use parking_lot::Mutex;

struct ProcessEnvRegistry {
    vars: DashMap<String, String>,
    batch_lock: Mutex<()>,
}

impl ProcessEnvRegistry {
    fn new() -> Self {
        Self {
            vars: DashMap::new(),
            batch_lock: Mutex::new(()),
        }
    }
}

static PROCESS_ENV: OnceLock<Arc<ProcessEnvRegistry>> = OnceLock::new();

fn registry() -> Arc<ProcessEnvRegistry> {
    PROCESS_ENV
        .get_or_init(|| Arc::new(ProcessEnvRegistry::new()))
        .clone()
}

#[inline]
pub fn set_runtime_var<K: AsRef<OsStr>, V: AsRef<OsStr>>(key: K, value: V) {
    let key = key.as_ref().to_string_lossy().into_owned();
    let value = value.as_ref().to_string_lossy().into_owned();
    registry().vars.insert(key, value);
}

#[inline]
pub fn remove_runtime_var<K: AsRef<OsStr>>(key: K) {
    let key = key.as_ref().to_string_lossy().into_owned();
    registry().vars.remove(&key);
}

pub fn strip_verbatim_prefix(path: PathBuf) -> PathBuf {
    let raw = path.to_string_lossy();
    if let Some(rest) = raw.strip_prefix(r"\\?\UNC\") {
        return PathBuf::from(format!(r"\\{rest}"));
    }
    if let Some(rest) = raw.strip_prefix(r"\\?\") {
        return PathBuf::from(rest.to_string());
    }
    path
}

pub fn path_is_within(child: &Path, ancestor: &Path) -> bool {
    let c = strip_verbatim_prefix(child.to_path_buf());
    let a = strip_verbatim_prefix(ancestor.to_path_buf());
    #[cfg(windows)]
    {
        let cs = c.to_string_lossy().to_lowercase();
        let asr = a.to_string_lossy().to_lowercase();
        let sep_back = format!("{asr}\\");
        let sep_fwd = format!("{asr}/");
        cs == asr || cs.starts_with(&sep_back) || cs.starts_with(&sep_fwd)
    }
    #[cfg(not(windows))]
    {
        c == a || c.starts_with(&a)
    }
}

pub fn path_relative_to(child: &Path, ancestor: &Path) -> Option<PathBuf> {
    let c = strip_verbatim_prefix(child.to_path_buf());
    let a = strip_verbatim_prefix(ancestor.to_path_buf());
    c.strip_prefix(&a).ok().map(Path::to_path_buf)
}

pub fn set_runtime_vars_batch(entries: &[(impl AsRef<str>, Option<impl AsRef<str>>)]) {
    let reg = registry();
    let _guard = reg.batch_lock.lock();
    for (key, value) in entries {
        let key = key.as_ref().to_string();
        match value {
            Some(v) => {
                reg.vars
                    .insert(key, v.as_ref().to_string());
            }
            None => {
                reg.vars.remove(&key);
            }
        }
    }
}

pub fn get_runtime_var(key: &str) -> Option<String> {
    if let Some(v) = registry().vars.get(key) {
        return Some(v.clone());
    }
    std::env::var(key).ok()
}

pub fn get_runtime_var_os(key: &str) -> Option<OsString> {
    if let Some(v) = registry().vars.get(key) {
        return Some(OsString::from(v.as_str()));
    }
    std::env::var_os(key)
}

#[inline]
pub fn is_bare_mode() -> bool {
    matches!(
        get_runtime_var("SEN_CLI_BARE").as_deref(),
        Some("1") | Some("true") | Some("yes") | Some("on")
    )
}

const SERIAL_ALLOWED_PATH_PREFIXES: &[&str] = &[
    "/dev/ttyACM",
    "/dev/ttyUSB",
    "/dev/tty.usbmodem",
    "/dev/cu.usbmodem",
    "/dev/tty.usbserial",
    "/dev/cu.usbserial",
    "COM",
];

pub fn is_serial_path_allowed(path: &str) -> bool {
    SERIAL_ALLOWED_PATH_PREFIXES
        .iter()
        .any(|prefix| path.starts_with(prefix))
}

#[inline]
pub fn truncate_with_ellipsis(s: &str, max_chars: usize) -> String {
    match s.char_indices().nth(max_chars) {
        Some((idx, _)) => {
            let truncated = &s[..idx];

            format!("{}...", truncated.trim_end())
        }
        None => s.to_string(),
    }
}

#[inline]
#[must_use]
pub fn floor_char_boundary(s: &str, max_bytes: usize) -> usize {
    if max_bytes >= s.len() {
        return s.len();
    }
    let mut idx = max_bytes;
    while idx > 0 && !s.is_char_boundary(idx) {
        idx -= 1;
    }
    idx
}

#[inline]
#[must_use]
pub fn ceil_char_boundary(s: &str, index: usize) -> usize {
    if index >= s.len() {
        return s.len();
    }
    let mut idx = index;
    while idx < s.len() && !s.is_char_boundary(idx) {
        idx += 1;
    }
    idx
}

#[inline]
#[must_use]
pub fn truncate_str_bytes(s: &str, max_bytes: usize) -> &str {
    &s[..floor_char_boundary(s, max_bytes)]
}

pub fn truncate_string_bytes(s: &mut String, max_bytes: usize) {
    if s.len() > max_bytes {
        let boundary = floor_char_boundary(s, max_bytes);
        s.truncate(boundary);
    }
}

pub trait SafeStrSlice {
    fn byte_prefix(&self, max_bytes: usize) -> &str;

    fn byte_suffix(&self, max_bytes: usize) -> &str;
}

impl SafeStrSlice for str {
    #[inline]
    fn byte_prefix(&self, max_bytes: usize) -> &str {
        truncate_str_bytes(self, max_bytes)
    }

    #[inline]
    fn byte_suffix(&self, max_bytes: usize) -> &str {
        let start = self.len().saturating_sub(max_bytes);
        &self[ceil_char_boundary(self, start)..]
    }
}

pub fn redact_secret(s: &str) -> String {
    if s.is_empty() {
        return "<empty>".to_string();
    }
    format!("<redacted len={}>", s.len())
}

pub enum MaybeSet<T> {
    Set(T),
    Unset,
    Null,
}

#[cfg(windows)]
pub const CREATE_NO_WINDOW: u32 = 0x0800_0000;

pub trait HiddenWindowCommandExt {
    fn hide_window(&mut self) -> &mut Self;
}

#[cfg(windows)]
impl HiddenWindowCommandExt for std::process::Command {
    fn hide_window(&mut self) -> &mut Self {
        use std::os::windows::process::CommandExt;
        self.creation_flags(CREATE_NO_WINDOW)
    }
}

#[cfg(not(windows))]
impl HiddenWindowCommandExt for std::process::Command {
    fn hide_window(&mut self) -> &mut Self {
        self
    }
}

#[cfg(windows)]
impl HiddenWindowCommandExt for tokio::process::Command {
    fn hide_window(&mut self) -> &mut Self {
        self.creation_flags(CREATE_NO_WINDOW)
    }
}

#[cfg(not(windows))]
impl HiddenWindowCommandExt for tokio::process::Command {
    fn hide_window(&mut self) -> &mut Self {
        self
    }
}

pub fn hidden_sync_command<S: AsRef<OsStr>>(program: S) -> std::process::Command {
    let mut cmd = std::process::Command::new(program);
    cmd.hide_window();
    cmd
}

pub fn hidden_async_command<S: AsRef<OsStr>>(program: S) -> tokio::process::Command {
    let mut cmd = tokio::process::Command::new(program);
    cmd.hide_window();
    cmd
}

pub fn decode_subprocess_bytes(raw: &[u8]) -> String {
    String::from_utf8_lossy(raw).into_owned()
}

pub fn describe_panic(payload: &(dyn std::any::Any + Send)) -> String {
    if let Some(s) = payload.downcast_ref::<&'static str>() {
        (*s).to_string()
    } else if let Some(s) = payload.downcast_ref::<String>() {
        s.clone()
    } else {
        "unknown panic".to_string()
    }
}

pub fn atomic_write(path: &std::path::Path, bytes: &[u8]) -> std::io::Result<()> {
    use std::io::Write;
    use std::path::PathBuf;
    let dir = path
        .parent()
        .filter(|p| !p.as_os_str().is_empty())
        .map(|p| p.to_path_buf())
        .unwrap_or_else(|| PathBuf::from("."));
    let base = path
        .file_name()
        .map(|n| n.to_string_lossy().into_owned())
        .unwrap_or_else(|| "out".to_string());
    let nanos = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| d.as_nanos())
        .unwrap_or(0);
    let tmp = dir.join(format!(".{base}.{}.{nanos}.tmp", std::process::id()));
    {
        let mut f = std::fs::File::create(&tmp)?;
        f.write_all(bytes)?;
        f.sync_all()?;
    }
    if let Ok(meta) = std::fs::metadata(path) {
        let _ = std::fs::set_permissions(&tmp, meta.permissions());
    }
    match std::fs::rename(&tmp, path) {
        Ok(()) => Ok(()),
        Err(e) => {
            let _ = std::fs::remove_file(&tmp);
            Err(e)
        }
    }
}

pub async fn atomic_write_async(
    path: impl AsRef<std::path::Path>,
    bytes: impl Into<Vec<u8>>,
) -> std::io::Result<()> {
    let path = path.as_ref().to_path_buf();
    let bytes = bytes.into();
    tokio::task::spawn_blocking(move || atomic_write(&path, &bytes))
        .await
        .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?
}
