use std::{str::FromStr, sync::Arc};

use crate::{
    api::{Meal, post_colle, post_selected_meals, post_submittable_meals, post_upcoming_colles},
    configuration::{get_config, get_cron, list_classes, load_config},
    parser::{
        authenticate, fetch_class_colles, fetch_colle, fetch_upcoming_colles, get_available_meals,
        login, request_session,
    },
};
use chrono::{DateTime, Utc};
use cron::Schedule;
use redis::AsyncCommands;
use redis::aio::MultiplexedConnection;
use redis::{Client, Connection, RedisError, RedisResult, TypedCommands};
use serde::Serialize;
use serde_json::Value;
use tokio::sync::Mutex;
use tokio_cron_scheduler::{Job, JobScheduler};
mod api;
mod configuration;
mod parser;
use std::fmt;

#[derive(Clone)]
struct RedactedEvent {
    inner: serde_json::Value,
}

impl fmt::Debug for RedactedEvent {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let redacted = redact_sensitive_fields(&self.inner);
        write!(
            f,
            "{}",
            serde_json::to_string_pretty(&redacted).unwrap_or_else(|_| "Invalid JSON".to_string())
        )
    }
}

fn redact_sensitive_fields(value: &serde_json::Value) -> serde_json::Value {
    match value {
        serde_json::Value::Object(map) => {
            let mut new_map = serde_json::Map::new();
            for (key, val) in map {
                let key_lower = key.to_lowercase();
                if key_lower.contains("username") || key_lower.contains("password") {
                    new_map.insert(
                        key.clone(),
                        serde_json::Value::String("***REDACTED***".to_string()),
                    );
                } else {
                    new_map.insert(key.clone(), redact_sensitive_fields(val));
                }
            }
            serde_json::Value::Object(new_map)
        }
        serde_json::Value::Array(arr) => {
            serde_json::Value::Array(arr.iter().map(redact_sensitive_fields).collect())
        }
        _ => value.clone(),
    }
}

#[tokio::main]
async fn main() -> redis::RedisResult<()> {
    // Load configuration
    let config = load_config();
    if config.is_err() {
        eprintln!("Failed to load configuration: {}", config.unwrap_err());
        return Err(redis::RedisError::from((
            redis::ErrorKind::InvalidClientConfig,
            "Configuration loading failed",
        )));
    }
    let config = config.unwrap();
    println!("Configuration loaded successfully");

    // Connect to Redis
    let redis_url = get_config(&config, "redis");
    let client = Client::open(redis_url)?;

    // Create separate connections for pubsub and job processing
    let mut pubsub_con = client.get_connection()?;
    let mut job_con = client.get_connection()?;
    let scheduler_con = client.get_multiplexed_async_connection().await.unwrap();
    let mut pubsub = pubsub_con.as_pubsub();
    pubsub.subscribe("jobs_queue")?;
    println!("Connected to Redis pubsub channel 'jobs_queue'");

    // Schedule cron jobs
    schedule_cron_jobs(&config, scheduler_con).await;
    println!("Cron jobs scheduled successfully");

    loop {
        let msg = pubsub.get_message()?;
        let payload: String = msg.get_payload()?;
        if let Ok(event) = serde_json::from_str::<Value>(&payload) {
            match process_job(&event, &mut job_con, &config).await {
                Ok(_) => {
                    let redacted_event = RedactedEvent { inner: event };
                    println!("Job processed successfully: {:?}", redacted_event);
                }
                Err(e) => {
                    let redacted_event = RedactedEvent { inner: event };
                    eprintln!("Error processing job: {:?}, Error: {}", redacted_event, e);
                }
            }
        }
    }
}

#[derive(Serialize)]
struct JobMessage {
    r#type: u8,
    class_name: String,
}

async fn schedule_cron_jobs(config: &configuration::Settings, con: MultiplexedConnection) {
    let scheduler = JobScheduler::new().await.unwrap();
    let classes: Vec<String> = list_classes(config);
    let class_colles_crons = get_cron(config, "class_colles");
    let upcoming_colles_crons = get_cron(config, "upcoming_colles");

    let con = Arc::new(Mutex::new(con));

    for class_name in classes {
        // Start job for upcoming colles
        let message = JobMessage {
            r#type: 2,
            class_name: class_name.clone(),
        };
        for cron in &upcoming_colles_crons {
            let job = start_job(&message, &cron, Arc::clone(&con)).await;
            scheduler.add(job).await.unwrap();
        }
        println!("Scheduled job for upcoming colles: {}", class_name);
        // Start job for class colles
        let message = JobMessage {
            r#type: 1,
            class_name: class_name.clone(),
        };
        for cron in &class_colles_crons {
            let job = start_job(&message, &cron, Arc::clone(&con)).await;
            scheduler.add(job).await.unwrap();
        }
        println!("Scheduled job for class colles: {}", class_name);
    }

    scheduler.start().await.unwrap();
}

async fn start_job(
    message: &JobMessage,
    cron: &str,
    con: Arc<Mutex<MultiplexedConnection>>,
) -> Job {
    let con_clone = Arc::clone(&con);
    let payload = serde_json::to_string(message).unwrap();

    // Register cron job
    Job::new_async(&cron, move |_uuid, _l| {
        let payload = payload.clone();
        let con_clone = Arc::clone(&con_clone);
        Box::pin(async move {
            let mut conn = con_clone.lock().await;
            let res: Result<(), RedisError> = conn.publish("jobs_queue", payload).await;
            if res.is_err() {
                eprintln!("Failed to publish job: {}", res.unwrap_err());
            }
        })
    })
    .unwrap()
}

async fn process_job(
    job: &Value,
    con: &mut Connection,
    config: &configuration::Settings,
) -> RedisResult<()> {
    // Retrieve the class name from the job
    let redacted_job = RedactedEvent { inner: job.clone() };
    println!("Processing job: {:?}", redacted_job);
    let class_name = job["class_name"].as_str();
    if class_name.is_none() {
        eprintln!("Job does not contain a class name.");
        return Err(redis::RedisError::from((
            redis::ErrorKind::InvalidClientConfig,
            "Job missing class name",
        )));
    }
    let class_name = class_name.unwrap();

    /*
     * Fetch ONE colle (id and secret)
     */
    if job["type"] == 0 {
        // Simulate fetching colles
        let colle_id = job["colle_id"].as_str().unwrap_or("unknown");
        let colle_secret = job["colle_secret"].as_str().unwrap_or("unknown");
        println!("Fetched colle: ID={}, Secret={}", colle_id, colle_secret);

        match fetch_colle(colle_id, colle_secret).await {
            Ok(colle) => {
                if colle.is_none() {
                    eprintln!("No colle found for ID: {}", colle_id);
                    return Ok(());
                }
                let mut colle = colle.unwrap();
                if colle.examiner.is_empty() {
                    colle.examiner = job["examiner_name"]
                        .as_str()
                        .unwrap_or("Unknown")
                        .to_string();
                }
                let res = post_colle(&colle, class_name, config).await;
                if res.is_err() {
                    eprintln!("Failed to post colle: {}", res.unwrap_err());
                    return Err(redis::RedisError::from((
                        redis::ErrorKind::ResponseError,
                        "Failed to post colle",
                    )));
                }
            }
            Err(e) => {
                eprintln!("Failed to fetch colle {}: {}", colle_id, e);
            }
        }
    } else if job["type"] == 4 || job["type"] == 5 {
        let user_id = job["user_id"].as_number().unwrap().as_i64().unwrap_or(-1);
        if user_id == -1 {
            eprintln!("Job does not contain a valid user ID.");
            return Err(redis::RedisError::from((
                redis::ErrorKind::InvalidClientConfig,
                "Job missing valid user ID",
            )));
        }

        // Authenticate (job data contains password and username)
        let username = job["bj_username"].as_str().unwrap_or_default();
        let password = job["bj_password"].as_str().unwrap_or_default();
        if username.is_empty() || password.is_empty() {
            eprintln!("Job does not contain username or password.");
            return Err(redis::RedisError::from((
                redis::ErrorKind::InvalidClientConfig,
                "Job missing username or password",
            )));
        }

        let session = request_session().await;
        if let Err(err) = session {
            eprintln!("Failed to request session: {}", err);
            return Err(redis::RedisError::from((
                redis::ErrorKind::AuthenticationFailed,
                "Session request failed",
            )));
        }
        let session_id = session.unwrap();

        let login_result = login(&username, &password, &session_id).await;
        if let Err(err) = login_result {
            if job["type"] == 5 {
                // Set auth failed in Redis
                let auth_key = format!("auth_success_{}", user_id);
                con.set_ex(&auth_key, "ERROR", 300)?;
            }
            eprintln!("Failed to login: {}", err);
            return Err(redis::RedisError::from((
                redis::ErrorKind::AuthenticationFailed,
                "Login failed",
            )));
        }

        if job["type"] == 4 {
            // Fetch already selected meals
            let selected_meals = get_available_meals(&session_id, true, false).await;
            if let Err(err) = selected_meals {
                eprintln!("Failed to fetch available meals: {}", err);
                return Err(redis::RedisError::from((
                    redis::ErrorKind::ResponseError,
                    "Failed to fetch available meals",
                )));
            }
            let selected_meals = selected_meals.unwrap();

            // Add job["meal"] (ONLY ONE MEAL) to selected meals
            let mut meals_parsed = selected_meals;
            let meal_to_add = Meal {
                date: job["meal"]["date"].as_str().unwrap_or_default().to_string(),
                meal_type: job["meal"]["meal_type"]
                    .as_str()
                    .unwrap_or("dinner")
                    .to_string(),
            };
            if !meals_parsed
                .iter()
                .any(|m| m.date == meal_to_add.date && m.meal_type == meal_to_add.meal_type)
            {
                meals_parsed.push(meal_to_add);

                // Submit meals
                let res = parser::submit_meals(&meals_parsed, &session_id).await;
                if res.is_err() {
                    eprintln!("Failed to submit meals: {}", res.unwrap_err());
                    return Err(redis::RedisError::from((
                        redis::ErrorKind::ResponseError,
                        "Failed to submit meals",
                    )));
                }
                println!("Meals submitted successfully.");
            } else {
                println!("Meal already selected, skipping addition.");
            }

            let meals = get_available_meals(&session_id, true, true).await;
            if meals.is_err() {
                eprintln!("Failed to fetch available meals: {}", meals.unwrap_err());
                return Err(redis::RedisError::from((
                    redis::ErrorKind::ResponseError,
                    "Failed to fetch available meals",
                )));
            }
            let meals = meals.unwrap();

            // Post selected meals to API
            let res = post_selected_meals(&meals, &user_id, config).await;
            if res.is_err() {
                eprintln!("Failed to post selected meals: {}", res.unwrap_err());
                return Err(redis::RedisError::from((
                    redis::ErrorKind::ResponseError,
                    "Failed to post selected meals",
                )));
            }
            println!("Posted {} selected meals successfully.", meals.len());
        } else if job["type"] == 5 {
            // Set auth successful in Redis
            let auth_key = format!("auth_success_{}", user_id);
            con.set_ex(&auth_key, "SUCCESS", 300)?;
        }
    } else {
        // Authenticate
        let session = authenticate(class_name, &mut *con, config).await;
        if session.is_err() {
            eprintln!("Failed to authenticate: {}", session.unwrap_err());
            return Err(redis::RedisError::from((
                redis::ErrorKind::AuthenticationFailed,
                "Authentication failed",
            )));
        }
        let session = session.unwrap();
        println!("Authenticated successfully: {}", session);

        /*
         * Fetch class colles (class name and optionally date)
         */
        if job["type"] == 1 {
            let last_hash_key = format!("class_colles_hash_{}", class_name);
            let last_hash = get_last_hash(con, &last_hash_key);

            // Fetch class colles
            match fetch_class_colles(&session, job["date"].as_str().or(None), &last_hash).await {
                Ok((colles, hash)) => {
                    set_last_hash(con, &last_hash_key, &hash)?;
                    // Add each colle to Redis queue pubsub
                    for (colle_id, colle_secret, examiner_name) in colles {
                        let colle_job = serde_json::json!({
                            "type": 0,
                            "colle_id": colle_id,
                            "colle_secret": colle_secret,
                            "class_name": class_name,
                            "examiner_name": examiner_name,
                        });
                        let colle_job_str =
                            serde_json::to_string(&colle_job).unwrap_or_else(|_| "{}".to_string());

                        // Publish the colle job to the Redis channel
                        con.publish("jobs_queue", &colle_job_str)?;
                        println!("Published colle job: {}", &colle_job_str);
                    }

                    // Set the last sync date in Redis
                    let last_sync_key = format!("last_sync_{}", class_name);
                    con.set(&last_sync_key, Utc::now().to_rfc3339().as_str())?;

                    let class_colles_crons = get_cron(config, "class_colles");
                    let healthy_until = next_run_any(&class_colles_crons).unwrap_or_default();
                    let healhy_until_key = format!("healthy_until_{}", class_name);
                    con.set(&healhy_until_key, healthy_until.to_rfc3339().as_str())?;
                }
                Err(e) => {
                    eprintln!("Failed to fetch class colles: {}", e);
                }
            }
        /*
         * Fetch upcoming colles (class name)
         */
        } else if job["type"] == 2 {
            // Retrieve the last hash from Redis and fetch upcoming colles
            let last_hash_key = format!("upcoming_colles_hash_{}", class_name);
            let last_hash = get_last_hash(con, &last_hash_key);

            let (colles, hash) = fetch_upcoming_colles(&session, &last_hash).await;
            set_last_hash(con, &last_hash_key, &hash)?;

            if colles.is_empty() {
                return Ok(());
            }
            let res = post_upcoming_colles(&colles, class_name, config).await;
            if res.is_err() {
                eprintln!("Failed to post upcoming colles: {}", res.unwrap_err());
                return Err(redis::RedisError::from((
                    redis::ErrorKind::ResponseError,
                    "Failed to post upcoming colles",
                )));
            }
            println!("Posted upcoming colles successfully.");
        } else if job["type"] == 3 {
            println!("Fetching available meals...");

            /*
             * Fetch available meals
             */
            match get_available_meals(&session, false, false).await {
                Ok(meals) => {
                    if meals.is_empty() {
                        println!("No available meals found.");
                        return Ok(());
                    }
                    // Post available meals to API
                    let res = post_submittable_meals(&meals, config).await;
                    if res.is_err() {
                        eprintln!("Failed to post submittable meals: {}", res.unwrap_err());
                        return Err(redis::RedisError::from((
                            redis::ErrorKind::ResponseError,
                            "Failed to post submittable meals",
                        )));
                    }
                    println!("Posted {} submittable meals successfully.", meals.len());
                }
                Err(e) => {
                    eprintln!("Failed to fetch available meals: {}", e);
                }
            }
        /*
         * Handle unknown job types
         * This is a catch-all for any job types that are not recognized.
         */
        } else {
            eprintln!("Unknown job type: {}", job["type"]);
        }
    }

    Ok(())
}

fn get_last_hash(con: &mut Connection, key: &str) -> String {
    let last_hash = con.get(key);
    // If the last hash is not found, use an empty string
    let last_hash = match last_hash {
        Ok(hash) => hash.unwrap_or_else(|| String::new()),
        Err(e) => {
            eprintln!("Failed to get last hash: {}", e);
            String::new()
        }
    };
    last_hash
}

fn set_last_hash(con: &mut Connection, key: &str, hash: &str) -> RedisResult<()> {
    con.set(key, hash)?;
    Ok(())
}

fn next_run_any<I>(expressions: I) -> Option<DateTime<Utc>>
where
    I: IntoIterator,
    I::Item: AsRef<str>,
{
    let now = Utc::now();

    expressions
        .into_iter()
        .filter_map(|expr| {
            let schedule = Schedule::from_str(expr.as_ref()).ok()?;
            schedule.after(&now).next()
        })
        .min()
}
