// 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::{
    env,
    fs::{self},
    io::Write,
    mem,
    path::PathBuf,
    process::{Command, Stdio},
};

use anyhow::{bail, Context, Result};
use comment::{CommentExt, Comments};
use regex::Regex;
use yt_dlp::wrapper::info_json::{Comment, InfoJson, Parent};

use crate::{
    app::App,
    storage::video_database::{
        getters::{get_currently_playing_video, get_video_info_json},
        Video,
    },
};

mod comment;
mod display;

fn get_runtime_path(component: &'static str) -> anyhow::Result<PathBuf> {
    let out: PathBuf = format!(
        "{}/{}",
        env::var("XDG_RUNTIME_DIR").expect("This should always exist"),
        component
    )
    .into();
    fs::create_dir_all(out.parent().expect("Parent should exist"))?;
    Ok(out)
}

const STATUS_PATH: &str = "ytcc/running";
pub fn status_path() -> anyhow::Result<PathBuf> {
    get_runtime_path(STATUS_PATH)
}

pub async fn get_comments(app: &App) -> Result<Comments> {
    let currently_playing_video: Video =
        if let Some(video) = get_currently_playing_video(&app).await? {
            video
        } else {
            bail!("Could not find a currently playing video!");
        };

    let mut info_json: InfoJson = get_video_info_json(&currently_playing_video)
        .await?
        .expect("A currently *playing* must be cached. And thus the info.json should be available");

    let base_comments = mem::take(&mut info_json.comments).expect("A video should have comments");
    drop(info_json);

    let mut comments = Comments::new();
    base_comments.into_iter().for_each(|c| {
        if let Parent::Id(id) = &c.parent {
            comments.insert(&(id.clone()), CommentExt::from(c));
        } else {
            comments.push(CommentExt::from(c));
        }
    });

    comments.vec.iter_mut().for_each(|comment| {
       let replies = mem::take(&mut comment.replies);
       let mut output_replies: Vec<CommentExt>  = vec![];

       let re = Regex::new(r"\u{200b}?(@[^\t\s]+)\u{200b}?").unwrap();
       for reply in replies {
           if let Some(replyee_match) =  re.captures(&reply.value.text){
               let full_match = replyee_match.get(0).expect("This always exists");
               let text = reply.
                   value.
                   text[0..full_match.start()]
                   .to_owned()
                   +
                   &reply
                   .value
                   .text[full_match.end()..];
               let text: &str = text.trim().trim_matches('\u{200b}');

               let replyee = replyee_match.get(1).expect("This should exist").as_str();


               if let Some(parent) = output_replies
                   .iter_mut()
                   // .rev()
                   .flat_map(|com| &mut com.replies)
                   .flat_map(|com| &mut com.replies)
                   .flat_map(|com| &mut com.replies)
                   .filter(|com| com.value.author == replyee)
                   .last()
               {
                   parent.replies.push(CommentExt::from(Comment {
                       text: text.to_owned(),
                       ..reply.value
                   }))
               } else if let Some(parent) = output_replies
                   .iter_mut()
                   // .rev()
                   .flat_map(|com| &mut com.replies)
                   .flat_map(|com| &mut com.replies)
                   .filter(|com| com.value.author == replyee)
                   .last()
               {
                   parent.replies.push(CommentExt::from(Comment {
                       text: text.to_owned(),
                       ..reply.value
                   }))
               } else if let Some(parent) = output_replies
                   .iter_mut()
                   // .rev()
                   .flat_map(|com| &mut com.replies)
                   .filter(|com| com.value.author == replyee)
                   .last()
               {
                   parent.replies.push(CommentExt::from(Comment {
                       text: text.to_owned(),
                       ..reply.value
                   }))
               } else if let Some(parent) = output_replies.iter_mut()
                   // .rev()
                   .filter(|com| com.value.author == replyee)
                   .last()
               {
                   parent.replies.push(CommentExt::from(Comment {
                       text: text.to_owned(),
                       ..reply.value
                   }))
               } else {
                   eprintln!(
                   "Failed to find a parent for ('{}') both directly and via replies! The reply text was:\n'{}'\n",
                   replyee,
                   reply.value.text
               );
                   output_replies.push(reply);
               }
           } else {
               output_replies.push(reply);
           }
       }
       comment.replies = output_replies;
    });

    Ok(comments)
}

pub async fn comments(app: &App) -> Result<()> {
    let comments = get_comments(app).await?;

    let mut less = Command::new("less")
        .args(["--raw-control-chars"])
        .stdin(Stdio::piped())
        .stderr(Stdio::inherit())
        .spawn()
        .context("Failed to run less")?;

    let mut child = Command::new("fmt")
        .args(["--uniform-spacing", "--split-only", "--width=90"])
        .stdin(Stdio::piped())
        .stderr(Stdio::inherit())
        .stdout(less.stdin.take().expect("Should be open"))
        .spawn()
        .context("Failed to run fmt")?;

    let mut stdin = child.stdin.take().context("Failed to open stdin")?;
    std::thread::spawn(move || {
        stdin
            .write_all(comments.render(true).as_bytes())
            .expect("Should be able to write to stdin of fmt");
    });

    let _ = less.wait().context("Failed to await less")?;

    Ok(())
}

#[cfg(test)]
mod test {
    #[test]
    fn test_string_replacement() {
        let s = "A \n\nB\n\nC".to_owned();
        assert_eq!("A \n  \n  B\n  \n  C", s.replace('\n', "\n  "))
    }
}