use serde_json::Value;
use std::collections::HashSet;
use std::path::Path;

/// tsconfig/jsconfig 的路径别名：(匹配前缀, 目标前缀, 是否通配)
/// 例：`@/*` -> `src/*` 存为 ("@/", "src/", true)
pub struct Alias {
    from: String,
    to: String,
    wildcard: bool,
}

/// 候选源码扩展名（按 TS 项目优先级排序）
const EXTS: [&str; 14] = [
    ".ts", ".tsx", ".d.ts", ".js", ".jsx", ".mjs", ".cjs", ".vue", ".py", ".go", ".rs", ".php",
    ".java", ".rb",
];

/// index 桶文件候选
const INDEX_FILES: [&str; 5] = [
    "/index.ts",
    "/index.tsx",
    "/index.js",
    "/index.jsx",
    "/index.mjs",
];

/// 读取项目 tsconfig.json / jsconfig.json，提取 compilerOptions.paths 别名。
/// 解析失败（含 JSONC 注释/尾逗号无法清理等）则返回空，relative 导入仍可解析。
pub fn load_aliases(root: &str) -> Vec<Alias> {
    for name in ["tsconfig.json", "jsconfig.json"] {
        let path = Path::new(root).join(name);
        let Ok(raw) = std::fs::read_to_string(&path) else {
            continue;
        };
        let cleaned = strip_jsonc(&raw);
        let Ok(json) = serde_json::from_str::<Value>(&cleaned) else {
            continue;
        };
        let opts = json.get("compilerOptions");
        let base_url = opts
            .and_then(|o| o.get("baseUrl"))
            .and_then(|v| v.as_str())
            .unwrap_or(".");
        let Some(paths) = opts.and_then(|o| o.get("paths")).and_then(|v| v.as_object()) else {
            continue;
        };
        let mut aliases = Vec::new();
        for (pattern, targets) in paths {
            let Some(target) = targets
                .as_array()
                .and_then(|a| a.first())
                .and_then(|v| v.as_str())
            else {
                continue;
            };
            let wildcard = pattern.ends_with('*');
            let from = pattern.trim_end_matches('*').to_string();
            let target_stripped = target.trim_end_matches('*');
            // 目标相对 baseUrl（baseUrl 相对项目根）
            let to = normalize(&join(base_url, target_stripped));
            let to = if to.is_empty() {
                String::new()
            } else {
                format!("{to}/").replace("//", "/")
            };
            // to 末尾统一带 '/' 便于通配拼接；非通配时去掉多余尾斜杠在使用处处理
            aliases.push(Alias {
                from,
                to: if wildcard {
                    to
                } else {
                    to.trim_end_matches('/').to_string()
                },
                wildcard,
            });
        }
        return aliases;
    }
    Vec::new()
}

/// 把一个导入 specifier 解析为项目内的实际文件 rel_path（正斜杠）；解析不到返回 None。
pub fn resolve_import(
    module: &str,
    from_rel: &str,
    files: &HashSet<String>,
    aliases: &[Alias],
) -> Option<String> {
    let base = if let Some(b) = apply_alias(module, aliases) {
        normalize(&b)
    } else if module.starts_with('.') {
        // 相对当前文件目录
        normalize(&join(&parent_dir(from_rel), module))
    } else {
        return None; // 裸 specifier（第三方/标准库）不解析
    };
    try_candidates(&base, files)
}

fn apply_alias(module: &str, aliases: &[Alias]) -> Option<String> {
    for a in aliases {
        if a.wildcard {
            if let Some(rest) = module.strip_prefix(&a.from) {
                return Some(format!("{}{}", a.to, rest));
            }
        } else if module == a.from {
            return Some(a.to.clone());
        }
    }
    None
}

fn try_candidates(base: &str, files: &HashSet<String>) -> Option<String> {
    if base.is_empty() {
        return None;
    }
    if files.contains(base) {
        return Some(base.to_string());
    }
    for ext in EXTS {
        let cand = format!("{base}{ext}");
        if files.contains(&cand) {
            return Some(cand);
        }
    }
    for idx in INDEX_FILES {
        let cand = format!("{base}{idx}");
        if files.contains(&cand) {
            return Some(cand);
        }
    }
    None
}

/// 父目录（正斜杠 rel 路径）；无父目录返回空串
fn parent_dir(rel: &str) -> String {
    match rel.rfind('/') {
        Some(i) => rel[..i].to_string(),
        None => String::new(),
    }
}

fn join(a: &str, b: &str) -> String {
    if a.is_empty() || a == "." {
        b.to_string()
    } else {
        format!("{a}/{b}")
    }
}

/// 规范化路径：处理 `.`、`..`、空段，输出正斜杠 rel
fn normalize(path: &str) -> String {
    let mut parts: Vec<&str> = Vec::new();
    for seg in path.split('/') {
        match seg {
            "" | "." => {}
            ".." => {
                parts.pop();
            }
            s => parts.push(s),
        }
    }
    parts.join("/")
}

/// 去除 JSONC 的 // 与 /* */ 注释及尾逗号，尽量让 serde_json 能解析 tsconfig
fn strip_jsonc(src: &str) -> String {
    let mut out = String::with_capacity(src.len());
    let bytes = src.as_bytes();
    let mut i = 0;
    let mut in_str = false;
    while i < bytes.len() {
        let c = bytes[i];
        if in_str {
            out.push(c as char);
            if c == b'\\' && i + 1 < bytes.len() {
                out.push(bytes[i + 1] as char);
                i += 2;
                continue;
            }
            if c == b'"' {
                in_str = false;
            }
            i += 1;
            continue;
        }
        match c {
            b'"' => {
                in_str = true;
                out.push('"');
                i += 1;
            }
            b'/' if i + 1 < bytes.len() && bytes[i + 1] == b'/' => {
                // 行注释
                i += 2;
                while i < bytes.len() && bytes[i] != b'\n' {
                    i += 1;
                }
            }
            b'/' if i + 1 < bytes.len() && bytes[i + 1] == b'*' => {
                // 块注释
                i += 2;
                while i + 1 < bytes.len() && !(bytes[i] == b'*' && bytes[i + 1] == b'/') {
                    i += 1;
                }
                i += 2;
            }
            _ => {
                out.push(c as char);
                i += 1;
            }
        }
    }
    // 去尾逗号：`,` 后面只有空白再接 } 或 ]
    strip_trailing_commas(&out)
}

fn strip_trailing_commas(s: &str) -> String {
    let mut out = String::with_capacity(s.len());
    let chars: Vec<char> = s.chars().collect();
    let mut i = 0;
    while i < chars.len() {
        if chars[i] == ',' {
            // 向后看是否只有空白后接 } 或 ]
            let mut j = i + 1;
            while j < chars.len() && chars[j].is_whitespace() {
                j += 1;
            }
            if j < chars.len() && (chars[j] == '}' || chars[j] == ']') {
                i += 1; // 跳过逗号
                continue;
            }
        }
        out.push(chars[i]);
        i += 1;
    }
    out
}

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

    fn fileset(items: &[&str]) -> HashSet<String> {
        items.iter().map(|s| s.to_string()).collect()
    }

    #[test]
    fn resolves_relative_with_ext_and_index() {
        let files = fileset(&["src/a.ts", "src/util/index.ts", "src/b/c.tsx"]);
        let r = resolve_import("./a", "src/main.ts", &files, &[]);
        assert_eq!(r.as_deref(), Some("src/a.ts"));
        let r2 = resolve_import("./util", "src/main.ts", &files, &[]);
        assert_eq!(r2.as_deref(), Some("src/util/index.ts"));
        let r3 = resolve_import("../b/c", "src/x/y.ts", &files, &[]);
        assert_eq!(r3.as_deref(), Some("src/b/c.tsx"));
    }

    #[test]
    fn resolves_alias() {
        let files = fileset(&["src/stores/cloud.ts"]);
        let aliases = vec![Alias {
            from: "@/".into(),
            to: "src/".into(),
            wildcard: true,
        }];
        let r = resolve_import("@/stores/cloud", "src/pages/home.ts", &files, &aliases);
        assert_eq!(r.as_deref(), Some("src/stores/cloud.ts"));
    }

    #[test]
    fn bare_specifier_unresolved() {
        let files = fileset(&["src/a.ts"]);
        assert!(resolve_import("pinia", "src/a.ts", &files, &[]).is_none());
        assert!(resolve_import("react", "src/a.ts", &files, &[]).is_none());
    }

    #[test]
    fn strips_jsonc_comments_and_trailing_commas() {
        let src = r#"{
            // line comment
            "compilerOptions": {
                "baseUrl": ".",
                "paths": { "@/*": ["src/*"], }, /* block */
            },
        }"#;
        let cleaned = strip_jsonc(src);
        let json: Value = serde_json::from_str(&cleaned).expect("应能解析");
        assert!(json["compilerOptions"]["paths"]["@/*"].is_array());
    }
}
