use liwe::graph::{Graph, GraphContext};
use liwe::model::config::MarkdownOptions;
use liwe::model::node::NodePointer;
use liwe::model::projector::Projector;
use liwe::model::tree::TreeIter;
use liwe::model::writer::{blocks_to_markdown_sparce_skip_frontmatter, Block};
use liwe::model::Key;
use liwe::retrieve::{DocumentOutput, EdgeRef, RetrieveOutput};
use serde::Serialize;
use serde_yaml::{Mapping, Value};

#[derive(Serialize)]
struct EdgeRefMeta {
    key: String,
    title: String,
    #[serde(rename = "sectionPath", skip_serializing_if = "Vec::is_empty")]
    section_path: Vec<String>,
}

impl From<&EdgeRef> for EdgeRefMeta {
    fn from(e: &EdgeRef) -> Self {
        EdgeRefMeta {
            key: e.key.clone(),
            title: e.title.clone(),
            section_path: e.section_path.clone(),
        }
    }
}

pub struct RetrieveRenderer<'a> {
    output: &'a RetrieveOutput,
    options: &'a MarkdownOptions,
    graph: &'a Graph,
    max_document_tokens: Option<usize>,
}

impl<'a> RetrieveRenderer<'a> {
    pub fn new(
        output: &'a RetrieveOutput,
        options: &'a MarkdownOptions,
        graph: &'a Graph,
        max_document_tokens: Option<usize>,
    ) -> Self {
        Self {
            output,
            options,
            graph,
            max_document_tokens,
        }
    }

    pub fn render(&self) -> String {
        self.output
            .documents
            .iter()
            .map(|doc| self.render_document(doc))
            .collect::<Vec<String>>()
            .join("\n")
    }

    fn render_document(&self, doc: &DocumentOutput) -> String {
        let frontmatter = build_retrieve_frontmatter(doc);
        let clipped = self.output.truncation.clipped.iter().any(|k| k == &doc.key);
        let body = if doc.content.is_empty() {
            String::new()
        } else {
            let rendered = render_body(self.graph, self.options, &doc.key);
            truncate_rendered_body(rendered, self.max_document_tokens, clipped)
        };
        render_block(&doc.key, &frontmatter, &[], &body)
    }
}

pub struct FindBlockRenderer<'a> {
    options: &'a MarkdownOptions,
    graph: &'a Graph,
    max_document_tokens: Option<usize>,
    clipped: &'a [String],
}

impl<'a> FindBlockRenderer<'a> {
    pub fn new(
        options: &'a MarkdownOptions,
        graph: &'a Graph,
        max_document_tokens: Option<usize>,
        clipped: &'a [String],
    ) -> Self {
        Self {
            options,
            graph,
            max_document_tokens,
            clipped,
        }
    }

    pub fn render(
        &self,
        keys: &[Key],
        results: &[Mapping],
        content_output_names: &[String],
        narrowed_content: bool,
        grep_output_names: &[String],
    ) -> String {
        if content_output_names.is_empty() {
            keys.iter()
                .zip(results.iter())
                .map(|(key, fm)| {
                    let mut line = render_index_line(key, fm, grep_output_names);
                    line.push_str(&render_grep_lines(key, fm, grep_output_names));
                    line
                })
                .collect::<String>()
        } else {
            keys.iter()
                .zip(results.iter())
                .map(|(key, fm)| {
                    let key_str = key.to_string();
                    let body = if narrowed_content {
                        content_output_names
                            .iter()
                            .filter_map(|n| fm.get(Value::String(n.clone())))
                            .filter_map(|v| v.as_str())
                            .filter(|s| !s.is_empty())
                            .collect::<Vec<&str>>()
                            .join("\n")
                    } else {
                        let clipped = self.clipped.iter().any(|k| k == &key_str);
                        let rendered = render_body(self.graph, self.options, &key_str);
                        truncate_rendered_body(rendered, self.max_document_tokens, clipped)
                    };
                    render_block(&key_str, fm, content_output_names, &body)
                })
                .collect::<Vec<String>>()
                .join("\n")
        }
    }
}

fn truncate_rendered_body(
    body: String,
    max_document_tokens: Option<usize>,
    clipped: bool,
) -> String {
    if !clipped {
        return body;
    }
    match max_document_tokens.filter(|&m| m > 0) {
        Some(max) => {
            let (head, omitted) = liwe::tokens::truncate_to_tokens(&body, max);
            if omitted > 0 {
                format!(
                    "{}{}",
                    head.trim_end_matches('\n'),
                    liwe::tokens::truncation_marker(omitted)
                )
            } else {
                body
            }
        }
        None => body,
    }
}

fn render_index_line(key: &Key, fm: &Mapping, skip: &[String]) -> String {
    let title = fm
        .get(Value::String("title".to_string()))
        .and_then(|v| v.as_str())
        .map(|s| s.to_string())
        .unwrap_or_else(|| key.to_string());

    let mut edges = String::new();
    let mut annotations = String::new();

    for (k, v) in fm {
        let name = match k.as_str() {
            Some(s) => s,
            None => continue,
        };
        if name == "key" || name == "title" {
            continue;
        }
        if skip.iter().any(|s| s == name) {
            continue;
        }
        match edge_direction(name) {
            Some(dir) => {
                if let Some(rendered) = render_edge_field(dir, v) {
                    edges.push(' ');
                    edges.push_str(&rendered);
                }
            }
            None => {
                annotations.push_str(" · ");
                annotations.push_str(name);
                annotations.push_str(": ");
                annotations.push_str(&render_annotation_value(v));
            }
        }
    }

    format!("- [{}]({}){}{}\n", title, key, edges, annotations)
}

fn render_grep_lines(key: &Key, fm: &Mapping, names: &[String]) -> String {
    let mut out = String::new();
    for name in names {
        let Some(Value::Sequence(entries)) = fm.get(Value::String(name.clone())) else {
            continue;
        };
        for entry in entries {
            let Some(m) = entry.as_mapping() else {
                continue;
            };
            let mut parts = vec![key.to_string()];
            if let Some(Value::Sequence(path)) = m.get(Value::String("path".to_string())) {
                for p in path {
                    if let Some(s) = p.as_str() {
                        parts.push(s.to_string());
                    }
                }
            }
            let tail = m
                .get(Value::String("text".to_string()))
                .and_then(|v| v.as_str())
                .and_then(|s| s.lines().next())
                .or_else(|| {
                    m.get(Value::String("target".to_string()))
                        .and_then(|v| v.as_str())
                })
                .or_else(|| {
                    m.get(Value::String("type".to_string()))
                        .and_then(|v| v.as_str())
                });
            if let Some(t) = tail {
                parts.push(t.to_string());
            }
            out.push_str(&parts.join(" › "));
            out.push('\n');
        }
    }
    out
}

fn edge_direction(name: &str) -> Option<&'static str> {
    match name {
        "includedBy" | "referencedBy" => Some("<-"),
        "includes" | "references" => Some("->"),
        _ => None,
    }
}

fn render_edge_field(dir: &str, v: &Value) -> Option<String> {
    let seq = v.as_sequence()?;
    let targets: Vec<String> = seq
        .iter()
        .filter_map(|item| {
            let m = item.as_mapping()?;
            let key = m.get(Value::String("key".to_string()))?.as_str()?;
            let title = m
                .get(Value::String("title".to_string()))
                .and_then(|t| t.as_str())
                .unwrap_or(key);
            Some(format!("[{}]({})", title, key))
        })
        .collect();
    if targets.is_empty() {
        return None;
    }
    Some(format!("{} {}", dir, targets.join(", ")))
}

fn is_scalar(v: &Value) -> bool {
    matches!(
        v,
        Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_)
    )
}

fn render_annotation_value(v: &Value) -> String {
    match v {
        Value::Sequence(items) if items.iter().all(is_scalar) => items
            .iter()
            .map(inline_value)
            .collect::<Vec<String>>()
            .join(", "),
        _ => inline_value(v),
    }
}

fn inline_value(v: &Value) -> String {
    match v {
        Value::Null => "null".to_string(),
        Value::Bool(b) => b.to_string(),
        Value::Number(n) => n.to_string(),
        Value::String(s) => s.clone(),
        Value::Sequence(items) => {
            let inner = items
                .iter()
                .map(inline_value)
                .collect::<Vec<String>>()
                .join(", ");
            format!("[{}]", inner)
        }
        Value::Mapping(m) => {
            let inner = m
                .iter()
                .filter_map(|(k, val)| {
                    k.as_str()
                        .map(|name| format!("{}: {}", name, inline_value(val)))
                })
                .collect::<Vec<String>>()
                .join(", ");
            format!("{{{}}}", inner)
        }
        Value::Tagged(t) => inline_value(&t.value),
    }
}

fn build_retrieve_frontmatter(doc: &DocumentOutput) -> Mapping {
    let mut fm = Mapping::new();
    fm.insert(
        Value::String("title".to_string()),
        Value::String(doc.title.clone()),
    );
    if !doc.references.is_empty() {
        fm.insert(
            Value::String("references".to_string()),
            edges_to_value(&doc.references),
        );
    }
    if !doc.includes.is_empty() {
        fm.insert(
            Value::String("includes".to_string()),
            edges_to_value(&doc.includes),
        );
    }
    if !doc.referenced_by.is_empty() {
        fm.insert(
            Value::String("referencedBy".to_string()),
            edges_to_value(&doc.referenced_by),
        );
    }
    if !doc.included_by.is_empty() {
        fm.insert(
            Value::String("includedBy".to_string()),
            edges_to_value(&doc.included_by),
        );
    }
    fm
}

fn edges_to_value(edges: &[EdgeRef]) -> Value {
    let metas: Vec<EdgeRefMeta> = edges.iter().map(EdgeRefMeta::from).collect();
    serde_yaml::to_value(metas).expect("edges serialize")
}

fn render_block(
    key: &str,
    frontmatter: &Mapping,
    omit_in_frontmatter: &[String],
    body: &str,
) -> String {
    let trimmed = trim_frontmatter(frontmatter, omit_in_frontmatter);
    let fence = "`".repeat(outer_fence_len(body));
    let has_frontmatter = !trimmed.is_empty();

    let mut block = String::new();
    block.push_str(&fence);
    block.push_str("markdown #");
    block.push_str(key);
    block.push('\n');

    if has_frontmatter {
        let yaml = serde_yaml::to_string(&Value::Mapping(trimmed)).expect("frontmatter serializes");
        block.push_str("---\n");
        block.push_str(&yaml);
        block.push_str("---\n");
    }

    if !body.is_empty() {
        if has_frontmatter {
            block.push('\n');
        }
        block.push_str(body.trim_end_matches('\n'));
        block.push('\n');
    }
    block.push_str(&fence);
    block.push('\n');
    block
}

fn trim_frontmatter(fm: &Mapping, omit: &[String]) -> Mapping {
    let mut out = Mapping::new();
    for (k, v) in fm {
        let name = match k.as_str() {
            Some(s) => s,
            None => continue,
        };
        if name == "key" {
            continue;
        }
        if omit.iter().any(|o| o == name) {
            continue;
        }
        let trimmed = strip_empty(v.clone());
        if is_empty_collection(&trimmed) {
            continue;
        }
        out.insert(k.clone(), trimmed);
    }
    out
}

fn strip_empty(v: Value) -> Value {
    match v {
        Value::Sequence(items) => Value::Sequence(items.into_iter().map(strip_empty).collect()),
        Value::Mapping(m) => {
            let mut out = Mapping::new();
            for (k, v) in m {
                let v = strip_empty(v);
                if is_empty_collection(&v) {
                    continue;
                }
                out.insert(k, v);
            }
            Value::Mapping(out)
        }
        other => other,
    }
}

fn is_empty_collection(v: &Value) -> bool {
    match v {
        Value::Sequence(s) => s.is_empty(),
        Value::Mapping(m) => m.is_empty(),
        _ => false,
    }
}

fn render_body(graph: &Graph, options: &MarkdownOptions, key: &str) -> String {
    let key = Key::name(key);
    let blocks = render_content(graph, &key);
    blocks_to_markdown_sparce_skip_frontmatter(&blocks, options)
}

fn render_content(graph: &Graph, key: &Key) -> Vec<Block> {
    let tree = graph.collect(key);

    let parent_lookup = |ref_key: &Key| -> Vec<(Key, String)> {
        let refs = graph.get_inclusion_edges_to(ref_key);
        let mut parents = Vec::new();

        for ref_id in refs {
            let node = graph.node(ref_id);
            if let Some(doc_node) = node.to_document() {
                if let Some(doc_key) = doc_node.document_key() {
                    if doc_key == *key {
                        continue;
                    }
                    let title = graph
                        .get_key_title(&doc_key)
                        .unwrap_or_else(|| doc_key.to_string());
                    if !parents.iter().any(|(k, _)| k == &doc_key) {
                        parents.push((doc_key, title));
                    }
                }
            }
        }

        parents
    };

    let annotated = tree.annotate_references(&parent_lookup, &key.parent());

    Projector::project(
        TreeIter::new(&annotated),
        &key.parent(),
        graph.format_options().refs_path(),
    )
}

fn outer_fence_len(body: &str) -> usize {
    let mut max_run = 0usize;
    for line in body.lines() {
        let trimmed = line.trim_start_matches(' ');
        let leading = line.len() - trimmed.len();
        if leading > 3 {
            continue;
        }
        let run = trimmed.chars().take_while(|&c| c == '`').count();
        if run > max_run {
            max_run = run;
        }
    }
    std::cmp::max(4, max_run + 1)
}

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

    #[test]
    fn fence_default_is_four() {
        assert_eq!(outer_fence_len(""), 4);
        assert_eq!(outer_fence_len("plain text\nno backticks"), 4);
    }

    #[test]
    fn fence_three_backticks_does_not_escalate() {
        assert_eq!(outer_fence_len("```\ncode\n```"), 4);
    }

    #[test]
    fn fence_escalates_past_inner_four_backticks() {
        assert_eq!(outer_fence_len("````\ninner\n````"), 5);
    }

    #[test]
    fn fence_escalates_past_inner_six_backticks() {
        assert_eq!(outer_fence_len("``````\ninner\n``````"), 7);
    }

    #[test]
    fn fence_ignores_deeply_indented_runs() {
        assert_eq!(outer_fence_len("    ````not a fence"), 4);
    }
}
