use std::collections::HashMap;
use std::time::SystemTime;

/// User account record loaded from the primary database.
#[derive(Debug, Clone)]
struct User {
    id: u64,
    name: String,
    email: String,
    created_at: SystemTime,
}

/// Connection pool shared across worker tasks.
struct Pool {
    inner: deadpool_postgres::Pool,
}

impl Pool {
    async fn connect(url: &str) -> Result<Self, DbError> {
        let cfg = deadpool_postgres::Config::from_url(url)?;
        let inner = cfg.create_pool(None, tokio_postgres::NoTls)?;
        Ok(Self { inner })
    }

    async fn query(&self, sql: &str, params: &[&(dyn ToSql + Sync)]) -> Result<Vec<Row>, DbError> {
        let client = self.inner.get().await?;
        let rows = client.query(sql, params).await?;
        Ok(rows.into_iter().map(Row::from).collect())
    }
}

struct Row {
    values: Vec<Value>,
}

impl Row {
    fn get<T: FromValue>(&self, idx: usize) -> T {
        self.values
            .get(idx)
            .and_then(|v| T::from_value(v))
            .unwrap_or_default()
    }
}

async fn fetch_users(pool: &Pool) -> Result<Vec<User>, DbError> {
    let rows = pool
        .query("SELECT id, name, email, created_at FROM users", &[])
        .await?;
    let users = rows
        .into_iter()
        .map(|row| User {
            id: row.get(0),
            name: row.get(1),
            email: row.get(2),
            created_at: row.get(3),
        })
        .collect();
    Ok(users)
}

fn validate_email(email: &str) -> Result<(), EmailError> {
    let trimmed = email.trim();
    if trimmed.is_empty() {
        return Err(EmailError::Empty);
    }
    if trimmed.len() > 254 {
        return Err(EmailError::TooLong);
    }
    let parts: Vec<&str> = trimmed.split('@').collect();
    if parts.len() != 2 {
        return Err(EmailError::MalformedShape);
    }
    let (local, domain) = (parts[0], parts[1]);
    if local.is_empty() || local.len() > 64 {
        return Err(EmailError::InvalidLocalPart);
    }
    if domain.is_empty() || !domain.contains('.') {
        return Err(EmailError::InvalidDomain);
    }
    if domain.starts_with('.') || domain.ends_with('.') {
        return Err(EmailError::InvalidDomain);
    }
    for ch in trimmed.chars() {
        if ch.is_control() || ch.is_whitespace() {
            return Err(EmailError::InvalidCharacters);
        }
    }
    Ok(())
}

fn normalize_email(email: &str) -> String {
    let lower = email.trim().to_lowercase();
    if let Some((local, domain)) = lower.split_once('@') {
        let local_clean = local.split('+').next().unwrap_or(local);
        format!("{local_clean}@{domain}")
    } else {
        lower
    }
}

fn group_by_domain(users: &[User]) -> HashMap<String, usize> {
    let mut counts = HashMap::new();
    for u in users {
        let domain = u.email.split('@').nth(1).unwrap_or("unknown");
        *counts.entry(domain.to_string()).or_insert(0) += 1;
    }
    counts
}

fn print_report(users: &[User], stats: &HashMap<String, usize>) {
    let mut sorted: Vec<_> = stats.iter().collect();
    sorted.sort_by(|a, b| b.1.cmp(a.1).then_with(|| a.0.cmp(b.0)));

    println!("┌─────────────── user report ───────────────┐");
    println!("│ total active users : {:>20} │", users.len());
    println!("│ unique domains     : {:>20} │", stats.len());
    println!("├────────────────────────────────────────────┤");
    println!("│ top domains                                │");
    for (domain, n) in sorted.iter().take(10) {
        println!("│   {:<32} {:>6} │", domain, n);
    }
    println!("└────────────────────────────────────────────┘");
}

#[tokio::main]
async fn main() -> Result<(), DbError> {
    let pool = Pool::connect("postgres://localhost/app").await?;
    let users = fetch_users(&pool).await?;
    let valid: Vec<User> = users
        .into_iter()
        .filter(|u| validate_email(&u.email).is_ok())
        .map(|mut u| {
            u.email = normalize_email(&u.email);
            u
        })
        .collect();
    let stats = group_by_domain(&valid);
    print_report(&valid, &stats);
    Ok(())
}
