mod database;
mod ffmpeg_utils;
mod image_store;
mod player;
mod scanner;
mod scanner_task;
mod tmdb;
mod tmdb_service;
mod ui;

extern crate ffmpeg_next as ffmpeg;
use std::io;

use clap::Parser;
use database::Database;
use player::Player;

#[derive(Parser)]
#[command(
    version,
    about,
    after_help = "Homepage: https://myplayer.chaoshome.net"
)]
struct Opts {
    #[clap(help = "the video file to process")]
    file: Option<String>,
    #[clap(long, default_value_t = false, help = "analyze the video file")]
    analyze: bool,
    #[clap(long, default_value_t = false, help = "Scan all movie libraries")]
    scan_movies: bool,
    #[clap(long, default_value_t = false, help = "Scan all TV show libraries")]
    scan_shows: bool,
    #[clap(
        long,
        default_value_t = false,
        help = "Re-fetch TMDB metadata and images for already matched items"
    )]
    recheck: bool,
    #[clap(
        long,
        default_value_t = false,
        help = "Reset skipped auto-match flags so unmatched items are retried"
    )]
    reset_matches: bool,
    #[clap(long, help = "Register a library at <PATH> (use with --media-type)")]
    add_library: Option<String>,
    #[clap(long, help = "Media type for --add-library (movie or show)")]
    media_type: Option<String>,
    #[clap(long, help = "Set TMDB API key")]
    set_api_key: Option<String>,
    #[clap(long, help = "Merge TMDB metadata and images from another database")]
    merge: Option<String>,
    #[clap(long, help = "Path to database file")]
    db_path: Option<String>,
    #[clap(long, short, action = clap::ArgAction::Count, help = "Verbosity level")]
    verbose: u8,
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let opts = Opts::parse();
    let log_level = match opts.verbose {
        0 => log::LevelFilter::Info,
        1 => log::LevelFilter::Debug,
        2 => log::LevelFilter::Trace,
        _ => {
            eprintln!("Don't go crazy on verbosity!");
            return Ok(());
        }
    };

    let data_dir = match &opts.db_path {
        Some(p) => std::path::PathBuf::from(p)
            .parent()
            .map(|p| p.to_path_buf())
            .unwrap_or_else(|| std::path::PathBuf::from(".")),
        None => {
            let dir = dirs::data_dir()
                .ok_or("could not determine data directory")?
                .join("myplayer");
            std::fs::create_dir_all(&dir)?;
            dir
        }
    };

    let log_path = data_dir.join("myplayer.log");
    // Truncate log file on startup
    let _ = std::fs::File::create(&log_path);

    fern::Dispatch::new()
        .format(|out, message, record| {
            out.finish(format_args!(
                "[{}][{}] {}",
                record.level(),
                record.target(),
                message
            ))
        })
        .level(log::LevelFilter::Warn)
        .level_for("myplayer", log_level)
        .chain(std::io::stderr())
        .chain(fern::log_file(&log_path)?)
        .apply()?;

    install_panic_hook();

    log::info!("Log file: {}", log_path.display());

    let db_path = match opts.db_path {
        Some(p) => std::path::PathBuf::from(p),
        None => data_dir.join("myplayer.db"),
    };
    let mut db = Database::open(db_path.to_str().ok_or("invalid database path")?)?;

    if let Some(token) = opts.set_api_key {
        db.set_host("tmdb_api_key", &token)?;
        log::info!("TMDB API key stored.");
        return Ok(());
    }

    extract_images(&mut db)?;

    if let Some(remote_path) = opts.merge {
        let report = db.merge_from(&remote_path)?;
        extract_images(&mut db)?; // extract newly merged images to cache
        log::info!("Merge complete:");
        log::info!("  Movies:   {}", report.movies);
        log::info!("  Shows:    {}", report.shows);
        log::info!("  Seasons:  {}", report.seasons);
        log::info!("  Episodes: {}", report.episodes);
        log::info!("  Images:   {}", report.images);
        return Ok(());
    }

    ffmpeg::init()?;

    if opts.analyze
        && let Some(input) = opts.file
    {
        ffmpeg_utils::print_metadata(&input)?;
        return Ok(());
    }

    if let Some(path) = opts.add_library {
        let media_type = opts
            .media_type
            .ok_or("--media-type is required with --add-library (movie or show)")?;
        if media_type != "movie" && media_type != "show" {
            return Err("--media-type must be 'movie' or 'show'".into());
        }
        let path = std::path::Path::new(&path)
            .canonicalize()
            .map_err(|_| format!("directory not found: {}", path))?;
        let path_str = path.to_str().ok_or("invalid path")?;
        if !path.is_dir() {
            return Err(format!("{} is not a directory", path_str).into());
        }
        db.find_or_create_library(path_str, &media_type)?;
        log::info!("Library registered: {} ({})", path_str, media_type);
        return Ok(());
    }

    if opts.reset_matches {
        db.reset_movie_auto_match()?;
        db.reset_show_auto_match()?;
        db.reset_season_auto_match()?;
        log::info!("Reset skip_auto_match for all unmatched items");
        return Ok(());
    }

    if opts.scan_movies {
        let libraries = db.find_libraries_by_media_type("movie")?;
        if libraries.is_empty() {
            log::warn!("No movie libraries registered. Use --add-library to add one.");
            return Ok(());
        }
        let scanner = scanner::MovieScanner::new()?;
        for lib in &libraries {
            let n = scanner.scan_movies(&mut db, &lib.root_path)?;
            log::info!("Scanned {} new movies from {}", n, lib.root_path);
        }
        if let Some(token) = db.get_host("tmdb_api_key")? {
            let mut tmdb_client = tmdb::TmdbClient::new(token);
            if opts.recheck {
                scanner.recheck_movies(&mut db, &mut tmdb_client)?;
            } else {
                scanner.lookup_movies(&mut db, &mut tmdb_client)?;
            }
        }
        return Ok(());
    }

    if opts.scan_shows {
        let libraries = db.find_libraries_by_media_type("show")?;
        if libraries.is_empty() {
            log::warn!("No show libraries registered. Use --add-library to add one.");
            return Ok(());
        }
        let scanner = scanner::MovieScanner::new()?;
        for lib in &libraries {
            scanner.scan_shows(&mut db, &lib.root_path)?;
            log::info!("Scanned shows from {}", lib.root_path);
        }
        if let Some(token) = db.get_host("tmdb_api_key")? {
            let mut tmdb_client = tmdb::TmdbClient::new(token);
            if opts.recheck {
                scanner.recheck_shows(&mut db, &mut tmdb_client)?;
            } else {
                scanner.lookup_shows(&mut db, &mut tmdb_client)?;
                scanner.lookup_new_seasons(&mut db, &mut tmdb_client)?;
            }
        }
        return Ok(());
    }

    if let Some(input) = opts.file {
        let player = Player::new(&input)?;
        ui::run(Some(player), db, log_path)?;
    } else {
        ui::run(None, db, log_path)?;
    }

    Ok(())
}

/// Routes panics into the log file.
///
/// Rust's default hook writes to stderr only and never goes through the `log`
/// facade. Under flatpak stderr ends up in the journal, which users never
/// collect, so a panic and a segfault both look like a log that simply stops.
/// Chains to the previous hook so stderr behaviour is unchanged.
fn install_panic_hook() {
    let default_hook = std::panic::take_hook();
    std::panic::set_hook(Box::new(move |info| {
        let thread = std::thread::current();
        let thread_name = thread.name().unwrap_or("<unnamed>");

        let payload = info.payload();
        let message = if let Some(s) = payload.downcast_ref::<&str>() {
            *s
        } else if let Some(s) = payload.downcast_ref::<String>() {
            s.as_str()
        } else {
            "<non-string panic payload>"
        };

        let location = match info.location() {
            Some(l) => format!("{}:{}:{}", l.file(), l.line(), l.column()),
            None => "<unknown location>".to_string(),
        };

        // force_capture so the user does not need RUST_BACKTRACE set.
        log::error!(
            "PANIC in thread '{thread_name}' at {location}: {message}\nbacktrace:\n{}",
            std::backtrace::Backtrace::force_capture()
        );
        log::logger().flush();

        default_hook(info);
    }));
}

fn extract_images(db: &mut Database) -> Result<(), io::Error> {
    let cache_dir = image_store::images_cache_dir();
    std::fs::create_dir_all(&cache_dir)?;

    let _ = db.delete_orphaned_images();
    let images = db.find_all_images().expect("database not corrupted");
    let mut extracted = 0;
    for img in &images {
        let path = cache_dir.join(img.filename.trim_start_matches('/'));
        if path.exists() && path.metadata().map(|m| m.len() as i64).unwrap_or(0) == img.size {
            continue;
        }
        std::fs::write(&path, &img.image)?;
        extracted += 1;
    }
    log::info!(
        "Extracted {} images ({} already cached) to {}",
        extracted,
        images.len() - extracted,
        cache_dir.display()
    );
    Ok(())
}
