use std::collections::HashMap;
use std::fs;
use std::ops::RangeInclusive;

use lazy_static::lazy_static;
use regex::Regex;

type Rules = HashMap<String, (RangeInclusive<u32>, RangeInclusive<u32>)>;
type Ticket = Vec<u32>;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let input = fs::read_to_string("input")?;
    let (rules, _, tickets) = parse_tickets(&input)?;

    // Part 1
    let invalid = get_invalid_values(&rules, &tickets);

    println!("{}", invalid.iter().sum::<u32>());

    Ok(())
}

fn parse_tickets(input: &str) -> Result<(Rules, Ticket, Vec<Ticket>), String> {
    let blocks: Vec<&str> = input.split("\n\n").collect();
    let parse_u32 = |val: &str| {
        val.parse::<u32>()
            .map_err(|e| format!("Invalid number: {}", e))
    };

    if blocks.len() < 3 {
        Err("Input incomplete")?;
    }

    let rules = blocks[0]
        .lines()
        .map(|line| {
            lazy_static! {
                static ref RULE_RE: Regex =
                    Regex::new(r"([[:alpha:]]+?): (\d+)-(\d+) or (\d+)-(\d+)")
                        .expect("Regex should compile");
            }

            let caps = RULE_RE
                .captures(line)
                .ok_or(format!("Invalid rule line: {}", line))?;

            let name = caps[1].to_string();
            let range1 = parse_u32(&caps[2])?..=parse_u32(&caps[3])?;
            let range2 = parse_u32(&caps[4])?..=parse_u32(&caps[5])?;

            Ok((name, (range1, range2)))
        })
        .collect::<Result<Rules, String>>()?;

    let own_ticket = blocks[1]
        .lines()
        .skip(1)
        .next()
        .ok_or("Input incomplete")?
        .split(',')
        .map(|c| parse_u32(c))
        .collect::<Result<Ticket, String>>()?;

    let other_tickets = blocks[2]
        .lines()
        .skip(1)
        .map(|line| line.split(',').map(|c| parse_u32(c)).collect())
        .collect::<Result<Vec<Ticket>, String>>()?;

    Ok((rules, own_ticket, other_tickets))
}

fn get_invalid_values(rules: &Rules, tickets: &Vec<Ticket>) -> Vec<u32> {
    tickets
        .iter()
        .flat_map(|ticket| {
            ticket.iter().filter(|value| {
                !rules
                    .values()
                    .any(|(rule1, rule2)| rule1.contains(value) || rule2.contains(value))
            })
        })
        .copied()
        .collect()
}

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

    #[test]
    fn test_simple() -> Result<(), Box<dyn std::error::Error>> {
        let input = indoc!(
            "
             class: 1-3 or 5-7
             row: 6-11 or 33-44
             seat: 13-40 or 45-50

             your ticket:
             7,1,14

             nearby tickets:
             7,3,47
             40,4,50
             55,2,20
             38,6,12
            "
        );

        let (rules, _, tickets) = parse_tickets(input)?;
        let invalid = get_invalid_values(&rules, &tickets);

        assert_eq!(invalid.iter().sum::<u32>(), 71);

        Ok(())
    }
}