#[cfg(test)]
mod test;

mod parse;

use std::{fmt::Display, str::FromStr};

use anyhow::{Error, anyhow, bail};
use nom::{Err, Needed};

use crate::commit::parse::parse_message;

pub const COMMIT_TYPES: [CommitType; 11] = [
    CommitType::None,
    CommitType::Fix,
    CommitType::Feature,
    CommitType::Build,
    CommitType::Chore,
    CommitType::Ci,
    CommitType::Docs,
    CommitType::Style,
    CommitType::Refactor,
    CommitType::Perf,
    CommitType::Test,
];

#[derive(Debug, Clone, PartialEq, Default)]
pub struct Message {
    pub ctype: CommitType,
    pub scope: Option<String>,
    pub description: String,
    pub body: String,
    pub breaking: Breaking,
    pub is_breaking: bool,
    pub footer: Footer, // No map because order matters and repeats are allowed
    pub comments: String,
}

impl Display for Message {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match (self.is_breaking, &self.scope) {
            (true, Some(s)) => writeln!(f, "{}({s})!: {}", self.ctype, self.description)?,
            (true, None) => writeln!(f, "{}!: {}", self.ctype, self.description)?,
            (false, Some(s)) => writeln!(f, "{}({s}): {}", self.ctype, self.description)?,
            (false, None) => {
                if self.ctype == CommitType::None {
                    writeln!(f, "{}", self.description)?
                } else {
                    writeln!(f, "{}: {}", self.ctype, self.description)?
                }
            }
        }

        if !self.body.is_empty() {
            writeln!(f)?;
            writeln!(f, "{}", self.body)?
        }

        if !self.breaking.0.is_empty() {
            writeln!(f)?;
            write!(f, "{}", self.breaking)?;
        }

        if !self.footer.0.is_empty() {
            writeln!(f)?;
            write!(f, "{}", self.footer)?;
        }

        if !self.comments.is_empty() {
            writeln!(f)?;
            for line in self.comments.lines() {
                match (line.chars().next(), line.chars().nth(1)) {
                    (Some(x), Some(y)) if x.is_whitespace() && y.is_whitespace() => {
                        writeln!(f, "#{y}{}", line)?
                    }
                    (Some(x), _) if x.is_whitespace() => writeln!(f, "#{}", line)?,
                    (Some(_), _) => writeln!(f, "# {}", line)?,
                    (None, _) => writeln!(f, "#")?,
                }
            }
        }

        Ok(())
    }
}

impl FromStr for Message {
    type Err = Error;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let (rest, message) = match parse_message(s) {
            Ok(x) => x,
            Err(Err::Error(e)) => {
                for (loc, err) in &e.errors {
                    eprintln!("{loc}: {err:?}");
                }
                bail!("Parsing error: {e}")
            }
            Err(Err::Failure(e)) => bail!("Parsing failure: {e}"),
            Err(Err::Incomplete(Needed::Unknown)) => bail!("Incomplete"),
            Err(Err::Incomplete(Needed::Size(x))) => bail!("Incomplete, {x} chars needed"),
        };
        if !rest.is_empty() {
            bail!("Leftover characters: {rest}")
        }
        Ok(message)
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum CommitType {
    #[default]
    None,
    Fix,
    Feature,
    Build,
    Chore,
    Ci,
    Docs,
    Style,
    Refactor,
    Perf,
    Test,
}

impl CommitType {
    pub fn ind(self) -> usize {
        COMMIT_TYPES.iter().position(|x| *x == self).unwrap()
    }
}

impl Display for CommitType {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(match self {
            CommitType::None => "",
            CommitType::Fix => "fix",
            CommitType::Feature => "feat",
            CommitType::Build => "build",
            CommitType::Chore => "chore",
            CommitType::Ci => "ci",
            CommitType::Docs => "docs",
            CommitType::Style => "style",
            CommitType::Refactor => "refactor",
            CommitType::Perf => "perf",
            CommitType::Test => "test",
        })
    }
}

impl FromStr for CommitType {
    type Err = anyhow::Error;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s {
            "" => Ok(CommitType::None),
            "fix" => Ok(CommitType::Fix),
            "feat" => Ok(CommitType::Feature),
            "build" => Ok(CommitType::Build),
            "chore" => Ok(CommitType::Chore),
            "ci" => Ok(CommitType::Ci),
            "docs" => Ok(CommitType::Docs),
            "style" => Ok(CommitType::Style),
            "refactor" => Ok(CommitType::Refactor),
            "perf" => Ok(CommitType::Perf),
            "test" => Ok(CommitType::Test),
            _ => Err(anyhow!("Invalid commit type {s}")),
        }
    }
}

#[derive(Debug, Clone, PartialEq, Default)]
pub struct Breaking(pub Vec<String>);

impl Display for Breaking {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        for b in &self.0 {
            writeln!(f, "BREAKING CHANGE: {b}")?
        }
        Ok(())
    }
}

impl FromStr for Breaking {
    type Err = Error;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let mut res = Vec::new();
        for line in s.lines() {
            let parts: Vec<_> = line.split(":").collect();
            if parts.len() != 2 {
                bail!("Invalid syntax for footer line");
            }
            if parts[0] != "BREAKING CHANGE" {
                bail!("Line has to start with \"BREAKING CHANGE\"");
            }
            res.push(parts[1].trim().to_string());
        }
        Ok(Self(res))
    }
}

#[derive(Debug, Clone, PartialEq, Default)]
pub struct Footer(pub Vec<(String, String)>);

impl Display for Footer {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        for (k, v) in &self.0 {
            writeln!(f, "{k}: {v}")?
        }
        Ok(())
    }
}

impl FromStr for Footer {
    type Err = Error;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let mut res = Vec::new();
        for line in s.lines() {
            let parts: Vec<_> = line.split(":").collect();
            if parts.len() != 2 {
                bail!("Invalid syntax for footer line");
            }
            res.push((parts[0].to_string(), parts[1].trim().to_string()));
        }
        Ok(Self(res))
    }
}
