// 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::path::PathBuf;

use anyhow::{bail, Error};
use chrono::NaiveDate;
use clap::{ArgAction, Args, Parser, Subcommand};
use url::Url;

use crate::{
    constants, select::selection_file::duration::Duration,
    storage::video_database::extractor_hash::LazyExtractorHash,
};

#[derive(Parser, Debug)]
#[clap(author, version, about, long_about = None)]
/// An command line interface to select, download and watch videos
pub struct CliArgs {
    #[command(subcommand)]
    /// The subcommand to execute [default: select]
    pub command: Option<Command>,

    /// Increase message verbosity
    #[arg(long="verbose", short = 'v', action = ArgAction::Count)]
    pub verbosity: u8,

    /// Set the path to the videos.db. Otherwise use the default location
    #[arg(long, short)]
    pub db_path: Option<PathBuf>,

    /// Silence all output
    #[arg(long, short = 'q')]
    pub quiet: bool,
}

#[derive(Subcommand, Debug)]
pub enum Command {
    /// Download and cache URLs
    Download {
        /// Forcefully re-download all cached videos (i.e. delete the cache path, then download).
        #[arg(short, long)]
        force: bool,

        /// The maximum size the download dir should have. Beware that the value must be given in
        /// bytes.
        #[arg(short, long, default_value = "3 GiB", value_parser = byte_parser)]
        max_cache_size: u64,
    },

    /// Watch the already cached (and selected) videos
    Watch {},

    /// Show, which videos have been selected to be watched (and their cache status)
    Status {},

    /// Perform various tests
    Check {
        #[command(subcommand)]
        command: CheckCommand,
    },

    /// Display the comments of the currently playing video
    Comments {},
    /// Display the description of the currently playing video
    Description {},

    /// Manipulate the video cache in the database
    #[command(visible_alias = "db")]
    Database {
        #[command(subcommand)]
        command: CacheCommand,
    },

    /// Change the state of videos in the database (the default)
    Select {
        #[command(subcommand)]
        cmd: Option<SelectCommand>,
    },

    /// Update the video database
    Update {
        #[arg(short, long, default_value = "20")]
        /// The number of videos to updating
        max_backlog: u32,

        #[arg(short, long)]
        /// The subscriptions to update (can be given multiple times)
        subscriptions: Vec<String>,

        #[arg(short, long, default_value = "6")]
        /// How many processes to spawn at the same time
        concurrent_processes: usize,
    },

    /// Manipulate subscription
    #[command(visible_alias = "subs")]
    Subscriptions {
        #[command(subcommand)]
        cmd: SubscriptionCommand,
    },
}

fn byte_parser(s: &str) -> Result<u64, Error> {
    const B: u64 = 1;

    const KIB: u64 = 1024 * B;
    const MIB: u64 = 1024 * KIB;
    const GIB: u64 = 1024 * MIB;

    const KB: u64 = 1000 * B;
    const MB: u64 = 1000 * KB;
    const GB: u64 = 1000 * MB;

    let s = s
        .chars()
        .filter(|elem| !elem.is_whitespace())
        .collect::<String>();

    let number: u64 = s
        .chars()
        .take_while(|x| x.is_numeric())
        .collect::<String>()
        .parse()?;
    let extension = s.chars().skip_while(|x| x.is_numeric()).collect::<String>();

    let output = match extension.to_lowercase().as_str() {
        "" => number,
        "b" => number * B,
        "kib" => number * KIB,
        "mib" => number * MIB,
        "gib" => number * GIB,
        "kb" => number * KB,
        "mb" => number * MB,
        "gb" => number * GB,
        other => bail!(
            "Your extension '{}' is not yet supported. Only KB,MB,GB or KiB,MiB,GiB are supported",
            other
        ),
    };

    Ok(output)
}

impl Default for Command {
    fn default() -> Self {
        Self::Select {
            cmd: Some(SelectCommand::default()),
        }
    }
}

#[derive(Subcommand, Clone, Debug)]
pub enum SubscriptionCommand {
    /// Subscribe to an URL
    Add {
        #[arg(short, long)]
        /// The human readable name of the subscription
        name: Option<String>,

        /// The URL to listen to
        url: Url,
    },

    /// Unsubscribe from an URL
    Remove {
        /// The human readable name of the subscription
        name: String,
    },

    /// Import a bunch of URLs as subscriptions.
    Import {
        /// The file containing the URLs. Will use Stdin otherwise.
        file: Option<PathBuf>,

        /// Remove any previous subscriptions
        #[arg(short, long)]
        force: bool,
    },

    /// List all subscriptions
    List {
        /// Only show the URLs
        #[arg(short, long)]
        url: bool,
    },
}

#[derive(Clone, Debug, Args)]
#[command(infer_subcommands = true)]
/// Mark the video given by the hash to be watched
pub struct SharedSelectionCommandArgs {
    /// The ordering priority (higher means more at the top)
    #[arg(short, long)]
    pub priority: Option<i64>,

    /// The short extractor hash
    pub hash: LazyExtractorHash,

    pub title: String,

    pub date: NaiveDate,

    pub publisher: String,

    pub duration: Duration,

    pub url: Url,
}

#[derive(Subcommand, Clone, Debug)]
#[command(infer_subcommands = true)]
// NOTE: Keep this in sync with the [`constants::HELP_STR`] constant. <2024-08-20>
pub enum SelectCommand {
    /// Open a `git rebase` like file to select the videos to watch (the default)
    File {
        /// Include done (watched, dropped) videos
        #[arg(long, short)]
        done: bool,
    },

    Watch {
        #[command(flatten)]
        shared: SharedSelectionCommandArgs,

        /// The subtitles to download (e.g. 'en,de,sv')
        #[arg(short = 'l', long, default_value = constants::DEFAULT_SUBTITLE_LANGS)]
        subtitle_langs: String,

        /// The speed to set mpv to
        // NOTE: KEEP THIS IN SYNC WITH THE `DEFAULT_MPV_PLAYBACK_SPEED` in `constants.rs` <2024-08-20>
        #[arg(short, long, default_value = "2.7")]
        speed: f64,
    },

    /// Mark the video given by the hash to be dropped
    Drop {
        #[command(flatten)]
        shared: SharedSelectionCommandArgs,
    },

    /// Open the video URL in Firefox's `timesinks.youtube` profile
    Url {
        #[command(flatten)]
        shared: SharedSelectionCommandArgs,
    },

    /// Reset the videos status to 'Pick'
    Pick {
        #[command(flatten)]
        shared: SharedSelectionCommandArgs,
    },
}
impl Default for SelectCommand {
    fn default() -> Self {
        Self::File { done: false }
    }
}

#[derive(Subcommand, Clone, Debug)]
pub enum CheckCommand {
    /// Check if the given info.json is deserializable
    InfoJson { path: PathBuf },

    /// Check if the given update info.json is deserializable
    UpdateInfoJson { path: PathBuf },
}

#[derive(Subcommand, Clone, Copy, Debug)]
pub enum CacheCommand {
    /// Invalidate all cache entries
    Invalidate {
        /// Also delete the cache path
        #[arg(short, long)]
        hard: bool,
    },

    /// Perform basic maintenance operations on the database.
    /// This helps recovering from invalid db states after a crash (or force exit via CTRL+C).
    ///
    /// 1. Check every path for validity (removing all invalid cache entries)
    /// 2. Reset all `status_change` bits of videos to false.
    #[command(verbatim_doc_comment)]
    Maintain {
        /// Check every video (otherwise only the videos to be watched are checked)
        #[arg(short, long)]
        all: bool,
    },
}