use anyhow::{Context, Result};
use chrono::{Datelike, Duration, Utc};
use clap::Parser;
use imap::Session;
use native_tls::TlsStream;
use serde::Deserialize;
use std::collections::HashMap;
use std::fs;
use std::net::TcpStream;

#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Args {
    /// Path to the configuration file
    #[arg(default_value = "rules.toml")]
    config_path: String,

    /// Dry run mode - only log what actions would be performed
    #[arg(short, long)]
    dry_run: bool,

    /// Verbose output for dry run (Shows message details instead of only counts)
    #[arg(short, long)]
    verbose: bool,

    /// List all folders on the server and their special use flags
    #[arg(long)]
    describe: bool,

    /// Only run the rule with this name
    #[arg(short, long)]
    rule_name: Option<String>,
}

#[derive(Debug, Deserialize)]
struct Config {
    server: ServerConfig,
    rules: HashMap<String, Rule>,
}

#[derive(Debug, Deserialize)]
struct ServerConfig {
    host: String,
    port: u16,
    username: String,
    password: String,
    trash_name: String,
}

#[derive(Debug, Deserialize)]
struct Rule {
    min_age: String,
    path: String,
    #[serde(default)]
    include_unread: bool,
    #[serde(default)]
    include_flagged: bool,
    action: Action,
    move_target: Option<String>,
    enabled: bool,
}

#[derive(Debug, Deserialize, PartialEq)]
#[serde(rename_all = "lowercase")]
enum Action {
    Move,
    Trash,
    Purge,
}

fn main() -> Result<()> {
    let args = Args::parse();

    let config_str = fs::read_to_string(&args.config_path)
        .with_context(|| format!("Failed to read config file: {}", args.config_path))?;
    let config: Config =
        toml::from_str(&config_str).with_context(|| "Failed to parse config file")?;

    if args.dry_run {
        println!("*** DRY RUN MODE ENABLED - No changes will be made to the server ***");
    }

    println!("Connecting to {}...", config.server.host);
    let tls = native_tls::TlsConnector::builder().build()?;
    let client = imap::connect(
        (config.server.host.as_str(), config.server.port),
        config.server.host.as_str(),
        &tls,
    )
    .with_context(|| "Failed to connect to IMAP server")?;

    let mut session = client
        .login(config.server.username, config.server.password)
        .map_err(|(e, _)| e)
        .with_context(|| "Failed to login to IMAP server")?;

    if args.describe {
        list_folders(&mut session)?;
        session.logout()?;
        return Ok(());
    }

    if let Some(target_rule_name) = &args.rule_name {
        if let Some(rule) = config.rules.get(target_rule_name) {
            if !rule.enabled {
                println!("Rule '{}' is disabled, skipping.", target_rule_name);
                return Ok(());
            }
            println!("Processing rule: {}", target_rule_name);
            process_rule(
                &mut session,
                rule,
                &config.server.trash_name,
                args.dry_run,
                args.verbose,
            )?;
        } else {
            anyhow::bail!("Rule '{}' not found in config file", target_rule_name);
        }
    } else {
        for (name, rule) in &config.rules {
            if !rule.enabled {
                println!("Skipping disabled rule: {}", name);
                continue;
            }
            println!("Processing rule: {}", name);
            process_rule(
                &mut session,
                rule,
                &config.server.trash_name,
                args.dry_run,
                args.verbose,
            )?;
        }
    }

    session.logout()?;
    Ok(())
}

fn process_rule(
    session: &mut Session<TlsStream<TcpStream>>,
    rule: &Rule,
    trash_name: &str,
    dry_run: bool,
    verbose: bool,
) -> Result<()> {
    session
        .select(&rule.path)
        .with_context(|| format!("Failed to select folder: {}", rule.path))?;

    let min_age_days: i64 = if rule.min_age.ends_with('d') {
        rule.min_age[..rule.min_age.len() - 1]
            .parse()
            .with_context(|| format!("Invalid min_age: {}", rule.min_age))?
    } else {
        anyhow::bail!(
            "Unsupported min_age format: {}. Only 'd' (days) is supported.",
            rule.min_age
        );
    };

    let before_date = Utc::now() - Duration::days(min_age_days);
    let date_str = format_imap_date(before_date);

    let mut search_query = format!("BEFORE {}", date_str);
    if !rule.include_unread {
        search_query.push_str(" SEEN");
    }
    if !rule.include_flagged {
        search_query.push_str(" UNFLAGGED");
    }

    println!("  Searching with query: {}", search_query);
    let found_messages = session
        .uid_search(search_query)
        .with_context(|| "Failed to search messages")?;

    if found_messages.is_empty() {
        println!("  No messages found.");
        return Ok(());
    }

    let mut message_ids: Vec<_> = found_messages.iter().collect();
    message_ids.sort();
    let seq_string = message_ids
        .iter()
        .map(|id| id.to_string())
        .collect::<Vec<String>>()
        .join(",");

    if dry_run {
        println!("  Found {} messages to be processed:", found_messages.len());
        if verbose {
            print_verbose(session, &seq_string)?;
        }
    } else {
        println!("  Found {} messages.", found_messages.len());
    }

    match rule.action {
        Action::Move => {
            let target = rule
                .move_target
                .as_ref()
                .with_context(|| "move_target is required for action 'move'")?;
            if dry_run {
                println!("  [DRY RUN] Would move messages to {}", target);
            } else {
                session
                    .uid_mv(&seq_string, target)
                    .with_context(|| format!("Failed to move messages to {}", target))?;
                println!("  Moved messages to {}", target);
            }
        }
        Action::Trash => {
            if dry_run {
                println!("  [DRY RUN] Would Trash messages (move to {})", trash_name);
            } else {
                session
                    .uid_mv(&seq_string, trash_name)
                    .with_context(|| format!("Failed to move messages to {}", trash_name))?;
                println!("  Trashed messages (moved to {})", trash_name);
            }
        }
        Action::Purge => {
            if dry_run {
                println!("  [DRY RUN] Would purge messages (UID EXPUNGE)");
            } else {
                session
                    .uid_store(&seq_string, "+FLAGS (\\Deleted)")
                    .with_context(|| "Failed to mark messages as deleted")?;
                session
                    .uid_expunge(&seq_string)
                    .with_context(|| "Failed to expunge deleted messages")?;
                println!("  Purged messages");
            }
        }
    }

    Ok(())
}

fn print_verbose(session: &mut Session<TlsStream<TcpStream>>, seq_string: &str) -> Result<()> {
    let fetches = session
        .uid_fetch(seq_string, "ENVELOPE")
        .with_context(|| "Failed to fetch message details for dry run")?;
    let mut sorted_fetches: Vec<_> = fetches.iter().collect();
    sorted_fetches.sort_by_key(|f| f.message);
    for fetch in sorted_fetches {
        let envelope = fetch
            .envelope()
            .context("Fetch response missing envelope")?;
        let subject = envelope
            .subject
            .map(|s| String::from_utf8_lossy(s))
            .unwrap_or_default();
        let from = envelope
            .from
            .as_ref()
            .and_then(|f| f.first())
            .map(|addr| {
                let name = addr
                    .name
                    .map(|n| String::from_utf8_lossy(n))
                    .unwrap_or_default();
                let mailbox = addr
                    .mailbox
                    .map(|m| String::from_utf8_lossy(m))
                    .unwrap_or_default();
                let host = addr
                    .host
                    .map(|h| String::from_utf8_lossy(h))
                    .unwrap_or_default();
                if name.is_empty() {
                    if mailbox.is_empty() && host.is_empty() {
                        "Unknown".to_string()
                    } else {
                        format!("{}@{}", mailbox, host)
                    }
                } else {
                    if mailbox.is_empty() && host.is_empty() {
                        name.to_string()
                    } else {
                        format!("{} <{}@{}>", name, mailbox, host)
                    }
                }
            })
            .unwrap_or_else(|| "Unknown".to_string());
        println!("    - From: {}, Subject: {}", from, subject);
    }
    Ok(())
}

fn list_folders(session: &mut Session<TlsStream<TcpStream>>) -> Result<()> {
    let folders = session
        .list(None, Some("*"))
        .with_context(|| "Failed to list folders")?;

    println!("Folders on server:");
    for folder in folders.iter() {
        let attrs: Vec<String> = folder
            .attributes()
            .iter()
            .map(|a| format!("{:?}", a))
            .collect();
        println!("  - {} (Flags: {})", folder.name(), attrs.join(", "));
    }
    Ok(())
}

fn format_imap_date(date: chrono::DateTime<Utc>) -> String {
    let months = [
        "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
    ];
    format!(
        "{}-{}-{}",
        date.day(),
        months[date.month() as usize - 1],
        date.year()
    )
}
