use anyhow::{bail, Context};
use downloader::Downloadable;
use serde::Deserialize;
use url::Url;

pub mod constants;
pub mod downloader;

#[derive(Deserialize)]
pub struct YtccListData {
    pub url: String,
    pub title: String,
    pub description: String,
    pub publish_date: String,
    pub watch_date: Option<f64>,
    pub duration: String,
    pub thumbnail_url: String,
    pub extractor_hash: String,
    pub id: u32,
    pub playlists: Vec<YtccPlaylistData>,
}

impl std::fmt::Display for YtccListData {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
        write!(
            f,
            r#"pick {} "{}" "{}" "{}" "{}" "{}"{}"#,
            self.id,
            self.title.replace(['"', '„', '”'], "'"),
            self.publish_date,
            self.playlists
                .iter()
                .map(|p| p.name.replace('"', "'"))
                .collect::<Vec<String>>()
                .join(", "),
            Duration::from(self.duration.trim()),
            self.url.replace('"', "'"),
            "\n"
        )
    }
}

#[derive(Deserialize)]
pub struct YtccPlaylistData {
    pub name: String,
    pub url: String,
    pub reverse: bool,
}

pub enum LineCommand {
    Pick,
    Drop,
    Watch,
    Url,
}

impl std::str::FromStr for LineCommand {
    type Err = anyhow::Error;
    fn from_str(v: &str) -> Result<Self, <Self as std::str::FromStr>::Err> {
        match v {
            "pick" | "p" => Ok(Self::Pick),
            "drop" | "d" => Ok(Self::Drop),
            "watch" | "w" => Ok(Self::Watch),
            "url" | "u" => Ok(Self::Url),
            other => bail!("'{}' is not a recognized command!", other),
        }
    }
}

pub struct Line {
    pub cmd: LineCommand,
    pub id: u32,
    pub url: Url,
}

/// We expect that each line is correctly formatted, and simply use default ones if they are not
impl From<&str> for Line {
    fn from(v: &str) -> Self {
        let buf: Vec<_> = v.split_whitespace().collect();
        let url: Url = Url::parse(
            buf.last()
                .expect("This should always exists")
                .trim_matches('"'),
        )
        .expect("This parsing should work,as the url is generated");

        Line {
            cmd: buf
                .get(0)
                .unwrap_or(&"pick")
                .parse()
                .unwrap_or(LineCommand::Pick),
            id: buf.get(1).unwrap_or(&"0").parse().unwrap_or(0),
            url,
        }
    }
}

pub struct Duration {
    time: u32,
}

impl From<&str> for Duration {
    fn from(v: &str) -> Self {
        let buf: Vec<_> = v.split(':').take(2).collect();
        Self {
            time: (buf[0].parse::<u32>().expect("Should be a number") * 60)
                + buf[1].parse::<u32>().expect("Should be a number"),
        }
    }
}

impl std::fmt::Display for Duration {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
        const SECOND: u32 = 1;
        const MINUTE: u32 = 60 * SECOND;
        const HOUR: u32 = 60 * MINUTE;

        let base_hour = self.time - (self.time % HOUR);
        let base_min = (self.time % HOUR) - ((self.time % HOUR) % MINUTE);
        let base_sec = (self.time % HOUR) % MINUTE;

        let h = base_hour / HOUR;
        let m = base_min / MINUTE;
        let s = base_sec / SECOND;

        if self.time == 0 {
            write!(f, "[No Duration]")
        } else if h > 0 {
            write!(f, "[{h}h {m}m]")
        } else {
            write!(f, "[{m}m {s}s]")
        }
    }
}
#[cfg(test)]
mod test {
    use crate::Duration;

    #[test]
    fn test_display_duration_1h() {
        let dur = Duration { time: 60 * 60 };
        assert_eq!("[1h 0m]".to_owned(), dur.to_string());
    }
    #[test]
    fn test_display_duration_30min() {
        let dur = Duration { time: 60 * 30 };
        assert_eq!("[30m 0s]".to_owned(), dur.to_string());
    }
}

pub fn ytcc_drop(id: u32) -> anyhow::Result<()> {
    let mut ytcc = std::process::Command::new("ytcc");
    ytcc.args(["mark", &format!("{}", id)]);
    if !ytcc.status().context("Failed to run ytcc")?.success() {
        bail!("`ytcc mark {}` failed to execute", id)
    }
    Ok(())
}

pub fn filter_line(line: &str) -> anyhow::Result<Option<Downloadable>> {
    // Filter out comments and empty lines
    if line.starts_with('#') || line.trim().is_empty() {
        return Ok(None);
    }

    let line = Line::from(line);
    match line.cmd {
        LineCommand::Pick => Ok(None),
        LineCommand::Drop => ytcc_drop(line.id)
            .with_context(|| format!("Failed to drop: {}", line.id))
            .map(|_| None),
        LineCommand::Watch => Ok(Some(Downloadable {
            id: Some(line.id),
            url: line.url,
        })),
        LineCommand::Url => {
            let mut firefox = std::process::Command::new("firefox");
            firefox.args(["-P", "timesinks.youtube"]);
            firefox.arg(line.url.as_str());
            let _handle = firefox.spawn().context("Failed to run firefox")?;
            Ok(None)
        }
    }
}