use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine as _};
use flate2::read::GzDecoder;
use html5ever::{parse_document, tendril::TendrilSink};
use markup5ever_rcdom::{Handle, NodeData, RcDom};
use serde::Deserialize;
use serde_json::{Map, Value};
use sha2::{Digest, Sha256};
use std::collections::BTreeMap;
use std::io::Read;

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ImportMode {
    Exact,
    Semantic,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ImportedHmd {
    pub source: String,
    pub mode: ImportMode,
    pub warnings: Vec<String>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ImportError {
    pub message: String,
}

impl std::fmt::Display for ImportError {
    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        formatter.write_str(&self.message)
    }
}

impl std::error::Error for ImportError {}

#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
struct HmdManifest {
    hmd: Option<String>,
    mode: Option<String>,
    profile: Option<String>,
    source_hash: Option<String>,
    source_encoding: Option<String>,
    source: Option<String>,
    semantic: Option<SemanticDocument>,
}

#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
struct SemanticDocument {
    hmd: Option<String>,
    profile: Option<String>,
    #[serde(default)]
    uses: Vec<String>,
    #[serde(default)]
    meta: Map<String, Value>,
    #[serde(default)]
    children: Vec<SemanticNode>,
}

#[derive(Debug, Clone, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
enum SemanticNode {
    HmdBlock(SemanticBlock),
    Markdown(SemanticMarkdown),
    RawHtml(SemanticRawHtml),
}

#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
struct SemanticBlock {
    block_type: String,
    id: Option<String>,
    profile: Option<String>,
    #[serde(default)]
    meta: Map<String, Value>,
    #[serde(default)]
    children: Vec<SemanticNode>,
}

#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
struct SemanticMarkdown {
    text: String,
}

#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
struct SemanticRawHtml {
    html: String,
}

pub fn import_html(source: &str) -> Result<ImportedHmd, ImportError> {
    let dom = parse_document(RcDom::default(), Default::default()).one(source.to_string());

    if let Some(manifest) = find_manifest(&dom.document)? {
        if manifest.mode.as_deref() == Some("exact") {
            if let Some(imported) = import_exact_manifest(&manifest)? {
                return Ok(imported);
            }
        }
        if let Some(semantic) = &manifest.semantic {
            return Ok(import_semantic_manifest(&manifest, semantic));
        }
    }

    import_semantic(&dom.document)
}

fn import_exact_manifest(manifest: &HmdManifest) -> Result<Option<ImportedHmd>, ImportError> {
    let Some(encoded_source) = &manifest.source else {
        return Ok(None);
    };

    let encoding = manifest.source_encoding.as_deref().unwrap_or("utf8");
    let decoded = match encoding {
        "utf8" => encoded_source.clone(),
        "utf8+gzip+base64" => decode_gzip_base64(encoded_source)?,
        other => {
            return Err(error(format!(
                "unsupported HMD manifest sourceEncoding '{other}'"
            )));
        }
    };

    if let Some(expected_hash) = &manifest.source_hash {
        let actual_hash = source_hash(&decoded);
        if expected_hash != &actual_hash {
            return Err(error(format!(
                "source hash mismatch: expected {expected_hash}, got {actual_hash}"
            )));
        }
    }

    Ok(Some(ImportedHmd {
        source: decoded,
        mode: ImportMode::Exact,
        warnings: Vec::new(),
    }))
}

fn import_semantic_manifest(manifest: &HmdManifest, semantic: &SemanticDocument) -> ImportedHmd {
    ImportedHmd {
        source: semantic_document_to_hmd(semantic, Some(manifest)),
        mode: ImportMode::Semantic,
        warnings: vec![
            "semantic import reconstructs canonical HMD, not original formatting".to_string(),
        ],
    }
}

fn import_semantic(root: &Handle) -> Result<ImportedHmd, ImportError> {
    let manifest = find_manifest(root)?;
    let blocks = collect_top_blocks(root);
    if blocks.is_empty() {
        return Err(error(
            "no exact HMD manifest or semantic data-hmd blocks found",
        ));
    }

    let profile = manifest
        .as_ref()
        .and_then(|manifest| manifest.profile.clone())
        .or_else(|| blocks.iter().find_map(|block| block.profile.clone()))
        .unwrap_or_else(|| "general@0.1".to_string());
    let hmd = manifest
        .as_ref()
        .and_then(|manifest| manifest.hmd.clone())
        .unwrap_or_else(|| "0.1".to_string());
    let document = SemanticDocument {
        hmd: Some(hmd),
        profile: Some(profile),
        uses: Vec::new(),
        meta: Map::new(),
        children: blocks.into_iter().map(SemanticNode::HmdBlock).collect(),
    };

    Ok(ImportedHmd {
        source: semantic_document_to_hmd(&document, manifest.as_ref()),
        mode: ImportMode::Semantic,
        warnings: vec![
            "semantic import reconstructs canonical HMD, not original formatting".to_string(),
        ],
    })
}

fn find_manifest(root: &Handle) -> Result<Option<HmdManifest>, ImportError> {
    let mut manifests = Vec::new();
    collect_manifest_text(root, &mut manifests);
    let Some(source) = manifests.into_iter().next() else {
        return Ok(None);
    };
    let manifest = serde_json::from_str::<HmdManifest>(&source)
        .map_err(|error| self::error(format!("invalid HMD manifest JSON: {error}")))?;
    Ok(Some(manifest))
}

fn collect_manifest_text(handle: &Handle, manifests: &mut Vec<String>) {
    if is_manifest_script(handle) {
        manifests.push(text_content(handle));
        return;
    }

    for child in handle.children.borrow().iter() {
        collect_manifest_text(child, manifests);
    }
}

fn is_manifest_script(handle: &Handle) -> bool {
    let NodeData::Element { name, attrs, .. } = &handle.data else {
        return false;
    };
    if name.local.as_ref() != "script" {
        return false;
    }

    attrs.borrow().iter().any(|attr| {
        attr.name.local.as_ref() == "type" && attr.value.as_ref() == "application/hmd+json"
    })
}

fn collect_top_blocks(root: &Handle) -> Vec<SemanticBlock> {
    let mut blocks = Vec::new();
    collect_top_blocks_inner(root, false, &mut blocks);
    blocks
}

fn collect_top_blocks_inner(
    handle: &Handle,
    inside_hmd_block: bool,
    blocks: &mut Vec<SemanticBlock>,
) {
    if !inside_hmd_block && has_hmd_type(handle) {
        blocks.push(parse_semantic_block(handle));
        return;
    }

    let inside = inside_hmd_block || has_hmd_type(handle);
    for child in handle.children.borrow().iter() {
        collect_top_blocks_inner(child, inside, blocks);
    }
}

fn collect_child_blocks(handle: &Handle) -> Vec<SemanticNode> {
    let mut blocks = Vec::new();
    for child in handle.children.borrow().iter() {
        collect_child_blocks_inner(child, &mut blocks);
    }
    blocks.into_iter().map(SemanticNode::HmdBlock).collect()
}

fn collect_child_blocks_inner(handle: &Handle, blocks: &mut Vec<SemanticBlock>) {
    if has_hmd_type(handle) {
        blocks.push(parse_semantic_block(handle));
        return;
    }

    for child in handle.children.borrow().iter() {
        collect_child_blocks_inner(child, blocks);
    }
}

fn parse_semantic_block(handle: &Handle) -> SemanticBlock {
    let attrs = element_attrs(handle);
    let block_type = attrs
        .get("data-hmd-type")
        .cloned()
        .unwrap_or_else(|| "block".to_string());
    let profile = attrs.get("data-hmd-profile").cloned();
    let mut meta = attrs
        .get("data-hmd-meta")
        .and_then(|value| serde_json::from_str::<Map<String, Value>>(value).ok())
        .unwrap_or_default();

    let id = attrs.get("data-hmd-id").cloned();
    if let Some(id) = &id {
        meta.entry("id".to_string())
            .or_insert_with(|| Value::String(id.clone()));
    }

    SemanticBlock {
        block_type,
        id,
        profile,
        meta,
        children: collect_child_blocks(handle),
    }
}

fn has_hmd_type(handle: &Handle) -> bool {
    element_attrs(handle).contains_key("data-hmd-type")
}

fn element_attrs(handle: &Handle) -> BTreeMap<String, String> {
    let NodeData::Element { attrs, .. } = &handle.data else {
        return BTreeMap::new();
    };

    attrs
        .borrow()
        .iter()
        .map(|attr| (attr.name.local.to_string(), attr.value.to_string()))
        .collect()
}

fn semantic_document_to_hmd(document: &SemanticDocument, manifest: Option<&HmdManifest>) -> String {
    let hmd = document
        .hmd
        .clone()
        .or_else(|| manifest.and_then(|manifest| manifest.hmd.clone()))
        .or_else(|| string_meta(&document.meta, "hmd"))
        .unwrap_or_else(|| "0.1".to_string());
    let profile = document
        .profile
        .clone()
        .or_else(|| manifest.and_then(|manifest| manifest.profile.clone()))
        .or_else(|| string_meta(&document.meta, "profile"))
        .unwrap_or_else(|| "general@0.1".to_string());
    let uses = if document.uses.is_empty() {
        string_array_meta(&document.meta, "uses")
    } else {
        document.uses.clone()
    };

    let mut output = String::new();
    output.push_str("+++\n");
    output.push_str(&format!("hmd = {}\n", toml_string(&hmd)));
    output.push_str(&format!("profile = {}\n", toml_string(&profile)));
    if !uses.is_empty() {
        output.push_str("uses = ");
        output.push_str(&toml_literal(&Value::Array(
            uses.iter().cloned().map(Value::String).collect(),
        )));
        output.push('\n');
    }
    for (key, value) in &document.meta {
        if matches!(key.as_str(), "hmd" | "profile" | "uses") {
            continue;
        }
        output.push_str(key);
        output.push_str(" = ");
        output.push_str(&toml_literal(value));
        output.push('\n');
    }
    output.push_str("+++\n\n");

    for child in &document.children {
        let rendered = semantic_node_to_hmd(child);
        if rendered.trim().is_empty() {
            continue;
        }
        output.push_str(&rendered);
        if !output.ends_with("\n\n") {
            output.push('\n');
        }
    }

    output
}

fn semantic_node_to_hmd(node: &SemanticNode) -> String {
    match node {
        SemanticNode::HmdBlock(block) => semantic_block_to_hmd(block),
        SemanticNode::Markdown(markdown) => markdown_to_hmd(&markdown.text),
        SemanticNode::RawHtml(raw_html) => markdown_to_hmd(&raw_html.html),
    }
}

fn semantic_block_to_hmd(block: &SemanticBlock) -> String {
    let fence = ":".repeat(semantic_block_fence_length(block));
    let mut output = String::new();
    output.push_str(&format!("{fence} {}\n", block.block_type));
    output.push_str("+++\n");
    let mut meta = block.meta.clone();
    if let Some(id) = &block.id {
        meta.entry("id".to_string())
            .or_insert_with(|| Value::String(id.clone()));
    }
    for (key, value) in &meta {
        output.push_str(key);
        output.push_str(" = ");
        output.push_str(&toml_literal(value));
        output.push('\n');
    }
    output.push_str("+++\n");

    if !block.children.is_empty() {
        output.push('\n');
        for child in &block.children {
            let rendered = semantic_node_to_hmd(child);
            if rendered.trim().is_empty() {
                continue;
            }
            output.push_str(&rendered);
            if !output.ends_with("\n\n") {
                output.push('\n');
            }
        }
    }

    output.push_str(&format!("{fence}\n"));
    output
}

fn semantic_block_fence_length(block: &SemanticBlock) -> usize {
    block
        .children
        .iter()
        .filter_map(|child| match child {
            SemanticNode::HmdBlock(block) => Some(semantic_block_fence_length(block)),
            _ => None,
        })
        .max()
        .map_or(3, |child_length| child_length + 1)
}

fn markdown_to_hmd(markdown: &str) -> String {
    let trimmed = markdown.trim_matches('\n');
    if trimmed.trim().is_empty() {
        String::new()
    } else {
        format!("{trimmed}\n")
    }
}

fn string_meta(meta: &Map<String, Value>, key: &str) -> Option<String> {
    meta.get(key)
        .and_then(Value::as_str)
        .map(ToString::to_string)
}

fn string_array_meta(meta: &Map<String, Value>, key: &str) -> Vec<String> {
    meta.get(key)
        .and_then(Value::as_array)
        .map(|values| {
            values
                .iter()
                .filter_map(Value::as_str)
                .map(ToString::to_string)
                .collect()
        })
        .unwrap_or_default()
}

fn text_content(handle: &Handle) -> String {
    let mut text = String::new();
    collect_text(handle, &mut text);
    text
}

fn collect_text(handle: &Handle, output: &mut String) {
    match &handle.data {
        NodeData::Text { contents } => output.push_str(&contents.borrow()),
        _ => {
            for child in handle.children.borrow().iter() {
                collect_text(child, output);
            }
        }
    }
}

fn decode_gzip_base64(source: &str) -> Result<String, ImportError> {
    let compressed = BASE64_STANDARD
        .decode(source)
        .map_err(|error| self::error(format!("invalid base64 HMD source: {error}")))?;
    let mut decoder = GzDecoder::new(compressed.as_slice());
    let mut decoded = String::new();
    decoder
        .read_to_string(&mut decoded)
        .map_err(|error| self::error(format!("invalid gzip HMD source: {error}")))?;
    Ok(decoded)
}

fn source_hash(source: &str) -> String {
    let mut hasher = Sha256::new();
    hasher.update(source.as_bytes());
    format!("sha256-{}", to_hex(&hasher.finalize()))
}

fn to_hex(bytes: &[u8]) -> String {
    const HEX: &[u8; 16] = b"0123456789abcdef";
    let mut output = String::with_capacity(bytes.len() * 2);
    for byte in bytes {
        output.push(HEX[(byte >> 4) as usize] as char);
        output.push(HEX[(byte & 0x0f) as usize] as char);
    }
    output
}

fn toml_string(value: &str) -> String {
    serde_json::to_string(value).expect("serializes TOML string")
}

fn toml_literal(value: &Value) -> String {
    match value {
        Value::Null => "\"\"".to_string(),
        Value::Bool(value) => value.to_string(),
        Value::Number(value) => value.to_string(),
        Value::String(value) => toml_string(value),
        Value::Array(values) => {
            let values = values.iter().map(toml_literal).collect::<Vec<_>>();
            format!("[{}]", values.join(", "))
        }
        Value::Object(values) => {
            let values = values
                .iter()
                .map(|(key, value)| format!("{key} = {}", toml_literal(value)))
                .collect::<Vec<_>>();
            format!("{{ {} }}", values.join(", "))
        }
    }
}

fn error(message: impl Into<String>) -> ImportError {
    ImportError {
        message: message.into(),
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use hmd_render_html::{render_document, RenderAsset, RenderOptions, RoundtripMode};
    use std::fs;
    use std::path::{Path, PathBuf};

    #[test]
    fn exact_import_recovers_original_source() {
        let source = fs::read_to_string(repo_path("fixtures/valid/todo-basic.hmd")).unwrap();
        let document = hmd_parse::parse_document(&source);
        let html = render_document(
            &document,
            RenderOptions::default()
                .with_roundtrip(RoundtripMode::Exact)
                .with_source(source.clone()),
        );

        let imported = import_html(&html).unwrap();
        assert_eq!(imported.mode, ImportMode::Exact);
        assert_eq!(imported.source, source);
    }

    #[test]
    fn exact_import_detects_hash_mismatch() {
        let source = fs::read_to_string(repo_path("fixtures/valid/todo-basic.hmd")).unwrap();
        let document = hmd_parse::parse_document(&source);
        let html = render_document(
            &document,
            RenderOptions::default()
                .with_roundtrip(RoundtripMode::Exact)
                .with_source(source),
        )
        .replacen("sha256-", "sha256-dead", 1);

        let error = import_html(&html).unwrap_err();
        assert!(error.message.contains("source hash mismatch"));
    }

    #[test]
    fn semantic_import_reconstructs_canonical_hmd() {
        let source = fs::read_to_string(repo_path("fixtures/valid/todo-basic.hmd")).unwrap();
        let document = hmd_parse::parse_document(&source);
        let html = render_document(
            &document,
            RenderOptions::default().with_roundtrip(RoundtripMode::Semantic),
        );

        let imported = import_html(&html).unwrap();
        assert_eq!(imported.mode, ImportMode::Semantic);
        assert!(imported.source.contains("profile = \"todo@0.1\""));
        assert!(imported.source.contains("title = \"Tasks\""));
        assert!(imported.source.contains("# Tasks"));
        assert!(imported.source.contains("::: task"));
        assert!(imported.source.contains("status = \"todo\""));
        assert!(imported
            .source
            .contains("Parse frontmatter and semantic blocks."));
        assert!(!imported.warnings.is_empty());
    }

    #[test]
    fn semantic_import_preserves_decision_body_and_frontmatter() {
        let source = fs::read_to_string(repo_path("examples/decision.hmd")).unwrap();
        let document = hmd_parse::parse_document(&source);
        let html = render_document(
            &document,
            RenderOptions::default().with_roundtrip(RoundtripMode::Semantic),
        );

        let imported = import_html(&html).unwrap();
        assert_eq!(imported.mode, ImportMode::Semantic);
        assert!(imported.source.contains("uses = [\"general@0.1\"]"));
        assert!(imported
            .source
            .contains("title = \"Decision Profile Example\""));
        assert!(imported.source.contains(":::: decision"));
        assert!(imported
            .source
            .contains("We need a foundation stack for HMD"));
        assert!(imported
            .source
            .contains("Rust gives HMD a single native implementation"));
    }

    #[test]
    fn semantic_import_omits_source_comments() {
        let source = r#"+++
# private toml comment
hmd = "0.1"
profile = "todo@0.1"
title = "Visible"
+++

# Visible

<!-- private markdown comment -->

::: task
+++
id = "T-visible"
title = "Visible task"
status = "todo"
+++

Visible task body.

:::
"#;
        let document = hmd_parse::parse_document(source);
        let html = render_document(
            &document,
            RenderOptions::default().with_roundtrip(RoundtripMode::Semantic),
        );

        let imported = import_html(&html).unwrap();
        assert!(imported.source.contains("Visible task body."));
        assert!(!imported.source.contains("private toml comment"));
        assert!(!imported.source.contains("private markdown comment"));
    }

    #[test]
    fn semantic_import_preserves_inlined_asset_source_metadata() {
        let source = fs::read_to_string(repo_path("examples/general.hmd")).unwrap();
        let svg = fs::read_to_string(repo_path("examples/assets/hmd-flow.svg")).unwrap();
        let asset = RenderAsset::inline_svg("./assets/hmd-flow.svg", svg).unwrap();
        let document = hmd_parse::parse_document(&source);
        let html = render_document(
            &document,
            RenderOptions::default()
                .with_roundtrip(RoundtripMode::Semantic)
                .with_asset(asset),
        );

        assert!(html.contains("data-hmd-asset-src=\"./assets/hmd-flow.svg\""));
        let imported = import_html(&html).unwrap();
        assert_eq!(imported.mode, ImportMode::Semantic);
        assert!(imported.source.contains("::: figure"));
        assert!(imported.source.contains("src = \"./assets/hmd-flow.svg\""));
        assert!(!imported.source.contains("<svg"));
    }

    fn repo_path(path: impl AsRef<Path>) -> PathBuf {
        Path::new(env!("CARGO_MANIFEST_DIR"))
            .join("../..")
            .join(path)
    }
}
