use crate::db::Movie;

/// A notification event sent to bot backends.
#[derive(Clone)]
pub enum NotifyEvent {
    /// A movie appeared in the system for the first time.
    New(Movie),
    /// An existing movie is now available in better quality.
    Upgrade {
        movie: Movie,
        /// The best previously-known resolution string (e.g. "1920x1080").
        previous_resolution: String,
    },
    /// The last copy of a movie was removed from the library.
    Removed(Movie),
    /// An existing movie now has a new main-language audio track.
    LanguageUpgrade {
        movie: Movie,
        /// ISO 639 codes that are newly present (subset of `main_languages`).
        new_languages: Vec<String>,
    },
    /// The library has hit a round-number milestone.
    Milestone { count: i64 },
    /// Monthly summary of library activity.
    MonthlyStats {
        month_name: String,
        added_count: usize,
        /// (username, count), sorted descending by count.
        per_user: Vec<(String, usize)>,
        total_count: i64,
        total_size_bytes: i64,
    },
}

/// Channel sender used to deliver notifications to a backend (Telegram, `WhatsApp`, …).
pub type NotifySender = tokio::sync::mpsc::Sender<Vec<NotifyEvent>>;

/// Parse the height component of a `"WIDTHxHEIGHT"` resolution string.
pub(crate) fn parse_resolution_height(resolution: &str) -> Option<u32> {
    resolution.split('x').nth(1)?.parse().ok()
}

/// Returns a comparable quality score `(resolution_height, is_hdr)`.
/// Higher resolution wins; at equal resolution, HDR beats SDR.
pub(crate) fn quality_score(movie: &Movie) -> (u32, bool) {
    let height = movie
        .resolution
        .as_deref()
        .and_then(parse_resolution_height)
        .unwrap_or(0);
    let is_hdr = movie
        .dynamic_range
        .as_deref()
        .map(|dr| dr.to_ascii_uppercase().contains("HDR"))
        .unwrap_or(false);
    (height, is_hdr)
}

/// Resolves an ISO 639 language code to its German name where known,
/// falling back to the English name from the iso639 database.
fn resolve_language(code: &str) -> String {
    let lc = code.trim().to_lowercase();
    // Common language codes → German name
    let german = match lc.as_str() {
        "en" | "eng" => Some("Englisch"),
        "de" | "deu" | "ger" => Some("Deutsch"),
        "fr" | "fra" | "fre" => Some("Französisch"),
        "es" | "spa" => Some("Spanisch"),
        "it" | "ita" => Some("Italienisch"),
        "ja" | "jpn" => Some("Japanisch"),
        "ko" | "kor" => Some("Koreanisch"),
        "zh" | "chi" | "zho" => Some("Chinesisch"),
        "ar" | "ara" => Some("Arabisch"),
        "ru" | "rus" => Some("Russisch"),
        "pt" | "por" => Some("Portugiesisch"),
        "nl" | "nld" | "dut" => Some("Niederländisch"),
        "pl" | "pol" => Some("Polnisch"),
        "tr" | "tur" => Some("Türkisch"),
        "sv" | "swe" => Some("Schwedisch"),
        "no" | "nor" => Some("Norwegisch"),
        "da" | "dan" => Some("Dänisch"),
        "fi" | "fin" => Some("Finnisch"),
        "cs" | "ces" | "cze" => Some("Tschechisch"),
        "hu" | "hun" => Some("Ungarisch"),
        "ro" | "ron" | "rum" => Some("Rumänisch"),
        "uk" | "ukr" => Some("Ukrainisch"),
        "he" | "heb" => Some("Hebräisch"),
        "th" | "tha" => Some("Thailändisch"),
        "vi" | "vie" => Some("Vietnamesisch"),
        "id" | "ind" => Some("Indonesisch"),
        "hi" | "hin" => Some("Hindi"),
        _ => None,
    };
    if let Some(name) = german {
        return name.to_owned();
    }
    // Fall back to English name from iso639
    if let Some(l) = rust_iso639::from_code_1(&lc) {
        return l.name.to_owned();
    }
    if let Some(l) = rust_iso639::from_code_2t(&lc) {
        return l.name.to_owned();
    }
    if let Some(l) = rust_iso639::from_code_2b(&lc) {
        return l.name.to_owned();
    }
    lc
}

/// Joins a slice of `&str` with a separator word using German list style.
/// `["A"]` → `"A"`, `["A", "B"]` → `"A und B"`, `["A", "B", "C"]` → `"A, B und C"`.
fn join_with(items: &[&str], sep: &str) -> String {
    match items {
        [] => String::new(),
        [a] => a.to_string(),
        [a, b] => format!("{a} {sep} {b}"),
        _ => {
            let (head, tail) = items.split_at(items.len() - 1);
            format!("{} {sep} {}", head.join(", "), tail[0])
        }
    }
}

/// Formats audio or subtitle language information into a single descriptive German line.
///
/// Returns `None` when the movie has no tracks for that type.
///
/// Examples with `main_languages = ["en", "de"]`:
/// - tracks contain Englisch + Deutsch          → `"Tonspur: Englisch und Deutsch"`
/// - tracks contain Englisch + Französisch      → `"Tonspur: Englisch und weitere"`
/// - tracks contain only Französisch            → `"Tonspur: kein Englisch oder Deutsch"`
fn format_languages(
    codes: &Option<String>,
    main_languages: &[String],
    kind: &str,
) -> Option<String> {
    let codes = codes.as_ref()?;
    let track_names: Vec<String> = codes
        .split(',')
        .map(str::trim)
        .filter(|s| !s.is_empty())
        .map(resolve_language)
        .collect();

    if track_names.is_empty() {
        return None;
    }

    if main_languages.is_empty() {
        // No main languages configured: list all detected tracks.
        let refs: Vec<&str> = track_names.iter().map(String::as_str).collect();
        return Some(format!("{kind}: {}", join_with(&refs, "und")));
    }

    let main_names: Vec<String> = main_languages.iter().map(|c| resolve_language(c)).collect();

    // Which configured main languages are present in this movie?
    let matched: Vec<&str> = main_names
        .iter()
        .filter(|main| track_names.iter().any(|t| t.eq_ignore_ascii_case(main)))
        .map(String::as_str)
        .collect();

    // Are there any tracks that are NOT among the main languages?
    let has_additional = track_names
        .iter()
        .any(|t| !main_names.iter().any(|m| m.eq_ignore_ascii_case(t)));

    if matched.is_empty() {
        let main_refs: Vec<&str> = main_names.iter().map(String::as_str).collect();
        return Some(format!("{kind}: kein {}", join_with(&main_refs, "oder")));
    }

    if has_additional {
        let mut parts = matched.clone();
        parts.push("weitere");
        Some(format!("{kind}: {}", join_with(&parts, "und")))
    } else {
        Some(format!("{kind}: {}", join_with(&matched, "und")))
    }
}

/// Builds the language section appended to a notification message.
fn lang_section(movie: &Movie, main_languages: &[String]) -> String {
    let audio = format_languages(&movie.audio_languages, main_languages, "Tonspur");
    let subs = format_languages(&movie.subtitle_languages, main_languages, "Untertitel");
    match (audio, subs) {
        (None, None) => String::new(),
        (a, s) => {
            let mut section = String::from("\n\n");
            if let Some(line) = a {
                section.push_str(&line);
                section.push('\n');
            }
            if let Some(line) = s {
                section.push_str(&line);
                section.push('\n');
            }
            section
        }
    }
}

/// Formats a new-movie notification message.
pub async fn format_movie_text(movie: &Movie, main_languages: &[String]) -> String {
    let size = movie.size as f64 / 1e9;
    let bitrate = movie.bitrate.map_or(-1.0, |b| b as f64 / 1e6);
    let s = |o: &Option<String>| o.clone().unwrap_or_default();

    format!(
        "{}{} ({})\n\n{} {}\n{:.2} GB - {:.2} Mb/s{}",
        if movie.user.is_empty() {
            String::new()
        } else {
            format!("{} hat nun\n", movie.user)
        },
        movie.title,
        movie.year,
        s(&movie.resolution),
        s(&movie.dynamic_range),
        size,
        bitrate,
        lang_section(movie, main_languages),
    )
}

/// Formats a quality-upgrade notification message.
pub async fn format_upgrade_text(
    movie: &Movie,
    previous_resolution: &str,
    main_languages: &[String],
) -> String {
    let size = movie.size as f64 / 1e9;
    let bitrate = movie.bitrate.map_or(-1.0, |b| b as f64 / 1e6);
    let s = |o: &Option<String>| o.clone().unwrap_or_default();

    format!(
        "{}{} ({}) jetzt in besserer Qualität\n\n{} {} (vorher: {})\n{:.2} GB - {:.2} Mb/s{}",
        if movie.user.is_empty() {
            String::new()
        } else {
            format!("{} hat ", movie.user)
        },
        movie.title,
        movie.year,
        s(&movie.resolution),
        s(&movie.dynamic_range),
        previous_resolution,
        size,
        bitrate,
        lang_section(movie, main_languages),
    )
}

/// Formats a removal notification message.
pub fn format_removed_text(movie: &Movie) -> String {
    format!(
        "Film wurde aus der Bibliothek entfernt\n{} ({})\n",
        movie.title, movie.year
    )
}

/// Returns the subset of `main_languages` codes that appear in `incoming_codes`
/// but are absent from every existing movie's `audio_languages`.
pub fn find_new_main_languages(
    incoming_codes: &Option<String>,
    existing: &[Movie],
    main_languages: &[String],
) -> Vec<String> {
    let incoming_codes = match incoming_codes {
        Some(c) => c,
        None => return vec![],
    };
    let incoming: Vec<String> = incoming_codes
        .split(',')
        .map(|s| s.trim().to_lowercase())
        .filter(|s| !s.is_empty())
        .collect();

    let existing_codes: Vec<String> = existing
        .iter()
        .flat_map(|m| {
            m.audio_languages
                .as_deref()
                .unwrap_or("")
                .split(',')
                .map(|s| s.trim().to_lowercase())
                .filter(|s| !s.is_empty())
                .collect::<Vec<_>>()
        })
        .collect();

    main_languages
        .iter()
        .map(|c| c.trim().to_lowercase())
        .filter(|main| incoming.contains(main) && !existing_codes.contains(main))
        .collect()
}

/// Formats a language-upgrade notification message.
pub async fn format_language_upgrade_text(movie: &Movie, new_languages: &[String]) -> String {
    let size = movie.size as f64 / 1e9;
    let bitrate = movie.bitrate.map_or(-1.0, |b| b as f64 / 1e6);
    let s = |o: &Option<String>| o.clone().unwrap_or_default();

    let lang_names: Vec<String> = new_languages.iter().map(|c| resolve_language(c)).collect();
    let lang_refs: Vec<&str> = lang_names.iter().map(String::as_str).collect();
    let langs = join_with(&lang_refs, "und");

    format!(
        "{} ({}) jetzt mit {} als Tonspur verfügbar\n{} {}\n{:.2} GB - {:.2} Mb/s\n",
        movie.title,
        movie.year,
        langs,
        s(&movie.resolution),
        s(&movie.dynamic_range),
        size,
        bitrate,
    )
}

/// Formats an integer with dots as thousands separators (German style).
/// e.g. 1247 → "1.247"
pub fn fmt_number(n: i64) -> String {
    let s = n.to_string();
    let mut out = String::new();
    for (i, ch) in s.chars().rev().enumerate() {
        if i > 0 && i % 3 == 0 {
            out.push('.');
        }
        out.push(ch);
    }
    out.chars().rev().collect()
}

/// Returns true if `count` is a milestone threshold.
pub fn is_milestone(count: i64) -> bool {
    (count > 0 && count <= 1000 && count % 100 == 0) || (count > 1000 && count % 500 == 0)
}

/// Formats a milestone notification message.
pub fn format_milestone_text(count: i64) -> String {
    format!("Die Bibliothek hat jetzt {} Filme!\n", fmt_number(count))
}

/// Formats the total library size in a human-readable German string.
fn fmt_size(bytes: i64) -> String {
    let tb = bytes as f64 / 1e12;
    if tb >= 1.0 {
        format!("{:.1} TB", tb).replace('.', ",")
    } else {
        let gb = bytes as f64 / 1e9;
        format!("{:.1} GB", gb).replace('.', ",")
    }
}

/// Formats a monthly stats notification message.
pub fn format_monthly_stats_text(
    month_name: &str,
    added_count: usize,
    per_user: &[(String, usize)],
    total_count: i64,
    total_size_bytes: i64,
) -> String {
    let mut s = format!("Rückblick {month_name}\n\n");
    if added_count == 0 {
        s.push_str("Kein neuer Film im letzten Monat.\n");
    } else {
        s.push_str(&format!(
            "{} Filme neu hinzugekommen\n\n",
            fmt_number(added_count as i64)
        ));
        for (user, count) in per_user {
            s.push_str(&format!("{user}: {count} Filme\n"));
        }
    }
    s.push_str(&format!(
        "\nGesamt: {} Filme | {}\n",
        fmt_number(total_count),
        fmt_size(total_size_bytes)
    ));
    s
}
