// 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::{
    fs::{self, canonicalize},
    io::{stderr, stdout, Read},
    mem,
    os::unix::fs::symlink,
    path::PathBuf,
    process::Command,
    sync::mpsc::{self, Receiver, Sender},
    thread::{self, JoinHandle},
};

use anyhow::{bail, Context, Result};
use log::{debug, error, warn};
use url::Url;

use crate::constants::{status_path, CONCURRENT, DOWNLOAD_DIR, MPV_FLAGS, YT_DLP_FLAGS};

#[derive(Debug)]
pub struct Downloadable {
    pub url: Url,
    pub id: Option<u32>,
}

impl std::fmt::Display for Downloadable {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
        write!(
            f,
            "{}|{}",
            self.url.as_str().replace('|', ";"),
            self.id.unwrap_or(0),
        )
    }
}

pub struct Downloader {
    sent: usize,
    download_thread: JoinHandle<Result<()>>,
    orx: Receiver<(PathBuf, Option<u32>)>,
    itx: Option<Sender<Downloadable>>,
    playspec: Vec<Downloadable>,
}

impl Downloader {
    pub fn new(mut playspec: Vec<Downloadable>) -> anyhow::Result<Downloader> {
        let (itx, irx): (Sender<Downloadable>, Receiver<Downloadable>) = mpsc::channel();
        let (otx, orx) = mpsc::channel();

        let jh = thread::spawn(move || -> Result<()> {
            while let Ok(pt) = irx.recv() {
                debug!("Got '{}' to be downloaded", pt);
                let path = download_url(&pt.url)
                    .with_context(|| format!("Failed to download url: '{}'", &pt.url))?;
                otx.send((path, pt.id)).expect("Should not be dropped");
            }
            debug!("Finished Downloading everything");
            Ok(())
        });

        playspec.reverse();
        let mut output = Downloader {
            sent: 0,
            download_thread: jh,
            orx,
            itx: Some(itx),
            playspec,
        };

        if output.playspec.len() <= CONCURRENT as usize {
            output.add(output.playspec.len() as u32)?;
        } else {
            output.add(CONCURRENT)?;
        }
        Ok(output)
    }

    pub fn add(&mut self, number_to_add: u32) -> Result<()> {
        debug!("Adding {} to be downloaded concurrently", number_to_add);
        for _ in 0..number_to_add {
            let pt = self.playspec.pop().expect("This call should be guarded");
            self.itx.as_ref().expect("Should still be valid").send(pt)?;
            self.sent += 1;
        }
        Ok(())
    }

    /// Return the next video already downloaded, will block until the download is complete
    pub fn next(&mut self) -> Option<(PathBuf, Option<u32>)> {
        debug!("Requesting next output");
        match self.orx.recv() {
            Ok(ok) => {
                debug!("Output downloaded to: {}", ok.0.display());
                if !self.playspec.is_empty() {
                    self.add(1).ok()?;
                } else {
                    debug!(
                        "Done sending videos to be downloaded, downoladed: {} videos",
                        self.sent
                    );
                    let itx = mem::take(&mut self.itx);
                    drop(itx)
                }
                debug!("Returning: {}|{}", ok.0.display(), ok.1.unwrap_or(0));
                Some(ok)
            }
            Err(err) => {
                debug!("Received error while listening: {}", err);
                None
            }
        }
    }

    pub fn drop(self) -> anyhow::Result<()> {
        // Check that we really downloaded everything
        assert_eq!(self.playspec.len(), 0);
        match self.download_thread.join() {
            Ok(ok) => ok,
            Err(err) => panic!("Failed to join downloader thread: '{:#?}'", err),
        }
    }

    pub fn consume(mut self) -> anyhow::Result<()> {
        while let Some((path, id)) = self.next() {
            debug!("Next path to play is: '{}'", path.display());
            let mut info_json = canonicalize(&path).context("Failed to canoncialize path")?;
            info_json.set_extension("info.json");

            if status_path()?.is_symlink() {
                fs::remove_file(status_path()?).context("Failed to delete old status file")?;
            } else if !status_path()?.exists() {
                debug!(
                    "The status path at '{}' does not exists",
                    status_path()?.display()
                );
            } else {
                bail!(
                    "The status path ('{}') is not a symlink but exists!",
                    status_path()?.display()
                );
            }

            symlink(info_json, status_path()?).context("Failed to symlink")?;

            let mut mpv = Command::new("mpv");
            mpv.stdout(stdout());
            mpv.stderr(stderr());
            mpv.args(MPV_FLAGS);
            // TODO: Set the title to the name of the video, not the path <2024-02-09>
            // mpv.arg(format!("--title="))
            mpv.arg(&path);

            let status = mpv.status().context("Failed to run mpv")?;
            if status.success() {
                fs::remove_file(&path)?;
                if let Some(id) = id {
                    println!("\x1b[32;1mMarking {} as watched!\x1b[0m", id);
                    let mut ytcc = std::process::Command::new("ytcc");
                    ytcc.stdout(stdout());
                    ytcc.stderr(stderr());
                    ytcc.args(["mark"]);
                    ytcc.arg(id.to_string());
                    let status = ytcc.status().context("Failed to run ytcc")?;
                    if let Some(code) = status.code() {
                        if code != 0 {
                            bail!("Ytcc failed with status: {}", code);
                        }
                    }
                }
                debug!("mpv exited with: '{}'", status);
            } else {
                warn!("mpv exited with: '{}'", status);
            }
        }
        self.drop()?;
        Ok(())
    }
}

fn download_url(url: &Url) -> Result<PathBuf> {
    let output_file = tempfile::NamedTempFile::new().context("Failed to create tempfile")?;
    output_file
        .as_file()
        .set_len(0)
        .context("Failed to truncate temp-file")?;
    if !Into::<PathBuf>::into(DOWNLOAD_DIR).exists() {
        fs::create_dir_all(DOWNLOAD_DIR)
            .with_context(|| format!("Failed to create download dir at: {}", DOWNLOAD_DIR))?
    }
    let mut yt_dlp = Command::new("yt-dlp");
    yt_dlp.current_dir(DOWNLOAD_DIR);
    yt_dlp.stdout(stdout());
    yt_dlp.stderr(stderr());
    yt_dlp.args(YT_DLP_FLAGS);
    yt_dlp.args([
        "--output",
        "%(channel)s/%(title)s.%(ext)s",
        url.as_str(),
        "--print-to-file",
        "after_move:filepath",
    ]);
    yt_dlp.arg(output_file.path().as_os_str());

    let status = yt_dlp.status().context("Failed to run yt-dlp")?;
    if !status.success() {
        error!("yt-dlp execution failed with error: '{}'", status);
    }

    let mut path = String::new();
    output_file
        .as_file()
        .read_to_string(&mut path)
        .context("Failed to read output file temp file")?;
    let path = path.trim();
    Ok(path.into())
}