use anyhow::Context;

use super::*;

#[derive(clap::Args)]
pub struct CommandVideo {
    /// 强制刷新
    #[arg(short, long, default_value = "false")]
    force: bool,

    #[command(subcommand)]
    command: VideoCommands,
}

#[derive(Subcommand)]
enum VideoCommands {
    /// 获取课程回放列表
    #[command(visible_alias("ls"))]
    List {
        /// 显示所有学期的课程回放
        #[arg(long, default_value = "false")]
        all_term: bool,
    },

    /// 下载课程回放视频 (MP4 格式)，支持断点续传
    #[command(visible_alias("down"))]
    #[cfg(feature = "video-download")]
    Download {
        /// 课程回放 ID (形如 `e780808c9eb81f61`, 可通过 `pku3b video list` 查看)
        id: String,

        /// 在所有学期的课程回放范围中查找
        #[arg(long, default_value = "false")]
        all_term: bool,

        /// 文件下载目录 (支持相对路径)
        #[arg(short = 'o', long)]
        outdir: Option<std::path::PathBuf>,
    },
}

pub async fn run(cmd: CommandVideo) -> anyhow::Result<()> {
    match cmd.command {
        VideoCommands::List { all_term } => list(cmd.force, !all_term).await?,
        #[cfg(feature = "video-download")]
        VideoCommands::Download {
            outdir,
            id,
            all_term,
        } => download(outdir.as_deref(), cmd.force, id, !all_term).await?,
    }
    Ok(())
}

pub async fn list(force: bool, cur_term: bool) -> anyhow::Result<()> {
    let courses = load_courses(force, cur_term).await?;

    let pb = pbar::new(courses.len() as u64);
    let futs = courses.into_iter().map(async |c| -> anyhow::Result<_> {
        let c = c.get().await.context("fetch course")?;
        let vs = c.get_video_list().await.context("fetch video list")?;
        pb.inc(1);
        Ok((c, vs))
    });
    let courses = try_join_all(futs).await?;
    pb.finish_and_clear();

    let mut outbuf = Vec::new();
    let title = "课程回放";

    writeln!(outbuf, "{D}>{D:#} {B}{title}{B:#} {D}<{D:#}\n")?;

    for (c, vs) in courses {
        if vs.is_empty() {
            continue;
        }

        writeln!(outbuf, "{BL}{H1}[{}]{H1:#}{BL:#}\n", c.meta().title())?;

        for v in vs {
            writeln!(
                outbuf,
                "{D}•{D:#} {} ({}) {D}{}{D:#}",
                v.meta().title(),
                v.meta().time(),
                v.id()
            )?;
        }

        writeln!(outbuf)?;
    }

    buf_try!(@try fs::stdout().write_all(outbuf).await);
    Ok(())
}

#[cfg(feature = "video-download")]
pub async fn download(
    outdir: Option<&std::path::Path>,
    force: bool,
    id: String,
    cur_term: bool,
) -> anyhow::Result<()> {
    let outdir = outdir.unwrap_or(std::path::Path::new("."));
    if !outdir.exists() {
        anyhow::bail!("output directory {:?} not exists", outdir.display());
    }

    let (_, courses, sp) = load_client_courses(force, cur_term).await?;

    sp.set_message("finding video...");
    let mut target_video = None;
    for c in courses {
        let c = c.get().await.context("fetch course")?;

        let vs = c.get_video_list().await?;
        for v in vs {
            if v.id() == id {
                target_video = Some(v);
                break;
            }
        }

        if target_video.is_some() {
            break;
        }
    }
    let Some(v) = target_video else {
        anyhow::bail!("video with id {} not found", id);
    };

    sp.set_message("fetch video metadata...");
    let v = v.get().await?;

    drop(sp);

    println!("下载课程回放：{} ({})", v.course_name(), v.meta().title());

    // prepare download dir
    let dir = utils::projectdir()
        .cache_dir()
        .join("video_download")
        .join(&id);
    fs::create_dir_all(&dir)
        .await
        .context("create dir failed")?;

    let paths = download_segments(&v, &dir)
        .await
        .context("download ts segments")?;

    let m3u8 = dir.join("playlist").with_extension("m3u8");
    buf_try!(@try fs::write(&m3u8, v.m3u8_raw()).await);

    // merge all segments into one file
    let merged = dir.join("merged").with_extension("ts");
    merge_segments(&merged, &paths).await?;

    let dest = format!("{}_{}.mp4", v.course_name(), v.meta().title());
    let dest = outdir.join(&dest);
    log::info!("Merged segments to {}", merged.display());
    log::info!(
        r#"You may execute `ffmpeg -i "{}" -c copy "{}"` to convert it to mp4"#,
        merged.display(),
        dest.display(),
    );

    // convert the merged ts file to mp4. overwrite existing file
    let sp = pbar::new_spinner();
    sp.set_message("Converting to mp4 file...");
    let c = compio::process::Command::new("ffmpeg")
        .args(["-y", "-hide_banner", "-loglevel", "quiet"])
        .args(["-i", merged.to_string_lossy().as_ref()])
        .args(["-c", "copy"])
        .arg(&dest)
        .output()
        .await
        .context("execute ffmpeg")?;
    drop(sp);

    if c.status.success() {
        println!(
            "下载完成, 文件保存为: {GR}{H2}{}{H2:#}{GR:#}",
            dest.display()
        );
    } else {
        anyhow::bail!("ffmpeg failed with exit code {:?}", c.status.code());
    }

    Ok(())
}

#[cfg(feature = "video-download")]
async fn download_segments(
    v: &CourseVideo,
    dir: impl AsRef<std::path::Path>,
) -> anyhow::Result<Vec<std::path::PathBuf>> {
    let dir = dir.as_ref();
    if !dir.exists() {
        anyhow::bail!("dir {} not exists", dir.display());
    }

    let tot = v.len_segments();
    let pb = pbar::new(tot as u64).with_prefix("download");
    pb.tick();

    let mut key = None;
    let mut paths = Vec::new();
    // faster than try_join_all
    for i in 0..tot {
        key = v.refresh_key(i, key);
        let path = dir.join(&v.segment(i).uri).with_extension("ts");

        if !path.exists() {
            log::debug!("key: {key:?}");
            let seg = v
                .get_segment_data(i, key)
                .await
                .with_context(|| format!("get segment #{i} with key {key:?}"))?;

            // fs::write is not atomic, so we write to a tmp file first
            let tmpath = path.with_extension("tmp");
            buf_try!(@try fs::write(&tmpath, seg).await);
            fs::rename(tmpath, &path).await.context("rename tmp file")?;
        }

        pb.inc(1);
        paths.push(path);
    }
    pb.finish_and_clear();

    Ok(paths)
}

async fn merge_segments(
    dest: impl AsRef<std::path::Path>,
    paths: &[std::path::PathBuf],
) -> anyhow::Result<()> {
    let f = fs::File::create(&dest)
        .await
        .context("create merged file failed")?;
    let mut f = std::io::Cursor::new(f);

    let pb = pbar::new(paths.len() as u64).with_prefix("merge segments");
    pb.tick();
    for p in paths {
        let data = fs::read(p).await.context("read segments failed")?;
        buf_try!(@try f.write(data).await);
        pb.inc(1);
    }
    pb.finish_and_clear();

    Ok(())
}
