use std::path::{Component, Path, PathBuf};

use eidos_kernel::schema::{Edge, EdgeEvidence, Node, Span};

use super::target::resolve_target;

/// Wikilink targets that are syntax examples in prose, not real references.
pub(super) fn is_placeholder_wikilink(target: &str) -> bool {
    matches!(
        target.to_lowercase().trim(),
        "file"
            | "files"
            | "link"
            | "links"
            | "wikilink"
            | "wikilinks"
            | "name"
            | "target"
            | "id"
            | "node"
            | "..."
            | "x"
            | "y"
            | "foo"
            | "bar"
    )
}

pub(super) fn strip_code_fences(text: &str) -> String {
    let mut out = String::new();
    let mut in_fence = false;
    for line in text.lines() {
        if line.trim_start().starts_with("```") {
            in_fence = !in_fence;
            continue;
        }
        if !in_fence {
            out.push_str(line);
            out.push('\n');
        }
    }
    out
}

struct WikilinkOccurrence {
    target: String,
    line: usize,
}

fn wikilink_occurrences(text: &str, start_line: usize) -> Vec<WikilinkOccurrence> {
    let mut out = Vec::new();
    let mut in_fence = false;
    for (offset, line) in text.lines().enumerate() {
        if line.trim_start().starts_with("```") {
            in_fence = !in_fence;
            continue;
        }
        if in_fence {
            continue;
        }
        let mut rest = line;
        while let Some(i) = rest.find("[[") {
            rest = &rest[i + 2..];
            match rest.find("]]") {
                Some(j) => {
                    let target = rest[..j].trim().to_string();
                    if !target.is_empty() {
                        out.push(WikilinkOccurrence {
                            target,
                            line: start_line + offset,
                        });
                    }
                    rest = &rest[j + 2..];
                }
                None => break,
            }
        }
    }
    out
}

pub(super) fn wikilink_targets(text: &str) -> Vec<String> {
    wikilink_occurrences(text, 1)
        .into_iter()
        .map(|occurrence| occurrence.target)
        .collect()
}

pub(super) fn wikilink_edges(
    from: &Node,
    source_path: &Path,
    body: &str,
    body_start_line: usize,
    nodes: &[Node],
) -> Vec<Edge> {
    let mut edges = Vec::new();
    let source = source_path.to_string_lossy().into_owned();
    for occurrence in wikilink_occurrences(body, body_start_line) {
        if is_placeholder_wikilink(&occurrence.target) {
            continue;
        }
        if let Some(to) = resolve_target(&occurrence.target, nodes)
            && to != from.id
        {
            edges.push(Edge {
                from: from.id.clone(),
                to,
                relation: eidos_kernel::schema::relation::LINKS_TO.into(),
                evidence: "wikilink".into(),
                evidence_refs: vec![line_evidence("wikilink", &source, occurrence.line)],
                // Authored wikilink — earned Resolved.
                basis: eidos_kernel::schema::EdgeBasis::Resolved,
                ..Default::default()
            });
        }
    }
    edges
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) struct MarkdownLink {
    pub label: String,
    pub target: String,
    pub line: usize,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) enum MarkdownLinkTarget {
    Internal(String),
    External,
    NonMarkdown,
    Malformed,
}

pub(super) fn markdown_links(text: &str, start_line: usize) -> Vec<MarkdownLink> {
    let mut out = Vec::new();
    let mut in_fence = false;
    for (offset, line) in text.lines().enumerate() {
        if line.trim_start().starts_with("```") {
            in_fence = !in_fence;
            continue;
        }
        if in_fence {
            continue;
        }
        let mut rest = line;
        let mut consumed = 0;
        while let Some(start) = rest.find('[') {
            let absolute_start = consumed + start;
            if absolute_start > 0 && line.as_bytes().get(absolute_start - 1) == Some(&b'!') {
                rest = &rest[start + 1..];
                consumed = absolute_start + 1;
                continue;
            }
            let after_start = &rest[start + 1..];
            let Some(label_end) = after_start.find("](") else {
                break;
            };
            let label = after_start[..label_end].trim();
            let after_open = &after_start[label_end + 2..];
            let Some(target_end) = after_open.find(')') else {
                break;
            };
            let target = markdown_destination(&after_open[..target_end]);
            if !label.is_empty() || !target.is_empty() {
                out.push(MarkdownLink {
                    label: label.to_string(),
                    target,
                    line: start_line + offset,
                });
            }
            let advance = start + 1 + label_end + 2 + target_end + 1;
            rest = &rest[advance..];
            consumed += advance;
        }
    }
    out
}

fn markdown_destination(raw: &str) -> String {
    let trimmed = raw.trim();
    if let Some(stripped) = trimmed.strip_prefix('<')
        && let Some(end) = stripped.find('>')
    {
        return stripped[..end].trim().to_string();
    }
    trimmed
        .split_ascii_whitespace()
        .next()
        .unwrap_or("")
        .trim()
        .to_string()
}

pub(super) fn classify_markdown_target(target: &str) -> MarkdownLinkTarget {
    let target = target.trim();
    if target.is_empty() {
        return MarkdownLinkTarget::Malformed;
    }
    let lower = target.to_ascii_lowercase();
    if lower.starts_with("http://") || lower.starts_with("https://") {
        return MarkdownLinkTarget::External;
    }
    let without_fragment = target.split(['#', '?']).next().unwrap_or("").trim();
    if without_fragment.is_empty() {
        return MarkdownLinkTarget::NonMarkdown;
    }
    if !without_fragment.to_ascii_lowercase().ends_with(".md") {
        return MarkdownLinkTarget::NonMarkdown;
    }
    if without_fragment.contains('\0') {
        return MarkdownLinkTarget::Malformed;
    }
    MarkdownLinkTarget::Internal(without_fragment.to_string())
}

pub(super) fn markdown_link_edges(
    from: &Node,
    source_path: &Path,
    body: &str,
    body_start_line: usize,
    nodes: &[Node],
) -> Vec<Edge> {
    let mut edges = Vec::new();
    let source = source_path.to_string_lossy().into_owned();
    for link in markdown_links(body, body_start_line) {
        let MarkdownLinkTarget::Internal(target) = classify_markdown_target(&link.target) else {
            continue;
        };
        if let Some(to) = resolve_markdown_link_target(source_path, &target, nodes)
            && to != from.id
        {
            edges.push(Edge {
                from: from.id.clone(),
                to,
                relation: eidos_kernel::schema::relation::LINKS_TO.into(),
                evidence: format!(
                    "markdown_link source_text=\"{}\" target_text=\"{}\"",
                    link.label, link.target
                ),
                evidence_refs: vec![line_evidence("markdown_link", &source, link.line)],
                // Authored markdown link — earned Resolved.
                basis: eidos_kernel::schema::EdgeBasis::Resolved,
                ..Default::default()
            });
        }
    }
    edges
}

fn line_evidence(reason: &str, source: &str, line: usize) -> EdgeEvidence {
    EdgeEvidence {
        reason: reason.to_string(),
        source: Some(source.to_string()),
        span: Some(Span {
            path: source.to_string(),
            start_line: line,
            end_line: line,
        }),
    }
}

pub(super) fn resolve_markdown_link_target(
    source_path: &Path,
    target: &str,
    nodes: &[Node],
) -> Option<String> {
    let candidates = markdown_target_candidates(source_path, target);
    for candidate in &candidates {
        if let Some(id) = node_id_for_path(candidate, nodes) {
            return Some(id);
        }
    }
    let suffix = target.trim_start_matches('/').replace('\\', "/");
    nodes.iter().find_map(|node| {
        node.source_files.iter().find_map(|source| {
            let source = source.replace('\\', "/");
            (source.ends_with(&suffix)).then(|| node.id.clone())
        })
    })
}

fn markdown_target_candidates(source_path: &Path, target: &str) -> Vec<PathBuf> {
    let target_path = Path::new(target);
    let mut candidates = Vec::new();
    if target_path.is_absolute() {
        candidates.push(normalize_path(target_path));
    } else if let Some(parent) = source_path.parent() {
        candidates.push(normalize_path(&parent.join(target_path)));
    }
    candidates.push(normalize_path(target_path));
    candidates
}

fn node_id_for_path(candidate: &Path, nodes: &[Node]) -> Option<String> {
    let candidate = normalize_path(candidate);
    nodes.iter().find_map(|node| {
        node.source_files.iter().find_map(|source| {
            let source = normalize_path(Path::new(source));
            (source == candidate).then(|| node.id.clone())
        })
    })
}

fn normalize_path(path: &Path) -> PathBuf {
    let mut out = PathBuf::new();
    for component in path.components() {
        match component {
            Component::CurDir => {}
            Component::ParentDir => {
                out.pop();
            }
            other => out.push(other.as_os_str()),
        }
    }
    out
}
