use crate::{
    database::{
        Database, DatabaseError,
        entities::{Movie, NewEpisode, NewMovie, NewSeason, NewShow, Show},
    },
    tmdb::TmdbClient,
};
use dirs;
use regex::Regex;
use std::{
    collections::HashSet,
    path::{Path, PathBuf},
};
use thiserror::Error;

const SUPPORTED_VIDEO_EXTENSIONS: &[&str] = &["mp4", "mkv", "avi"];

#[derive(Debug, Error)]
#[error("library scanner failed")]
pub enum ScannerError {
    Database(#[from] DatabaseError),
    Regex(#[from] regex::Error),
    Io(#[from] std::io::Error),
    InvalidDirectory(String),
}

pub struct MovieScanner {
    title_year_regex: regex::Regex,
}

pub struct ShowMatcher {
    regex: Regex,
    show: Option<String>,
    season: Option<i64>,
    episode: Option<i64>,
}

impl MovieScanner {
    pub fn new() -> Result<Self, ScannerError> {
        // Matches patterns like "Movie.Title.2023.1080p" - fixed from [aA-zZ] to [A-Za-z]
        let title_year_regex =
            regex::Regex::new(r"^([A-Za-z0-9_.]+)\.([0-9]{4})(?:.*(?:1080[Pp]).*)?$")?;
        Ok(Self { title_year_regex })
    }

    pub fn scan_shows(&self, db: &mut Database, path: &str) -> Result<(), ScannerError> {
        let root = Path::new(path);
        if !root.is_dir() {
            return Err(ScannerError::InvalidDirectory(path.to_string()));
        }

        let library_id = db
            .find_or_create_library(path, "show")
            .map_err(ScannerError::Database)?;

        let known_files: HashSet<String> = db
            .find_episode_files_by_library(library_id)?
            .into_iter()
            .collect();

        let mut matcher = ShowMatcher {
            regex: Regex::new(r"(?i)([A-Za-z0-9_.]+).s([0-9]+)e([0-9]+)")?,
            show: None,
            season: None,
            episode: None,
        };

        self.walk_shows(
            db,
            root,
            root,
            library_id,
            &mut matcher,
            false,
            &known_files,
        )?;

        Ok(())
    }

    fn walk_shows(
        &self,
        db: &mut Database,
        root: &Path,
        parent: &Path,
        library_id: i64,
        matcher: &mut ShowMatcher,
        is_show_dir: bool,
        known_files: &HashSet<String>,
    ) -> Result<(), ScannerError> {
        'walk: for entry in std::fs::read_dir(parent)? {
            let entry = entry?;
            let entry_path = entry.path();
            log::debug!("{}", entry_path.to_string_lossy());

            if entry_path.is_dir() {
                if let Some(dirname) = entry_path.file_name().map(|d| d.to_string_lossy()) {
                    let (_, season, episode) = self.extract_show_info(&dirname, matcher);
                    matcher.season = season;
                    matcher.episode = episode;
                    if !is_show_dir {
                        matcher.show = Some(dirname.into());
                    }
                }
                self.walk_shows(
                    db,
                    root,
                    &entry_path,
                    library_id,
                    matcher,
                    true,
                    known_files,
                )?;
            } else if entry_path.is_file() && self.is_video_file(&entry_path) {
                if let Some(filename) = entry_path.file_stem().map(|f| f.to_string_lossy()) {
                    let (_, file_season, file_episode) = self.extract_show_info(&filename, matcher);
                    let season = match file_season.or(matcher.season) {
                        Some(s) => s,
                        None => {
                            log::warn!("{:?}", entry_path);
                            continue 'walk;
                        }
                    };
                    let episode = match file_episode.or(matcher.episode) {
                        Some(e) => e,
                        None => {
                            log::warn!("{:?}", entry_path);
                            continue 'walk;
                        }
                    };

                    let show = match matcher.show.clone() {
                        Some(s) => s,
                        None => {
                            log::warn!("{:?}", entry_path);
                            continue 'walk;
                        }
                    };

                    log::debug!(
                        "show: {}\n\tseason: {} episode: {} file: {:?}",
                        show,
                        season,
                        episode,
                        &entry_path,
                    );

                    let show_id = if let Some(show) = db.find_show_by_name(&show)? {
                        show.id
                    } else {
                        let id = db.create_show(NewShow {
                            title: show.clone(),
                        })?;
                        log::info!("created new show ({id}) name: {show}");
                        id
                    };

                    let season_id =
                        if let Some(season) = db.find_season_by_number(show_id, season)? {
                            season.id
                        } else {
                            let id = db.create_season(NewSeason {
                                show_id,
                                season_number: season,
                            })?;
                            log::info!("created new season ({id}) show: {show} number: {season}");
                            id
                        };

                    let relative_file = entry_path
                        .strip_prefix(root)
                        .unwrap_or(&entry_path)
                        .to_string_lossy()
                        .to_string();

                    if known_files.contains(&relative_file) {
                        continue 'walk;
                    }

                    match db.create_episode(NewEpisode {
                        season_id,
                        library_id,
                        episode_number: episode,
                        file: relative_file,
                    }) {
                        Ok(id) => log::info!(
                            "created new episode ({id}) show: {show} season: {season} number: {episode}"
                        ),
                        Err(e) => log::warn!(
                            "skipped episode {:?} (show: {show} s{season}e{episode}): {e}",
                            entry_path
                        ),
                    }
                }
            }
        }

        Ok(())
    }

    fn extract_show_info(
        &self,
        search: &str,
        matcher: &ShowMatcher,
    ) -> (Option<String>, Option<i64>, Option<i64>) {
        if let Some(caps) = matcher.regex.captures(&search) {
            let show = caps.get(1).map(|s| s.as_str().to_string());

            let season = caps
                .get(2)
                .map(|s| s.as_str())
                .map(|n| n.parse::<i64>().ok())
                .flatten();

            let episode = caps
                .get(3)
                .map(|s| s.as_str())
                .map(|n| n.parse::<i64>().ok())
                .flatten();

            (show, season, episode)
        } else {
            (None, None, None)
        }
    }

    pub fn scan_movies(&self, db: &mut Database, root_path: &str) -> Result<usize, ScannerError> {
        let root = Path::new(root_path);
        if !root.is_dir() {
            return Err(ScannerError::InvalidDirectory(root_path.to_string()));
        }

        let library_id = db
            .find_or_create_library(root_path, "movie")
            .map_err(ScannerError::Database)?;

        let known_files: HashSet<String> = db
            .find_movie_files_by_library(library_id)?
            .into_iter()
            .collect();

        let mut count = 0;
        let mut processed_dirs = HashSet::new();

        self.walk_movies(
            db,
            root,
            root,
            library_id,
            &mut count,
            &mut processed_dirs,
            &known_files,
        )?;

        Ok(count)
    }

    fn walk_movies(
        &self,
        db: &mut Database,
        root: &Path,
        path: &Path,
        library_id: i64,
        count: &mut usize,
        processed_dirs: &mut HashSet<PathBuf>,
        known_files: &HashSet<String>,
    ) -> Result<(), ScannerError> {
        for entry in std::fs::read_dir(path)? {
            let entry = entry?;
            let entry_path = entry.path();

            if entry_path.is_dir() {
                self.walk_movies(
                    db,
                    root,
                    &entry_path,
                    library_id,
                    count,
                    processed_dirs,
                    known_files,
                )?;
            } else if entry_path.is_file() && self.is_video_file(&entry_path) {
                if let Some(parent) = entry_path.parent() {
                    if processed_dirs.contains(parent) {
                        continue;
                    }

                    let dirname = match parent.file_name() {
                        Some(name) => name.to_string_lossy(),
                        None => continue,
                    };

                    let (title, year) = self.parse_movie_info(&dirname);

                    let relative_file = entry_path
                        .strip_prefix(root)
                        .unwrap_or(&entry_path)
                        .to_string_lossy()
                        .to_string();

                    if known_files.contains(&relative_file) {
                        continue;
                    }

                    let new_movie = NewMovie {
                        library_id,
                        title: title.to_string(),
                        release_year: year,
                        file: relative_file,
                    };

                    match db.create_movie(new_movie) {
                        Ok(_) => {
                            log::info!(
                                "Added movie: {} ({}) - {}",
                                title,
                                year.unwrap_or(0),
                                entry_path.display()
                            );
                            *count += 1;
                            processed_dirs.insert(parent.to_path_buf());
                        }
                        Err(e) => {
                            log::warn!("Failed to add movie {}: {}", title, e);
                        }
                    }
                }
            }
        }

        Ok(())
    }

    pub fn lookup_movies(
        &self,
        db: &mut Database,
        tmdb_client: &mut TmdbClient,
    ) -> Result<(), ScannerError> {
        let cache_dir = dirs::cache_dir()
            .unwrap_or_else(|| PathBuf::from(".cache"))
            .join("myplayer")
            .join("images");
        let _ = std::fs::create_dir_all(&cache_dir);

        let movies = db.find_unmatched_movies()?;
        log::info!("Found {} unmatched movies", movies.len());
        for (i, movie) in movies.iter().enumerate() {
            log::info!("[{}/{}] Looking up '{}'", i + 1, movies.len(), movie.title);
            if let Err(e) = self.fetch_movie_metadata(db, movie, tmdb_client, &cache_dir) {
                log::warn!("Failed to lookup TMDB data for '{}': {}", movie.title, e);
            }
            std::thread::sleep(std::time::Duration::from_millis(250));
        }
        Ok(())
    }

    fn fetch_movie_metadata(
        &self,
        db: &mut Database,
        movie: &Movie,
        tmdb_client: &mut TmdbClient,
        cache_dir: &Path,
    ) -> Result<(), Box<dyn std::error::Error>> {
        let results =
            tmdb_client.search_movie(&movie.title, movie.release_year.map(|y| y as i32))?;

        let result = if results.len() == 1 {
            &results[0]
        } else if results.len() > 1 {
            let movie_year = movie.release_year.map(|y| y as i32);
            match results.iter().find(|r| {
                let title_match = r.title.eq_ignore_ascii_case(&movie.title);
                let year_match = match (movie_year, r.release_year()) {
                    (Some(my), Some(ry)) => my == ry,
                    _ => false,
                };
                title_match && year_match
            }) {
                Some(r) => r,
                None => {
                    log::info!(
                        "TMDB search for '{}' returned {} results, no exact match",
                        movie.title,
                        results.len()
                    );
                    db.set_movie_skip_auto_match(movie.id)?;
                    return Ok(());
                }
            }
        } else {
            log::info!("TMDB search for '{}' returned no results", movie.title);
            db.set_movie_skip_auto_match(movie.id)?;
            return Ok(());
        };
        db.update_movie_tmdb(
            movie.id,
            result.id as i64,
            &result.original_title,
            result.overview.as_deref(),
            result.vote_average,
        )?;
        log::info!("TMDB matched '{}' → '{}'", movie.title, result.title);

        let img_config = tmdb_client.image_configuration()?;

        let poster = result
            .get_poster_url(&img_config)
            .and_then(|url| tmdb_client.download_image(&url).ok())
            .filter(|b| !b.is_empty())
            .map(|bytes| {
                let filename = result
                    .poster_path
                    .as_deref()
                    .unwrap_or("poster.jpg")
                    .to_string();
                (filename, bytes)
            });

        let backdrop = result
            .get_backdrop_url(&img_config)
            .and_then(|url| tmdb_client.download_image(&url).ok())
            .filter(|b| !b.is_empty())
            .map(|bytes| {
                let filename = result
                    .backdrop_path
                    .as_deref()
                    .unwrap_or("backdrop.jpg")
                    .to_string();
                (filename, bytes)
            });

        if let Err(e) =
            crate::image_store::store_movie_images(db, movie.id, poster, backdrop, cache_dir)
        {
            log::warn!("failed to store images for '{}': {e}", movie.title);
        }

        Ok(())
    }

    fn parse_movie_info(&self, dirname: &str) -> (String, Option<i32>) {
        if let Some(caps) = self.title_year_regex.captures(dirname) {
            let title = caps
                .get(1)
                .unwrap()
                .as_str()
                .replace('.', " ")
                .replace('_', " ")
                .trim()
                .to_string();

            let year = caps.get(2).and_then(|y| y.as_str().parse::<i32>().ok());

            (title, year)
        } else {
            // No year found, use dirname as title
            let title = dirname
                .replace('.', " ")
                .replace('_', " ")
                .trim()
                .to_string();
            (title, None)
        }
    }

    fn is_video_file(&self, path: &Path) -> bool {
        if let Some(ext) = path.extension() {
            let ext_lower = ext.to_string_lossy().to_lowercase();
            SUPPORTED_VIDEO_EXTENSIONS.contains(&ext_lower.as_str())
        } else {
            false
        }
    }

    pub fn lookup_shows(
        &self,
        db: &mut Database,
        tmdb_client: &mut TmdbClient,
    ) -> Result<(), ScannerError> {
        let cache_dir = dirs::cache_dir()
            .unwrap_or_else(|| PathBuf::from(".cache"))
            .join("myplayer")
            .join("images");
        let _ = std::fs::create_dir_all(&cache_dir);

        let shows = db.find_unmatched_shows()?;
        log::info!("Found {} unmatched shows", shows.len());
        for (i, show) in shows.iter().enumerate() {
            log::info!("[{}/{}] Looking up '{}'", i + 1, shows.len(), show.title);
            if let Err(e) = self.fetch_show_metadata(db, show, tmdb_client, &cache_dir) {
                log::warn!("Failed to lookup TMDB data for '{}': {}", show.title, e);
            }
            std::thread::sleep(std::time::Duration::from_millis(250));
        }
        Ok(())
    }

    fn fetch_show_metadata(
        &self,
        db: &mut Database,
        show: &Show,
        tmdb_client: &mut TmdbClient,
        cache_dir: &Path,
    ) -> Result<(), Box<dyn std::error::Error>> {
        let search_title = show.title.replace('.', " ").replace('_', " ");
        let results = tmdb_client.search_tv(&search_title, None)?;

        let tv_result = if results.len() == 1 {
            &results[0]
        } else if results.len() > 1 {
            match results
                .iter()
                .find(|r| r.name.eq_ignore_ascii_case(&search_title))
            {
                Some(r) => r,
                None => {
                    log::info!(
                        "TMDB search for '{}' returned {} results, no exact match",
                        search_title,
                        results.len()
                    );
                    db.set_show_skip_auto_match(show.id)?;
                    return Ok(());
                }
            }
        } else {
            log::info!("TMDB search for '{}' returned no results", search_title);
            db.set_show_skip_auto_match(show.id)?;
            return Ok(());
        };
        let series_id = tv_result.id;

        let details = tmdb_client.tv_details(series_id)?;
        db.update_show_tmdb(
            show.id,
            details.id as i64,
            &details.original_name,
            details.release_year().map(|y| y as i64),
            details.overview.as_deref(),
            details.vote_average,
        )?;
        log::info!("TMDB matched '{}' → '{}'", show.title, details.name);

        let img_config = tmdb_client.image_configuration()?;

        let poster = details
            .get_poster_url(&img_config)
            .and_then(|url| tmdb_client.download_image(&url).ok())
            .filter(|b| !b.is_empty())
            .map(|bytes| {
                let filename = details
                    .poster_path
                    .as_deref()
                    .unwrap_or("poster.jpg")
                    .to_string();
                (filename, bytes)
            });

        let backdrop = details
            .get_backdrop_url(&img_config)
            .and_then(|url| tmdb_client.download_image(&url).ok())
            .filter(|b| !b.is_empty())
            .map(|bytes| {
                let filename = details
                    .backdrop_path
                    .as_deref()
                    .unwrap_or("backdrop.jpg")
                    .to_string();
                (filename, bytes)
            });

        let db_seasons = db.find_seasons_by_show(show.id)?;
        let mut seasons_data: Vec<(i64, Option<(String, Vec<u8>)>)> = Vec::new();

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

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

            db.update_season_tmdb(
                db_season.id,
                tmdb_season.id as i64,
                &tmdb_season.name,
                tmdb_season.release_year().map(|y| y as i64),
                tmdb_season.overview.as_deref(),
                tmdb_season.vote_average,
            )?;

            let season_poster = tmdb_season
                .get_poster_url(&img_config)
                .and_then(|url| tmdb_client.download_image(&url).ok())
                .filter(|b| !b.is_empty())
                .map(|bytes| {
                    let filename = tmdb_season
                        .poster_path
                        .as_deref()
                        .unwrap_or("poster.jpg")
                        .to_string();
                    (filename, bytes)
                });

            seasons_data.push((db_season.id, season_poster));

            let season_details =
                tmdb_client.season_details(series_id, db_season.season_number as u32)?;
            let db_episodes = db.find_episodes_by_season(db_season.id)?;
            for db_episode in &db_episodes {
                let tmdb_episode = season_details
                    .episodes
                    .iter()
                    .find(|e| e.episode_number == db_episode.episode_number as u32);

                if let Some(ep) = tmdb_episode {
                    db.update_episode_tmdb(ep.id as i64, &ep.name, db_episode.id)?;
                }
            }
        }

        if let Err(e) = crate::image_store::store_show_images(
            db,
            show.id,
            poster,
            backdrop,
            &seasons_data,
            cache_dir,
        ) {
            log::warn!("failed to store images for show '{}': {e}", show.title);
        }

        Ok(())
    }

    pub fn recheck_movies(
        &self,
        db: &mut Database,
        tmdb_client: &mut TmdbClient,
    ) -> Result<(), ScannerError> {
        let cache_dir = dirs::cache_dir()
            .unwrap_or_else(|| PathBuf::from(".cache"))
            .join("myplayer")
            .join("images");
        let _ = std::fs::create_dir_all(&cache_dir);

        let movies = db.find_matched_movies()?;
        log::info!("Rechecking {} matched movies", movies.len());
        for (i, movie) in movies.iter().enumerate() {
            let tmdb_id = match movie.tmdb_id {
                Some(id) => id as u32,
                None => continue,
            };
            log::info!("[{}/{}] Rechecking '{}'", i + 1, movies.len(), movie.title);
            if let Err(e) = self.recheck_movie(db, &movie, tmdb_id, tmdb_client, &cache_dir) {
                log::warn!("Failed to recheck '{}': {}", movie.title, e);
            }
            std::thread::sleep(std::time::Duration::from_millis(250));
        }
        Ok(())
    }

    fn recheck_movie(
        &self,
        db: &mut Database,
        movie: &Movie,
        tmdb_id: u32,
        tmdb_client: &mut TmdbClient,
        cache_dir: &Path,
    ) -> Result<(), Box<dyn std::error::Error>> {
        let details = tmdb_client.movie_details(tmdb_id)?;

        db.update_movie_tmdb(
            movie.id,
            details.id as i64,
            &details.original_title,
            details.overview.as_deref(),
            details.vote_average,
        )?;

        let img_config = tmdb_client.image_configuration()?;

        let poster = details
            .get_poster_url(&img_config)
            .and_then(|url| tmdb_client.download_image(&url).ok())
            .filter(|b| !b.is_empty())
            .map(|bytes| {
                let filename = details
                    .poster_path
                    .as_deref()
                    .unwrap_or("poster.jpg")
                    .to_string();
                (filename, bytes)
            });

        let backdrop = details
            .get_backdrop_url(&img_config)
            .and_then(|url| tmdb_client.download_image(&url).ok())
            .filter(|b| !b.is_empty())
            .map(|bytes| {
                let filename = details
                    .backdrop_path
                    .as_deref()
                    .unwrap_or("backdrop.jpg")
                    .to_string();
                (filename, bytes)
            });

        if let Err(e) =
            crate::image_store::store_movie_images(db, movie.id, poster, backdrop, cache_dir)
        {
            log::warn!("failed to store images for '{}': {e}", movie.title);
        }

        Ok(())
    }

    pub fn recheck_shows(
        &self,
        db: &mut Database,
        tmdb_client: &mut TmdbClient,
    ) -> Result<(), ScannerError> {
        let cache_dir = dirs::cache_dir()
            .unwrap_or_else(|| PathBuf::from(".cache"))
            .join("myplayer")
            .join("images");
        let _ = std::fs::create_dir_all(&cache_dir);

        let shows = db.find_matched_shows()?;
        log::info!("Rechecking {} matched shows", shows.len());
        for (i, show) in shows.iter().enumerate() {
            let tmdb_id = match show.tmdb_id {
                Some(id) => id as u32,
                None => continue,
            };
            log::info!("[{}/{}] Rechecking '{}'", i + 1, shows.len(), show.title);
            if let Err(e) = self.recheck_show(db, &show, tmdb_id, tmdb_client, &cache_dir) {
                log::warn!("Failed to recheck '{}': {}", show.title, e);
            }
            std::thread::sleep(std::time::Duration::from_millis(250));
        }
        Ok(())
    }

    fn recheck_show(
        &self,
        db: &mut Database,
        show: &Show,
        tmdb_id: u32,
        tmdb_client: &mut TmdbClient,
        cache_dir: &Path,
    ) -> Result<(), Box<dyn std::error::Error>> {
        let details = tmdb_client.tv_details(tmdb_id)?;

        db.update_show_tmdb(
            show.id,
            details.id as i64,
            &details.original_name,
            details.release_year().map(|y| y as i64),
            details.overview.as_deref(),
            details.vote_average,
        )?;

        let img_config = tmdb_client.image_configuration()?;

        let poster = details
            .get_poster_url(&img_config)
            .and_then(|url| tmdb_client.download_image(&url).ok())
            .filter(|b| !b.is_empty())
            .map(|bytes| {
                let filename = details
                    .poster_path
                    .as_deref()
                    .unwrap_or("poster.jpg")
                    .to_string();
                (filename, bytes)
            });

        let backdrop = details
            .get_backdrop_url(&img_config)
            .and_then(|url| tmdb_client.download_image(&url).ok())
            .filter(|b| !b.is_empty())
            .map(|bytes| {
                let filename = details
                    .backdrop_path
                    .as_deref()
                    .unwrap_or("backdrop.jpg")
                    .to_string();
                (filename, bytes)
            });

        let db_seasons = db.find_seasons_by_show(show.id)?;
        let mut seasons_data: Vec<(i64, Option<(String, Vec<u8>)>)> = Vec::new();

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

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

            db.update_season_tmdb(
                db_season.id,
                tmdb_season.id as i64,
                &tmdb_season.name,
                tmdb_season.release_year().map(|y| y as i64),
                tmdb_season.overview.as_deref(),
                tmdb_season.vote_average,
            )?;

            let season_poster = tmdb_season
                .get_poster_url(&img_config)
                .and_then(|url| tmdb_client.download_image(&url).ok())
                .filter(|b| !b.is_empty())
                .map(|bytes| {
                    let filename = tmdb_season
                        .poster_path
                        .as_deref()
                        .unwrap_or("poster.jpg")
                        .to_string();
                    (filename, bytes)
                });

            seasons_data.push((db_season.id, season_poster));

            let series_id = tmdb_id;
            let season_details =
                tmdb_client.season_details(series_id, db_season.season_number as u32)?;
            let db_episodes = db.find_episodes_by_season(db_season.id)?;
            for db_episode in &db_episodes {
                let tmdb_episode = season_details
                    .episodes
                    .iter()
                    .find(|e| e.episode_number == db_episode.episode_number as u32);

                if let Some(ep) = tmdb_episode {
                    db.update_episode_tmdb(ep.id as i64, &ep.name, db_episode.id)?;
                }
            }
        }

        if let Err(e) = crate::image_store::store_show_images(
            db,
            show.id,
            poster,
            backdrop,
            &seasons_data,
            cache_dir,
        ) {
            log::warn!("failed to store images for show '{}': {e}", show.title);
        }

        Ok(())
    }

    pub fn lookup_new_seasons(
        &self,
        db: &mut Database,
        tmdb_client: &mut TmdbClient,
    ) -> Result<(), ScannerError> {
        let cache_dir = dirs::cache_dir()
            .unwrap_or_else(|| PathBuf::from(".cache"))
            .join("myplayer")
            .join("images");
        let _ = std::fs::create_dir_all(&cache_dir);

        let unmatched = db.find_unmatched_seasons_with_matched_shows()?;
        if unmatched.is_empty() {
            return Ok(());
        }
        log::info!(
            "Found {} unmatched seasons on matched shows",
            unmatched.len()
        );

        for (i, season) in unmatched.iter().enumerate() {
            log::info!(
                "[{}/{}] Looking up '{}' season {}",
                i + 1,
                unmatched.len(),
                season.show_title,
                season.season_number
            );
            if let Err(e) = self.fetch_season_metadata(db, season, tmdb_client, &cache_dir) {
                log::warn!(
                    "Failed to lookup TMDB data for '{}' season {}: {}",
                    season.show_title,
                    season.season_number,
                    e
                );
            }
            std::thread::sleep(std::time::Duration::from_millis(250));
        }
        Ok(())
    }

    fn fetch_season_metadata(
        &self,
        db: &mut Database,
        season: &crate::database::entities::UnmatchedSeason,
        tmdb_client: &mut TmdbClient,
        cache_dir: &Path,
    ) -> Result<(), Box<dyn std::error::Error>> {
        let series_id = season.show_tmdb_id as u32;
        let details = tmdb_client.tv_details(series_id)?;
        let img_config = tmdb_client.image_configuration()?;

        let tmdb_season = details
            .seasons
            .iter()
            .find(|s| s.season_number == season.season_number as u32);

        let tmdb_season = match tmdb_season {
            Some(s) => s,
            None => {
                log::info!(
                    "TMDB has no season {} for '{}'",
                    season.season_number,
                    season.show_title
                );
                db.set_season_skip_auto_match(season.id)?;
                return Ok(());
            }
        };

        db.update_season_tmdb(
            season.id,
            tmdb_season.id as i64,
            &tmdb_season.name,
            tmdb_season.release_year().map(|y| y as i64),
            tmdb_season.overview.as_deref(),
            tmdb_season.vote_average,
        )?;
        log::info!(
            "TMDB matched '{}' season {} → '{}'",
            season.show_title,
            season.season_number,
            tmdb_season.name
        );

        let season_poster = tmdb_season
            .get_poster_url(&img_config)
            .and_then(|url| tmdb_client.download_image(&url).ok())
            .filter(|b| !b.is_empty())
            .map(|bytes| {
                let filename = tmdb_season
                    .poster_path
                    .as_deref()
                    .unwrap_or("poster.jpg")
                    .to_string();
                (filename, bytes)
            });

        let seasons_data = vec![(season.id, season_poster)];
        if let Err(e) = crate::image_store::store_show_images(
            db,
            season.show_id,
            None,
            None,
            &seasons_data,
            cache_dir,
        ) {
            log::warn!("failed to store season images: {e}");
        }

        let season_details = tmdb_client.season_details(series_id, season.season_number as u32)?;
        let db_episodes = db.find_episodes_by_season(season.id)?;
        for db_episode in &db_episodes {
            let tmdb_episode = season_details
                .episodes
                .iter()
                .find(|e| e.episode_number == db_episode.episode_number as u32);

            if let Some(ep) = tmdb_episode {
                db.update_episode_tmdb(ep.id as i64, &ep.name, db_episode.id)?;
            }
        }

        Ok(())
    }
}
