use std::str::FromStr;

use nom::{
    AsChar, Parser,
    bytes::complete::{take_until, take_while},
    character::{char, complete::newline},
    combinator::{complete, eof, opt},
    error::context,
    sequence::delimited,
};
use nom_language::error::VerboseError;

use super::{Breaking, CommitType, Footer, Message};

type IResult<I, O> = nom::IResult<I, O, VerboseError<I>>;

#[derive(Debug, Clone, Default)]
struct Header {
    ctype: CommitType,
    is_breaking: bool,
    scope: Option<String>,
    description: String,
}

fn parse_header(input: &str) -> IResult<&str, Header> {
    if !input.contains(":") {
        let (input, description) = context(
            "description",
            take_while(|x| !AsChar::is_newline(x)).map(String::from),
        )
        .parse(input)?;
        let (input, _) = opt(newline).parse(input)?;
        return Ok((
            input,
            Header {
                description,
                ..Default::default()
            },
        ));
    }
    let (input, ctype) = context(
        "type",
        take_while(AsChar::is_alpha).map_res(CommitType::from_str),
    )
    .parse(input)?;
    let (input, scope) = context(
        "scope",
        opt(complete(delimited(char('('), take_until(")"), char(')'))).map(String::from)),
    )
    .parse(input)?;
    let (input, excl) = context("exclamation", opt(complete(char('!')))).parse(input)?;
    let (input, _) = context(
        "delimiter",
        opt(complete(char(':'))).and(take_while(AsChar::is_space)),
    )
    .parse(input)?;
    let (input, description) = context(
        "description",
        take_while(|x| !AsChar::is_newline(x)).map(String::from),
    )
    .parse(input)?;
    let (input, _) = opt(newline).parse(input)?;

    Ok((
        input,
        Header {
            ctype,
            is_breaking: excl.is_some(),
            scope,
            description,
        },
    ))
}

fn parse_comment_line(input: &str) -> IResult<&str, &str> {
    let (input, _) = char('#').parse(input)?;
    let (input, _) = opt(complete(char(' '))).parse(input)?;
    Ok(("", input.trim_end()))
}

pub fn parse_footer_line(input: &str) -> IResult<&str, (&str, &str)> {
    let (input, key) = context("key", take_until(":")).parse(input)?;
    let (input, _) = char(':').parse(input)?;
    let (input, _) = take_while(AsChar::is_space).parse(input)?;
    let (input, value) = context("value", take_while(|x| !AsChar::is_newline(x))).parse(input)?;
    Ok((input, (key, value)))
}

pub fn parse_message(input: &str) -> IResult<&str, Message> {
    let mut breaking = Vec::new();
    let mut footer = Vec::new();
    let mut body = String::new();
    let mut comments = String::new();
    let mut header = None;
    for line in input.lines() {
        let (rest, comment) = context("comment", opt(complete(parse_comment_line))).parse(line)?;
        if let Some(c) = comment {
            eof(rest)?;
            comments += c;
            comments += "\n";
            continue;
        }

        if header.is_none() {
            let (rest, h) = context(
                "header",
                take_while(|x| !AsChar::is_newline(x)).and_then(parse_header),
            )
            .parse(line)?;
            header = Some(h);
            eof(rest)?;
            continue;
        }

        let (rest, f) = context("footer_line", opt(complete(parse_footer_line))).parse(line)?;
        if let Some((k, v)) = f {
            eof(rest)?;
            if k == "BREAKING CHANGE" {
                breaking.push(v.to_string());
            } else {
                footer.push((k.to_string(), v.to_string()));
            }
            continue;
        }

        body += line;
        body += "\n";
    }

    let header = header.unwrap_or_default();

    Ok((
        "", // We checked with EOF before that every line is read
        Message {
            ctype: header.ctype,
            is_breaking: header.is_breaking,
            scope: header.scope,
            description: header.description,
            body: body.trim().to_string(),
            breaking: Breaking(breaking),
            footer: Footer(footer),
            comments: comments.trim().to_string(),
        },
    ))
}
