Documentation
use std::collections::HashMap;

use percent_encoding::percent_decode_str;

use crate::model::config::WikiLinkPath;
use crate::model::reference::ReferenceType;
use crate::model::{strip_doc_extension, Key};

#[derive(Clone, Default)]
pub struct KeyIndex {
    by_basename: HashMap<String, Vec<Key>>,
}

impl KeyIndex {
    pub fn build<'a>(keys: impl Iterator<Item = &'a Key>) -> KeyIndex {
        let mut by_basename: HashMap<String, Vec<Key>> = HashMap::new();

        for key in keys {
            by_basename
                .entry(key.source())
                .or_default()
                .push(key.clone());
        }

        for bucket in by_basename.values_mut() {
            bucket.sort_by(resolution_order);
        }

        KeyIndex { by_basename }
    }

    pub fn wiki_target(&self, target: &Key, mode: WikiLinkPath) -> String {
        match mode {
            WikiLinkPath::Short => self.shorten_wiki(target),
            WikiLinkPath::Full | WikiLinkPath::Preserve => target.to_library_url(),
        }
    }

    pub fn insert(&mut self, key: &Key) {
        let bucket = self.by_basename.entry(key.source()).or_default();
        if !bucket.contains(key) {
            bucket.push(key.clone());
            bucket.sort_by(resolution_order);
        }
    }

    pub fn remove(&mut self, key: &Key) {
        if let Some(bucket) = self.by_basename.get_mut(&key.source()) {
            bucket.retain(|existing| existing != key);
            if bucket.is_empty() {
                self.by_basename.remove(&key.source());
            }
        }
    }

    pub fn resolve_wiki(&self, url: &str) -> Key {
        let decoded = percent_decode_str(url).decode_utf8_lossy().into_owned();
        let target = strip_doc_extension(&decoded).to_string();
        let segs = segments(&target);

        let Some(basename) = segs.last() else {
            return Key::name(&target);
        };
        let Some(bucket) = self.by_basename.get(*basename) else {
            return Key::name(&target);
        };

        bucket
            .iter()
            .find(|key| ends_with_segments(key, &segs))
            .or_else(|| bucket.first())
            .cloned()
            .unwrap_or_else(|| Key::name(&target))
    }

    pub fn shorten_wiki(&self, target: &Key) -> String {
        let path = target.relative_path.to_string();

        let segs = segments(&path);

        let Some(basename) = segs.last() else {
            return path;
        };
        let Some(bucket) = self.by_basename.get(*basename) else {
            return path;
        };

        for length in 1..=segs.len() {
            let suffix = &segs[segs.len() - length..];
            let mut matching = bucket.iter().filter(|key| ends_with_segments(key, suffix));
            if let (Some(only), None) = (matching.next(), matching.next()) {
                if only == target {
                    return suffix.join("/");
                }
            }
        }

        path
    }

    pub fn resolve_link_key(
        &self,
        url: &str,
        relative_to: &str,
        reference_type: ReferenceType,
    ) -> Key {
        match reference_type {
            ReferenceType::Regular => Key::from_rel_link_url(url, relative_to),
            ReferenceType::WikiLink | ReferenceType::WikiLinkPiped => self.resolve_wiki(url),
        }
    }
}

fn segments(path: &str) -> Vec<&str> {
    path.split('/').filter(|s| !s.is_empty()).collect()
}

fn ends_with_segments(key: &Key, suffix: &[&str]) -> bool {
    let key_segs = segments(&key.relative_path);
    key_segs.len() >= suffix.len() && key_segs[key_segs.len() - suffix.len()..] == *suffix
}

fn segment_count(key: &Key) -> usize {
    segments(&key.relative_path).len()
}

fn resolution_order(a: &Key, b: &Key) -> std::cmp::Ordering {
    segment_count(a)
        .cmp(&segment_count(b))
        .then_with(|| a.relative_path.cmp(&b.relative_path))
}

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

    fn index(paths: &[&str]) -> KeyIndex {
        let keys: Vec<Key> = paths.iter().map(|p| Key::name(p)).collect();
        KeyIndex::build(keys.iter())
    }

    #[test]
    fn bare_name_resolves_across_directories() {
        let index = index(&["first/note", "second/target"]);
        assert_eq!(Key::name("second/target"), index.resolve_wiki("target"));
    }

    #[test]
    fn bare_name_with_no_match_stays_as_root_key() {
        let index = index(&["first/note"]);
        assert_eq!(Key::name("missing"), index.resolve_wiki("missing"));
    }

    #[test]
    fn single_match_is_returned() {
        let index = index(&["folder/only"]);
        assert_eq!(Key::name("folder/only"), index.resolve_wiki("only"));
    }

    #[test]
    fn ambiguous_match_prefers_fewest_segments_then_lexicographic() {
        let index = index(&["zeta/target", "alpha/beta/target", "target"]);
        assert_eq!(Key::name("target"), index.resolve_wiki("target"));
    }

    #[test]
    fn ambiguous_match_with_equal_segments_prefers_lexicographic() {
        let index = index(&["zeta/target", "alpha/target"]);
        assert_eq!(Key::name("alpha/target"), index.resolve_wiki("target"));
    }

    #[test]
    fn prefixed_path_matches_exact_key_from_root() {
        let index = index(&["folder/target", "other/target"]);
        assert_eq!(
            Key::name("folder/target"),
            index.resolve_wiki("folder/target")
        );
    }

    #[test]
    fn prefixed_path_without_exact_match_falls_back_to_basename() {
        let index = index(&["clippings/target"]);
        assert_eq!(
            Key::name("clippings/target"),
            index.resolve_wiki("missing/target")
        );
    }

    #[test]
    fn md_extension_is_trimmed() {
        let index = index(&["folder/target"]);
        assert_eq!(Key::name("folder/target"), index.resolve_wiki("target.md"));
    }

    #[test]
    fn suffix_disambiguates_shared_basename() {
        let index = index(&["x/a/note", "y/b/note"]);
        assert_eq!(Key::name("x/a/note"), index.resolve_wiki("a/note"));
        assert_eq!(Key::name("y/b/note"), index.resolve_wiki("b/note"));
    }

    #[test]
    fn shorten_unique_basename_to_bare_name() {
        let index = index(&["clippings/target", "notes/note"]);
        assert_eq!("target", index.shorten_wiki(&Key::name("clippings/target")));
    }

    #[test]
    fn shorten_shared_basename_to_shortest_unique_suffix() {
        let index = index(&["x/a/note", "y/b/note"]);
        assert_eq!("a/note", index.shorten_wiki(&Key::name("x/a/note")));
        assert_eq!("b/note", index.shorten_wiki(&Key::name("y/b/note")));
    }

    #[test]
    fn shorten_falls_back_to_full_path_when_suffix_still_shared() {
        let index = index(&["x/a/note", "y/a/note"]);
        assert_eq!("x/a/note", index.shorten_wiki(&Key::name("x/a/note")));
        assert_eq!("y/a/note", index.shorten_wiki(&Key::name("y/a/note")));
    }

    #[test]
    fn shorten_then_resolve_round_trips() {
        let index = index(&["x/a/note", "y/b/note"]);
        let shortened = index.shorten_wiki(&Key::name("x/a/note"));
        assert_eq!(Key::name("x/a/note"), index.resolve_wiki(&shortened));
    }

    #[test]
    fn shorten_keeps_full_path_for_target_absent_from_index() {
        let index = index(&["first/note"]);
        assert_eq!("second/note", index.shorten_wiki(&Key::name("second/note")));
    }

    #[test]
    fn wiki_target_full_keeps_full_path() {
        let index = index(&["clippings/target", "notes/note"]);
        assert_eq!(
            "clippings/target",
            index.wiki_target(&Key::name("clippings/target"), WikiLinkPath::Full)
        );
    }

    #[test]
    fn wiki_target_short_uses_shortest_suffix() {
        let index = index(&["clippings/target", "notes/note"]);
        assert_eq!(
            "target",
            index.wiki_target(&Key::name("clippings/target"), WikiLinkPath::Short)
        );
    }

    #[test]
    fn wiki_target_preserve_keeps_full_path() {
        let index = index(&["clippings/target", "notes/note"]);
        assert_eq!(
            "clippings/target",
            index.wiki_target(&Key::name("clippings/target"), WikiLinkPath::Preserve)
        );
    }

    #[test]
    fn insert_makes_a_new_key_resolvable() {
        let mut index = index(&["first/note"]);
        assert_eq!(Key::name("target"), index.resolve_wiki("target"));
        index.insert(&Key::name("second/target"));
        assert_eq!(Key::name("second/target"), index.resolve_wiki("target"));
    }

    #[test]
    fn insert_is_idempotent() {
        let mut index = index(&["first/target"]);
        index.insert(&Key::name("first/target"));
        assert_eq!(Key::name("first/target"), index.resolve_wiki("target"));
    }

    #[test]
    fn insert_keeps_resolution_order() {
        let mut index = index(&["zeta/target"]);
        index.insert(&Key::name("alpha/target"));
        assert_eq!(Key::name("alpha/target"), index.resolve_wiki("target"));
    }

    #[test]
    fn remove_drops_the_key_from_resolution() {
        let mut index = index(&["first/note", "second/target"]);
        index.remove(&Key::name("second/target"));
        assert_eq!(Key::name("target"), index.resolve_wiki("target"));
    }
}