use std::io;
use std::path::PathBuf;
use std::time::SystemTime;

use md5::{Digest, Md5};
use serde::Deserialize;

/// Chrome 북마크 JSON 최상위 구조
#[derive(Deserialize)]
pub struct Bookmarks {
    roots: Roots,
}

/// 북마크 루트 노드 (bookmark_bar, other, synced)
#[derive(Deserialize)]
struct Roots {
    bookmark_bar: BookmarkNode,
    other: BookmarkNode,
    synced: BookmarkNode,
}

/// 북마크 노드 (폴더 또는 URL)
#[derive(Deserialize)]
struct BookmarkNode {
    #[serde(default)]
    name: String,
    #[serde(rename = "type")]
    node_type: String,
    #[serde(default)]
    url: Option<String>,
    #[serde(default)]
    children: Option<Vec<BookmarkNode>>,
}

/// 파싱된 북마크 항목
#[derive(Clone)]
pub struct BookmarkEntry {
    pub folder: String,
    pub name: String,
    pub url: String,
    pub is_empty_folder: bool,
}

/// URL 검사 결과
#[derive(Debug, Clone)]
pub struct CheckResult {
    pub folder: String,
    pub name: String,
    pub url: String,
    pub status: String,
    pub is_valid: bool,
}

/// 탐색된 Chrome 프로필 정보
pub struct ProfileInfo {
    pub dir_name: String,
    pub display_name: String,
    pub bookmarks_path: PathBuf,
}

/// 북마크 노드를 재귀적으로 탐색하여 URL 항목과 빈 폴더를 수집
fn collect_entries(node: &BookmarkNode, folder_path: &str, entries: &mut Vec<BookmarkEntry>) {
    let current_path = if folder_path.is_empty() {
        node.name.clone()
    } else {
        format!("{}/{}", folder_path, node.name)
    };

    if node.node_type == "url" {
        if let Some(url) = &node.url {
            entries.push(BookmarkEntry {
                folder: folder_path.to_string(),
                name: node.name.clone(),
                url: url.clone(),
                is_empty_folder: false,
            });
        }
    }

    if let Some(children) = &node.children {
        if node.node_type == "folder" && children.is_empty() {
            entries.push(BookmarkEntry {
                folder: folder_path.to_string(),
                name: node.name.clone(),
                url: format!("folder://{current_path}"),
                is_empty_folder: true,
            });
        }
        for child in children {
            collect_entries(child, &current_path, entries);
        }
    }
}

/// OS별 Chrome 데이터 기본 디렉토리 반환
fn chrome_base_dir() -> PathBuf {
    let home = dirs::home_dir().expect("Failed to get home directory");
    if cfg!(target_os = "macos") {
        home.join("Library/Application Support/Google/Chrome")
    } else if cfg!(target_os = "linux") {
        home.join(".config/google-chrome")
    } else {
        home.join(r"AppData\Local\Google\Chrome\User Data")
    }
}

/// Chrome 디렉토리를 스캔하여 Bookmarks 파일이 있는 프로필 목록 반환
pub fn discover_profiles() -> Vec<ProfileInfo> {
    let base = chrome_base_dir();
    if !base.exists() {
        return Vec::new();
    }

    let mut profiles = Vec::new();
    let Ok(entries) = std::fs::read_dir(&base) else {
        return Vec::new();
    };

    for entry in entries.flatten() {
        let path = entry.path();
        if !path.is_dir() {
            continue;
        }

        let account_bookmarks = path.join("AccountBookmarks");
        let bookmarks_path = path.join("Bookmarks");
        let bookmarks_bak = path.join("Bookmarks.bak");
        if !account_bookmarks.exists() && !bookmarks_path.exists() && !bookmarks_bak.exists() {
            continue;
        }

        let dir_name = path
            .file_name()
            .unwrap_or_default()
            .to_string_lossy()
            .to_string();

        let display_name = path
            .join("Preferences")
            .pipe_read()
            .and_then(|content| {
                let v: serde_json::Value = serde_json::from_str(&content).ok()?;
                v["profile"]["name"].as_str().map(String::from)
            })
            .unwrap_or_else(|| dir_name.clone());

        let primary_path = if account_bookmarks.exists() {
            account_bookmarks
        } else if bookmarks_path.exists() {
            bookmarks_path
        } else {
            bookmarks_bak
        };

        profiles.push(ProfileInfo {
            dir_name,
            display_name,
            bookmarks_path: primary_path,
        });
    }

    profiles.sort_by(|a, b| a.dir_name.cmp(&b.dir_name));
    profiles
}

trait PipeRead {
    fn pipe_read(&self) -> Option<String>;
}

impl PipeRead for PathBuf {
    fn pipe_read(&self) -> Option<String> {
        std::fs::read_to_string(self).ok()
    }
}

/// AccountBookmarks → Bookmarks → Bookmarks.bak 순서로 유효한 파일 경로 반환
pub fn resolve_bookmarks_path(path: &PathBuf) -> PathBuf {
    // AccountBookmarks 우선
    if let Some(parent) = path.parent() {
        let account = parent.join("AccountBookmarks");
        if account.exists() {
            let entries = parse_bookmarks_from(&account);
            if entries.iter().any(|e| !e.is_empty_folder) {
                return account;
            }
        }
    }

    // Bookmarks
    let entries = parse_bookmarks_from(path);
    if entries.iter().any(|e| !e.is_empty_folder) {
        return path.clone();
    }

    // Bookmarks.bak
    let bak = path.with_extension("bak");
    if bak.exists() {
        let bak_entries = parse_bookmarks_from(&bak);
        if bak_entries.iter().any(|e| !e.is_empty_folder) {
            return bak;
        }
    }

    path.clone()
}

pub fn parse_bookmarks(path: &PathBuf) -> Vec<BookmarkEntry> {
    parse_bookmarks_from(path)
}

/// 지정된 경로의 북마크 파일을 파싱
fn parse_bookmarks_from(path: &PathBuf) -> Vec<BookmarkEntry> {
    let content = match std::fs::read_to_string(path) {
        Ok(c) => c,
        Err(_) => return Vec::new(),
    };

    let bookmarks: Bookmarks = match serde_json::from_str(&content) {
        Ok(b) => b,
        Err(_) => return Vec::new(),
    };

    let mut entries = Vec::new();
    collect_entries(&bookmarks.roots.bookmark_bar, "", &mut entries);
    collect_entries(&bookmarks.roots.other, "", &mut entries);
    collect_entries(&bookmarks.roots.synced, "", &mut entries);
    entries
}

/// URL에 HTTP HEAD 요청을 보내 상태 코드와 상태 텍스트 반환
pub async fn check_url(client: &reqwest::Client, url: &str) -> (u16, String) {
    match client.head(url).send().await {
        Ok(resp) => {
            let status = resp.status().as_u16();
            (status, resp.status().to_string())
        }
        Err(e) => {
            if e.is_timeout() {
                (0, "TIMEOUT".to_string())
            } else if e.is_connect() {
                (0, "CONN_ERR".to_string())
            } else {
                let msg = e.to_string();
                let short = if msg.len() > 20 {
                    format!("{}...", &msg[..20])
                } else {
                    msg
                };
                (0, short)
            }
        }
    }
}

/// Chrome 북마크 checksum 계산 (Chromium bookmark_codec.cc 호환)
/// 각 노드: id(UTF-8) + name(UTF-16LE) + type(UTF-8) + url(UTF-8, url 노드만)
fn hash_node(hasher: &mut Md5, node: &serde_json::Value) {
    // id: UTF-8
    if let Some(id) = node.get("id").and_then(|v| v.as_str()) {
        hasher.update(id.as_bytes());
    }
    // name/title: UTF-16LE raw bytes
    if let Some(name) = node.get("name").and_then(|v| v.as_str()) {
        for ch in name.encode_utf16() {
            hasher.update(ch.to_le_bytes());
        }
    }
    // type: UTF-8
    let node_type = node.get("type").and_then(|v| v.as_str()).unwrap_or("");
    hasher.update(node_type.as_bytes());
    match node_type {
        "url" => {
            // url: UTF-8
            if let Some(url) = node.get("url").and_then(|v| v.as_str()) {
                hasher.update(url.as_bytes());
            }
        }
        "folder" => {
            if let Some(children) = node.get("children").and_then(|v| v.as_array()) {
                for child in children {
                    hash_node(hasher, child);
                }
            }
        }
        _ => {}
    }
}

/// roots 객체로부터 Chrome 호환 checksum 문자열 계산
fn compute_checksum(roots: &serde_json::Value) -> String {
    let mut hasher = Md5::new();
    for key in &["bookmark_bar", "other", "synced"] {
        if let Some(root) = roots.get(*key) {
            hash_node(&mut hasher, root);
        }
    }
    format!("{:x}", hasher.finalize())
}

/// 현재 시간을 Chrome 타임스탬프(1601-01-01 기준 마이크로초)로 변환
fn chrome_timestamp_now() -> String {
    // Windows epoch (1601-01-01) ~ Unix epoch (1970-01-01) 차이: 11644473600초
    const WINDOWS_EPOCH_DELTA: u64 = 11_644_473_600;
    let unix_secs = SystemTime::now()
        .duration_since(SystemTime::UNIX_EPOCH)
        .unwrap_or_default()
        .as_micros() as u64;
    let chrome_ts = unix_secs + WINDOWS_EPOCH_DELTA * 1_000_000;
    chrome_ts.to_string()
}

/// 노드의 date_modified를 현재 시간으로 업데이트
fn touch_date_modified(node: &mut serde_json::Value) {
    if node.get("type").and_then(|t| t.as_str()) == Some("folder") {
        node["date_modified"] = serde_json::Value::String(chrome_timestamp_now());
    }
}

/// JSON 값에 checksum을 업데이트하고 파일에 저장
fn save_bookmarks(path: &PathBuf, value: &mut serde_json::Value) -> io::Result<()> {
    if let Some(roots) = value.get("roots") {
        let checksum = compute_checksum(roots);
        value["checksum"] = serde_json::Value::String(checksum);
    }
    // Chrome이 sync_metadata 기준으로 북마크를 복원하지 않도록 제거
    if let Some(obj) = value.as_object_mut() {
        obj.remove("sync_metadata");
    }
    let output = serde_json::to_string_pretty(value)
        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
    std::fs::write(path, output)?;
    // Chrome이 .bak에서 복원하지 않도록 삭제
    let bak = path.with_extension("bak");
    if bak.exists() {
        let _ = std::fs::remove_file(&bak);
    }
    // Chrome이 Sync LevelDB 기준으로 북마크를 복원하지 않도록 삭제
    if let Some(profile_dir) = path.parent() {
        let sync_db = profile_dir.join("Sync Data").join("LevelDB");
        if sync_db.is_dir() {
            let _ = std::fs::remove_dir_all(&sync_db);
        }
    }
    Ok(())
}

/// Bookmarks JSON 파일에서 URL로 항목을 찾아 name/url 업데이트
pub fn update_bookmarks_file(
    path: &PathBuf,
    original_url: &str,
    new_name: &str,
    new_url: &str,
) -> io::Result<()> {
    let content = std::fs::read_to_string(path)?;
    let mut value: serde_json::Value = serde_json::from_str(&content)
        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;

    fn update_node(
        node: &mut serde_json::Value,
        original_url: &str,
        new_name: &str,
        new_url: &str,
    ) -> bool {
        if node.get("type").and_then(|t| t.as_str()) == Some("url")
            && node.get("url").and_then(|u| u.as_str()) == Some(original_url)
        {
            node["name"] = serde_json::Value::String(new_name.to_string());
            node["url"] = serde_json::Value::String(new_url.to_string());
            return true;
        }
        if let Some(children) = node.get_mut("children").and_then(|c| c.as_array_mut()) {
            for child in children {
                if update_node(child, original_url, new_name, new_url) {
                    return true;
                }
            }
        }
        false
    }

    fn update_node_and_touch_parent(
        node: &mut serde_json::Value,
        original_url: &str,
        new_name: &str,
        new_url: &str,
    ) -> bool {
        if let Some(children) = node.get("children").and_then(|c| c.as_array()) {
            let found = children.iter().any(|child| {
                child.get("type").and_then(|t| t.as_str()) == Some("url")
                    && child.get("url").and_then(|u| u.as_str()) == Some(original_url)
            });
            if found {
                update_node(node, original_url, new_name, new_url);
                touch_date_modified(node);
                return true;
            }
        }
        if let Some(children) = node.get_mut("children").and_then(|c| c.as_array_mut()) {
            for child in children {
                if update_node_and_touch_parent(child, original_url, new_name, new_url) {
                    return true;
                }
            }
        }
        false
    }

    if let Some(roots) = value.get_mut("roots") {
        for key in &["bookmark_bar", "other", "synced"] {
            if let Some(root) = roots.get_mut(*key) {
                if update_node_and_touch_parent(root, original_url, new_name, new_url) {
                    touch_date_modified(root);
                    break;
                }
            }
        }
    }

    save_bookmarks(path, &mut value)
}

/// Bookmarks JSON 파일에서 URL로 항목을 찾아 삭제
pub fn delete_bookmark_from_file(path: &PathBuf, target_url: &str) -> io::Result<()> {
    let content = std::fs::read_to_string(path)?;
    let mut value: serde_json::Value = serde_json::from_str(&content)
        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;

    fn remove_node(node: &mut serde_json::Value, target_url: &str) -> bool {
        if let Some(children) = node.get_mut("children").and_then(|c| c.as_array_mut()) {
            let before = children.len();
            children.retain(|child| {
                !(child.get("type").and_then(|t| t.as_str()) == Some("url")
                    && child.get("url").and_then(|u| u.as_str()) == Some(target_url))
            });
            if children.len() < before {
                touch_date_modified(node);
                return true;
            }
            for child in children {
                if remove_node(child, target_url) {
                    return true;
                }
            }
        }
        false
    }

    if let Some(roots) = value.get_mut("roots") {
        for key in &["bookmark_bar", "other", "synced"] {
            if let Some(root) = roots.get_mut(*key) {
                if remove_node(root, target_url) {
                    touch_date_modified(root);
                    break;
                }
            }
        }
    }

    save_bookmarks(path, &mut value)
}

/// Bookmarks JSON 파일에서 빈 폴더를 찾아 삭제
pub fn delete_empty_folder_from_file(
    path: &PathBuf,
    parent_folder: &str,
    folder_name: &str,
) -> io::Result<()> {
    let content = std::fs::read_to_string(path)?;
    let mut value: serde_json::Value = serde_json::from_str(&content)
        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;

    /// 폴더 경로를 따라 내려가서 빈 폴더 제거
    fn navigate_and_remove(
        node: &mut serde_json::Value,
        path_parts: &[&str],
        target_name: &str,
    ) -> bool {
        if path_parts.is_empty() {
            if let Some(children) = node.get_mut("children").and_then(|c| c.as_array_mut()) {
                let before = children.len();
                children.retain(|child| {
                    !(child.get("type").and_then(|t| t.as_str()) == Some("folder")
                        && child.get("name").and_then(|n| n.as_str()) == Some(target_name)
                        && child
                            .get("children")
                            .and_then(|c| c.as_array())
                            .is_some_and(|c| c.is_empty()))
                });
                if children.len() < before {
                    touch_date_modified(node);
                    return true;
                }
            }
            return false;
        }
        if let Some(children) = node.get_mut("children").and_then(|c| c.as_array_mut()) {
            for child in children.iter_mut() {
                if child.get("type").and_then(|t| t.as_str()) == Some("folder")
                    && child.get("name").and_then(|n| n.as_str()) == Some(path_parts[0])
                    && navigate_and_remove(child, &path_parts[1..], target_name)
                {
                    return true;
                }
            }
        }
        false
    }

    // parent_folder: "Bookmarks Bar/sub" → ["Bookmarks Bar", "sub"]
    let parts: Vec<&str> = parent_folder.split('/').filter(|s| !s.is_empty()).collect();

    if let Some(roots) = value.get_mut("roots") {
        for key in &["bookmark_bar", "other", "synced"] {
            if let Some(root) = roots.get_mut(*key) {
                let root_name = root
                    .get("name")
                    .and_then(|n| n.as_str())
                    .unwrap_or_default();
                if !parts.is_empty() && parts[0] == root_name {
                    if navigate_and_remove(root, &parts[1..], folder_name) {
                        break;
                    }
                } else if parts.is_empty() && navigate_and_remove(root, &[], folder_name) {
                    break;
                }
            }
        }
    }

    save_bookmarks(path, &mut value)
}

/// Bookmarks JSON에서 URL 항목을 다른 폴더로 이동
pub fn move_bookmark_to_folder(
    path: &PathBuf,
    target_url: &str,
    new_folder: &str,
) -> io::Result<()> {
    let content = std::fs::read_to_string(path)?;
    let mut value: serde_json::Value = serde_json::from_str(&content)
        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;

    // 1단계: 대상 노드를 찾아서 제거하고 복사본 보관
    fn remove_and_capture(
        node: &mut serde_json::Value,
        target_url: &str,
    ) -> Option<serde_json::Value> {
        if let Some(children) = node.get_mut("children").and_then(|c| c.as_array_mut()) {
            let pos = children.iter().position(|child| {
                child.get("type").and_then(|t| t.as_str()) == Some("url")
                    && child.get("url").and_then(|u| u.as_str()) == Some(target_url)
            });
            if let Some(idx) = pos {
                let captured = children.remove(idx);
                touch_date_modified(node);
                return Some(captured);
            }
            for child in children.iter_mut() {
                if let Some(captured) = remove_and_capture(child, target_url) {
                    return Some(captured);
                }
            }
        }
        None
    }

    // 2단계: 폴더 경로로 대상 폴더 노드를 찾거나 생성
    fn find_or_create_folder<'a>(
        node: &'a mut serde_json::Value,
        folder_parts: &[&str],
    ) -> &'a mut serde_json::Value {
        if folder_parts.is_empty() {
            return node;
        }
        let target_name = folder_parts[0];
        let rest = &folder_parts[1..];

        let children = node
            .get_mut("children")
            .and_then(|c| c.as_array_mut())
            .expect("folder node must have children");

        // 이름이 일치하는 폴더 찾기
        let pos = children.iter().position(|child| {
            child.get("type").and_then(|t| t.as_str()) == Some("folder")
                && child.get("name").and_then(|n| n.as_str()) == Some(target_name)
        });

        let idx = if let Some(i) = pos {
            i
        } else {
            // 폴더가 없으면 새로 생성
            let new_folder = serde_json::json!({
                "children": [],
                "name": target_name,
                "type": "folder"
            });
            children.push(new_folder);
            children.len() - 1
        };

        find_or_create_folder(&mut children[idx], rest)
    }

    // 루트에서 대상 노드 제거
    let mut captured = None;
    if let Some(roots) = value.get_mut("roots") {
        for key in &["bookmark_bar", "other", "synced"] {
            if let Some(root) = roots.get_mut(*key) {
                captured = remove_and_capture(root, target_url);
                if captured.is_some() {
                    break;
                }
            }
        }
    }

    let Some(bookmark_node) = captured else {
        return Ok(());
    };

    // 새 폴더 경로 파싱: "root_name/sub1/sub2" → root_name에서 시작
    let parts: Vec<&str> = new_folder.split('/').filter(|s| !s.is_empty()).collect();
    if parts.is_empty() {
        return Ok(());
    }

    // 첫 번째 파트가 루트 키인지 확인
    let root_keys = ["bookmark_bar", "other", "synced"];
    if let Some(roots) = value.get_mut("roots") {
        let (root_node, remaining) = if root_keys.contains(&parts[0]) {
            (roots.get_mut(parts[0]).unwrap(), &parts[1..])
        } else {
            // 루트 키가 아니면 루트 이름으로 매칭 시도
            let found = root_keys.iter().find(|&&key| {
                roots
                    .get(key)
                    .and_then(|r| r.get("name"))
                    .and_then(|n| n.as_str())
                    == Some(parts[0])
            });
            if let Some(&key) = found {
                (roots.get_mut(key).unwrap(), &parts[1..])
            } else {
                // 기본적으로 bookmark_bar에 넣기
                (roots.get_mut("bookmark_bar").unwrap(), &parts[..])
            }
        };

        let target_folder = find_or_create_folder(root_node, remaining);
        if let Some(children) = target_folder
            .get_mut("children")
            .and_then(|c| c.as_array_mut())
        {
            children.push(bookmark_node);
        }
        touch_date_modified(target_folder);
    }

    save_bookmarks(path, &mut value)
}

/// Chrome 프로세스 실행 여부 확인
pub fn is_chrome_running() -> bool {
    let output = if cfg!(target_os = "macos") {
        std::process::Command::new("pgrep")
            .args(["-x", "Google Chrome"])
            .output()
    } else if cfg!(target_os = "linux") {
        std::process::Command::new("pgrep")
            .args(["-x", "chrome"])
            .output()
    } else {
        std::process::Command::new("tasklist")
            .args(["/FI", "IMAGENAME eq chrome.exe", "/NH"])
            .output()
    };
    match output {
        Ok(o) => {
            if cfg!(target_os = "windows") {
                String::from_utf8_lossy(&o.stdout).contains("chrome.exe")
            } else {
                o.status.success()
            }
        }
        Err(_) => false,
    }
}

/// Bookmarks JSON 파일을 Chrome import용 HTML(Netscape Bookmark File Format)로 내보내기
pub fn export_bookmarks_html(path: &PathBuf) -> io::Result<PathBuf> {
    let content = std::fs::read_to_string(path)?;
    let value: serde_json::Value = serde_json::from_str(&content)
        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;

    let mut html = String::new();
    html.push_str("<!DOCTYPE NETSCAPE-Bookmark-file-1>\n");
    html.push_str("<!-- This is an automatically generated file.\n");
    html.push_str("     It will be read and overwritten.\n");
    html.push_str("     DO NOT EDIT! -->\n");
    html.push_str("<META HTTP-EQUIV=\"Content-Type\" CONTENT=\"text/html; charset=UTF-8\">\n");
    html.push_str("<TITLE>Bookmarks</TITLE>\n");
    html.push_str("<H1>Bookmarks</H1>\n");
    html.push_str("<DL><p>\n");

    if let Some(roots) = value.get("roots") {
        for key in &["bookmark_bar", "other", "synced"] {
            if let Some(root) = roots.get(*key) {
                write_html_node(&mut html, root, 1);
            }
        }
    }

    html.push_str("</DL><p>\n");

    let output_path = std::env::current_dir()
        .unwrap_or_default()
        .join("Bookmarks.html");
    std::fs::write(&output_path, &html)?;
    Ok(output_path)
}

/// 북마크 노드를 HTML 형식으로 재귀 출력
fn write_html_node(html: &mut String, node: &serde_json::Value, depth: usize) {
    let indent = "    ".repeat(depth);
    let node_type = node.get("type").and_then(|t| t.as_str()).unwrap_or("");
    let name = node
        .get("name")
        .and_then(|n| n.as_str())
        .unwrap_or("")
        .replace('&', "&amp;")
        .replace('<', "&lt;")
        .replace('>', "&gt;")
        .replace('"', "&quot;");

    match node_type {
        "folder" => {
            html.push_str(&format!("{indent}<DT><H3>{name}</H3>\n"));
            html.push_str(&format!("{indent}<DL><p>\n"));
            if let Some(children) = node.get("children").and_then(|c| c.as_array()) {
                for child in children {
                    write_html_node(html, child, depth + 1);
                }
            }
            html.push_str(&format!("{indent}</DL><p>\n"));
        }
        "url" => {
            let url = node
                .get("url")
                .and_then(|u| u.as_str())
                .unwrap_or("")
                .replace('&', "&amp;")
                .replace('"', "&quot;");
            html.push_str(&format!("{indent}<DT><A HREF=\"{url}\">{name}</A>\n"));
        }
        _ => {}
    }
}

/// OS별 기본 브라우저로 URL 열기
pub fn open_url(url: &str) -> io::Result<()> {
    if cfg!(target_os = "macos") {
        std::process::Command::new("open").arg(url).spawn()?;
    } else if cfg!(target_os = "linux") {
        std::process::Command::new("xdg-open").arg(url).spawn()?;
    } else {
        std::process::Command::new("cmd")
            .args(["/C", "start", url])
            .spawn()?;
    }
    Ok(())
}
