pub mod actions;
pub mod command_module;
pub mod parsing;
pub mod site;
pub mod types;

use tail_fin_common::page::{ensure_on_domain, page_fetch};
use tail_fin_common::BrowserSession;
use tail_fin_common::TailFinError;

pub use site::RedditSite;
pub use types::{
    ActionResult, RedditComment, RedditCommentsListing, RedditListing, RedditPost, RedditThread,
    RedditUser,
};

pub(crate) const REDDIT_DOMAINS: &[&str] = &["www.reddit.com", "old.reddit.com", "reddit.com"];

pub struct RedditClient {
    session: BrowserSession,
}

impl RedditClient {
    pub fn new(session: BrowserSession) -> Self {
        Self { session }
    }

    async fn reddit_get(&self, path: &str) -> Result<serde_json::Value, TailFinError> {
        ensure_on_domain(&self.session, REDDIT_DOMAINS).await?;
        let headers = serde_json::json!({});
        let url = format!("https://www.reddit.com{}", path);
        page_fetch(&self.session, &url, "GET", &headers).await
    }

    fn after_param(after: Option<&str>) -> String {
        match after {
            Some(a) => format!("&after={}", a),
            None => String::new(),
        }
    }

    pub async fn hot(
        &self,
        limit: usize,
        after: Option<&str>,
    ) -> Result<RedditListing, TailFinError> {
        let data = self
            .reddit_get(&format!(
                "/hot.json?limit={}&raw_json=1{}",
                limit,
                Self::after_param(after)
            ))
            .await?;
        Ok(parsing::parse_listing(&data, limit))
    }

    pub async fn frontpage(
        &self,
        limit: usize,
        after: Option<&str>,
    ) -> Result<RedditListing, TailFinError> {
        let data = self
            .reddit_get(&format!(
                "/.json?limit={}&raw_json=1{}",
                limit,
                Self::after_param(after)
            ))
            .await?;
        Ok(parsing::parse_listing(&data, limit))
    }

    pub async fn popular(
        &self,
        limit: usize,
        after: Option<&str>,
    ) -> Result<RedditListing, TailFinError> {
        let data = self
            .reddit_get(&format!(
                "/r/popular.json?limit={}&raw_json=1{}",
                limit,
                Self::after_param(after)
            ))
            .await?;
        Ok(parsing::parse_listing(&data, limit))
    }

    pub async fn search(
        &self,
        query: &str,
        limit: usize,
        after: Option<&str>,
    ) -> Result<RedditListing, TailFinError> {
        let q = urlencoding::encode(query);
        let data = self
            .reddit_get(&format!(
                "/search.json?q={}&limit={}&type=link&raw_json=1{}",
                q,
                limit,
                Self::after_param(after)
            ))
            .await?;
        Ok(parsing::parse_listing(&data, limit))
    }

    pub async fn subreddit(
        &self,
        name: &str,
        limit: usize,
        after: Option<&str>,
    ) -> Result<RedditListing, TailFinError> {
        let data = self
            .reddit_get(&format!(
                "/r/{}.json?limit={}&raw_json=1{}",
                name,
                limit,
                Self::after_param(after)
            ))
            .await?;
        Ok(parsing::parse_listing(&data, limit))
    }

    pub async fn read(&self, post_id: &str, depth: u32) -> Result<RedditThread, TailFinError> {
        let id = post_id.trim_start_matches("t3_");
        let data = self
            .reddit_get(&format!("/comments/{}.json?depth={}&raw_json=1", id, depth))
            .await?;
        parsing::parse_thread(&data, depth)
            .ok_or_else(|| TailFinError::Parse("Failed to parse Reddit thread".into()))
    }

    pub async fn user(&self, username: &str) -> Result<RedditUser, TailFinError> {
        let data = self
            .reddit_get(&format!("/user/{}/about.json?raw_json=1", username))
            .await?;
        parsing::parse_user(&data)
            .ok_or_else(|| TailFinError::Parse(format!("User not found: {}", username)))
    }

    pub async fn user_posts(
        &self,
        username: &str,
        limit: usize,
        after: Option<&str>,
    ) -> Result<RedditListing, TailFinError> {
        let data = self
            .reddit_get(&format!(
                "/user/{}/submitted.json?limit={}&raw_json=1{}",
                username,
                limit,
                Self::after_param(after)
            ))
            .await?;
        Ok(parsing::parse_listing(&data, limit))
    }

    pub async fn user_comments(
        &self,
        username: &str,
        limit: usize,
        after: Option<&str>,
    ) -> Result<RedditCommentsListing, TailFinError> {
        let data = self
            .reddit_get(&format!(
                "/user/{}/comments.json?limit={}&raw_json=1{}",
                username,
                limit,
                Self::after_param(after)
            ))
            .await?;
        Ok(parsing::parse_comments_listing(&data, limit))
    }

    pub async fn saved(
        &self,
        limit: usize,
        after: Option<&str>,
    ) -> Result<RedditListing, TailFinError> {
        let me = self.get_my_username().await?;
        let data = self
            .reddit_get(&format!(
                "/user/{}/saved.json?limit={}&raw_json=1{}",
                me,
                limit,
                Self::after_param(after)
            ))
            .await?;
        Ok(parsing::parse_listing(&data, limit))
    }

    pub async fn upvoted(
        &self,
        limit: usize,
        after: Option<&str>,
    ) -> Result<RedditListing, TailFinError> {
        let me = self.get_my_username().await?;
        let data = self
            .reddit_get(&format!(
                "/user/{}/upvoted.json?limit={}&raw_json=1{}",
                me,
                limit,
                Self::after_param(after)
            ))
            .await?;
        Ok(parsing::parse_listing(&data, limit))
    }

    pub async fn upvote(&self, post_id: &str) -> Result<ActionResult, TailFinError> {
        actions::browser_upvote(&self.session, post_id).await
    }

    pub async fn save_post(&self, post_id: &str) -> Result<ActionResult, TailFinError> {
        actions::browser_save(&self.session, post_id).await
    }

    pub async fn comment(&self, post_id: &str, text: &str) -> Result<ActionResult, TailFinError> {
        actions::browser_comment(&self.session, post_id, text).await
    }

    pub async fn subscribe(&self, subreddit: &str) -> Result<ActionResult, TailFinError> {
        actions::browser_subscribe(&self.session, subreddit).await
    }

    async fn get_my_username(&self) -> Result<String, TailFinError> {
        let data = self.reddit_get("/api/me.json").await?;
        data.pointer("/data/name")
            .or_else(|| data.get("name"))
            .and_then(|v| v.as_str())
            .map(|s| s.to_string())
            .ok_or(TailFinError::AuthRequired)
    }
}
