//! X API v2 client, authentication, and tier detection.
//!
//! Provides a trait-based client abstraction for all X API operations,
//! OAuth 2.0 PKCE authentication with token management, and API tier
//! detection for adaptive behavior.

pub mod auth;
pub mod client;
pub mod local_mode;
pub mod media;
pub mod null_client;
pub mod retry;
pub mod scopes;
pub mod scraper_health;
pub mod tier;
pub mod types;

pub use client::XApiHttpClient;
pub use local_mode::session::ScraperSession;
pub use local_mode::LocalModeXClient;
pub use null_client::NullXApiClient;
pub use scraper_health::{new_scraper_health, ScraperHealth, ScraperHealthSnapshot, ScraperState};
pub use types::*;

use std::path::Path;
use std::sync::Arc;

use crate::config::XApiConfig;
use crate::error::XApiError;

/// Create a local-mode X API client if `provider_backend = "scraper"`.
///
/// Returns `Some(Arc<dyn XApiClient>)` for scraper backend, `None` for
/// official backend (caller must construct `XApiHttpClient` with OAuth tokens).
///
/// When `data_dir` is provided, attempts to load a cookie-auth session from
/// `scraper_session.json` in that directory to enable posting.
pub async fn create_local_client(config: &XApiConfig) -> Option<Arc<dyn XApiClient>> {
    create_local_client_with_data_dir(config, None).await
}

/// Create a local-mode client with an optional data directory for cookie-auth.
pub async fn create_local_client_with_data_dir(
    config: &XApiConfig,
    data_dir: Option<&Path>,
) -> Option<Arc<dyn XApiClient>> {
    if config.provider_backend == "scraper" {
        let client = match data_dir {
            Some(dir) => LocalModeXClient::with_session(config.scraper_allow_mutations, dir).await,
            None => LocalModeXClient::new(config.scraper_allow_mutations),
        };
        Some(Arc::new(client))
    } else {
        None
    }
}

/// Trait abstracting all X API v2 operations.
///
/// Implementations include `XApiHttpClient` for real API calls and
/// mock implementations for testing.
#[async_trait::async_trait]
pub trait XApiClient: Send + Sync {
    /// Search recent tweets matching the given query.
    ///
    /// Returns up to `max_results` tweets. If `since_id` is provided,
    /// only returns tweets newer than that ID.
    async fn search_tweets(
        &self,
        query: &str,
        max_results: u32,
        since_id: Option<&str>,
        pagination_token: Option<&str>,
    ) -> Result<SearchResponse, XApiError>;

    /// Get mentions for the authenticated user.
    ///
    /// If `since_id` is provided, only returns mentions newer than that ID.
    async fn get_mentions(
        &self,
        user_id: &str,
        since_id: Option<&str>,
        pagination_token: Option<&str>,
    ) -> Result<MentionResponse, XApiError>;

    /// Post a new tweet.
    async fn post_tweet(&self, text: &str) -> Result<PostedTweet, XApiError>;

    /// Reply to an existing tweet.
    async fn reply_to_tweet(
        &self,
        text: &str,
        in_reply_to_id: &str,
    ) -> Result<PostedTweet, XApiError>;

    /// Get a single tweet by ID.
    async fn get_tweet(&self, tweet_id: &str) -> Result<Tweet, XApiError>;

    /// Get the authenticated user's profile.
    async fn get_me(&self) -> Result<User, XApiError>;

    /// Get recent tweets from a specific user.
    async fn get_user_tweets(
        &self,
        user_id: &str,
        max_results: u32,
        pagination_token: Option<&str>,
    ) -> Result<SearchResponse, XApiError>;

    /// Look up a user by their username.
    async fn get_user_by_username(&self, username: &str) -> Result<User, XApiError>;

    /// Upload media to X API for attaching to tweets.
    ///
    /// Default implementation returns an error — override in concrete clients.
    async fn upload_media(
        &self,
        _data: &[u8],
        _media_type: MediaType,
    ) -> Result<MediaId, XApiError> {
        Err(XApiError::MediaUploadError {
            message: "upload_media not implemented".to_string(),
        })
    }

    /// Post a new tweet with media attachments.
    ///
    /// Default delegates to `post_tweet` (ignoring media) for backward compat.
    async fn post_tweet_with_media(
        &self,
        text: &str,
        _media_ids: &[String],
    ) -> Result<PostedTweet, XApiError> {
        self.post_tweet(text).await
    }

    /// Reply to an existing tweet with media attachments.
    ///
    /// Default delegates to `reply_to_tweet` (ignoring media) for backward compat.
    async fn reply_to_tweet_with_media(
        &self,
        text: &str,
        in_reply_to_id: &str,
        _media_ids: &[String],
    ) -> Result<PostedTweet, XApiError> {
        self.reply_to_tweet(text, in_reply_to_id).await
    }

    /// Post a quote tweet referencing another tweet.
    async fn quote_tweet(
        &self,
        _text: &str,
        _quoted_tweet_id: &str,
    ) -> Result<PostedTweet, XApiError> {
        Err(XApiError::ApiError {
            status: 0,
            message: "not implemented".to_string(),
        })
    }

    /// Like a tweet on behalf of the authenticated user.
    async fn like_tweet(&self, _user_id: &str, _tweet_id: &str) -> Result<bool, XApiError> {
        Err(XApiError::ApiError {
            status: 0,
            message: "not implemented".to_string(),
        })
    }

    /// Follow a user on behalf of the authenticated user.
    async fn follow_user(&self, _user_id: &str, _target_user_id: &str) -> Result<bool, XApiError> {
        Err(XApiError::ApiError {
            status: 0,
            message: "not implemented".to_string(),
        })
    }

    /// Unfollow a user on behalf of the authenticated user.
    async fn unfollow_user(
        &self,
        _user_id: &str,
        _target_user_id: &str,
    ) -> Result<bool, XApiError> {
        Err(XApiError::ApiError {
            status: 0,
            message: "not implemented".to_string(),
        })
    }

    /// Retweet a tweet on behalf of the authenticated user.
    async fn retweet(&self, _user_id: &str, _tweet_id: &str) -> Result<bool, XApiError> {
        Err(XApiError::ApiError {
            status: 0,
            message: "not implemented".to_string(),
        })
    }

    /// Undo a retweet on behalf of the authenticated user.
    async fn unretweet(&self, _user_id: &str, _tweet_id: &str) -> Result<bool, XApiError> {
        Err(XApiError::ApiError {
            status: 0,
            message: "not implemented".to_string(),
        })
    }

    /// Delete a tweet by its ID.
    async fn delete_tweet(&self, _tweet_id: &str) -> Result<bool, XApiError> {
        Err(XApiError::ApiError {
            status: 0,
            message: "not implemented".to_string(),
        })
    }

    /// Get the authenticated user's home timeline (reverse chronological).
    async fn get_home_timeline(
        &self,
        _user_id: &str,
        _max_results: u32,
        _pagination_token: Option<&str>,
    ) -> Result<SearchResponse, XApiError> {
        Err(XApiError::ApiError {
            status: 0,
            message: "not implemented".to_string(),
        })
    }

    /// Unlike a tweet on behalf of the authenticated user.
    async fn unlike_tweet(&self, _user_id: &str, _tweet_id: &str) -> Result<bool, XApiError> {
        Err(XApiError::ApiError {
            status: 0,
            message: "not implemented".to_string(),
        })
    }

    /// Get followers of a user.
    async fn get_followers(
        &self,
        _user_id: &str,
        _max_results: u32,
        _pagination_token: Option<&str>,
    ) -> Result<UsersResponse, XApiError> {
        Err(XApiError::ApiError {
            status: 0,
            message: "not implemented".to_string(),
        })
    }

    /// Get accounts a user is following.
    async fn get_following(
        &self,
        _user_id: &str,
        _max_results: u32,
        _pagination_token: Option<&str>,
    ) -> Result<UsersResponse, XApiError> {
        Err(XApiError::ApiError {
            status: 0,
            message: "not implemented".to_string(),
        })
    }

    /// Get a user by their ID.
    async fn get_user_by_id(&self, _user_id: &str) -> Result<User, XApiError> {
        Err(XApiError::ApiError {
            status: 0,
            message: "not implemented".to_string(),
        })
    }

    /// Get tweets liked by a user.
    async fn get_liked_tweets(
        &self,
        _user_id: &str,
        _max_results: u32,
        _pagination_token: Option<&str>,
    ) -> Result<SearchResponse, XApiError> {
        Err(XApiError::ApiError {
            status: 0,
            message: "not implemented".to_string(),
        })
    }

    /// Get the authenticated user's bookmarks.
    async fn get_bookmarks(
        &self,
        _user_id: &str,
        _max_results: u32,
        _pagination_token: Option<&str>,
    ) -> Result<SearchResponse, XApiError> {
        Err(XApiError::ApiError {
            status: 0,
            message: "not implemented".to_string(),
        })
    }

    /// Bookmark a tweet.
    async fn bookmark_tweet(&self, _user_id: &str, _tweet_id: &str) -> Result<bool, XApiError> {
        Err(XApiError::ApiError {
            status: 0,
            message: "not implemented".to_string(),
        })
    }

    /// Remove a bookmark.
    async fn unbookmark_tweet(&self, _user_id: &str, _tweet_id: &str) -> Result<bool, XApiError> {
        Err(XApiError::ApiError {
            status: 0,
            message: "not implemented".to_string(),
        })
    }

    /// Get multiple users by their IDs.
    async fn get_users_by_ids(&self, _user_ids: &[&str]) -> Result<UsersResponse, XApiError> {
        Err(XApiError::ApiError {
            status: 0,
            message: "not implemented".to_string(),
        })
    }

    /// Get users who liked a specific tweet.
    async fn get_tweet_liking_users(
        &self,
        _tweet_id: &str,
        _max_results: u32,
        _pagination_token: Option<&str>,
    ) -> Result<UsersResponse, XApiError> {
        Err(XApiError::ApiError {
            status: 0,
            message: "not implemented".to_string(),
        })
    }

    /// Execute a raw HTTP request against the X API.
    ///
    /// The caller is responsible for constructing a valid, pre-validated URL.
    /// The implementation adds Bearer token authentication automatically.
    /// Returns the raw HTTP response including status, selected headers, and body text.
    ///
    /// `method` must be one of: `GET`, `POST`, `PUT`, `DELETE`.
    async fn raw_request(
        &self,
        _method: &str,
        _url: &str,
        _query: Option<&[(String, String)]>,
        _body: Option<&str>,
        _headers: Option<&[(String, String)]>,
    ) -> Result<RawApiResponse, XApiError> {
        Err(XApiError::ApiError {
            status: 0,
            message: "raw_request not implemented".to_string(),
        })
    }
}

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

    /// Minimal concrete impl for testing default trait methods.
    struct StubClient;

    #[async_trait::async_trait]
    impl XApiClient for StubClient {
        async fn search_tweets(
            &self,
            _q: &str,
            _m: u32,
            _s: Option<&str>,
            _p: Option<&str>,
        ) -> Result<SearchResponse, XApiError> {
            Ok(SearchResponse {
                data: vec![],
                includes: None,
                meta: SearchMeta {
                    newest_id: None,
                    oldest_id: None,
                    result_count: 0,
                    next_token: None,
                },
            })
        }
        async fn get_mentions(
            &self,
            _u: &str,
            _s: Option<&str>,
            _p: Option<&str>,
        ) -> Result<MentionResponse, XApiError> {
            Ok(MentionResponse {
                data: vec![],
                includes: None,
                meta: SearchMeta {
                    newest_id: None,
                    oldest_id: None,
                    result_count: 0,
                    next_token: None,
                },
            })
        }
        async fn post_tweet(&self, text: &str) -> Result<PostedTweet, XApiError> {
            Ok(PostedTweet {
                id: "stub_id".to_string(),
                text: text.to_string(),
            })
        }
        async fn reply_to_tweet(
            &self,
            text: &str,
            _reply_to: &str,
        ) -> Result<PostedTweet, XApiError> {
            Ok(PostedTweet {
                id: "reply_id".to_string(),
                text: text.to_string(),
            })
        }
        async fn get_tweet(&self, _id: &str) -> Result<Tweet, XApiError> {
            Err(XApiError::ApiError {
                status: 404,
                message: "not found".into(),
            })
        }
        async fn get_me(&self) -> Result<User, XApiError> {
            Ok(User {
                id: "me".into(),
                username: "stub".into(),
                name: "Stub".into(),
                profile_image_url: None,
                description: None,
                location: None,
                url: None,
                public_metrics: UserMetrics::default(),
            })
        }
        async fn get_user_tweets(
            &self,
            _u: &str,
            _m: u32,
            _p: Option<&str>,
        ) -> Result<SearchResponse, XApiError> {
            Ok(SearchResponse {
                data: vec![],
                includes: None,
                meta: SearchMeta {
                    newest_id: None,
                    oldest_id: None,
                    result_count: 0,
                    next_token: None,
                },
            })
        }
        async fn get_user_by_username(&self, _u: &str) -> Result<User, XApiError> {
            Err(XApiError::ApiError {
                status: 404,
                message: "not found".into(),
            })
        }
    }

    // ── Default trait method tests ──────────────────────────────

    #[tokio::test]
    async fn upload_media_default_returns_error() {
        let client = StubClient;
        let result = client.upload_media(b"data", MediaType::Gif).await;
        assert!(matches!(result, Err(XApiError::MediaUploadError { .. })));
    }

    #[tokio::test]
    async fn post_tweet_with_media_delegates_to_post_tweet() {
        let client = StubClient;
        let result = client
            .post_tweet_with_media("hello", &["media1".to_string()])
            .await
            .unwrap();
        assert_eq!(result.text, "hello");
        assert_eq!(result.id, "stub_id");
    }

    #[tokio::test]
    async fn reply_to_tweet_with_media_delegates_to_reply() {
        let client = StubClient;
        let result = client
            .reply_to_tweet_with_media("reply text", "tweet_123", &["m1".to_string()])
            .await
            .unwrap();
        assert_eq!(result.text, "reply text");
        assert_eq!(result.id, "reply_id");
    }

    #[tokio::test]
    async fn quote_tweet_default_not_implemented() {
        let client = StubClient;
        let result = client.quote_tweet("text", "quoted_id").await;
        assert!(result.is_err());
    }

    #[tokio::test]
    async fn like_tweet_default_not_implemented() {
        let client = StubClient;
        let result = client.like_tweet("user1", "tweet1").await;
        assert!(result.is_err());
    }

    #[tokio::test]
    async fn follow_user_default_not_implemented() {
        let client = StubClient;
        let result = client.follow_user("u1", "u2").await;
        assert!(result.is_err());
    }

    #[tokio::test]
    async fn unfollow_user_default_not_implemented() {
        let client = StubClient;
        let result = client.unfollow_user("u1", "u2").await;
        assert!(result.is_err());
    }

    #[tokio::test]
    async fn retweet_default_not_implemented() {
        let client = StubClient;
        let result = client.retweet("u1", "t1").await;
        assert!(result.is_err());
    }

    #[tokio::test]
    async fn unretweet_default_not_implemented() {
        let client = StubClient;
        let result = client.unretweet("u1", "t1").await;
        assert!(result.is_err());
    }

    #[tokio::test]
    async fn delete_tweet_default_not_implemented() {
        let client = StubClient;
        let result = client.delete_tweet("t1").await;
        assert!(result.is_err());
    }

    #[tokio::test]
    async fn get_home_timeline_default_not_implemented() {
        let client = StubClient;
        let result = client.get_home_timeline("u1", 10, None).await;
        assert!(result.is_err());
    }

    #[tokio::test]
    async fn unlike_tweet_default_not_implemented() {
        let client = StubClient;
        let result = client.unlike_tweet("u1", "t1").await;
        assert!(result.is_err());
    }

    #[tokio::test]
    async fn get_followers_default_not_implemented() {
        let client = StubClient;
        let result = client.get_followers("u1", 10, None).await;
        assert!(result.is_err());
    }

    #[tokio::test]
    async fn get_following_default_not_implemented() {
        let client = StubClient;
        let result = client.get_following("u1", 10, None).await;
        assert!(result.is_err());
    }

    #[tokio::test]
    async fn get_user_by_id_default_not_implemented() {
        let client = StubClient;
        let result = client.get_user_by_id("u1").await;
        assert!(result.is_err());
    }

    #[tokio::test]
    async fn get_liked_tweets_default_not_implemented() {
        let client = StubClient;
        let result = client.get_liked_tweets("u1", 10, None).await;
        assert!(result.is_err());
    }

    #[tokio::test]
    async fn get_bookmarks_default_not_implemented() {
        let client = StubClient;
        let result = client.get_bookmarks("u1", 10, None).await;
        assert!(result.is_err());
    }

    #[tokio::test]
    async fn bookmark_tweet_default_not_implemented() {
        let client = StubClient;
        let result = client.bookmark_tweet("u1", "t1").await;
        assert!(result.is_err());
    }

    #[tokio::test]
    async fn unbookmark_tweet_default_not_implemented() {
        let client = StubClient;
        let result = client.unbookmark_tweet("u1", "t1").await;
        assert!(result.is_err());
    }

    #[tokio::test]
    async fn get_users_by_ids_default_not_implemented() {
        let client = StubClient;
        let result = client.get_users_by_ids(&["u1", "u2"]).await;
        assert!(result.is_err());
    }

    #[tokio::test]
    async fn get_tweet_liking_users_default_not_implemented() {
        let client = StubClient;
        let result = client.get_tweet_liking_users("t1", 10, None).await;
        assert!(result.is_err());
    }

    #[tokio::test]
    async fn raw_request_default_not_implemented() {
        let client = StubClient;
        let result = client.raw_request("GET", "/test", None, None, None).await;
        assert!(result.is_err());
    }

    // ── create_local_client tests ───────────────────────────────

    #[tokio::test]
    async fn create_local_client_non_scraper_returns_none() {
        let config = XApiConfig::default();
        let result = create_local_client(&config).await;
        assert!(result.is_none());
    }

    #[tokio::test]
    async fn create_local_client_scraper_returns_some() {
        let mut config = XApiConfig::default();
        config.provider_backend = "scraper".to_string();
        let result = create_local_client(&config).await;
        assert!(result.is_some());
    }

    #[tokio::test]
    async fn create_local_client_with_data_dir_non_scraper() {
        let config = XApiConfig::default();
        let dir = tempfile::tempdir().expect("temp dir");
        let result = create_local_client_with_data_dir(&config, Some(dir.path())).await;
        assert!(result.is_none());
    }

    #[tokio::test]
    async fn create_local_client_with_data_dir_scraper() {
        let mut config = XApiConfig::default();
        config.provider_backend = "scraper".to_string();
        let dir = tempfile::tempdir().expect("temp dir");
        let result = create_local_client_with_data_dir(&config, Some(dir.path())).await;
        assert!(result.is_some());
    }

    #[tokio::test]
    async fn create_local_client_with_data_dir_none() {
        let mut config = XApiConfig::default();
        config.provider_backend = "scraper".to_string();
        let result = create_local_client_with_data_dir(&config, None).await;
        assert!(result.is_some());
    }
}
