use color_eyre::Result;
use rand::distributions::Alphanumeric;
use rand::{Rng, thread_rng};

use crate::api::LoginCredentials;
use crate::db::{Movie, NewMovieAddition, User, connect};

use crate::notify::{
    NotifyEvent, NotifySender, find_new_main_languages, is_milestone, quality_score,
};
use crate::settings::Settings;

pub async fn movies(settings: &Settings, _api_key: String) -> Vec<mendia_api::Movie> {
    if let Ok(mut connection) = connect(&settings.database).await
        && let Ok(result) = connection.load_movies()
    {
        return result
            .into_iter()
            .map(|m| mendia_api::Movie {
                title: m.title,
                year: m.year,
                size: m.size,
                hash: m.hash,
                tmdb_id: m.tmdb_id.unwrap_or(0) as i32,
                audio_languages: m.audio_languages.unwrap_or_default(),
                subtitle_languages: m.subtitle_languages.unwrap_or_default(),
                resolution: m.resolution.unwrap_or_default(),
                dynamic_range: m.dynamic_range.unwrap_or_default(),
                bitrate: m.bitrate.unwrap_or(0) as i32,
                user: m.user,
            })
            .collect();
    }
    Vec::new()
}

pub async fn add_movies(
    settings: &Settings,
    username: String,
    movie_list: &[mendia_api::Movie],
    silent: bool,
    telegram_tx: Option<NotifySender>,
    whatsapp_tx: Option<NotifySender>,
) {
    let db_movies: Vec<Movie> = movie_list
        .iter()
        .map(|m| Movie {
            title: m.title.clone(),
            year: m.year,
            size: m.size,
            hash: m.hash.clone(),
            tmdb_id: Some(m.tmdb_id as i64),
            audio_languages: Some(m.audio_languages.clone()),
            subtitle_languages: Some(m.subtitle_languages.clone()),
            resolution: Some(m.resolution.clone()),
            dynamic_range: Some(m.dynamic_range.clone()),
            bitrate: Some(m.bitrate as i64),
            user: username.clone(),
        })
        .collect();

    let Ok(mut connection) = connect(&settings.database).await else {
        log::error!("Failed to connect to the database; skipping add_movies.");
        return;
    };

    // Classify incoming movies as new, quality upgrades, or language upgrades.
    // Must happen before the sync so we can compare against the current DB state.
    let mut new_movies: Vec<&Movie> = Vec::new();
    let mut upgrade_movies: Vec<(&Movie, String)> = Vec::new(); // (movie, previous_resolution)
    let mut language_upgrade_movies: Vec<(Movie, Vec<String>)> = Vec::new();

    for movie in &db_movies {
        let tmdb_id = movie.tmdb_id;
        if tmdb_id == Some(0) || tmdb_id.is_none() {
            continue;
        }
        let Ok(count) = connection.tmdb_id_count(tmdb_id) else {
            log::warn!(
                "DB query failed for '{}'; skipping notification check.",
                movie.title
            );
            continue;
        };
        if count == 0 {
            new_movies.push(movie);
        } else {
            // Check whether this incoming version beats the best existing quality.
            let Ok(existing) = connection.load_movies_by_tmdb_id(tmdb_id) else {
                continue;
            };
            let new_score = quality_score(movie);
            if let Some(best_existing) = existing.iter().max_by_key(|m| quality_score(m))
                && new_score > quality_score(best_existing)
            {
                let prev_res = best_existing.resolution.clone().unwrap_or_default();
                upgrade_movies.push((movie, prev_res));
            }
            // Check for newly added main-language audio tracks.
            if !settings.notifications.main_languages.is_empty() {
                let new_langs = find_new_main_languages(
                    &movie.audio_languages,
                    &existing,
                    &settings.notifications.main_languages,
                );
                if !new_langs.is_empty() {
                    language_upgrade_movies.push((movie.clone(), new_langs));
                }
            }
        }
    }

    // Detect movies that will disappear from the library entirely after this sync.
    let incoming_hashes: Vec<String> = db_movies.iter().map(|m| m.hash.clone()).collect();
    let mut removed_movies: Vec<Movie> = Vec::new();
    match connection.load_movies_for_user_not_in(&username, &incoming_hashes) {
        Ok(about_to_delete) => {
            for movie in about_to_delete {
                if movie.tmdb_id == Some(0) || movie.tmdb_id.is_none() {
                    continue;
                }
                // Skip if an incoming movie already covers this tmdb_id (it's a replacement, not a removal).
                if db_movies.iter().any(|m| m.tmdb_id == movie.tmdb_id) {
                    continue;
                }
                match connection.tmdb_id_count(movie.tmdb_id) {
                    Ok(1) => removed_movies.push(movie), // last copy globally
                    Ok(_) => {}
                    Err(e) => log::warn!(
                        "DB query failed checking removal for '{}': {e}",
                        movie.title
                    ),
                }
            }
        }
        Err(e) => log::error!("Failed to load stale movies for {username}: {e}"),
    }

    // Full sync: remove stale entries for this user, then insert the current list.
    if let Err(e) = connection.delete_movies_not_in(&username, &incoming_hashes) {
        log::error!("Failed to remove stale movies for {username}: {e}");
    }

    connection
        .insert_or_ignore_movies(&db_movies)
        .expect("Failed to insert movies");

    // Log new movies for monthly stats (regardless of silent flag).
    if !new_movies.is_empty() {
        let additions: Vec<NewMovieAddition> = new_movies
            .iter()
            .map(|m| NewMovieAddition {
                tmdb_id: m.tmdb_id.unwrap_or(0),
                title: m.title.clone(),
                year: m.year,
                user: m.user.clone(),
                size: m.size,
            })
            .collect();
        if let Err(e) = connection.insert_movie_additions(&additions) {
            log::warn!("Failed to log movie additions for stats: {e}");
        }
    }

    let new_count = new_movies.len();
    let upgrade_count = upgrade_movies.len();
    let removed_count = removed_movies.len();
    let lang_upgrade_count = language_upgrade_movies.len();

    let mut events: Vec<NotifyEvent> = new_movies
        .into_iter()
        .cloned()
        .map(NotifyEvent::New)
        .collect();
    for (movie, prev_res) in upgrade_movies {
        events.push(NotifyEvent::Upgrade {
            movie: movie.clone(),
            previous_resolution: prev_res,
        });
    }
    for movie in removed_movies {
        events.push(NotifyEvent::Removed(movie));
    }
    for (movie, new_langs) in language_upgrade_movies {
        events.push(NotifyEvent::LanguageUpgrade {
            movie,
            new_languages: new_langs,
        });
    }

    // Check for library milestones.
    if let Ok(total) = connection.count_unique_movies()
        && is_milestone(total)
    {
        match connection.milestone_announced(total) {
            Ok(false) => {
                if let Err(e) = connection.announce_milestone(total) {
                    log::warn!("Failed to record milestone {total}: {e}");
                } else {
                    events.push(NotifyEvent::Milestone { count: total });
                }
            }
            Ok(true) => {}
            Err(e) => log::warn!("Milestone DB query failed: {e}"),
        }
    }

    if !silent && !events.is_empty() {
        if let Some(tx) = telegram_tx {
            let _ = tx.try_send(events.clone());
        }
        if let Some(tx) = whatsapp_tx {
            let _ = tx.try_send(events);
        }
    } else if silent
        && (new_count > 0 || upgrade_count > 0 || removed_count > 0 || lang_upgrade_count > 0)
    {
        log::info!(
            "Silent push: skipping notifications for {new_count} new, {upgrade_count} upgraded, {removed_count} removed, and {lang_upgrade_count} language-upgraded movie(s).",
        );
    }
}

pub async fn login(settings: &Settings, credentials: &LoginCredentials) -> Result<String> {
    use argon2::Config;

    let Ok(mut connection) = connect(&settings.database).await else {
        return Err(color_eyre::eyre::eyre!(
            "Failed to connect to the database via {:?}",
            settings.database,
        ));
    };

    let results: Vec<User> = connection.load_user(&credentials.username)?;

    if results.is_empty() {
        return Err(color_eyre::eyre::eyre!(
            "Username not {:?} found",
            credentials.username
        ));
    }

    let user = &results[0];
    let hash_value = argon2::hash_encoded(
        credentials.password.as_bytes(),
        user.salt.as_bytes(),
        &Config::original(),
    )?;

    let valid_hash = argon2::verify_encoded(&hash_value, credentials.password.as_bytes())?;

    if !valid_hash || hash_value != user.hash {
        return Err(color_eyre::eyre::eyre!("Invalid password"));
    }

    // https://rust-lang-nursery.github.io/rust-cookbook/algorithms/randomness.html#create-random-passwords-from-a-set-of-alphanumeric-characters
    let random_api_key: String = thread_rng()
        .sample_iter(&Alphanumeric)
        .take(30)
        .map(char::from)
        .collect();
    Ok(random_api_key)
}
