mod abort_signal;
mod clipboard;
mod command;
mod crypto;
mod html_to_md;
mod input;
mod loader;
mod logs;
pub mod native;
mod path;
mod render_prompt;
mod request;
mod spinner;
mod variables;

pub use self::abort_signal::*;
pub use self::clipboard::set_text;
pub use self::command::*;
pub use self::crypto::*;
pub use self::html_to_md::*;
pub use self::input::*;
pub use self::loader::*;
pub use self::logs::*;
pub use self::path::*;
pub use self::render_prompt::render_prompt;
pub use self::request::*;
pub use self::spinner::*;
pub use self::variables::*;

use anyhow::{Context, Result};
use fancy_regex::Regex;
use fuzzy_matcher::{FuzzyMatcher, skim::SkimMatcherV2};
use is_terminal::IsTerminal;
use nu_ansi_term::Color;
use serde_json::Value;
use std::borrow::Cow;
use std::collections::VecDeque;
use std::sync::atomic::AtomicBool;
use std::sync::{LazyLock, Mutex, OnceLock};
use std::{cmp, env, path::PathBuf, process};
use syntect::highlighting::{Highlighter, Theme};
use syntect::parsing::Scope;

pub static CODE_BLOCK_RE: LazyLock<Regex> =
    LazyLock::new(|| Regex::new(r"(?ms)```\w*(.*)```").unwrap());
pub static THINK_TAG_RE: LazyLock<Regex> =
    LazyLock::new(|| Regex::new(r"(?s)^\s*<think>.*?</think>(\s*|$)").unwrap());
pub static IS_STDOUT_TERMINAL: LazyLock<bool> = LazyLock::new(|| std::io::stdout().is_terminal());
pub static HEADLESS: AtomicBool = AtomicBool::new(false);
pub static ACP_SERVER: AtomicBool = AtomicBool::new(false);

static ACP_PERMISSION_QUEUE: Mutex<VecDeque<Value>> = Mutex::new(VecDeque::new());

pub fn queue_acp_permission(notification: Value) {
    if let Ok(mut q) = ACP_PERMISSION_QUEUE.lock() {
        q.push_back(notification);
    }
}

pub fn drain_acp_permissions() -> Vec<Value> {
    ACP_PERMISSION_QUEUE
        .lock()
        .map(|mut q| q.drain(..).collect())
        .unwrap_or_default()
}
pub static NO_COLOR: LazyLock<bool> = LazyLock::new(|| {
    env::var("NO_COLOR")
        .ok()
        .and_then(|v| parse_bool(&v))
        .unwrap_or_default()
        || !*IS_STDOUT_TERMINAL
});

static TOOL_DIM_COLOR: OnceLock<Color> = OnceLock::new();
static TOOL_FN_COLOR: OnceLock<Color> = OnceLock::new();
static TOOL_KEY_COLOR: OnceLock<Color> = OnceLock::new();
static TOOL_WARN_COLOR: OnceLock<Color> = OnceLock::new();
static REPLAY_LABEL_COLOR: OnceLock<Color> = OnceLock::new();

pub fn init_tool_colors(theme: &Theme) {
    fn resolve(theme: &Theme, scope_str: &str) -> Option<Color> {
        let scope = Scope::new(scope_str).ok()?;
        let style = Highlighter::new(theme).style_mod_for_stack(&[scope]);
        let fg = style.foreground.or(theme.settings.foreground)?;
        let mute = |ch: u8| -> u8 { ((ch as u16 + 128) / 2) as u8 };
        Some(Color::Rgb(mute(fg.r), mute(fg.g), mute(fg.b)))
    }
    if let Some(c) = resolve(theme, "comment") {
        let _ = TOOL_DIM_COLOR.set(c);
    }
    if let Some(c) = resolve(theme, "support.function") {
        let _ = TOOL_FN_COLOR.set(c);
    }
    if let Some(c) = resolve(theme, "constant.numeric") {
        let _ = TOOL_KEY_COLOR.set(c);
    }
    if let Some(c) = resolve(theme, "string") {
        let _ = TOOL_WARN_COLOR.set(c);
    }
    let replay_color = Scope::new("entity.name")
        .ok()
        .and_then(|scope| {
            let style = Highlighter::new(theme).style_mod_for_stack(&[scope]);
            style.foreground.or(theme.settings.foreground)
        })
        .map(|fg| Color::Rgb(fg.r, fg.g, fg.b));
    if let Some(c) = replay_color {
        let _ = REPLAY_LABEL_COLOR.set(c);
    }
}

pub fn now() -> String {
    chrono::Local::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, false)
}

pub fn now_timestamp() -> i64 {
    chrono::Local::now().timestamp()
}

pub fn get_env_name(key: &str) -> String {
    format!("{}_{key}", env!("CARGO_CRATE_NAME"),).to_ascii_uppercase()
}

pub fn normalize_env_name(value: &str) -> String {
    value.replace('-', "_").to_ascii_uppercase()
}

pub fn parse_bool(value: &str) -> Option<bool> {
    match value {
        "1" | "true" => Some(true),
        "0" | "false" => Some(false),
        _ => None,
    }
}

pub fn estimate_token_length(text: &str) -> usize {
    let weighted: usize = text.chars().map(|c| if c.is_ascii() { 1 } else { 2 }).sum();
    weighted.div_ceil(4)
}

pub fn strip_think_tag(text: &str) -> Cow<'_, str> {
    THINK_TAG_RE.replace_all(text, "")
}

pub fn extract_code_block(text: &str) -> &str {
    CODE_BLOCK_RE
        .captures(text)
        .ok()
        .and_then(|v| v?.get(1).map(|v| v.as_str().trim()))
        .unwrap_or(text)
}

pub fn convert_option_string(value: &str) -> Option<String> {
    if value.is_empty() {
        None
    } else {
        Some(value.to_string())
    }
}

pub fn fuzzy_filter<T, F>(values: Vec<T>, get: F, pattern: &str) -> Vec<T>
where
    F: Fn(&T) -> &str,
{
    let matcher = SkimMatcherV2::default();
    let mut list: Vec<(T, i64)> = values
        .into_iter()
        .filter_map(|v| {
            let score = matcher.fuzzy_match(get(&v), pattern)?;
            Some((v, score))
        })
        .collect();
    list.sort_unstable_by_key(|b| cmp::Reverse(b.1));
    list.into_iter().map(|(v, _)| v).collect()
}

pub fn pretty_error(err: &anyhow::Error) -> String {
    let mut output = vec![];
    output.push(format!("Error: {err}"));
    let causes: Vec<_> = err.chain().skip(1).collect();
    let causes_len = causes.len();
    if causes_len > 0 {
        output.push("\nCaused by:".to_string());
        if causes_len == 1 {
            output.push(format!("    {}", indent_text(causes[0], 4).trim()));
        } else {
            for (i, cause) in causes.into_iter().enumerate() {
                output.push(format!("{i:5}: {}", indent_text(cause, 7).trim()));
            }
        }
    }
    output.join("\n")
}

pub fn indent_text<T: ToString>(s: T, size: usize) -> String {
    let indent_str = " ".repeat(size);
    s.to_string()
        .split('\n')
        .map(|line| format!("{indent_str}{line}"))
        .collect::<Vec<String>>()
        .join("\n")
}

pub fn error_text(input: &str) -> String {
    color_text(input, Color::Red)
}

pub fn warning_text(input: &str) -> String {
    color_text(input, Color::Yellow)
}

pub fn muted_warning_text(input: &str) -> String {
    if *NO_COLOR {
        return input.to_string();
    }
    let color = TOOL_WARN_COLOR.get().copied().unwrap_or(Color::Fixed(136));
    color.paint(input).to_string()
}

pub fn color_text(input: &str, color: Color) -> String {
    if *NO_COLOR {
        return input.to_string();
    }
    nu_ansi_term::Style::new()
        .fg(color)
        .paint(input)
        .to_string()
}

pub fn dimmed_text(input: &str) -> String {
    if *NO_COLOR {
        return input.to_string();
    }
    let color = TOOL_DIM_COLOR.get().copied().unwrap_or(Color::Fixed(243));
    color.paint(input).to_string()
}

pub fn cyan_bold_text(input: &str) -> String {
    if *NO_COLOR {
        return input.to_string();
    }
    let color = TOOL_FN_COLOR.get().copied().unwrap_or(Color::Fixed(73));
    nu_ansi_term::Style::new()
        .fg(color)
        .bold()
        .paint(input)
        .to_string()
}

pub fn replay_label_text(input: &str) -> String {
    if *NO_COLOR {
        return input.to_string();
    }
    let color = REPLAY_LABEL_COLOR.get().copied().unwrap_or(Color::Green);
    nu_ansi_term::Style::new()
        .fg(color)
        .bold()
        .paint(input)
        .to_string()
}

pub fn magenta_text(input: &str) -> String {
    if *NO_COLOR {
        return input.to_string();
    }
    let color = TOOL_KEY_COLOR.get().copied().unwrap_or(Color::Fixed(133));
    color.paint(input).to_string()
}

pub fn multiline_text(input: &str) -> String {
    input
        .split('\n')
        .enumerate()
        .map(|(i, v)| {
            if i == 0 {
                v.to_string()
            } else {
                format!(".. {v}")
            }
        })
        .collect::<Vec<String>>()
        .join("\n")
}

pub fn temp_file(prefix: &str, suffix: &str) -> PathBuf {
    env::temp_dir().join(format!(
        "{}-{}{prefix}{}{suffix}",
        env!("CARGO_CRATE_NAME").to_lowercase(),
        process::id(),
        uuid::Uuid::new_v4()
    ))
}

pub fn is_url(path: &str) -> bool {
    path.starts_with("http://") || path.starts_with("https://")
}

pub fn set_proxy(
    mut builder: reqwest::ClientBuilder,
    proxy: &str,
) -> Result<reqwest::ClientBuilder> {
    builder = builder.no_proxy();
    if !proxy.is_empty() && proxy != "-" {
        builder = builder
            .proxy(reqwest::Proxy::all(proxy).with_context(|| format!("Invalid proxy `{proxy}`"))?);
    };
    Ok(builder)
}

pub fn decode_bin<T: serde::de::DeserializeOwned>(data: &[u8]) -> Result<T> {
    let (v, _) = bincode::serde::decode_from_slice(data, bincode::config::legacy())?;
    Ok(v)
}

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

    #[test]
    #[cfg(not(target_os = "windows"))]
    fn test_safe_join_path() {
        assert_eq!(
            safe_join_path("/home/user/dir1", "files/file1"),
            Some(PathBuf::from("/home/user/dir1/files/file1"))
        );
        assert!(safe_join_path("/home/user/dir1", "/files/file1").is_none());
        assert!(safe_join_path("/home/user/dir1", "../file1").is_none());
    }

    #[test]
    #[cfg(target_os = "windows")]
    fn test_safe_join_path() {
        assert_eq!(
            safe_join_path("C:\\Users\\user\\dir1", "files/file1"),
            Some(PathBuf::from("C:\\Users\\user\\dir1\\files\\file1"))
        );
        assert!(safe_join_path("C:\\Users\\user\\dir1", "/files/file1").is_none());
        assert!(safe_join_path("C:\\Users\\user\\dir1", "../file1").is_none());
    }
}
