use harness_agents::claude::ClaudeCodeAgent;
use harness_agents::codex::{CodexAgent, CodexReviewRequest};
use harness_core::{
    agent::AgentRequest,
    agent::CodeAgent,
    config::{agents::SandboxMode, HarnessConfig},
    prompts,
    review::{parse_review_report, ReviewDecision, ReviewProviderKind},
};
use std::path::PathBuf;
use tokio::time::{sleep, Duration};

const CODEX_CLI_REVIEW_PROVIDER_ID: &str = "codex_cli_review";

fn create_agent(config: &HarnessConfig) -> ClaudeCodeAgent {
    ClaudeCodeAgent::new(
        config.agents.claude.cli_path.clone(),
        config.agents.claude.default_model.clone(),
        config.agents.sandbox_mode,
    )
    .with_no_session_persistence_probe()
    .with_stream_timeout(config.agents.stream_timeout_secs)
}

pub async fn fix(
    config: &HarnessConfig,
    issue: u64,
    wait: u64,
    max_rounds: u32,
    project: PathBuf,
) -> anyhow::Result<()> {
    let agent = create_agent(config);

    println!("[harness] Round 1 — Implementing issue #{issue} and creating PR");

    let req = AgentRequest {
        prompt: prompts::implement_from_issue(issue, None, None).to_prompt_string(),
        project_root: project.clone(),
        ..Default::default()
    };

    let resp = agent.execute(req).await?;
    println!("{}", resp.output);

    let pr_url = prompts::parse_pr_url(&resp.output)
        .ok_or_else(|| anyhow::anyhow!("PR_URL=<url> not found in agent output"))?;
    let pr_number = prompts::extract_pr_number(&pr_url)
        .ok_or_else(|| anyhow::anyhow!("Cannot parse PR number from URL: {pr_url}"))?;

    println!("[harness] PR #{pr_number} created: {pr_url}");

    run_review_loop(
        &agent,
        &project,
        ReviewLoopOptions {
            issue: Some(issue),
            pr: pr_number,
            pr_url: Some(&pr_url),
            review_bot_command: &config.agents.review.review_bot_command,
            reviewer_name: &config.agents.review.reviewer_name,
            wait,
            max_rounds,
        },
    )
    .await
}

pub async fn loop_pr(
    config: &HarnessConfig,
    pr: u64,
    wait: u64,
    max_rounds: u32,
    project: PathBuf,
) -> anyhow::Result<()> {
    let agent = create_agent(config);

    println!("[harness] Starting review loop for PR #{pr}");

    run_review_loop(
        &agent,
        &project,
        ReviewLoopOptions {
            issue: None,
            pr,
            pr_url: None,
            review_bot_command: &config.agents.review.review_bot_command,
            reviewer_name: &config.agents.review.reviewer_name,
            wait,
            max_rounds,
        },
    )
    .await
}

pub async fn review(
    config: &HarnessConfig,
    pr: u64,
    provider: String,
    base: Option<String>,
    project: PathBuf,
) -> anyhow::Result<()> {
    if provider != CODEX_CLI_REVIEW_PROVIDER_ID {
        anyhow::bail!(
            "unsupported review provider `{provider}`; supported provider: {CODEX_CLI_REVIEW_PROVIDER_ID}"
        );
    }

    let mut review_config = config.agents.review.codex_cli_review.clone();
    if let Some(base) = base {
        review_config.base_ref = base;
    }
    if review_config.base_ref.trim().is_empty() {
        anyhow::bail!("codex_cli_review requires a non-empty base ref");
    }

    let mut agent = CodexAgent::new(
        review_config.cli_path.clone(),
        SandboxMode::ReadOnlyWithNetwork,
    )
    .with_stream_timeout(Some(review_config.timeout_secs));
    agent.default_model = review_config.model.clone();
    agent.reasoning_effort = review_config.reasoning_effort.clone();

    let started_at = chrono::Utc::now();
    let response = tokio::time::timeout(
        Duration::from_secs(review_config.timeout_secs.max(1)),
        agent.execute_review(CodexReviewRequest {
            project_root: project,
            instructions: Some(codex_cli_review_instructions(
                pr,
                &review_config.base_ref,
                &review_config.output_format,
            )),
            base_ref: Some(review_config.base_ref.clone()),
            model: Some(review_config.model),
            reasoning_effort: Some(review_config.reasoning_effort),
            sandbox_mode: SandboxMode::ReadOnlyWithNetwork,
            approval_policy: Some("never".to_string()),
            env_vars: Default::default(),
        }),
    )
    .await
    .map_err(|_| {
        anyhow::anyhow!(
            "codex_cli_review timed out after {}s",
            review_config.timeout_secs.max(1)
        )
    })??;
    let completed_at = chrono::Utc::now();

    let report = parse_review_report(
        CODEX_CLI_REVIEW_PROVIDER_ID,
        ReviewProviderKind::LocalCli,
        &response.output,
        started_at,
        completed_at,
    );
    println!("{}", serde_json::to_string_pretty(&report)?);

    match report.decision {
        ReviewDecision::Approved => Ok(()),
        ReviewDecision::ChangesRequested => {
            anyhow::bail!("codex_cli_review requested changes for PR #{pr}")
        }
        ReviewDecision::Failed | ReviewDecision::TimedOut | ReviewDecision::Skipped => {
            anyhow::bail!(
                "codex_cli_review did not approve PR #{pr}: {}",
                report.summary
            )
        }
    }
}

fn codex_cli_review_instructions(pr: u64, base_ref: &str, output_format: &str) -> String {
    let report_format = if output_format.eq_ignore_ascii_case("json") {
        "Return exactly one fenced `harness-review-report` JSON block with this shape:\n\
         ```harness-review-report\n\
         {\"decision\":\"approved|changes_requested|failed|timed_out|skipped\",\
         \"summary\":\"concise summary\",\
         \"findings\":[{\"severity\":\"critical|high|medium|low\",\
         \"category\":\"security|correctness|data_integrity|concurrency|performance|test_gap|maintainability|other\",\
         \"path\":\"optional path or null\",\
         \"line\":123,\
         \"message\":\"finding\",\
         \"evidence\":\"optional evidence or null\",\
         \"recommendation\":\"optional recommendation or null\",\
         \"blocking\":true,\
         \"confidence\":0.9}]}\n\
         ```"
    } else {
        "If everything is safe, put APPROVED on the last line. Otherwise list each blocking issue on its own line prefixed with ISSUE:."
    };

    format!(
        "You are the configured codex_cli_review provider for PR #{pr}.\n\
         Review the local workspace against base ref `{base_ref}` as a read-only provider.\n\n\
         Requirements:\n\
         - Inspect the PR intent, diff, and changed files before deciding.\n\
         - Focus on security, logic, data integrity, error handling, and missing tests.\n\
         - Do not modify files, commit, push, or post GitHub comments.\n\
         - Treat unparseable or incomplete evidence as a failed provider review.\n\
         {report_format}"
    )
}

/// Resolve `owner/repo` slug from a PR URL.
async fn resolve_repo_slug(pr: u64, pr_url: Option<&str>) -> anyhow::Result<String> {
    if let Some(url) = pr_url {
        return Ok(prompts::repo_slug_from_pr_url(Some(url)));
    }

    Err(anyhow::anyhow!(
        "PR URL is required for PR #{pr}; Harness CLI no longer invokes gh to resolve repository metadata"
    ))
}

struct ReviewLoopOptions<'a> {
    issue: Option<u64>,
    pr: u64,
    pr_url: Option<&'a str>,
    review_bot_command: &'a str,
    reviewer_name: &'a str,
    wait: u64,
    max_rounds: u32,
}

async fn run_review_loop(
    agent: &impl CodeAgent,
    project: &PathBuf,
    options: ReviewLoopOptions<'_>,
) -> anyhow::Result<()> {
    let ReviewLoopOptions {
        issue,
        pr,
        pr_url,
        review_bot_command,
        reviewer_name,
        wait,
        max_rounds,
    } = options;
    let url_display: std::borrow::Cow<str> = match pr_url {
        Some(url) => std::borrow::Cow::Borrowed(url),
        None => std::borrow::Cow::Owned(format!("PR #{pr}")),
    };

    // Resolve once before entering the loop so failures surface immediately.
    let repo = resolve_repo_slug(pr, pr_url).await?;

    let mut prev_fixed = false;
    let mut round = 1u32;

    while round <= max_rounds {
        println!("[harness] Waiting {wait}s for CI and review bot...");
        sleep(Duration::from_secs(wait)).await;

        println!("[harness] Review round {round}/{max_rounds}, PR #{pr}");

        let req = AgentRequest {
            prompt: prompts::review_prompt(
                issue,
                pr,
                round,
                prev_fixed,
                review_bot_command,
                reviewer_name,
                &repo,
                false,
            ),
            project_root: project.clone(),
            ..Default::default()
        };

        let resp = agent.execute(req).await?;
        println!("{}", resp.output);

        if prompts::is_waiting(&resp.output) {
            println!("[harness] Review bot hasn't re-reviewed yet, retrying...");
            continue;
        }

        if prompts::is_lgtm(&resp.output) {
            println!("[harness] LGTM — {url_display}");
            return Ok(());
        }

        prev_fixed = true;
        round += 1;
    }

    println!("[harness] Reached max rounds ({max_rounds}), PR status: {url_display}");
    Ok(())
}

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

    #[tokio::test]
    async fn resolve_repo_slug_with_url_does_not_call_gh() -> anyhow::Result<()> {
        // When pr_url is Some, the slug is derived from the URL without
        // spawning any subprocess.
        let slug = resolve_repo_slug(42, Some("https://github.com/owner/myrepo/pull/42")).await?;
        assert_eq!(slug, "owner/myrepo");
        Ok(())
    }

    #[tokio::test]
    async fn resolve_repo_slug_with_url_various_formats() -> anyhow::Result<()> {
        let cases = [
            ("https://github.com/org/repo/pull/1", "org/repo"),
            ("https://github.com/org/repo/pull/1/files", "org/repo"),
        ];
        for (url, expected) in cases {
            let slug = resolve_repo_slug(1, Some(url)).await?;
            assert_eq!(slug, expected, "url = {url}");
        }
        Ok(())
    }

    #[tokio::test]
    async fn resolve_repo_slug_none_fails_without_gh() {
        // When pr_url is None, resolve_repo_slug calls `gh pr view`.  In a
        // test environment without a real gh context this should return an
        // Err rather than the literal "{owner}/{repo}" placeholder.
        //
        // We use a non-existent PR number to guarantee gh exits non-zero even
        // if gh is installed and authenticated.
        let result = resolve_repo_slug(u64::MAX, None).await;
        // Either gh is not installed (IoError) or it returned non-zero — both
        // map to Err.  The key assertion is that we do NOT get Ok("{owner}/{repo}").
        match result {
            Ok(slug) => {
                assert_ne!(
                    slug, "{owner}/{repo}",
                    "resolve_repo_slug must never return the literal placeholder"
                );
            }
            Err(_) => {
                // Expected path in CI / no-auth environments.
            }
        }
    }
}
