//! Blocking TMDB workflows (search, match-payload fetch, apply) shared by the
//! UI matcher. All network functions run blocking `ureq` I/O and must be
//! dispatched through [`run_blocking`] from async contexts. Image and season
//! downloads fan out over a small scoped-thread pool.

use crate::database::Database;
use crate::tmdb::{ImageConfiguration, TmdbClient, TvSeriesSeason};
use std::path::PathBuf;
use std::sync::atomic::{AtomicUsize, Ordering};

/// Upper bound for concurrent TMDB requests (stays well under rate limits).
const MAX_PARALLEL_REQUESTS: usize = 6;

/// Unified search result for movies AND shows. Show searches leave the
/// movie-only fields (`overview`, `vote_average`, `backdrop_path`) empty.
#[derive(Debug, Clone)]
pub(crate) struct TmdbSearchItem {
    pub(crate) tmdb_id: u32,
    pub(crate) title: String,
    pub(crate) original_title: String,
    /// release_date (movie) / first_air_date (show); empty if absent.
    pub(crate) date: String,
    pub(crate) overview: Option<String>,
    pub(crate) vote_average: Option<f64>,
    pub(crate) poster_path: Option<String>,
    pub(crate) backdrop_path: Option<String>,
    pub(crate) poster_image: Option<Vec<u8>>,
}

impl TmdbSearchItem {
    pub(crate) fn year_str(&self) -> &str {
        if self.date.len() >= 4 {
            &self.date[..4]
        } else {
            &self.date
        }
    }
}

#[derive(Debug, Clone)]
pub(crate) struct MovieMatchData {
    pub(crate) tmdb_id: u32,
    pub(crate) original_title: String,
    pub(crate) description: Option<String>,
    pub(crate) rating: Option<f64>,
    pub(crate) poster: Option<(String, Vec<u8>)>,
    pub(crate) backdrop: Option<(String, Vec<u8>)>,
}

#[derive(Debug, Clone)]
pub(crate) struct ShowMatchData {
    pub(crate) tmdb_id: u32,
    pub(crate) original_name: String,
    pub(crate) release_year: Option<i64>,
    pub(crate) description: Option<String>,
    pub(crate) rating: Option<f64>,
    pub(crate) poster: Option<(String, Vec<u8>)>,
    pub(crate) backdrop: Option<(String, Vec<u8>)>,
    pub(crate) seasons: Vec<SeasonMatchData>,
}

#[derive(Debug, Clone)]
pub(crate) struct SeasonMatchData {
    pub(crate) season_number: i64,
    pub(crate) tmdb_id: u32,
    pub(crate) name: String,
    pub(crate) release_year: Option<i64>,
    pub(crate) description: Option<String>,
    pub(crate) rating: Option<f64>,
    pub(crate) poster: Option<(String, Vec<u8>)>,
    pub(crate) episodes: Vec<EpisodeMatchData>,
}

#[derive(Debug, Clone)]
pub(crate) struct EpisodeMatchData {
    pub(crate) episode_number: i64,
    pub(crate) tmdb_id: u32,
    pub(crate) name: String,
}

/// Cache paths of the images an apply stored, for the UI to pick up.
#[derive(Debug, Default)]
pub(crate) struct AppliedImages {
    pub(crate) cover: Option<PathBuf>,
    pub(crate) backdrop: Option<PathBuf>,
}

/// Run blocking work on a dedicated OS thread; await the result without
/// occupying the (single-threaded) smol executor. A dropped sender (worker
/// panic) surfaces as an error instead of a hang.
pub(crate) fn run_blocking<T: Send + 'static>(
    f: impl FnOnce() -> Result<T, String> + Send + 'static,
) -> impl std::future::Future<Output = Result<T, String>> + Send {
    let (tx, rx) = async_channel::bounded(1);
    std::thread::spawn(move || {
        let _ = tx.send_blocking(f());
    });
    async move {
        rx.recv()
            .await
            .unwrap_or_else(|_| Err("TMDB task terminated unexpectedly".into()))
    }
}

/// Download every `Some` url concurrently (bounded pool), preserving order.
/// Failed or empty downloads yield `None`.
fn download_many(client: &TmdbClient, urls: &[Option<String>]) -> Vec<Option<Vec<u8>>> {
    let mut out: Vec<Option<Vec<u8>>> = Vec::new();
    out.resize_with(urls.len(), || None);

    let pending = urls.iter().filter(|u| u.is_some()).count();
    let workers = pending.min(MAX_PARALLEL_REQUESTS);
    if workers == 0 {
        return out;
    }

    let next = AtomicUsize::new(0);
    let downloaded: Vec<Vec<(usize, Vec<u8>)>> = std::thread::scope(|s| {
        let handles: Vec<_> = (0..workers)
            .map(|_| {
                s.spawn(|| {
                    let mut local = Vec::new();
                    loop {
                        let i = next.fetch_add(1, Ordering::Relaxed);
                        if i >= urls.len() {
                            break;
                        }
                        if let Some(url) = &urls[i]
                            && let Ok(bytes) = client.download_image(url)
                            && !bytes.is_empty()
                        {
                            local.push((i, bytes));
                        }
                    }
                    local
                })
            })
            .collect();
        handles
            .into_iter()
            .map(|h| h.join().unwrap_or_default())
            .collect()
    });

    for (i, bytes) in downloaded.into_iter().flatten() {
        out[i] = Some(bytes);
    }
    out
}

pub(crate) fn search_movies(token: String, query: String) -> Result<Vec<TmdbSearchItem>, String> {
    let mut client = TmdbClient::new(token);
    let results = client
        .search_movie(&query, None)
        .map_err(|e| e.to_string())?;
    let img_config = client.image_configuration().map_err(|e| e.to_string())?;

    let urls: Vec<Option<String>> = results
        .iter()
        .map(|r| r.get_poster_url(&img_config))
        .collect();
    let posters = download_many(&client, &urls);

    Ok(results
        .into_iter()
        .zip(posters)
        .map(|(r, poster_image)| TmdbSearchItem {
            tmdb_id: r.id,
            title: r.title,
            original_title: r.original_title,
            date: r.release_date.unwrap_or_default(),
            overview: r.overview,
            vote_average: r.vote_average,
            poster_path: r.poster_path,
            backdrop_path: r.backdrop_path,
            poster_image,
        })
        .collect())
}

pub(crate) fn search_shows(token: String, query: String) -> Result<Vec<TmdbSearchItem>, String> {
    let mut client = TmdbClient::new(token);
    let results = client.search_tv(&query, None).map_err(|e| e.to_string())?;
    let img_config = client.image_configuration().map_err(|e| e.to_string())?;

    let urls: Vec<Option<String>> = results
        .iter()
        .map(|r| r.get_poster_url(&img_config))
        .collect();
    let posters = download_many(&client, &urls);

    Ok(results
        .into_iter()
        .zip(posters)
        .map(|(r, poster_image)| TmdbSearchItem {
            tmdb_id: r.id,
            title: r.name,
            original_title: r.original_name,
            date: r.first_air_date.unwrap_or_default(),
            overview: None,
            vote_average: None,
            poster_path: r.poster_path,
            backdrop_path: None,
            poster_image,
        })
        .collect())
}

pub(crate) fn fetch_movie_match(
    token: String,
    item: TmdbSearchItem,
) -> Result<MovieMatchData, String> {
    let mut client = TmdbClient::new(token);
    let img_config = client.image_configuration().map_err(|e| e.to_string())?;

    let poster_url = item.poster_path.as_ref().map(|p| {
        format!(
            "{}/{}/{}",
            img_config.secure_base_url,
            img_config.poster_w342(),
            p
        )
    });
    let backdrop_url = item
        .backdrop_path
        .as_ref()
        .map(|p| format!("{}/w1280/{}", img_config.secure_base_url, p));

    let mut images = download_many(&client, &[poster_url, backdrop_url]);
    let backdrop_bytes = images.pop().flatten();
    let poster_bytes = images.pop().flatten();

    Ok(MovieMatchData {
        tmdb_id: item.tmdb_id,
        original_title: item.original_title,
        description: item.overview,
        rating: item.vote_average,
        poster: item.poster_path.zip(poster_bytes),
        backdrop: item.backdrop_path.zip(backdrop_bytes),
    })
}

pub(crate) fn fetch_show_match(
    token: String,
    item: TmdbSearchItem,
) -> Result<ShowMatchData, String> {
    fetch_show_match_by_id(token, item.tmdb_id)
}

pub(crate) fn fetch_show_match_by_id(
    token: String,
    series_id: u32,
) -> Result<ShowMatchData, String> {
    let mut client = TmdbClient::new(token);
    let details = client.tv_details(series_id).map_err(|e| e.to_string())?;
    // &mut borrow must end before the scoped threads take &client.
    let img_config = client.image_configuration().map_err(|e| e.to_string())?;
    let client = &client;

    let seasons = &details.seasons;
    let next = AtomicUsize::new(0);

    let (poster, backdrop, mut season_results) = std::thread::scope(|s| {
        let poster_h = s.spawn(|| {
            details.get_poster_url(&img_config).and_then(|url| {
                let bytes = client.download_image(&url).ok()?;
                if bytes.is_empty() {
                    return None;
                }
                let filename = details
                    .poster_path
                    .as_deref()
                    .unwrap_or("poster.jpg")
                    .to_string();
                Some((filename, bytes))
            })
        });
        let backdrop_h = s.spawn(|| {
            details.get_backdrop_url(&img_config).and_then(|url| {
                let bytes = client.download_image(&url).ok()?;
                if bytes.is_empty() {
                    return None;
                }
                let filename = details
                    .backdrop_path
                    .as_deref()
                    .unwrap_or("backdrop.jpg")
                    .to_string();
                Some((filename, bytes))
            })
        });

        let workers = seasons.len().min(MAX_PARALLEL_REQUESTS);
        let season_handles: Vec<_> = (0..workers)
            .map(|_| {
                s.spawn(|| {
                    let mut local: Vec<(usize, Result<SeasonMatchData, String>)> = Vec::new();
                    loop {
                        let i = next.fetch_add(1, Ordering::Relaxed);
                        if i >= seasons.len() {
                            break;
                        }
                        local.push((i, fetch_season(client, &img_config, series_id, &seasons[i])));
                    }
                    local
                })
            })
            .collect();

        let poster = poster_h.join().unwrap_or(None);
        let backdrop = backdrop_h.join().unwrap_or(None);
        let mut season_results = Vec::with_capacity(seasons.len());
        for h in season_handles {
            season_results.extend(h.join().unwrap_or_default());
        }
        (poster, backdrop, season_results)
    });

    if season_results.len() != seasons.len() {
        return Err("season fetch worker terminated unexpectedly".into());
    }
    season_results.sort_unstable_by_key(|(i, _)| *i);

    let mut season_data = Vec::with_capacity(season_results.len());
    for (_, result) in season_results {
        // Any failed season aborts the whole match, matching the previous
        // sequential behavior.
        season_data.push(result?);
    }

    let release_year = details.release_year().map(|y| y as i64);
    Ok(ShowMatchData {
        tmdb_id: details.id,
        original_name: details.original_name,
        release_year,
        description: details.overview,
        rating: details.vote_average,
        poster,
        backdrop,
        seasons: season_data,
    })
}

fn fetch_season(
    client: &TmdbClient,
    img_config: &ImageConfiguration,
    series_id: u32,
    tmdb_season: &TvSeriesSeason,
) -> Result<SeasonMatchData, String> {
    let poster = tmdb_season.get_poster_url(img_config).and_then(|url| {
        let bytes = client.download_image(&url).ok()?;
        if bytes.is_empty() {
            return None;
        }
        let filename = tmdb_season
            .poster_path
            .as_deref()
            .unwrap_or("poster.jpg")
            .to_string();
        Some((filename, bytes))
    });

    let season_details = client
        .season_details(series_id, tmdb_season.season_number)
        .map_err(|e| e.to_string())?;

    let episodes: Vec<EpisodeMatchData> = season_details
        .episodes
        .iter()
        .map(|ep| EpisodeMatchData {
            episode_number: ep.episode_number as i64,
            tmdb_id: ep.id,
            name: ep.name.clone(),
        })
        .collect();

    Ok(SeasonMatchData {
        season_number: tmdb_season.season_number as i64,
        tmdb_id: tmdb_season.id,
        name: tmdb_season.name.clone(),
        release_year: tmdb_season.release_year().map(|y| y as i64),
        description: tmdb_season.overview.clone(),
        rating: tmdb_season.vote_average,
        poster,
        episodes,
    })
}

/// Persist a movie match. Errors only if the metadata update fails; image
/// storage problems are logged and skipped.
pub(crate) fn apply_movie_match(
    db: &mut Database,
    movie_id: i64,
    data: &MovieMatchData,
) -> Result<AppliedImages, String> {
    db.update_movie_tmdb(
        movie_id,
        data.tmdb_id as i64,
        &data.original_title,
        data.description.as_deref(),
        data.rating,
    )
    .map_err(|e| e.to_string())?;

    let mut applied = AppliedImages::default();
    match crate::image_store::store_movie_images(
        db,
        movie_id,
        data.poster.clone(),
        data.backdrop.clone(),
        &crate::image_store::images_cache_dir(),
    ) {
        Ok((cover, backdrop)) => {
            applied.cover = cover;
            applied.backdrop = backdrop;
        }
        Err(e) => log::warn!("failed to store movie images: {e}"),
    }
    Ok(applied)
}

/// Persist a show match including the season/episode cascade. Errors only if
/// the show metadata update fails; season/episode/image problems are logged
/// and skipped.
pub(crate) fn apply_show_match(
    db: &mut Database,
    show_id: i64,
    data: &ShowMatchData,
) -> Result<AppliedImages, String> {
    db.update_show_tmdb(
        show_id,
        data.tmdb_id as i64,
        &data.original_name,
        data.release_year,
        data.description.as_deref(),
        data.rating,
    )
    .map_err(|e| e.to_string())?;

    // Season metadata and episodes: join TMDB seasons against DB seasons by
    // season_number; collect (db_season_id, poster) for the image store.
    let db_seasons = db.find_seasons_by_show(show_id).unwrap_or_default();
    let mut seasons_data: Vec<crate::image_store::SeasonPosterRow> = Vec::new();

    for db_season in &db_seasons {
        let tmdb_season = data
            .seasons
            .iter()
            .find(|s| s.season_number == db_season.season_number);

        let tmdb_season = match tmdb_season {
            Some(s) => s,
            None => {
                seasons_data.push((db_season.id, None));
                continue;
            }
        };

        if let Err(e) = db.update_season_tmdb(
            db_season.id,
            tmdb_season.tmdb_id as i64,
            &tmdb_season.name,
            tmdb_season.release_year,
            tmdb_season.description.as_deref(),
            tmdb_season.rating,
        ) {
            log::error!("failed to update season tmdb: {e}");
            seasons_data.push((db_season.id, None));
            continue;
        }

        seasons_data.push((db_season.id, tmdb_season.poster.clone()));

        let db_episodes = db.find_episodes_by_season(db_season.id).unwrap_or_default();
        for db_episode in &db_episodes {
            let tmdb_episode = tmdb_season
                .episodes
                .iter()
                .find(|e| e.episode_number == db_episode.episode_number);

            if let Some(ep) = tmdb_episode
                && let Err(e) = db.update_episode_tmdb(ep.tmdb_id as i64, &ep.name, db_episode.id)
            {
                log::error!("failed to update episode tmdb: {e}");
            }
        }
    }

    let mut applied = AppliedImages::default();
    match crate::image_store::store_show_images(
        db,
        show_id,
        data.poster.clone(),
        data.backdrop.clone(),
        &seasons_data,
        &crate::image_store::images_cache_dir(),
    ) {
        Ok((cover, backdrop)) => {
            applied.cover = cover;
            applied.backdrop = backdrop;
        }
        Err(e) => log::error!("failed to store show images: {e}"),
    }
    Ok(applied)
}
