use std::{collections::HashMap, str::FromStr};
use anyhow::Result;
use regex::Regex;

#[derive(Debug,thiserror::Error)]
enum ParseError {
    #[error("Expected {0}")]
    Expected(String)
}

#[derive(Debug,PartialEq, Eq, Hash)]
enum EntryType {
    Byr,
    Iyr,
    Eyr,
    Hgt,
    Hcl,
    Ecl,
    Pid,
    Cid,
}

impl FromStr for EntryType {
    type Err = ParseError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s {
            "byr" => Ok(Self::Byr),
            "iyr" => Ok(Self::Iyr),
            "eyr" => Ok(Self::Eyr),
            "hgt" => Ok(Self::Hgt),
            "hcl" => Ok(Self::Hcl),
            "ecl" => Ok(Self::Ecl),
            "pid" => Ok(Self::Pid),
            "cid" => Ok(Self::Cid),
            _ => Err(ParseError::Expected(format!("byr, iyr, eyr, hgt, hcl, ecl, pid, cid, but got {}", s)))
        }
    }
}

impl EntryType {
    pub fn is_valid(&self, value: &Option<&str>) -> bool {
        match (*value, self) {
            (Some(value), EntryType::Byr) => {
                // four digits; at least 1920 and at most 2002.

                match str::parse::<i64>(value) {
                    Ok(number) => {
                        (1920..=2002).contains(&number)
                    }
                    Err(_) => { false }
                }
            }
            (Some(value), EntryType::Iyr) => {
                // four digits; at least 2010 and at most 2020.
                match str::parse::<i64>(value) {
                    Ok(number) => {
                        (2010..=2020).contains(&number)
                    }
                    Err(_) => { false }
                }
            }
            (Some(value), EntryType::Eyr) => {
                // four digits; at least 2020 and at most 2030.
                match str::parse::<i64>(value) {
                    Ok(number) if (2020..=2030)
                        .contains(&number) => true,
                    _ =>  false
                }
            }
            (Some(value), EntryType::Hgt) => {
                // a number followed by either cm or in:
                // if cm, the number must be at least 150 and at most 193.
                // if in, the number must be at least 59 and at most 76.
                let rex_cm = Regex::new(r"(\d*)cm").unwrap();
                let rex_in = Regex::new(r"(\d*)in").unwrap();
                if rex_cm.is_match(value) {
                    let m = rex_cm.captures(value);
                    // At least "None" fails the test
                    match str::parse::<i64>(m.unwrap().get(1).unwrap().as_str()) {
                        Ok(height) if (150..=193).contains(&height) => true,
                        _ => false 
                    }
                } else if rex_in.is_match(value) {
                    let m = rex_in.captures(value);
                    // At least "None" fails the test
                    match str::parse::<i64>(m.unwrap().get(1).unwrap().as_str()) {
                        Ok(height) if (59..=76).contains(&height) => true,
                        _ => false 
                    }
                } else {
                    false
                }
            }
            (Some(value), EntryType::Hcl) => {
                // a # followed by exactly six characters 0-9 or a-f.
                //probably ok as long as we deal with ascii right ?
                let rex = Regex::new(r"([a-z]|[0-9]){6}").unwrap();
                value.starts_with('#') && rex.is_match(&(value.chars().skip(1).collect::<String>()))
            }
            (Some(value), EntryType::Ecl) => {
                // exactly one of: amb blu brn gry grn hzl oth.
                let valid_values = vec!["amb", "blu", "brn", "gry", "grn", "hzl", "oth"];
                valid_values.contains(&value)
            }
            (Some(value), EntryType::Pid) => {
                // a nine-digit number, including leading zeroes.
                // probably ok if we deal only with ascii right ?
                value.as_bytes().len() == 9 && str::parse::<i64>(value).is_ok()
            }
            (_, EntryType::Cid) => {
                // ignored, missing or not
                true
            }
            _ => false
        }
    }
}

#[derive(Debug)]
struct Passport<'a> {
    fields: HashMap<EntryType, Option<&'a str>>
}

impl<'a> Passport<'a> {
    fn new() -> Self {
        let mut res = Self {
            fields: HashMap::new()
        };
        res.fields.insert(EntryType::Byr, None);
        res.fields.insert(EntryType::Iyr, None);
        res.fields.insert(EntryType::Eyr, None);
        res.fields.insert(EntryType::Hgt, None);
        res.fields.insert(EntryType::Hcl, None);
        res.fields.insert(EntryType::Ecl, None);
        res.fields.insert(EntryType::Pid, None);
        res.fields.insert(EntryType::Cid, None);
        res
    }
    fn parse(&mut self, s: &'static str) {
        for entry_s in s.split(&[' ', '\n'][..]) {
            let mut tokens = entry_s.split(':');
            if let Ok(etype) = EntryType::from_str(tokens.next().unwrap()) {
                self.fields.insert(etype, tokens.next());
            }
        }
    }
    
    fn is_valid_legacy(&self, mandatory_cid: bool) -> bool {
        self.fields.iter().all(|(key, value)| {
            (!mandatory_cid && *key == EntryType::Cid) ||
                *value != None
        })
    }

    fn is_valid(&self) -> bool {
        self.fields.iter().all(|(key, value)| {
            key.is_valid(value)
        })
    }
}

fn main() {
    let data = include_str!("../data/input.txt");
    // eprintln!("data = {}", data);
    //eprintln!("data.split() = {:?}", data.split("\n\n").collect::<Vec<&str>>());
    let passports: Vec<Passport> = data.split("\n\n").map(|pass_str| {
        let mut res = Passport::new();
        res.parse(pass_str);
        res
    }).collect();
    // eprintln!("passports = {:#?}", passports);
    // eprintln!("passports.size() = {:#?}", passports.len());
    let nb_valid = passports.iter().filter(|p| (*p).is_valid_legacy(false)).count();
    eprintln!("nb_valid = {:#?}", nb_valid);
    let nb_valid_secondpass = passports.iter().filter(|p| p.is_valid()).count();
    eprintln!("nb_valid_secondpass = {:#?}", nb_valid_secondpass);
}

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

    #[test]
    fn test_validation() {
        let mut pass = Passport::new();
        pass.parse("cid:271 pid:009457684 hcl:#80fab0\nbyr:1999 ecl:blu\niyr:2015\neyr:2029 hgt:159cm");
        eprintln!("pass = {:#?}", pass);
        assert_eq!(pass.is_valid(), true)
    }
}