use std::ops::RangeInclusive;
use anyhow::Result;

#[derive(Debug, PartialEq)]
struct PasswordPolicy {
    char: u8,
    range: RangeInclusive<usize>,
}

impl PasswordPolicy {
    fn check_password(&self, password: &str) -> bool {
        self.range.contains(
            &password
                .as_bytes()
                .iter().
                filter(|&&c| c == self.char)
                .count()
        )
    }
    
    fn check_password_newpolicy(&self, password: &str) -> bool {
        (password.chars().nth(self.range.start()-1).unwrap() as u8 == self.char) ^ (password.chars().nth(self.range.end()-1).unwrap() as u8 == self.char)
    }
}

#[derive(Debug, thiserror::Error)]
enum ParseError {
    #[error("Expected {0}")]
    Expected(&'static str)
}

fn parse_line(line: &str) -> Result<(PasswordPolicy,String)> {
    let (policy_s, password) = {
        let mut tokens = line.split(": ");
        (
            tokens.next().ok_or(ParseError::Expected("Coucou"))?,
            tokens.next().ok_or(ParseError::Expected("Coincoin"))?,
        )
    };
    let policy = {
        let (range_s, char_s) = {
            let mut tokens = policy_s.split(' ');
            (
                tokens.next().ok_or(ParseError::Expected("Range"))?,
                tokens.next().ok_or(ParseError::Expected("Char"))?,
            )
        };
        let (min_s, max_s) = {
            let mut tokens = range_s.split('-');
            (
                tokens.next().ok_or(ParseError::Expected("Min"))?,
                tokens.next().ok_or(ParseError::Expected("Max"))?,
            )
        };
        PasswordPolicy {
            char: if char_s.len() == 1 {
                char_s.as_bytes()[0]
            } else {
                // return Err(ParseError::Expected("policy byte to be exactly one byte"));
                return Err(ParseError::Expected("password policy byte to be exactly 1 byte").into());
            },
            range: (min_s.parse()?)..=(max_s.parse()?),
        }
    };
    Ok((policy, password.to_string()))
}

fn main() -> Result<()> {
    let count = include_str!("../data/input.txt")
        .lines()
        .map(parse_line)
        .map(Result::unwrap)
        .filter(|(policy,password)| policy.check_password(password))
        .count();
    println!("{} passes are valid", count);
    let count_new_policy = include_str!("../data/input.txt")
        .lines()
        .map(parse_line)
        .map(Result::unwrap)
        .filter(|(policy,password)| policy.check_password_newpolicy(password))
        .count();
    println!("{} passes are valid with new policy", count_new_policy);
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_is_valid() {
        let policy = PasswordPolicy{ char: b'b', range: 1..=4 };
        assert_eq!(policy.check_password("boumbaby"), true);
        assert_eq!(policy.check_password("b-b-b-boumbaby"), false);
        assert_eq!(policy.check_password("oumay"), false);
    }

    #[test]
    fn test_parse_line() {
        assert_eq!(
            parse_line("1-5 b: boulbi").unwrap(),
            (
                PasswordPolicy {
                    char: b'b',
                    range: 1..=5,
                },
                "boulbi".to_owned(),
            )
        )
    }
}
