use liwe::graph::{Graph, GraphContext};
use liwe::model::config::MarkdownOptions;
use liwe::model::graph::{blocks_to_markdown_sparce_skip_frontmatter, GraphBlock};
use liwe::model::node::NodePointer;
use liwe::model::projector::Projector;
use liwe::model::tree::TreeIter;
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,
}

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

    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 body = if doc.content.is_empty() {
            String::new()
        } else {
            render_body(self.graph, self.options, &doc.key)
        };
        render_block(&doc.key, &frontmatter, &[], &body)
    }
}

pub struct FindBlockRenderer<'a> {
    options: &'a MarkdownOptions,
    graph: &'a Graph,
}

impl<'a> FindBlockRenderer<'a> {
    pub fn new(options: &'a MarkdownOptions, graph: &'a Graph) -> Self {
        Self { options, graph }
    }

    pub fn render(
        &self,
        keys: &[Key],
        results: &[Mapping],
        content_output_names: &[String],
    ) -> String {
        keys.iter()
            .zip(results.iter())
            .map(|(key, fm)| {
                let body = render_body(self.graph, self.options, &key.to_string());
                render_block(&key.to_string(), fm, content_output_names, &body)
            })
            .collect::<Vec<String>>()
            .join("\n")
    }
}

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<GraphBlock> {
    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())
}

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);
    }
}
