//! reddit's command descriptor table + executor. The single source for reddit's
//! CLI/daemon surface; replaces the hand-written match in `tail-fin-cli`.

use tail_fin_core::{ArgSpec, ArgType, CommandSpec, Transport};

// Shared arg fragments (every listing command takes --limit/--after).
const LIMIT: ArgSpec = ArgSpec::flag_def("limit", ArgType::U64, "25", "Max items");
const AFTER: ArgSpec = ArgSpec::flag("after", ArgType::Str, "Pagination cursor");

pub const REDDIT_COMMANDS: &[CommandSpec] = &[
    CommandSpec::json(
        "hot",
        "reddit.hot",
        "Display trending posts",
        &[LIMIT, AFTER],
        Transport::Browser,
    ),
    CommandSpec::json(
        "frontpage",
        "reddit.frontpage",
        "Show frontpage feed",
        &[LIMIT, AFTER],
        Transport::Browser,
    ),
    CommandSpec::json(
        "popular",
        "reddit.popular",
        "View popular content",
        &[LIMIT, AFTER],
        Transport::Browser,
    ),
    CommandSpec::json(
        "search",
        "reddit.search",
        "Find posts and content",
        &[
            ArgSpec::pos("query", ArgType::Str, "Search query"),
            LIMIT,
            AFTER,
        ],
        Transport::Browser,
    ),
    CommandSpec::json(
        "subreddit",
        "reddit.subreddit",
        "Access specific community",
        &[
            ArgSpec::pos("name", ArgType::Str, "Subreddit name"),
            LIMIT,
            AFTER,
        ],
        Transport::Browser,
    ),
    CommandSpec::json(
        "read",
        "reddit.read",
        "Open post thread",
        &[
            ArgSpec::pos(
                "post_id",
                ArgType::Str,
                "Post ID (e.g. abc123 or t3_abc123)",
            ),
            ArgSpec::flag_def("depth", ArgType::U64, "5", "Comment depth"),
        ],
        Transport::Browser,
    ),
    CommandSpec::json(
        "user",
        "reddit.user",
        "View user profile",
        &[ArgSpec::pos("username", ArgType::Str, "Username")],
        Transport::Browser,
    ),
    CommandSpec::json(
        "user-posts",
        "reddit.user-posts",
        "List user's submissions",
        &[
            ArgSpec::pos("username", ArgType::Str, "Username"),
            LIMIT,
            AFTER,
        ],
        Transport::Browser,
    ),
    CommandSpec::json(
        "user-comments",
        "reddit.user-comments",
        "List user's replies",
        &[
            ArgSpec::pos("username", ArgType::Str, "Username"),
            LIMIT,
            AFTER,
        ],
        Transport::Browser,
    ),
    CommandSpec::json(
        "saved",
        "reddit.saved",
        "Display bookmarks (requires login)",
        &[LIMIT, AFTER],
        Transport::Browser,
    ),
    CommandSpec::json(
        "upvoted",
        "reddit.upvoted",
        "Show liked posts (requires login)",
        &[LIMIT, AFTER],
        Transport::Browser,
    ),
    CommandSpec::json(
        "upvote",
        "reddit.upvote",
        "Like a post",
        &[ArgSpec::pos("post_id", ArgType::Str, "Post ID")],
        Transport::Browser,
    ),
    CommandSpec::json(
        "save",
        "reddit.save",
        "Bookmark content",
        &[ArgSpec::pos("post_id", ArgType::Str, "Post ID")],
        Transport::Browser,
    ),
    CommandSpec::json(
        "comment",
        "reddit.comment",
        "Reply to post",
        &[
            ArgSpec::pos("post_id", ArgType::Str, "Post ID"),
            ArgSpec::pos("text", ArgType::Str, "Comment body"),
        ],
        Transport::Browser,
    ),
    CommandSpec::json(
        "subscribe",
        "reddit.subscribe",
        "Follow community",
        &[ArgSpec::pos(
            "subreddit",
            ArgType::Str,
            "Subreddit name (without r/)",
        )],
        Transport::Browser,
    ),
];

use async_trait::async_trait;
use serde_json::Value;
use tail_fin_common::TailFinError;
use tail_fin_core::{params, CommandModule, Executor, SessionCtx};

use crate::RedditClient;

/// Per-session reddit executor wrapping a browser-bound `RedditClient`.
pub struct RedditExecutor {
    client: RedditClient,
}

#[async_trait]
impl Executor for RedditExecutor {
    async fn execute(&self, wire: &str, p: Value) -> Result<Value, TailFinError> {
        let c = &self.client;
        let v = match wire {
            "reddit.hot" => serde_json::to_value(
                c.hot(
                    params::opt_u64(&p, "limit").unwrap_or(25) as usize,
                    params::opt_str(&p, "after"),
                )
                .await?,
            )?,
            "reddit.frontpage" => serde_json::to_value(
                c.frontpage(
                    params::opt_u64(&p, "limit").unwrap_or(25) as usize,
                    params::opt_str(&p, "after"),
                )
                .await?,
            )?,
            "reddit.popular" => serde_json::to_value(
                c.popular(
                    params::opt_u64(&p, "limit").unwrap_or(25) as usize,
                    params::opt_str(&p, "after"),
                )
                .await?,
            )?,
            "reddit.search" => serde_json::to_value(
                c.search(
                    params::req_str(&p, "query")?,
                    params::opt_u64(&p, "limit").unwrap_or(25) as usize,
                    params::opt_str(&p, "after"),
                )
                .await?,
            )?,
            "reddit.subreddit" => serde_json::to_value(
                c.subreddit(
                    params::req_str(&p, "name")?,
                    params::opt_u64(&p, "limit").unwrap_or(25) as usize,
                    params::opt_str(&p, "after"),
                )
                .await?,
            )?,
            "reddit.read" => serde_json::to_value(
                c.read(
                    params::req_str(&p, "post_id")?,
                    params::opt_u64(&p, "depth").unwrap_or(5) as u32,
                )
                .await?,
            )?,
            "reddit.user" => serde_json::to_value(c.user(params::req_str(&p, "username")?).await?)?,
            "reddit.user-posts" => serde_json::to_value(
                c.user_posts(
                    params::req_str(&p, "username")?,
                    params::opt_u64(&p, "limit").unwrap_or(25) as usize,
                    params::opt_str(&p, "after"),
                )
                .await?,
            )?,
            "reddit.user-comments" => serde_json::to_value(
                c.user_comments(
                    params::req_str(&p, "username")?,
                    params::opt_u64(&p, "limit").unwrap_or(25) as usize,
                    params::opt_str(&p, "after"),
                )
                .await?,
            )?,
            "reddit.saved" => serde_json::to_value(
                c.saved(
                    params::opt_u64(&p, "limit").unwrap_or(25) as usize,
                    params::opt_str(&p, "after"),
                )
                .await?,
            )?,
            "reddit.upvoted" => serde_json::to_value(
                c.upvoted(
                    params::opt_u64(&p, "limit").unwrap_or(25) as usize,
                    params::opt_str(&p, "after"),
                )
                .await?,
            )?,
            "reddit.upvote" => {
                serde_json::to_value(c.upvote(params::req_str(&p, "post_id")?).await?)?
            }
            "reddit.save" => {
                serde_json::to_value(c.save_post(params::req_str(&p, "post_id")?).await?)?
            }
            "reddit.comment" => serde_json::to_value(
                c.comment(
                    params::req_str(&p, "post_id")?,
                    params::req_str(&p, "text")?,
                )
                .await?,
            )?,
            "reddit.subscribe" => {
                serde_json::to_value(c.subscribe(params::req_str(&p, "subreddit")?).await?)?
            }
            other => return Err(TailFinError::Api(format!("unknown reddit wire: {other}"))),
        };
        Ok(v)
    }
}

/// reddit's registration entry point.
pub struct RedditModule;

impl CommandModule for RedditModule {
    fn site_id() -> &'static str {
        "reddit"
    }
    fn specs() -> &'static [CommandSpec] {
        REDDIT_COMMANDS
    }
    fn build_executor(ctx: SessionCtx) -> Box<dyn Executor> {
        // reddit is browser-only; the generic adapter always hands us Browser.
        match ctx {
            SessionCtx::Browser { session, .. } => Box::new(RedditExecutor {
                client: RedditClient::new(session),
            }),
            _ => tail_fin_core::unavailable_executor(
                "reddit",
                "a browser session (Transport::Browser)",
            ),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use tail_fin_core::OutputShape;

    #[test]
    fn commands_are_well_formed() {
        assert_eq!(REDDIT_COMMANDS.len(), 15);
        tail_fin_core::assert_commands_well_formed("reddit", REDDIT_COMMANDS);
        // reddit is entirely browser + JSON.
        assert!(REDDIT_COMMANDS
            .iter()
            .all(|c| matches!(c.transport, Transport::Browser)));
        assert!(REDDIT_COMMANDS
            .iter()
            .all(|c| matches!(c.output, OutputShape::Json)));
    }

    // The executor's handled wires, kept in lockstep with REDDIT_COMMANDS.
    // If you add a command to REDDIT_COMMANDS, add its wire here AND an arm to
    // RedditExecutor::execute; this test fails until the lists match.
    const HANDLED_WIRES: &[&str] = &[
        "reddit.hot",
        "reddit.frontpage",
        "reddit.popular",
        "reddit.search",
        "reddit.subreddit",
        "reddit.read",
        "reddit.user",
        "reddit.user-posts",
        "reddit.user-comments",
        "reddit.saved",
        "reddit.upvoted",
        "reddit.upvote",
        "reddit.save",
        "reddit.comment",
        "reddit.subscribe",
    ];

    #[test]
    fn every_spec_wire_has_a_handler() {
        let spec_wires: std::collections::BTreeSet<&str> =
            REDDIT_COMMANDS.iter().map(|c| c.wire).collect();
        let handled: std::collections::BTreeSet<&str> = HANDLED_WIRES.iter().copied().collect();
        assert_eq!(
            spec_wires, handled,
            "REDDIT_COMMANDS and RedditExecutor arms diverged"
        );
    }
}
