use std::fs::{self, File, OpenOptions};
use std::io::{BufRead, BufReader, Write};
use std::path::{Path, PathBuf};
use std::{env, io};

use clap::{Parser, Subcommand};
use nucleo_picker::nucleo::pattern::{CaseMatching, Normalization, Pattern};
use nucleo_picker::nucleo::{Config, Matcher};

use crate::error::{AppError, AppResult};
use crate::init::{Shell, init};
use crate::pickers::{pick_one, pick_one_last_dim};

mod error;
mod index_renderer;
mod init;
mod pickers;

#[derive(Parser)]
#[command(name = "pathmarks")]
#[command(about = "Path bookmark manager", version)]
struct Cli {
    #[command(subcommand)]
    command: Cmd,
}

#[derive(Subcommand)]
enum Cmd {
    Save,
    Remove {
        path: Option<String>,
    },
    Prune,
    List,
    Guess {
        paths: Vec<String>,
    },
    Pick,
    Init {
        shell: Shell,
        command: Option<String>,
    },
}

const MIN_MATCH_SCORE: u32 = 60;

fn main() {
    let cli = Cli::parse();
    let Ok(bookmark_path) = bookmarks_file() else {
        return;
    };

    match app(cli, bookmark_path) {
        Ok(res) => {
            if let Some(res) = res {
                println!("{res}")
            }
        }
        Err(err) => println!("{err}"),
    };
}

fn app(cli: Cli, bookmarks_file: PathBuf) -> AppResult<Option<String>> {
    match cli.command {
        Cmd::Save => {
            let cwd = env::current_dir().map_err(AppError::Io)?;
            let mut bookmarks = read_bookmarks(&bookmarks_file)?;
            if !bookmarks.iter().any(|bookmark| bookmark == &cwd) {
                bookmarks.push(cwd);
            }
            write_bookmarks(&bookmarks, &bookmarks_file)?;
            Ok(None)
        }
        Cmd::Remove { path } => {
            let mut bookmarks = read_bookmarks(&bookmarks_file)?;

            let target = if let Some(path) = path {
                if !is_absolute(&path) {
                    return Err(AppError::InvalidPath);
                }
                Some(path)
            } else {
                pick_one(&bookmarks)?.and_then(|x| x.to_str().map(|x| x.to_string()))
            };

            if let Some(target) = target {
                let before = bookmarks.len();
                bookmarks.retain(|s| s != &target);
                if bookmarks.len() == before {
                    return Err(AppError::NotFound(target));
                }
                write_bookmarks(&bookmarks, &bookmarks_file)?;
            }

            Ok(None)
        }

        Cmd::Guess { paths } => {
            let Some(first) = paths.first() else {
                return Ok(None);
            };

            if is_absolute(first) {
                return Ok(Some(first.clone()));
            }

            let bookmarks = read_bookmarks(&bookmarks_file)?;
            let current_dir = env::current_dir()?;

            let mut current = match find_case_insensitive(&current_dir, first) {
                Some(path) => path,
                None => {
                    match best_bookmark_match(first, bookmarks.iter().flat_map(|s| s.to_str())) {
                        Some(bookmark) => PathBuf::from(bookmark),
                        None => return Ok(Some(paths.join("/"))),
                    }
                }
            };

            for segment in paths.iter().skip(1) {
                match find_case_insensitive(&current, segment) {
                    Some(next) => current = next,
                    None => return Ok(Some(current.join(segment).to_string_lossy().into_owned())),
                }
            }

            Ok(Some(current.to_string_lossy().into_owned()))
        }
        Cmd::Prune => {
            let bookmarks = read_bookmarks(&bookmarks_file)?;
            let mut kept = Vec::new();
            for bookmark in bookmarks {
                if Path::new(&bookmark).exists() {
                    kept.push(bookmark);
                }
            }
            write_bookmarks(&kept, &bookmarks_file)?;
            Ok(None)
        }
        Cmd::List => {
            let bookmarks = read_bookmarks(&bookmarks_file)?;
            let current_dir = env::current_dir()?;
            let out = map_relative_paths(&current_dir, bookmarks);

            let out: Vec<_> = out
                .into_iter()
                .flat_map(|x| x.to_str().map(|x| x.to_string()))
                .collect();

            Ok(Some(out.join("\n")))
        }
        Cmd::Pick => {
            let bookmarks = read_bookmarks(&bookmarks_file)?;
            let current_dir = env::current_dir()?;

            let relative_bookmarks = map_relative_paths(&current_dir, bookmarks);
            let sub_directories = list_child_dirs(&current_dir, false)?;
            let mut relative_sub_directories = map_relative_paths(&current_dir, sub_directories);
            relative_sub_directories.push(PathBuf::from(".."));

            match pick_one_last_dim(&relative_sub_directories, &relative_bookmarks)? {
                Some(bookmark) => Ok(bookmark.to_str().map(|x| x.into())),
                None => Ok(None),
            }
        }
        Cmd::Init { shell, command } => Ok(Some(init(shell, command))),
    }
}

fn best_bookmark_match<'a>(
    query: &str,
    bookmarks: impl IntoIterator<Item = &'a str>,
) -> Option<&'a str> {
    let mut matcher = Matcher::new(Config::DEFAULT.match_paths());

    let results = Pattern::parse(query, CaseMatching::Smart, Normalization::Smart)
        .match_list(bookmarks, &mut matcher);

    results
        .into_iter()
        .filter(|(_, score)| *score >= MIN_MATCH_SCORE)
        .max_by(|(a_str, a_score), (b_str, b_score)| {
            a_score
                .cmp(b_score)
                .then_with(|| b_str.len().cmp(&a_str.len()))
        })
        .map(|(s, _)| s)
}

fn find_fuzzy(root: &Path, query: &str) -> Option<PathBuf> {
    let mut matcher = Matcher::new(Config::DEFAULT.match_paths());

    let dir_names = fs::read_dir(root).ok()?.filter_map(|entry| {
        let entry = entry.ok()?;
        if entry.file_type().ok()?.is_dir() {
            Some(entry.file_name().to_string_lossy().into_owned())
        } else {
            None
        }
    });

    let results = Pattern::parse(query, CaseMatching::Smart, Normalization::Smart)
        .match_list(dir_names, &mut matcher);

    results
        .into_iter()
        .filter(|(_, score)| *score >= MIN_MATCH_SCORE)
        .max_by(|(a_str, a_score), (b_str, b_score)| {
            a_score
                .cmp(b_score)
                .then_with(|| b_str.len().cmp(&a_str.len()))
        })
        .map(|(s, _)| root.join(s))
}

fn find_case_insensitive(root: &Path, query: &str) -> Option<PathBuf> {
    if let Some(fuzzy) = find_fuzzy(root, query) {
        return Some(fuzzy);
    }

    let components: Vec<_> = query
        .trim_end_matches('/')
        .split('/')
        .map(|s| s.to_lowercase())
        .collect();

    let mut current = root.to_path_buf();

    for wanted in components {
        let mut matched = None;

        for entry in fs::read_dir(&current).ok()? {
            let entry = entry.ok()?;

            if !entry.file_type().ok()?.is_dir() {
                continue;
            }

            let name = entry.file_name().to_string_lossy().into_owned();

            if name.eq_ignore_ascii_case(&wanted) {
                matched = Some(entry.path());
                break;
            }
        }

        current = matched?;
    }

    Some(current)
}

fn bookmarks_file() -> AppResult<PathBuf> {
    let file = if let Some(data_dir) = dirs::data_local_dir() {
        data_dir.join("pathmarks").join("bookmarks.txt")
    } else {
        return Err(AppError::DataDirectoryNotFound);
    };

    if !file.exists() {
        write_bookmarks(&[], &file)?;
    }

    Ok(file)
}

fn read_bookmarks(file: &Path) -> AppResult<Vec<PathBuf>> {
    let file = File::open(file)?;
    let reader = BufReader::new(file);
    let mut bookmarks = Vec::new();
    for line in reader.lines() {
        let line = line?;
        let line = line.trim().to_string();
        if !line.is_empty() {
            bookmarks.push(PathBuf::from(line));
        }
    }
    Ok(bookmarks)
}

fn write_bookmarks(bookmarks: &[PathBuf], file: &PathBuf) -> AppResult<()> {
    if let Some(parent) = file.parent() {
        fs::create_dir_all(parent)?;
    }
    let mut file = OpenOptions::new()
        .write(true)
        .truncate(true)
        .create(true)
        .open(file)?;

    for bookmark in bookmarks.iter().flat_map(|x| x.to_str()) {
        writeln!(file, "{}", bookmark)?;
    }
    Ok(())
}

fn is_absolute(p: &str) -> bool {
    Path::new(p).is_absolute()
}

fn list_child_dirs(dir: &Path, include_hidden: bool) -> io::Result<Vec<PathBuf>> {
    let mut out = Vec::new();

    for entry_res in fs::read_dir(dir)? {
        let entry = entry_res?;
        let file_type = entry.file_type()?;

        let is_dir = if file_type.is_symlink() {
            let target = fs::read_link(entry.path())?;
            let target_abs = if target.is_absolute() {
                target
            } else {
                dir.join(target)
            };
            target_abs.is_dir()
        } else {
            file_type.is_dir()
        };

        if !is_dir {
            continue;
        }

        if let Some(name) = entry.file_name().to_str() {
            if !include_hidden && name.starts_with('.') {
                continue;
            }
            out.push(entry.path());
        }
    }

    out.sort_unstable();
    Ok(out)
}

fn relative_if_descendant(base: &Path, child: &Path) -> Option<PathBuf> {
    if !base.is_absolute() || !child.is_absolute() {
        return None;
    }
    if !child.starts_with(base) {
        return None;
    }

    child
        .strip_prefix(base)
        .map(|rel| {
            if rel.as_os_str().is_empty() {
                PathBuf::from(".")
            } else {
                rel.to_path_buf()
            }
        })
        .ok()
}

fn map_relative_paths<I>(base: &Path, paths: I) -> Vec<PathBuf>
where
    I: IntoIterator<Item = PathBuf>,
{
    paths
        .into_iter()
        .map(|p| relative_if_descendant(base, &p).unwrap_or(p))
        .filter(|p| p.to_str() != Some("."))
        .collect()
}

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

    #[test]
    fn best_with_same_score() {
        let paths = [
            "/path/with/many/sub/directories",
            "/path/with/",
            "/path/with/many/sub/",
        ];

        let best = best_bookmark_match("pathwith", paths).unwrap();

        assert_eq!(best, paths[1]);
    }

    #[test]
    fn test_find_case_insensitive_nested() {
        let temp = tempfile::tempdir().unwrap();
        let root = temp.path();

        let dir_path = root.join("Dir");
        let subdir_path = dir_path.join("SubDir");

        fs::create_dir_all(&subdir_path).unwrap();

        let found = find_case_insensitive(root, "dIr/sUbDiR").unwrap();

        assert_eq!(found, subdir_path);
    }

    #[test]
    fn test_find_case_insensitive_fuzzy() {
        let temp = tempfile::tempdir().unwrap();
        let root = temp.path();

        let dir_1 = root.join("Test_Project");
        let dir_2 = root.join("other_directory");

        fs::create_dir_all(&dir_1).unwrap();
        fs::create_dir_all(&dir_2).unwrap();

        let found = find_case_insensitive(root, "tesproj").unwrap();

        assert_eq!(found, dir_1);
    }

    #[test]
    fn test_find_case_insensitive_not_fuzzy_sub() {
        let temp = tempfile::tempdir().unwrap();
        let root = temp.path();

        let dir_path = root.join("Dir");
        let subdir_path = dir_path.join("SubDirectory");

        fs::create_dir_all(&subdir_path).unwrap();

        let found = find_case_insensitive(root, "subdir");

        assert_eq!(found, None);
    }

    #[test]
    fn test_find_case_insensitive_not_files() {
        let temp = tempfile::tempdir().unwrap();
        let root = temp.path();

        let dir_path = root.join("Dir");

        fs::create_dir_all(&dir_path).unwrap();

        let file_path = dir_path.join("testfile.txt");
        fs::write(&file_path, "hello").unwrap();

        let found = find_case_insensitive(root, "testf");

        assert_eq!(found, None);
    }
}
