pub mod git;
pub mod github;
pub mod logging;
pub mod models;
pub mod pagination;
pub mod tools;

use anyhow::Context;
use anyhow::Result;
use models::CommentSourceType;
use models::PrSummary;
use models::PrSummaryList;
use models::ReviewComment;
use models::ReviewCommentList;
use models::Thread;
use pagination::PaginationCache;
use pagination::QueryLock;
use pagination::make_key;
use pagination::make_pr_list_key;
use pagination::paginate_slice;
use std::sync::Arc;

// Re-export agentic-tools types for MCP server usage
pub use tools::build_registry;

/// AI response prefix to clearly identify automated replies.
pub const AI_PREFIX: &str = "\u{1F916} AI response: ";

fn guarded_post_fetch_reset<T>(query_lock: &Arc<QueryLock<T>>, entries: Vec<T>, page_size: usize) {
    let mut state = query_lock.state.lock().unwrap();
    if state.is_empty() || state.is_expired() {
        state.reset(entries, (), page_size);
    }
}

/// Prepend the AI prefix to a message body if it's not already present.
/// Avoids double-prefixing, handles leading whitespace.
pub fn with_ai_prefix(body: &str) -> String {
    if body.trim_start().starts_with(AI_PREFIX) {
        body.to_string()
    } else {
        format!("{}{}", AI_PREFIX, body)
    }
}

#[derive(Clone)]
pub struct PrComments {
    owner: String,
    repo: String,
    token: Option<String>,
    pager: Arc<PaginationCache<Thread>>,
    pr_list_pager: Arc<PaginationCache<PrSummary>>,
    init_error: Option<String>,
}

impl PrComments {
    fn get_token() -> Result<Option<String>> {
        // 1) Check environment variables first (explicit override)
        if let Ok(t) = std::env::var("GH_TOKEN").or_else(|_| std::env::var("GITHUB_TOKEN")) {
            tracing::debug!("Using GitHub token from environment");
            return Ok(Some(t));
        }

        // 2) Try gh-config (hosts.yml → keyring)
        match gh_config::Hosts::load() {
            Ok(hosts) => match hosts.retrieve_token(gh_config::GITHUB_COM) {
                Ok(Some(t)) => {
                    tracing::debug!("Using GitHub token from gh-config");
                    Ok(Some(t))
                }
                Ok(None) => {
                    tracing::debug!("No token found in gh-config");
                    Ok(None)
                }
                Err(e) => {
                    tracing::debug!("gh-config token retrieval failed: {}", e);
                    Ok(None)
                }
            },
            Err(e) => {
                tracing::debug!("gh-config unavailable: {}", e);
                Ok(None)
            }
        }
    }

    /// Get page size from environment variable PR_COMMENTS_PAGE_SIZE.
    /// Defaults to 10, clamped to [1, 1000].
    fn page_size_from_env() -> usize {
        std::env::var("PR_COMMENTS_PAGE_SIZE")
            .ok()
            .and_then(|s| s.parse::<usize>().ok())
            .filter(|n| *n >= 1 && *n <= 1000)
            .unwrap_or(10)
    }

    pub fn new() -> Result<Self> {
        let git_info = git::get_git_info().context("Failed to get git information")?;
        let token = Self::get_token()?;

        Ok(Self {
            owner: git_info.owner,
            repo: git_info.repo,
            token,
            pager: Arc::new(PaginationCache::new()),
            pr_list_pager: Arc::new(PaginationCache::new()),
            init_error: None,
        })
    }

    pub fn with_repo(owner: String, repo: String) -> Self {
        let token = Self::get_token().ok().flatten();
        Self {
            owner,
            repo,
            token,
            pager: Arc::new(PaginationCache::new()),
            pr_list_pager: Arc::new(PaginationCache::new()),
            init_error: None,
        }
    }

    /// Create a disabled instance that will fail fast with a clear error message.
    /// Used when ambient repo detection fails.
    pub fn disabled(init_error: String) -> Self {
        let token = Self::get_token().ok().flatten();
        Self {
            owner: String::new(),
            repo: String::new(),
            token,
            pager: Arc::new(PaginationCache::new()),
            pr_list_pager: Arc::new(PaginationCache::new()),
            init_error: Some(init_error),
        }
    }

    /// Check that repo context is available before making API calls.
    /// Returns an error with actionable guidance if owner/repo are empty.
    fn ensure_repo_configured(&self) -> Result<()> {
        if !self.owner.is_empty() && !self.repo.is_empty() {
            return Ok(());
        }

        let mut msg = "invalid argument: pr_comments repository context is not available.\n\
This tool relies on ambient git repo detection. Run it inside a git checkout with a GitHub remote."
            .to_string();

        if let Some(e) = &self.init_error {
            msg.push_str(&format!("\n\nAmbient detection error: {}", e));
        }

        anyhow::bail!(msg);
    }

    async fn get_pr_number(&self, pr_number: Option<u64>) -> Result<u64> {
        if let Some(pr) = pr_number {
            return Ok(pr);
        }

        // Try to detect from current branch
        let git_info = git::get_git_info()?;
        let branch = git_info
            .current_branch
            .context("Could not determine current git branch")?;

        let client =
            github::GitHubClient::new(self.owner.clone(), self.repo.clone(), self.token.clone())?;

        match client.get_pr_from_branch(&branch).await {
            Ok(Some(pr)) => Ok(pr),
            Ok(None) => Err(anyhow::anyhow!(
                "No open PR found for branch '{}' in {}/{}. \n\
                Make sure you have an open PR for this branch.",
                branch,
                self.owner,
                self.repo
            )),
            Err(e) => {
                let msg = e.to_string();
                if msg.contains("401") || msg.contains("403") || msg.contains("Not Found") {
                    Err(anyhow::anyhow!(
                        "Failed to access {}/{}: {}\n\n\
                        Hint: For private repositories, ensure your GITHUB_TOKEN has the 'repo' scope.\n\
                        Current token: {}",
                        self.owner,
                        self.repo,
                        msg,
                        if self.token.is_some() {
                            "Set"
                        } else {
                            "Not set (required for private repos)"
                        }
                    ))
                } else {
                    Err(e)
                }
            }
        }
    }
}

// Removed universal-tool-core macros; Tool impls live in tools.rs
impl PrComments {
    /// Get PR review comments with thread-level pagination.
    /// Repeated calls with same params return next page.
    pub async fn get_comments(
        &self,
        pr_number: Option<u64>,
        comment_source_type: Option<CommentSourceType>,
        include_resolved: Option<bool>,
    ) -> Result<ReviewCommentList> {
        self.ensure_repo_configured()
            .context("invalid argument: missing repository context")?;

        let pr = self
            .get_pr_number(pr_number)
            .await
            .context("invalid argument: failed to determine PR number")?;

        let src = comment_source_type.unwrap_or_default();
        let include_resolved = include_resolved.unwrap_or(false);
        let page_size = Self::page_size_from_env();
        let pr_url = format!(
            "https://github.com/{}/{}/pull/{}",
            self.owner, self.repo, pr
        );

        // Sweep expired cache entries opportunistically
        self.pager.sweep_expired();

        // Build cache key
        let key = make_key(
            &self.owner,
            &self.repo,
            pr,
            src,
            include_resolved,
            page_size,
        );

        // Get or create per-query lock
        let query_lock = self.pager.get_or_create(&key);

        // Check if we need to fetch data (quick check, release lock before await)
        let needs_fetch = {
            let state = query_lock.state.lock().unwrap();
            state.is_empty() || state.is_expired()
        };

        // If we need to fetch, do async work without holding the lock
        if needs_fetch {
            let client = github::GitHubClient::new(
                self.owner.clone(),
                self.repo.clone(),
                self.token.clone(),
            )
            .context("internal: failed to create GitHub client")?;

            // Fetch all comments
            let comments = client.fetch_review_comments(pr).await.map_err(|e| {
                let msg = e.to_string();
                if msg.contains("401") || msg.contains("403") {
                    anyhow::anyhow!(
                        "{}\n\nHint: For private repositories, ensure your token has the 'repo' scope.",
                        msg
                    )
                } else {
                    anyhow::anyhow!("{}", msg)
                }
            })?;

            // Get resolution map
            let resolution_map = if !include_resolved {
                client
                    .get_review_thread_resolution_status(pr)
                    .await
                    .unwrap_or_default()
            } else {
                std::collections::HashMap::new()
            };

            // Build and filter threads
            let threads = client.build_threads(comments, &resolution_map);
            let filtered = client.filter_threads(threads, src, include_resolved);

            guarded_post_fetch_reset(&query_lock, filtered, page_size);
        }

        // Now paginate (re-acquire lock for pagination)
        let mut state = query_lock.state.lock().unwrap();
        let (page_threads, has_more) =
            paginate_slice(&state.results, state.next_offset, state.page_size);
        state.next_offset += page_threads.len();

        // Compute pagination metadata
        let total_threads = state.results.len();
        let shown_threads = state.next_offset;

        // Flatten threads to comments for output
        let comments: Vec<_> = page_threads
            .into_iter()
            .flat_map(|t| {
                let mut cs = vec![t.parent];
                cs.extend(t.replies);
                cs
            })
            .collect();

        // Build pagination message only when there are more pages
        let message = if has_more {
            Some(format!(
                "Showing {} out of {} threads. Call gh_get_comments again for more.",
                shown_threads, total_threads
            ))
        } else {
            None
        };

        // If no more pages, drop cache entry
        if !has_more {
            drop(state);
            self.pager.remove_if_same(&key, &query_lock);
        }

        Ok(ReviewCommentList {
            owner: self.owner.clone(),
            repo: self.repo.clone(),
            pr_number: pr,
            pr_url,
            comments,
            shown_threads,
            total_threads,
            has_more,
            message,
        })
    }

    /// List pull requests in the repository
    pub async fn list_prs(&self, state: Option<String>) -> Result<PrSummaryList> {
        self.ensure_repo_configured()
            .context("invalid argument: missing repository context")?;

        let state = state.unwrap_or_else(|| "open".to_string());
        let page_size = Self::page_size_from_env();

        self.pr_list_pager.sweep_expired();

        let key = make_pr_list_key(&self.owner, &self.repo, &state, page_size);
        let query_lock = self.pr_list_pager.get_or_create(&key);

        // TODO(2): On cache miss, list_prs blocks page 1 on a full GitHub PR fetch so we can compute
        // total_prs from results.len(). Consider incremental remote pagination or relaxing exact totals
        // if cold-cache latency matters.
        let needs_fetch = {
            let state = query_lock.state.lock().unwrap();
            state.is_empty() || state.is_expired()
        };

        if needs_fetch {
            let client = github::GitHubClient::new(
                self.owner.clone(),
                self.repo.clone(),
                self.token.clone(),
            )
            .context("internal: failed to create GitHub client")?;

            let prs = client.list_prs(Some(state.clone())).await.map_err(|e| {
                let msg = e.to_string();
                if msg.contains("401") || msg.contains("403") {
                    anyhow::anyhow!(
                        "{}\n\nHint: For private repositories, ensure your GITHUB_TOKEN has the 'repo' scope.",
                        msg
                    )
                } else {
                    anyhow::anyhow!("{}", msg)
                }
            })?;

            guarded_post_fetch_reset(&query_lock, prs, page_size);
        }

        let mut pager_state = query_lock.state.lock().unwrap();
        let (prs, has_more) = paginate_slice(
            &pager_state.results,
            pager_state.next_offset,
            pager_state.page_size,
        );
        pager_state.next_offset += prs.len();

        let total_prs = pager_state.results.len();
        let shown_prs = pager_state.next_offset;
        let message = if has_more {
            Some(format!(
                "Showing {} out of {} pull requests. Call gh_get_prs again for more.",
                shown_prs, total_prs
            ))
        } else {
            None
        };

        if !has_more {
            drop(pager_state);
            self.pr_list_pager.remove_if_same(&key, &query_lock);
        }

        Ok(PrSummaryList {
            owner: self.owner.clone(),
            repo: self.repo.clone(),
            state,
            prs,
            shown_prs,
            total_prs,
            has_more,
            message,
        })
    }

    /// Reply to a PR review comment. Automatically prefixes with AI identifier.
    pub async fn add_comment_reply(
        &self,
        pr_number: Option<u64>,
        comment_id: u64,
        body: String,
    ) -> Result<ReviewComment> {
        // Validate body is not empty
        anyhow::ensure!(
            !body.trim().is_empty(),
            "invalid argument: Body cannot be empty"
        );

        self.ensure_repo_configured()
            .context("invalid argument: missing repository context")?;

        let pr = self
            .get_pr_number(pr_number)
            .await
            .context("invalid argument: failed to determine PR number")?;

        let client =
            github::GitHubClient::new(self.owner.clone(), self.repo.clone(), self.token.clone())
                .context("internal: failed to create GitHub client")?;

        // Apply AI prefix to clearly identify automated responses
        let prefixed_body = with_ai_prefix(&body);

        let comment = client
            .reply_to_comment(pr, comment_id, &prefixed_body)
            .await
            .map_err(|e| {
                let msg = e.to_string();
                if msg.contains("401") || msg.contains("403") {
                    anyhow::anyhow!(
                        "{}\n\nHint: For private repositories, ensure your token has the 'repo' scope.",
                        msg
                    )
                } else if msg.contains("404") {
                    anyhow::anyhow!("not found: Comment {} not found on PR #{}", comment_id, pr)
                } else {
                    anyhow::anyhow!("{}", msg)
                }
            })?;

        Ok(comment)
    }
}

// Removed universal-tool-core MCP server; use ToolRegistry in tools.rs

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

    fn sample_comment(id: u64) -> ReviewComment {
        ReviewComment {
            id,
            user: "alice".into(),
            is_bot: false,
            body: format!("Comment {id}"),
            path: "src/lib.rs".into(),
            line: Some(id),
            side: Some("RIGHT".into()),
            created_at: "2025-01-01T00:00:00Z".into(),
            updated_at: "2025-01-01T00:00:00Z".into(),
            html_url: format!("https://example.com/review/{id}"),
            pull_request_review_id: Some(42),
            in_reply_to_id: None,
        }
    }

    fn sample_thread(id: u64) -> Thread {
        Thread {
            parent: sample_comment(id),
            replies: vec![],
            is_resolved: false,
        }
    }

    fn sample_pr_summary(number: u64) -> PrSummary {
        PrSummary {
            number,
            title: format!("PR {number}"),
            author: "alice".into(),
            state: "open".into(),
            created_at: "2025-01-01T00:00:00Z".into(),
            updated_at: "2025-01-01T00:00:00Z".into(),
            comment_count: 0,
            review_comment_count: 0,
        }
    }

    #[test]
    fn late_concurrent_caller_does_not_rewind_next_offset() {
        let cache: PaginationCache<Thread> = PaginationCache::new();
        let key = make_key("owner", "repo", 123, CommentSourceType::All, false, 2);
        let query_lock = cache.get_or_create(&key);

        let needs_fetch_a = {
            let st = query_lock.state.lock().unwrap();
            st.is_empty() || st.is_expired()
        };
        let needs_fetch_b = {
            let st = query_lock.state.lock().unwrap();
            st.is_empty() || st.is_expired()
        };
        assert!(needs_fetch_a);
        assert!(needs_fetch_b);

        guarded_post_fetch_reset(
            &query_lock,
            vec![sample_thread(1), sample_thread(2), sample_thread(3)],
            2,
        );

        {
            let mut st = query_lock.state.lock().unwrap();
            let (page, has_more) = paginate_slice(&st.results, st.next_offset, st.page_size);
            st.next_offset += page.len();

            assert_eq!(page.len(), 2);
            assert_eq!(page[0].parent.id, 1);
            assert_eq!(page[1].parent.id, 2);
            assert!(has_more);
            assert_eq!(st.next_offset, 2);
        }

        guarded_post_fetch_reset(
            &query_lock,
            vec![sample_thread(100), sample_thread(101), sample_thread(102)],
            2,
        );

        {
            let st = query_lock.state.lock().unwrap();
            assert_eq!(st.next_offset, 2);
            assert_eq!(st.results[0].parent.id, 1);
            assert_eq!(st.results[1].parent.id, 2);
            assert_eq!(st.results[2].parent.id, 3);
        }

        {
            let mut st = query_lock.state.lock().unwrap();
            let (page, has_more) = paginate_slice(&st.results, st.next_offset, st.page_size);
            st.next_offset += page.len();

            assert_eq!(page.len(), 1);
            assert_eq!(page[0].parent.id, 3);
            assert!(!has_more);
        }
    }

    #[test]
    fn with_ai_prefix_adds_prefix() {
        let body = "This is a reply";
        let prefixed = with_ai_prefix(body);
        assert!(prefixed.starts_with(AI_PREFIX));
        assert_eq!(prefixed, format!("{}This is a reply", AI_PREFIX));
    }

    #[test]
    fn with_ai_prefix_avoids_double_prefix() {
        let already_prefixed = format!("{}Already prefixed", AI_PREFIX);
        let result = with_ai_prefix(&already_prefixed);
        assert_eq!(result, already_prefixed);
        // Ensure no double prefix
        assert_eq!(result.matches(AI_PREFIX).count(), 1);
    }

    #[test]
    fn with_ai_prefix_handles_empty_body() {
        let body = "";
        let prefixed = with_ai_prefix(body);
        assert_eq!(prefixed, AI_PREFIX);
    }

    #[test]
    fn with_ai_prefix_handles_leading_whitespace() {
        // Body with leading whitespace before prefix should not double-prefix
        let body_with_space = format!("  {}Already prefixed", AI_PREFIX);
        let result = with_ai_prefix(&body_with_space);
        assert_eq!(result, body_with_space);
        // Ensure no double prefix
        assert_eq!(result.matches(AI_PREFIX).count(), 1);
    }

    #[test]
    fn ai_prefix_contains_robot_emoji() {
        // Verify the prefix contains the robot emoji for clear AI identification
        assert!(AI_PREFIX.contains('\u{1F916}')); // 🤖
    }

    #[test]
    fn ensure_repo_configured_fails_with_empty_owner_repo() {
        let disabled = PrComments::disabled("test error".into());
        let result = disabled.ensure_repo_configured();

        assert!(result.is_err());
        let err = result.unwrap_err().to_string();
        assert!(
            err.contains("repository context is not available"),
            "Error should mention missing repo context"
        );
        assert!(
            err.contains("ambient git repo detection"),
            "Error should mention ambient detection"
        );
        assert!(
            err.contains("test error"),
            "Error should include the original init error"
        );
    }

    #[test]
    fn ensure_repo_configured_succeeds_with_valid_repo() {
        let valid = PrComments::with_repo("owner".into(), "repo".into());
        let result = valid.ensure_repo_configured();
        assert!(result.is_ok());
    }

    #[test]
    fn completed_comment_pagination_restarts_with_fresh_state_on_next_identical_call() {
        let cache: PaginationCache<Thread> = PaginationCache::new();
        let key = make_key("owner", "repo", 123, CommentSourceType::All, false, 10);

        let first_lock = cache.get_or_create(&key);
        {
            let mut state = first_lock.state.lock().unwrap();
            state.reset(vec![sample_thread(1), sample_thread(2)], (), 10);

            let (page_threads, has_more) =
                paginate_slice(&state.results, state.next_offset, state.page_size);
            state.next_offset += page_threads.len();

            assert_eq!(page_threads.len(), 2);
            assert!(!has_more);
        }

        cache.remove_if_same(&key, &first_lock);

        let restarted_lock = cache.get_or_create(&key);
        assert!(!Arc::ptr_eq(&first_lock, &restarted_lock));

        let restarted_state = restarted_lock.state.lock().unwrap();
        assert!(restarted_state.is_empty());
        assert_eq!(restarted_state.next_offset, 0);
    }

    #[test]
    fn completed_pr_list_pagination_restarts_with_fresh_state_on_next_identical_call() {
        let cache: PaginationCache<PrSummary> = PaginationCache::new();
        let key = make_pr_list_key("owner", "repo", "open", 10);

        let first_lock = cache.get_or_create(&key);
        {
            let mut state = first_lock.state.lock().unwrap();
            state.reset(vec![sample_pr_summary(1), sample_pr_summary(2)], (), 10);

            let (page_prs, has_more) =
                paginate_slice(&state.results, state.next_offset, state.page_size);
            state.next_offset += page_prs.len();

            assert_eq!(page_prs.len(), 2);
            assert!(!has_more);
        }

        cache.remove_if_same(&key, &first_lock);

        let restarted_lock = cache.get_or_create(&key);
        assert!(!Arc::ptr_eq(&first_lock, &restarted_lock));

        let restarted_state = restarted_lock.state.lock().unwrap();
        assert!(restarted_state.is_empty());
        assert_eq!(restarted_state.next_offset, 0);
    }
}
