Documentation
use std::path::{Path, PathBuf};
use std::str::FromStr;

use lsp_types::*;
use percent_encoding::percent_decode_str;
use url::Url;

use diwe::config::Format;
use liwe::model::{strip_doc_extension, Key};

pub struct BasePath {
    url: Url,
    format: Format,
}

impl BasePath {
    pub fn new(base_path: String, format: Format) -> Self {
        let url = Url::parse(&base_path).expect("valid base URL");
        Self {
            url: canonical(url),
            format,
        }
    }

    pub fn from_path(path: &str, format: Format) -> Self {
        let url = Url::from_directory_path(path).expect("valid base path");
        Self {
            url: canonical(url),
            format,
        }
    }

    fn dot_extension(&self) -> String {
        format!(".{}", self.format.extension())
    }

    pub fn key_to_url(&self, key: &Key) -> Uri {
        self.build_url(key.as_str())
    }

    pub fn relative_to_full_path(&self, path: &str) -> Uri {
        self.build_url(path)
    }

    pub fn name_to_url(&self, name: &str) -> Uri {
        self.build_url(name)
    }

    pub fn url_to_key(&self, uri: &Uri) -> Key {
        let url = canonical(Url::parse(&uri.to_string()).expect("valid URI"));
        let base = self.url.to_file_path().expect("base is a file URL");
        let target = url.to_file_path().expect("URI is a file URL");
        let relative = target.strip_prefix(&base).unwrap_or(&target);

        relative_path_key(relative)
    }

    pub fn maybe_url_to_key(&self, uri: &Uri) -> Option<Key> {
        let url = canonical(Url::parse(&uri.to_string()).ok()?);
        let base = self.url.to_file_path().ok()?;
        let target = url.to_file_path().ok()?;
        let relative = target.strip_prefix(&base).ok()?;

        Some(relative_path_key(relative))
    }

    pub fn key_to_path(&self, key: &Key) -> Option<PathBuf> {
        Url::parse(&self.key_to_url(key).to_string())
            .ok()?
            .to_file_path()
            .ok()
    }

    pub fn resolve_relative_url(&self, link: &str, relative_to: &str) -> Uri {
        let mut source = self.url.clone();
        {
            let mut segs = source.path_segments_mut().expect("path-based URL");
            segs.pop_if_empty();
            for s in relative_to.split('/').filter(|s| !s.is_empty()) {
                segs.push(s);
            }
            segs.push("");
        }

        let mut resolved = source.join(link).expect("valid link");

        let last = resolved
            .path_segments()
            .and_then(|s| s.last())
            .unwrap_or("");
        if !last.is_empty() && !last.ends_with(&self.dot_extension()) {
            let decoded = percent_decode_str(last).decode_utf8_lossy().into_owned();
            resolved
                .path_segments_mut()
                .expect("path-based URL")
                .pop()
                .push(&format!("{}{}", decoded, self.dot_extension()));
        }

        Uri::from_str(resolved.as_str()).expect("valid URI")
    }

    fn build_url(&self, key_or_path: &str) -> Uri {
        let trimmed = strip_doc_extension(key_or_path);
        let mut url = self.url.clone();
        {
            let mut segs = url.path_segments_mut().expect("path-based URL");
            segs.pop_if_empty();
            let parts: Vec<&str> = trimmed.split('/').filter(|s| !s.is_empty()).collect();
            if let Some((last, rest)) = parts.split_last() {
                segs.extend(rest);
                segs.push(&format!("{}{}", last, self.dot_extension()));
            }
        }
        Uri::from_str(url.as_str()).expect("valid URI")
    }
}

fn relative_path_key(relative: &Path) -> Key {
    let joined = relative
        .components()
        .filter_map(|c| match c {
            std::path::Component::Normal(os) => Some(os.to_string_lossy().to_string()),
            _ => None,
        })
        .collect::<Vec<_>>()
        .join("/");

    Key::name(&joined)
}

fn canonical(url: Url) -> Url {
    if url.scheme() != "file" {
        return url;
    }
    let was_directory = url.path().ends_with('/');
    let Ok(path) = url.to_file_path() else {
        return lowercase_url_drive(url);
    };
    let result = if was_directory {
        Url::from_directory_path(&path)
    } else {
        Url::from_file_path(&path)
    };
    lowercase_url_drive(result.unwrap_or(url))
}

fn lowercase_url_drive(mut url: Url) -> Url {
    let path = url.path();
    let bytes = path.as_bytes();
    if bytes.len() >= 3 && bytes[0] == b'/' && bytes[1].is_ascii_uppercase() && bytes[2] == b':' {
        let lowered = format!("/{}{}", (bytes[1] as char).to_ascii_lowercase(), &path[2..]);
        url.set_path(&lowered);
    }
    url
}

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

    fn abs_path(rel: &str) -> String {
        if cfg!(windows) {
            format!("C:/{rel}")
        } else {
            format!("/{rel}")
        }
    }

    fn file_url(rel: &str) -> String {
        if cfg!(windows) {
            format!("file:///c:/{rel}")
        } else {
            format!("file:///{rel}")
        }
    }

    #[test]
    fn test_url_to_key_with_danish_characters() {
        let base_path = BasePath::new(file_url("basepath/"), Format::Markdown);
        let uri = Uri::from_str(&file_url("basepath/t%C3%B8j.md")).unwrap();
        let key = base_path.url_to_key(&uri);
        assert_eq!(key.to_string(), "tøj");
    }

    #[test]
    fn test_url_to_key_with_regular_characters() {
        let base_path = BasePath::new(file_url("basepath/"), Format::Markdown);

        let uri = Uri::from_str(&file_url("basepath/regular.md")).unwrap();
        let key = base_path.url_to_key(&uri);
        assert_eq!(key.to_string(), "regular");
    }

    #[test]
    fn test_url_to_key_with_spaces_in_base_path() {
        let base_path = BasePath::from_path(&abs_path("path with spaces/docs"), Format::Markdown);
        let uri = Uri::from_str(&file_url("path%20with%20spaces/docs/test.md")).unwrap();
        let key = base_path.url_to_key(&uri);
        assert_eq!(key.to_string(), "test");
    }

    #[test]
    fn test_key_to_url_with_spaces_in_base_path() {
        let base_path = BasePath::from_path(&abs_path("path with spaces/docs"), Format::Markdown);
        let key = liwe::model::Key::name("test.md");
        let url = base_path.key_to_url(&key);
        assert_eq!(
            url.to_string(),
            file_url("path%20with%20spaces/docs/test.md")
        );
    }

    #[test]
    fn test_url_to_key_with_spaces_in_key() {
        let base_path = BasePath::from_path(&abs_path("basepath"), Format::Markdown);
        let uri = Uri::from_str(&file_url("basepath/my%20document.md")).unwrap();
        let key = base_path.url_to_key(&uri);
        assert_eq!(key.to_string(), "my document");
    }

    #[test]
    fn test_key_to_url_with_spaces_in_key() {
        let base_path = BasePath::from_path(&abs_path("basepath"), Format::Markdown);
        let key = liwe::model::Key::name("my document.md");
        let url = base_path.key_to_url(&key);
        assert_eq!(url.to_string(), file_url("basepath/my%20document.md"));
    }

    #[test]
    fn canonical_lowercases_drive_letter_and_preserves_other_segments() {
        let url = Url::parse("file:///C:/Base/Note.md").unwrap();
        assert_eq!(canonical(url).as_str(), "file:///c:/Base/Note.md");
    }

    #[test]
    fn test_url_to_key_windows_case_and_percent_encoding_mismatch() {
        let base_path = BasePath::new("file:///C:/base/".to_string(), Format::Markdown);
        let uri = Uri::from_str("file:///c%3A/base/one.md").unwrap();
        let key = base_path.url_to_key(&uri);
        assert_eq!(key.to_string(), "one");
    }

    #[test]
    fn test_resolve_relative_url_doubles_md_suffix_when_link_has_md_extension() {
        let base_path = BasePath::from_path(&abs_path("basepath"), Format::Markdown);
        let url = base_path.resolve_relative_url("one.md", "");
        assert_eq!(url.to_string(), file_url("basepath/one.md"));
    }

    #[test]
    fn test_name_to_url_with_md_suffix_in_name() {
        let base_path = BasePath::from_path(&abs_path("basepath"), Format::Markdown);
        let url = base_path.name_to_url("one.md");
        assert_eq!(url.to_string(), file_url("basepath/one.md"));
    }

    #[test]
    fn test_resolve_relative_url_with_anchor_fragment() {
        let base_path = BasePath::from_path(&abs_path("basepath"), Format::Markdown);
        let url = base_path.resolve_relative_url("one#section", "");
        assert_eq!(url.to_string(), file_url("basepath/one.md#section"));
    }

    #[test]
    fn test_from_rel_link_url_decodes_percent_encoded_spaces() {
        let key = liwe::model::Key::from_rel_link_url("a%20b.md", "");
        assert_eq!(key.to_string(), "a b");
    }

    #[test]
    fn key_to_path_appends_extension_for_nested_key() {
        let base_path = BasePath::from_path(&abs_path("basepath"), Format::Markdown);
        let path = base_path.key_to_path(&Key::name("sub/dir/note")).unwrap();
        assert_eq!(path, PathBuf::from(abs_path("basepath/sub/dir/note.md")));
    }

    #[test]
    fn key_to_path_preserves_spaces_in_key() {
        let base_path = BasePath::from_path(&abs_path("basepath"), Format::Markdown);
        let path = base_path.key_to_path(&Key::name("my document")).unwrap();
        assert_eq!(path, PathBuf::from(abs_path("basepath/my document.md")));
    }

    #[test]
    fn maybe_url_to_key_resolves_uri_within_base() {
        let base_path = BasePath::from_path(&abs_path("basepath"), Format::Markdown);
        let uri = Uri::from_str(&file_url("basepath/sub/note.md")).unwrap();
        assert_eq!(
            base_path.maybe_url_to_key(&uri),
            Some(Key::name("sub/note"))
        );
    }

    #[test]
    fn maybe_url_to_key_rejects_uri_outside_base() {
        let base_path = BasePath::from_path(&abs_path("basepath"), Format::Markdown);
        let uri = Uri::from_str(&file_url("elsewhere/note.md")).unwrap();
        assert_eq!(base_path.maybe_url_to_key(&uri), None);
    }

    #[test]
    fn test_url_to_key_matches_parsed_reference_key() {
        let base_path = BasePath::new("file:///C:/base/".to_string(), Format::Markdown);

        let source_uri = Uri::from_str("file:///c%3A/base/one.md").unwrap();
        let target_uri = Uri::from_str("file:///c%3A/base/two.md").unwrap();

        let source_key = base_path.url_to_key(&source_uri);
        let target_key = base_path.url_to_key(&target_uri);

        let parsed_target_key = liwe::model::Key::from_rel_link_url("two", &source_key.parent());

        assert_eq!(parsed_target_key.to_string(), target_key.to_string());
    }
}