use std::collections::HashMap;

use chrono::{DateTime, Utc};
use quick_xml::escape::unescape;
use quick_xml::events::Event;
use quick_xml::Reader;

use crate::model::{Comment, CommentId};
use crate::Error;

pub(crate) fn parse_comments_part(xml: &str) -> Result<CommentsParseResult, Error> {
    let mut reader = Reader::from_str(xml);
    reader.config_mut().trim_text(false);
    let mut buf = Vec::new();

    let mut comments = Vec::new();
    let mut para_id_by_comment_id = HashMap::new();
    let mut current: Option<CommentBuilder> = None;

    loop {
        match reader.read_event_into(&mut buf)? {
            Event::Start(ref event) => match event.local_name().as_ref() {
                b"comment" => current = Some(parse_comment_builder(event)),
                b"p" => {
                    if let Some(comment) = current.as_mut() {
                        comment.capture_para_id(event);
                    }
                }
                b"t" | b"delText" => {
                    if let Some(comment) = current.as_mut() {
                        let text = decode_xml_text(&reader.read_text(event.name())?)?;
                        comment.text.push_str(text.as_ref());
                    }
                }
                _ => {}
            },
            Event::Empty(ref event) => {
                if event.local_name().as_ref() == b"tab"
                    && let Some(comment) = current.as_mut()
                {
                    comment.text.push('\t');
                }
            }
            Event::End(ref event) => {
                if event.local_name().as_ref() == b"comment"
                    && let Some(comment) = current.take()
                {
                    let imported_id = comment.imported_id.clone();
                    let para_id = comment.para_id.clone();
                    let built = comment.build();
                    if let (Some(imported_id), Some(para_id)) = (imported_id, para_id) {
                        para_id_by_comment_id.insert(imported_id, para_id);
                    }
                    comments.push(built);
                }
            }
            Event::Eof => break,
            _ => {}
        }

        buf.clear();
    }

    Ok(CommentsParseResult {
        comments,
        para_id_by_comment_id,
    })
}

fn decode_xml_text(text: &str) -> Result<String, Error> {
    Ok(unescape(text)?.into_owned())
}

#[derive(Debug, Default)]
pub(crate) struct CommentsParseResult {
    pub(crate) comments: Vec<Comment>,
    pub(crate) para_id_by_comment_id: HashMap<String, String>,
}

#[derive(Debug)]
struct CommentBuilder {
    imported_id: Option<String>,
    author: Option<String>,
    date: Option<DateTime<Utc>>,
    text: String,
    para_id: Option<String>,
}

impl CommentBuilder {
    fn capture_para_id(&mut self, event: &quick_xml::events::BytesStart<'_>) {
        if self.para_id.is_some() {
            return;
        }

        for attribute in event.attributes().flatten() {
            if attribute.key.local_name().as_ref() == b"paraId" {
                self.para_id = Some(String::from_utf8_lossy(attribute.value.as_ref()).into_owned());
                break;
            }
        }
    }

    fn build(self) -> Comment {
        let imported_id = self.imported_id;
        let id = imported_id
            .as_deref()
            .map(|id| format!("comment:{id}"))
            .unwrap_or_else(|| "comment:unknown".to_string());

        Comment {
            id,
            imported_id,
            author: self.author,
            date: self.date,
            text: self.text,
            anchors: Vec::new(),
            anchored_text: None,
            resolved: None,
            parent_id: None,
            replies: Vec::new(),
        }
    }
}

fn parse_comment_builder(event: &quick_xml::events::BytesStart<'_>) -> CommentBuilder {
    let mut imported_id = None;
    let mut author = None;
    let mut date = None;

    for attribute in event.attributes().flatten() {
        match attribute.key.local_name().as_ref() {
            b"id" => imported_id = Some(String::from_utf8_lossy(attribute.value.as_ref()).into_owned()),
            b"author" => author = Some(String::from_utf8_lossy(attribute.value.as_ref()).into_owned()),
            b"date" => {
                let raw = String::from_utf8_lossy(attribute.value.as_ref()).into_owned();
                date = DateTime::parse_from_rfc3339(&raw)
                    .ok()
                    .map(|value| value.with_timezone(&Utc));
            }
            _ => {}
        }
    }

    CommentBuilder {
        imported_id,
        author,
        date,
        text: String::new(),
        para_id: None,
    }
}

pub(crate) fn apply_comment_metadata(
    comments: &mut [Comment],
    para_id_by_comment_id: &HashMap<String, String>,
    comments_extended_xml: Option<&str>,
) -> Result<(), Error> {
    let extended = comments_extended_xml
        .map(parse_comments_extended_part)
        .transpose()?
        .unwrap_or_default();

    let mut para_to_comment_id = HashMap::<String, CommentId>::new();
    for comment in comments.iter() {
        if let Some(imported_id) = comment.imported_id.as_deref()
            && let Some(para_id) = para_id_by_comment_id.get(imported_id)
        {
            para_to_comment_id.insert(para_id.clone(), comment.id.clone());
        }
    }

    let mut replies_by_parent = HashMap::<CommentId, Vec<CommentId>>::new();

    for comment in comments.iter_mut() {
        let Some(imported_id) = comment.imported_id.as_deref() else {
            continue;
        };
        let Some(para_id) = para_id_by_comment_id.get(imported_id) else {
            continue;
        };

        if let Some(entry) = extended.by_para_id.get(para_id) {
            comment.resolved = entry.done;

            if let Some(parent_para_id) = entry.parent_para_id.as_deref()
                && let Some(parent_comment_id) = para_to_comment_id.get(parent_para_id)
            {
                comment.parent_id = Some(parent_comment_id.clone());
                replies_by_parent
                    .entry(parent_comment_id.clone())
                    .or_default()
                    .push(comment.id.clone());
            }
        }
    }

    for comment in comments.iter_mut() {
        if let Some(replies) = replies_by_parent.remove(&comment.id) {
            comment.replies = replies;
        }
    }

    Ok(())
}

#[derive(Debug, Default)]
struct CommentsExtendedData {
    by_para_id: HashMap<String, CommentExtendedEntry>,
}

#[derive(Debug)]
struct CommentExtendedEntry {
    parent_para_id: Option<String>,
    done: Option<bool>,
}

fn parse_comments_extended_part(xml: &str) -> Result<CommentsExtendedData, Error> {
    let mut reader = Reader::from_str(xml);
    reader.config_mut().trim_text(true);
    let mut buf = Vec::new();
    let mut by_para_id = HashMap::new();

    loop {
        match reader.read_event_into(&mut buf)? {
            Event::Start(ref event) | Event::Empty(ref event)
                if event.local_name().as_ref() == b"commentEx" =>
            {
                let mut para_id = None;
                let mut parent_para_id = None;
                let mut done = None;

                for attribute in event.attributes().flatten() {
                    match attribute.key.local_name().as_ref() {
                        b"paraId" => {
                            para_id = Some(String::from_utf8_lossy(attribute.value.as_ref()).into_owned())
                        }
                        b"paraIdParent" => {
                            parent_para_id =
                                Some(String::from_utf8_lossy(attribute.value.as_ref()).into_owned())
                        }
                        b"done" => {
                            done = Some(String::from_utf8_lossy(attribute.value.as_ref()) == "1")
                        }
                        _ => {}
                    }
                }

                if let Some(para_id) = para_id {
                    by_para_id.insert(
                        para_id,
                        CommentExtendedEntry {
                            parent_para_id,
                            done,
                        },
                    );
                }
            }
            Event::Eof => break,
            _ => {}
        }

        buf.clear();
    }

    Ok(CommentsExtendedData { by_para_id })
}

#[cfg(test)]
mod tests {
    use chrono::{TimeZone, Utc};

    use super::{apply_comment_metadata, parse_comments_part};

    #[test]
    fn parses_comment_bodies_and_metadata() {
        let xml = r#"
            <w:comments xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
              <w:comment w:id="1" w:author="Alice" w:date="2026-05-29T10:00:00Z">
                <w:p xmlns:w14="http://schemas.microsoft.com/office/word/2010/wordml" w14:paraId="P1">
                  <w:r><w:t>Hello</w:t></w:r>
                  <w:r><w:t xml:space="preserve"> world</w:t></w:r>
                </w:p>
              </w:comment>
            </w:comments>
        "#;

        let result = parse_comments_part(xml).unwrap();
        let comments = result.comments;

        assert_eq!(comments.len(), 1);
        assert_eq!(comments[0].id, "comment:1");
        assert_eq!(comments[0].imported_id.as_deref(), Some("1"));
        assert_eq!(comments[0].author.as_deref(), Some("Alice"));
        assert_eq!(comments[0].text, "Hello world");
        assert_eq!(result.para_id_by_comment_id.get("1").map(String::as_str), Some("P1"));
        assert_eq!(
            comments[0].date,
            Some(Utc.with_ymd_and_hms(2026, 5, 29, 10, 0, 0).unwrap())
        );
    }

    #[test]
    fn applies_threading_and_resolved_metadata() {
        let result = parse_comments_part(
            r#"
            <w:comments xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
                        xmlns:w14="http://schemas.microsoft.com/office/word/2010/wordml">
              <w:comment w:id="0" w:author="Alice"><w:p w14:paraId="P0"><w:r><w:t>Parent</w:t></w:r></w:p></w:comment>
              <w:comment w:id="1" w:author="Alice"><w:p w14:paraId="P1"><w:r><w:t>Reply</w:t></w:r></w:p></w:comment>
            </w:comments>
            "#,
        )
        .unwrap();
        let mut comments = result.comments;

        apply_comment_metadata(
            &mut comments,
            &result.para_id_by_comment_id,
            Some(
                r#"
                <w15:commentsEx xmlns:w15="http://schemas.microsoft.com/office/word/2012/wordml">
                  <w15:commentEx w15:paraId="P0" w15:done="1"/>
                  <w15:commentEx w15:paraId="P1" w15:paraIdParent="P0"/>
                </w15:commentsEx>
                "#,
            ),
        )
        .unwrap();

        assert_eq!(comments[0].resolved, Some(true));
        assert_eq!(comments[0].replies, vec!["comment:1".to_string()]);
        assert_eq!(comments[1].parent_id.as_deref(), Some("comment:0"));
        assert_eq!(comments[1].resolved, None);
    }
}
