//! Reviewer runner infrastructure for spawning Claude sessions.

use agentic_config::types::ReviewConfig;
use agentic_tools_core::ToolContext;
use agentic_tools_core::ToolError;
use claudecode::client::Client;
use claudecode::config::MCPConfig;
use claudecode::config::MCPServer;
use claudecode::config::SessionConfig;
use claudecode::types::Model;
use claudecode::types::OutputFormat;
use claudecode::types::PermissionMode;
use claudecode::types::Result as ClaudeResult;
use futures::future::BoxFuture;
use std::collections::HashMap;
use std::future::Future;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::Semaphore;

use crate::types::ReviewLens;

/// Reviewer sub-agent builtin tools (Claude Code native).
pub const REVIEWER_BUILTIN_TOOLS: [&str; 1] = ["Read"];
const CLAUDE_TOOL_LSP: &str = "LSP";

/// Reviewer capability profile for a lens-specific reviewer session.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ReviewerCapabilityProfile {
    Narrow,
    Completeness,
}

/// Reviewer sub-agent MCP tool allowlist for the narrow profile.
pub const REVIEWER_MCP_ALLOWLIST_NARROW: [&str; 3] = ["cli_ls", "cli_grep", "cli_glob"];

/// Reviewer sub-agent MCP tool allowlist for the completeness profile.
pub const REVIEWER_MCP_ALLOWLIST_COMPLETENESS: [&str; 4] =
    ["cli_ls", "cli_grep", "cli_glob", "ask_agent"];

/// Reviewer sub-agent MCP tool names for the narrow profile.
pub const REVIEWER_MCP_TOOL_NAMES_NARROW: [&str; 3] = [
    "mcp__agentic-mcp__cli_ls",
    "mcp__agentic-mcp__cli_grep",
    "mcp__agentic-mcp__cli_glob",
];

/// Reviewer sub-agent MCP tool names for the completeness profile.
pub const REVIEWER_MCP_TOOL_NAMES_COMPLETENESS: [&str; 4] = [
    "mcp__agentic-mcp__cli_ls",
    "mcp__agentic-mcp__cli_grep",
    "mcp__agentic-mcp__cli_glob",
    "mcp__agentic-mcp__ask_agent",
];

/// Maximum concurrent Claude reviewer sessions.
pub const MAX_CONCURRENT_SESSIONS: usize = 3;

/// Fixed retry delays for session attempts: 3 total attempts with [0ms, 500ms, 1000ms].
pub const RETRY_DELAYS: [Duration; 3] = [
    Duration::from_millis(0),
    Duration::from_millis(500),
    Duration::from_secs(1),
];

/// Trait for reviewer runners (enables mocking in tests).
pub trait ReviewerRunner: Send + Sync {
    /// Run a reviewer session and return the raw text output.
    fn run_text(
        &self,
        profile: ReviewerCapabilityProfile,
        system_prompt: String,
        user_prompt: String,
        ctx: ToolContext,
    ) -> BoxFuture<'static, Result<String, ToolError>>;
}

/// Select the reviewer capability profile for a lens.
pub fn capability_profile_for_lens(lens: ReviewLens) -> ReviewerCapabilityProfile {
    match lens {
        ReviewLens::Completeness => ReviewerCapabilityProfile::Completeness,
        ReviewLens::Security
        | ReviewLens::Correctness
        | ReviewLens::Maintainability
        | ReviewLens::Testing
        | ReviewLens::Simplification => ReviewerCapabilityProfile::Narrow,
    }
}

async fn wait_for_review_result<F, C, CFn>(
    ctx: &ToolContext,
    wait_fut: F,
    cancel_fn: CFn,
) -> Result<ClaudeResult, ToolError>
where
    F: Future<Output = claudecode::Result<ClaudeResult>>,
    C: Future<Output = claudecode::Result<()>>,
    CFn: FnOnce() -> C,
{
    tokio::select! {
        () = ctx.cancelled() => {
            cancel_fn()
                .await
                .map_err(|e| ToolError::Internal(format!("Failed to cancel Claude session: {e}")))?;
            Err(ToolError::cancelled(None))
        }
        result = wait_fut => {
            result.map_err(|e| ToolError::Internal(format!("Failed to run Claude session: {e}")))
        }
    }
}

async fn run_with_optional_timeout<T, F>(timeout_secs: u64, fut: F) -> Result<T, ToolError>
where
    F: Future<Output = Result<T, ToolError>>,
{
    if timeout_secs == 0 {
        return fut.await;
    }

    tokio::time::timeout(Duration::from_secs(timeout_secs), fut)
        .await
        .map_err(|_| ToolError::Internal(format!("Timed out after {timeout_secs}s")))?
}

/// Production implementation using Claude CLI.
#[derive(Clone)]
pub struct ClaudeCliRunner {
    config: ReviewConfig,
    semaphore: Arc<Semaphore>,
}

impl ClaudeCliRunner {
    /// Create a new Claude CLI runner with the shared semaphore.
    pub fn new() -> Self {
        Self::with_config(ReviewConfig::default())
    }

    pub fn with_config(config: ReviewConfig) -> Self {
        Self {
            config,
            semaphore: Arc::new(Semaphore::new(MAX_CONCURRENT_SESSIONS)),
        }
    }

    pub fn config(&self) -> &ReviewConfig {
        &self.config
    }

    /// Build the list of builtin tool names for reviewer sessions.
    fn builtin_tools() -> Vec<String> {
        REVIEWER_BUILTIN_TOOLS
            .iter()
            .map(|s| (*s).to_string())
            .collect()
    }

    /// Build the complete list of all tool names (builtin + MCP) for reviewer sessions.
    fn all_tools(profile: ReviewerCapabilityProfile) -> Vec<String> {
        REVIEWER_BUILTIN_TOOLS
            .iter()
            .chain(Self::mcp_tool_names(profile).iter())
            .map(|s| (*s).to_string())
            .collect()
    }

    /// Select allowed MCP short names for a reviewer profile.
    fn mcp_allowlist(profile: ReviewerCapabilityProfile) -> &'static [&'static str] {
        match profile {
            ReviewerCapabilityProfile::Narrow => &REVIEWER_MCP_ALLOWLIST_NARROW,
            ReviewerCapabilityProfile::Completeness => &REVIEWER_MCP_ALLOWLIST_COMPLETENESS,
        }
    }

    /// Select allowed MCP fully qualified tool names for a reviewer profile.
    fn mcp_tool_names(profile: ReviewerCapabilityProfile) -> &'static [&'static str] {
        match profile {
            ReviewerCapabilityProfile::Narrow => &REVIEWER_MCP_TOOL_NAMES_NARROW,
            ReviewerCapabilityProfile::Completeness => &REVIEWER_MCP_TOOL_NAMES_COMPLETENESS,
        }
    }

    /// Build MCP config for reviewer subagents.
    fn mcp_config(profile: ReviewerCapabilityProfile) -> MCPConfig {
        let mut servers: HashMap<String, MCPServer> = HashMap::new();
        let args = vec![
            "--allow".to_string(),
            Self::mcp_allowlist(profile).join(","),
        ];
        servers.insert(
            "agentic-mcp".to_string(),
            MCPServer::stdio("agentic-mcp", args),
        );
        MCPConfig {
            mcp_servers: servers,
        }
    }

    fn build_session_config(
        profile: ReviewerCapabilityProfile,
        system_prompt: String,
        user_prompt: String,
    ) -> Result<SessionConfig, ToolError> {
        SessionConfig::builder(user_prompt)
            .model(Model::Opus)
            .output_format(OutputFormat::Text)
            .permission_mode(PermissionMode::DontAsk)
            .system_prompt(system_prompt)
            .tools(Self::builtin_tools())
            .allowed_tools(Self::all_tools(profile))
            .disallow_tool(CLAUDE_TOOL_LSP)
            .mcp_config(Self::mcp_config(profile))
            .strict_mcp_config(true)
            .build()
            .map_err(|e| ToolError::Internal(format!("Failed to build session config: {e}")))
    }
}

impl Default for ClaudeCliRunner {
    fn default() -> Self {
        Self::new()
    }
}

impl ReviewerRunner for ClaudeCliRunner {
    fn run_text(
        &self,
        profile: ReviewerCapabilityProfile,
        system_prompt: String,
        user_prompt: String,
        ctx: ToolContext,
    ) -> BoxFuture<'static, Result<String, ToolError>> {
        let semaphore = Arc::clone(&self.semaphore);
        let run_timeout_secs = self.config.run_timeout_secs;

        Box::pin(async move {
            // Acquire semaphore permit
            let _permit = semaphore
                .acquire()
                .await
                .map_err(|_| ToolError::Internal("Semaphore closed".into()))?;

            // Build session config
            let cfg = Self::build_session_config(profile, system_prompt, user_prompt)?;

            let result = run_with_optional_timeout(run_timeout_secs, async {
                let client = Client::new()
                    .await
                    .map_err(|e| ToolError::Internal(format!("Claude CLI not runnable: {e}")))?;

                let session = client.launch(cfg).await.map_err(|e| {
                    ToolError::Internal(format!("Failed to start Claude session: {e}"))
                })?;

                wait_for_review_result(&ctx, session.wait(), || session.cancel()).await
            })
            .await?;

            claude_result_to_text(result)
        })
    }
}

/// Extract text from a `ClaudeResult`.
fn claude_result_to_text(result: ClaudeResult) -> Result<String, ToolError> {
    if result.is_error {
        return Err(ToolError::Internal(
            result
                .error
                .unwrap_or_else(|| "Reviewer session error".into()),
        ));
    }

    result
        .result
        .filter(|s| !s.trim().is_empty())
        .or_else(|| result.content.filter(|s| !s.trim().is_empty()))
        .ok_or_else(|| ToolError::Internal("Reviewer produced no text output".into()))
}

/// Mock runner for testing.
#[cfg(test)]
pub struct MockRunner {
    responses: std::sync::Mutex<Vec<Result<String, ToolError>>>,
}

#[cfg(test)]
impl MockRunner {
    pub fn new(responses: Vec<Result<String, ToolError>>) -> Self {
        Self {
            responses: std::sync::Mutex::new(responses),
        }
    }
}

#[cfg(test)]
impl ReviewerRunner for MockRunner {
    fn run_text(
        &self,
        _profile: ReviewerCapabilityProfile,
        _system_prompt: String,
        _user_prompt: String,
        _ctx: ToolContext,
    ) -> BoxFuture<'static, Result<String, ToolError>> {
        let response = {
            let mut responses = self.responses.lock().expect("lock poisoned");
            if responses.is_empty() {
                Err(ToolError::Internal("No mock responses left".into()))
            } else {
                responses.remove(0)
            }
        };
        Box::pin(async move { response })
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::sync::atomic::AtomicBool;
    use std::sync::atomic::Ordering;

    #[test]
    fn reviewer_tool_constants_cover_both_profiles() {
        assert_eq!(REVIEWER_BUILTIN_TOOLS, ["Read"]);
        assert_eq!(
            REVIEWER_MCP_ALLOWLIST_NARROW,
            ["cli_ls", "cli_grep", "cli_glob"]
        );
        assert_eq!(
            REVIEWER_MCP_TOOL_NAMES_NARROW,
            [
                "mcp__agentic-mcp__cli_ls",
                "mcp__agentic-mcp__cli_grep",
                "mcp__agentic-mcp__cli_glob"
            ]
        );
        assert_eq!(
            REVIEWER_MCP_ALLOWLIST_COMPLETENESS,
            ["cli_ls", "cli_grep", "cli_glob", "ask_agent"]
        );
        assert_eq!(
            REVIEWER_MCP_TOOL_NAMES_COMPLETENESS,
            [
                "mcp__agentic-mcp__cli_ls",
                "mcp__agentic-mcp__cli_grep",
                "mcp__agentic-mcp__cli_glob",
                "mcp__agentic-mcp__ask_agent"
            ]
        );
    }

    #[test]
    fn max_concurrent_sessions_is_3() {
        assert_eq!(MAX_CONCURRENT_SESSIONS, 3);
    }

    #[test]
    fn capability_profile_for_lens_maps_completeness_only() {
        assert_eq!(
            capability_profile_for_lens(ReviewLens::Completeness),
            ReviewerCapabilityProfile::Completeness
        );

        for lens in [
            ReviewLens::Security,
            ReviewLens::Correctness,
            ReviewLens::Maintainability,
            ReviewLens::Testing,
            ReviewLens::Simplification,
        ] {
            assert_eq!(
                capability_profile_for_lens(lens),
                ReviewerCapabilityProfile::Narrow
            );
        }
    }

    #[test]
    fn completeness_profile_includes_ask_agent_only() {
        let completeness_tools =
            ClaudeCliRunner::all_tools(ReviewerCapabilityProfile::Completeness);
        let narrow_tools = ClaudeCliRunner::all_tools(ReviewerCapabilityProfile::Narrow);

        assert!(completeness_tools.contains(&"mcp__agentic-mcp__cli_ls".to_string()));
        assert!(completeness_tools.contains(&"mcp__agentic-mcp__cli_grep".to_string()));
        assert!(completeness_tools.contains(&"mcp__agentic-mcp__cli_glob".to_string()));
        assert!(completeness_tools.contains(&"mcp__agentic-mcp__ask_agent".to_string()));
        assert!(narrow_tools.contains(&"mcp__agentic-mcp__cli_ls".to_string()));
        assert!(narrow_tools.contains(&"mcp__agentic-mcp__cli_grep".to_string()));
        assert!(narrow_tools.contains(&"mcp__agentic-mcp__cli_glob".to_string()));
        assert!(!narrow_tools.contains(&"mcp__agentic-mcp__ask_agent".to_string()));
    }

    #[test]
    fn review_timeout_default_is_30_minutes() {
        assert_eq!(ReviewConfig::default().run_timeout_secs, 1800);
        assert_eq!(ClaudeCliRunner::new().config().run_timeout_secs, 1800);
    }

    #[test]
    fn retry_delays_are_correct() {
        assert_eq!(
            RETRY_DELAYS,
            [
                Duration::from_millis(0),
                Duration::from_millis(500),
                Duration::from_secs(1)
            ]
        );
    }

    #[test]
    fn reviewer_session_config_disallows_lsp_for_all_profiles() {
        for profile in [
            ReviewerCapabilityProfile::Narrow,
            ReviewerCapabilityProfile::Completeness,
        ] {
            let config =
                ClaudeCliRunner::build_session_config(profile, "system".into(), "user".into())
                    .unwrap_or_else(|e| panic!("session config build failed: {e}"));

            assert!(
                config
                    .disallowed_tools
                    .as_ref()
                    .is_some_and(|tools| tools.iter().any(|tool| tool == CLAUDE_TOOL_LSP)),
                "missing LSP disallow for {profile:?}"
            );
        }
    }

    #[tokio::test]
    async fn mock_runner_returns_responses() {
        let runner = MockRunner::new(vec![Ok("test response".into())]);
        let result = runner
            .run_text(
                ReviewerCapabilityProfile::Narrow,
                "system".into(),
                "user".into(),
                ToolContext::default(),
            )
            .await;
        assert_eq!(result.unwrap(), "test response");
    }

    #[tokio::test]
    async fn mock_runner_exhaustion() {
        let runner = MockRunner::new(vec![]);
        let result = runner
            .run_text(
                ReviewerCapabilityProfile::Narrow,
                "system".into(),
                "user".into(),
                ToolContext::default(),
            )
            .await;
        assert!(result.is_err());
    }

    #[tokio::test]
    async fn wait_for_review_result_runs_cancel_branch() {
        let ctx = ToolContext::default();
        let cancel = ctx.cancellation_token();
        let cancelled = Arc::new(AtomicBool::new(false));
        let cancelled_flag = Arc::clone(&cancelled);

        let task = tokio::spawn(async move {
            wait_for_review_result(
                &ctx,
                std::future::pending::<claudecode::Result<ClaudeResult>>(),
                move || {
                    let cancelled_flag = Arc::clone(&cancelled_flag);
                    async move {
                        cancelled_flag.store(true, Ordering::SeqCst);
                        Ok(())
                    }
                },
            )
            .await
        });

        tokio::task::yield_now().await;
        cancel.cancel();

        let result = match task.await {
            Ok(result) => result,
            Err(err) => panic!("task join failed: {err}"),
        };
        assert!(matches!(result, Err(ToolError::Cancelled { .. })));
        assert!(cancelled.load(Ordering::SeqCst));
    }

    #[tokio::test(start_paused = true)]
    async fn optional_timeout_expires_when_enabled() {
        let handle = tokio::spawn(async {
            run_with_optional_timeout(1, async {
                tokio::time::sleep(Duration::from_secs(5)).await;
                Ok::<_, ToolError>(())
            })
            .await
        });

        tokio::task::yield_now().await;
        tokio::time::advance(Duration::from_secs(1)).await;

        let err = handle.await.expect("task join failed").unwrap_err();
        assert!(err.to_string().contains("Timed out after 1s"));
    }

    #[tokio::test(start_paused = true)]
    async fn zero_timeout_disables_optional_wrapper() {
        let handle = tokio::spawn(async {
            run_with_optional_timeout(0, async {
                tokio::time::sleep(Duration::from_secs(5)).await;
                Ok::<_, ToolError>(())
            })
            .await
        });

        tokio::task::yield_now().await;
        assert!(
            !handle.is_finished(),
            "timeout=0 should not time out immediately"
        );

        tokio::time::advance(Duration::from_secs(5)).await;
        assert!(handle.await.expect("task join failed").is_ok());
    }
}
