use std::{ops::Range, sync::LazyLock};

use regex::Regex;

static LINK_REGEX: LazyLock<Regex> =
    LazyLock::new(|| Regex::new(r"(?:\[(?P<text>.*?)\])\((?P<link>.*?)\)").unwrap());
static WIKI_REGEX: LazyLock<Regex> =
    LazyLock::new(|| Regex::new(r"\[\[(?P<link>.+?)(\|(?P<text>.+))?\]\]").unwrap());

/// The kinds a link can be.
///
/// This allows differentiating between Markdown's inline link (for both internal and external links)
/// and Obsidian's Wikilinks.
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
pub enum LinkKind {
    /// A Markdown link that points to a 'internal' Vault file.
    Internal,
    /// A Markdown link that points to a 'external' website outside the Vault.
    External,
    /// A Obsidian Wikilink.
    ///
    /// Can only be [`LinkKind::Internal`].
    Wiki,
}

/// The details of a link inside a Vault note.
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub struct Link {
    /// The kind of link this is.
    pub kind: LinkKind,
    /// The 'target' of the link.
    ///
    /// For Markdown links this is the URL,
    /// and for Obsidian Wikilinks this is the note title.
    pub href: String,
    /// The display text of the link.
    ///
    /// For Markdown links this is the name,
    /// and for Obsidian Wikilinks this is the `|` name.
    pub title: Option<String>,
    /// The byte position of the *whole* link.
    pub pos: Range<usize>,
}

impl Link {
    /// Extract any links from a Vault note's content.
    pub fn collect(s: &str) -> Vec<Self> {
        let mut tags = Vec::new();

        for captures in LINK_REGEX.captures_iter(s) {
            let Some(full) = captures.get(0) else {
                continue;
            };
            let Some(text) = captures.get(1) else {
                continue;
            };
            let Some(url) = captures.get(2) else {
                continue;
            };

            tags.push(Self {
                kind: LinkKind::External,
                href: url.as_str().to_string(),
                title: Some(text.as_str().to_string()),
                pos: full.range(),
            });
        }

        for captures in WIKI_REGEX.captures_iter(s) {
            let Some(full) = captures.get(0) else {
                continue;
            };
            let Some(capture) = captures.get(1) else {
                continue;
            };
            let title = captures.get(3).map(|c| c.as_str().to_string());

            tags.push(Self {
                kind: LinkKind::Wiki,
                href: capture.as_str().to_string(),
                title,
                pos: full.range(),
            });
        }

        tags
    }
}

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

    #[test]
    fn test_external() {
        let text = "[test](https://example.com)";

        let expected = &[Link {
            kind: LinkKind::External,
            href: "https://example.com".to_string(),
            title: Some("test".to_string()),
            pos: 0..27,
        }];
        let got = Link::collect(text);

        assert_eq!(expected, &got[..]);
    }

    #[test]
    fn test_wiki_no_title() {
        let text = "[[test]]";

        let expected = &[Link {
            kind: LinkKind::Wiki,
            href: "test".to_string(),
            title: None,
            pos: 0..8,
        }];
        let got = Link::collect(text);

        assert_eq!(expected, &got[..]);
    }

    #[test]
    fn test_wiki_with_title() {
        let text = "[[test|title]]";

        let expected = &[Link {
            kind: LinkKind::Wiki,
            href: "test".to_string(),
            title: Some("title".to_string()),
            pos: 0..14,
        }];
        let got = Link::collect(text);

        assert_eq!(expected, &got[..]);
    }

    #[test]
    fn test_fail_invalid_1() {
        let text = "[[test]";

        let got = Link::collect(text);

        assert!(got.is_empty());
    }

    #[test]
    fn test_fail_invalid_3() {
        let text = "[test]]";

        let got = Link::collect(text);

        assert!(got.is_empty());
    }

    #[test]
    fn test_fail_invalid_4() {
        let text = "[[test|title]";

        let got = Link::collect(text);

        assert!(got.is_empty());
    }

    #[test]
    fn test_fail_invalid_2() {
        let text = "[test|title]]";

        let got = Link::collect(text);

        assert!(got.is_empty());
    }
}
