// yt - A fully featured command line YouTube client
//
// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
// SPDX-License-Identifier: GPL-3.0-or-later
//
// This file is part of Yt.
//
// You should have received a copy of the License along with this program.
// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.

use std::{collections::HashMap, fs, sync::Arc, u8};

use anyhow::{bail, Context, Result};
use app::App;
use bytes::Bytes;
use cache::invalidate;
use clap::Parser;
use cli::{CacheCommand, CheckCommand, SelectCommand, SubscriptionCommand, VideosCommand};
use config::Config;
use log::info;
use select::cmds::handle_select_cmd;
use storage::video_database::getters::get_video_by_hash;
use tokio::{
    fs::File,
    io::{stdin, BufReader},
};
use url::Url;
use yt_dlp::wrapper::info_json::InfoJson;

use crate::{cli::Command, storage::subscriptions::get_subscriptions};

pub mod app;
pub mod cli;

pub mod cache;
pub mod comments;
pub mod config;
pub mod constants;
pub mod download;
pub mod select;
pub mod status;
pub mod storage;
pub mod subscribe;
pub mod update;
pub mod videos;
pub mod watch;

#[tokio::main]
async fn main() -> Result<()> {
    let args = cli::CliArgs::parse();

    // The default verbosity is 1 (Warn)
    let verbosity: u8 = args.verbosity + 1;

    stderrlog::new()
        .module(module_path!())
        .modules(&["yt_dlp".to_owned(), "libmpv2".to_owned()])
        .quiet(args.quiet)
        .show_module_names(false)
        .color(stderrlog::ColorChoice::Auto)
        .verbosity(verbosity as usize)
        .timestamp(stderrlog::Timestamp::Off)
        .init()
        .expect("Let's just hope that this does not panic");

    info!("Using verbosity level: '{} ({})'", verbosity, {
        match verbosity {
            0 => "Error",
            1 => "Warn",
            2 => "Info",
            3 => "Debug",
            4.. => "Trace",
        }
    });

    let app = {
        let config = Config::from_config_file(args.db_path, args.config_path)?;
        App::new(config).await?
    };

    match args.command.unwrap_or(Command::default()) {
        Command::Download {
            force,
            max_cache_size,
        } => {
            let max_cache_size =
                max_cache_size.unwrap_or(app.config.download.max_cache_size.as_u64());
            info!("Max cache size: '{}'", Bytes::new(max_cache_size));

            if force {
                invalidate(&app, true).await?;
            }

            download::Downloader::new()
                .consume(Arc::new(app), max_cache_size)
                .await?;
        }
        Command::Select { cmd } => {
            let cmd = cmd.unwrap_or(SelectCommand::default());

            match cmd {
                SelectCommand::File {
                    done,
                    use_last_selection,
                } => select::select(&app, done, use_last_selection).await?,
                _ => handle_select_cmd(&app, cmd, None).await?,
            }
        }
        Command::Videos { cmd } => match cmd {
            VideosCommand::List {
                search_query,
                limit,
            } => {
                videos::query(&app, limit, search_query)
                    .await
                    .context("Failed to query videos")?;
            }
            VideosCommand::Info { hash } => {
                let video = get_video_by_hash(&app, &hash.realize(&app).await?).await?;
                dbg!(video);
            }
        },
        Command::Update {
            max_backlog,
            subscriptions,
        } => {
            let all_subs = get_subscriptions(&app).await?;

            for sub in &subscriptions {
                if let None = all_subs.0.get(sub) {
                    bail!(
                        "Your specified subscription to update '{}' is not a subscription!",
                        sub
                    )
                }
            }

            let max_backlog = max_backlog.unwrap_or(app.config.update.max_backlog);

            update::update(&app, max_backlog, subscriptions, verbosity).await?;
        }
        Command::Subscriptions { cmd } => match cmd {
            SubscriptionCommand::Add { name, url } => {
                subscribe::subscribe(&app, name, url)
                    .await
                    .context("Failed to add a subscription")?;
            }
            SubscriptionCommand::Remove { name } => {
                subscribe::unsubscribe(&app, name)
                    .await
                    .context("Failed to remove a subscription")?;
            }
            SubscriptionCommand::List {} => {
                let all_subs = get_subscriptions(&app).await?;

                for (key, val) in all_subs.0 {
                    println!("{}: '{}'", key, val.url);
                }
            }
            SubscriptionCommand::Export {} => {
                let all_subs = get_subscriptions(&app).await?;
                for val in all_subs.0.values() {
                    println!("{}", val.url);
                }
            }
            SubscriptionCommand::Import { file, force } => {
                if let Some(file) = file {
                    let f = File::open(file).await?;

                    subscribe::import(&app, BufReader::new(f), force).await?
                } else {
                    subscribe::import(&app, BufReader::new(stdin()), force).await?
                };
            }
        },

        Command::Watch {} => watch::watch(&app).await?,

        Command::Status {} => status::show(&app).await?,
        Command::Config {} => status::config(&app)?,

        Command::Database { command } => match command {
            CacheCommand::Invalidate { hard } => cache::invalidate(&app, hard).await?,
            CacheCommand::Maintain { all } => cache::maintain(&app, all).await?,
        },

        Command::Check { command } => match command {
            CheckCommand::InfoJson { path } => {
                let string = fs::read_to_string(&path)
                    .with_context(|| format!("Failed to read '{}' to string!", path.display()))?;

                let _: InfoJson =
                    serde_json::from_str(&string).context("Failed to deserialize value")?;
            }
            CheckCommand::UpdateInfoJson { path } => {
                let string = fs::read_to_string(&path)
                    .with_context(|| format!("Failed to read '{}' to string!", path.display()))?;

                let _: HashMap<Url, InfoJson> =
                    serde_json::from_str(&string).context("Failed to deserialize value")?;
            }
        },
        Command::Comments {} => {
            comments::comments(&app).await?;
        }
        Command::Description {} => {
            todo!()
            // description::description(&app).await?;
        }
    }

    Ok(())
}