//! Browsr Client - HTTP client for browser automation
//!
//! This crate provides a client for interacting with Browsr servers for browser automation,
//! web scraping, and structured content extraction.
//!
//! # Quick Start
//!
//! ```rust,ignore
//! use browsr_client::{BrowsrClient, BrowsrClientConfig};
//! use browsr_types::Commands;
//!
//! // From environment variables
//! let client = BrowsrClient::from_env();
//!
//! // Navigate to a page
//! let response = client.navigate("https://example.com", None).await?;
//!
//! // Extract structured content
//! let data = client.extract_structured(
//!     "Extract the main heading and first paragraph",
//!     None,
//!     None,
//! ).await?;
//! ```
//!
//! # Configuration
//!
//! The client can be configured via environment variables or programmatically:
//!
//! - `BROWSR_BASE_URL`: Base URL (defaults to `https://api.browsr.dev`)
//! - `BROWSR_API_KEY`: Optional API key for authentication

mod config;

pub use config::{BrowsrClientConfig, DEFAULT_BASE_URL, ENV_API_KEY, ENV_BASE_URL};

// Re-export browser_step types for convenient access
pub use browsr_types::{
    BrowserStepInput, BrowserStepRequest, BrowserStepResult, CrawlApiRequest, CrawlApiResponse,
    JsonExtractionOptions, NetworkAccess, ObserveOptions, RelayEvent, RelayEventsResponse,
    RelaySessionInfo, RelaySessionListResponse, ScrapeAction, ScrapeApiRequest,
    ScrapeApiResponse, ScrapeData, ScrapeFormat, SessionCreated, SessionDetail,
    SessionListResponse, SessionType, ShellCreateSessionRequest,
    ShellCreateSessionResponse, ShellExecRequest, ShellExecResponse, ShellExecResult,
    ShellListItem, ShellListResponse, ShellTerminateResponse,
};

use browsr_types::{
    AutomateResponse, BrowserContext, Commands, ObserveResponse, SearchOptions,
    SearchResponse,
};
use reqwest::StatusCode;
use serde::{Deserialize, Serialize, de::DeserializeOwned};
use serde_json::{Value, json};
use thiserror::Error;

#[derive(Debug, Clone)]
pub enum TransportConfig {
    Http { base_url: String },
}

/// Browsr HTTP client for browser automation.
///
/// # Example
///
/// ```rust,ignore
/// use browsr_client::BrowsrClient;
///
/// // From environment variables (BROWSR_BASE_URL, BROWSR_API_KEY)
/// let client = BrowsrClient::from_env();
///
/// // From explicit URL (for local development)
/// let client = BrowsrClient::new("http://localhost:8082");
///
/// // With API key authentication
/// let client = BrowsrClient::new("https://api.browsr.dev")
///     .with_api_key("your-api-key");
/// ```
#[derive(Debug, Clone)]
pub struct BrowsrClient {
    transport: BrowsrTransport,
    config: BrowsrClientConfig,
}

#[derive(Debug, Clone)]
enum BrowsrTransport {
    Http(HttpTransport),
}

impl BrowsrClient {
    /// Create a new client with the specified base URL (no authentication).
    /// For local development, use this method.
    pub fn new(base_url: impl Into<String>) -> Self {
        let config = BrowsrClientConfig::new(base_url);
        Self::from_client_config(config)
    }

    /// Create a new client from environment variables.
    ///
    /// - `BROWSR_BASE_URL`: Base URL (defaults to `https://api.browsr.dev`)
    /// - `BROWSR_API_KEY`: Optional API key for authentication
    pub fn from_env() -> Self {
        let config = BrowsrClientConfig::from_env();
        Self::from_client_config(config)
    }

    /// Create a new client from explicit configuration.
    pub fn from_client_config(config: BrowsrClientConfig) -> Self {
        let http = config
            .build_http_client()
            .expect("Failed to build HTTP client");

        Self {
            transport: BrowsrTransport::Http(HttpTransport::new_with_client(
                &config.base_url,
                http,
            )),
            config,
        }
    }

    /// Set the API key for authentication.
    /// This rebuilds the HTTP client with the new authentication header.
    pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
        self.config = self.config.with_api_key(api_key);
        let http = self
            .config
            .build_http_client()
            .expect("Failed to build HTTP client");
        self.transport =
            BrowsrTransport::Http(HttpTransport::new_with_client(&self.config.base_url, http));
        self
    }

    /// Create HTTP transport client (legacy method).
    pub fn new_http(base_url: impl Into<String>) -> Self {
        Self::new(base_url)
    }

    /// Create client from transport config (legacy method).
    pub fn from_config(cfg: TransportConfig) -> Self {
        match cfg {
            TransportConfig::Http { base_url } => Self::new_http(base_url),
        }
    }

    /// Get the base URL.
    pub fn base_url(&self) -> &str {
        &self.config.base_url
    }

    /// Get the current configuration.
    pub fn config(&self) -> &BrowsrClientConfig {
        &self.config
    }

    /// Check if the client has authentication configured.
    pub fn has_auth(&self) -> bool {
        self.config.has_auth()
    }

    /// Check if this is a local development client.
    pub fn is_local(&self) -> bool {
        self.config.is_local()
    }

    // ============================================================
    // Session Management
    // ============================================================

    /// List all active browser sessions.
    pub async fn list_sessions(&self) -> Result<SessionListResponse, ClientError> {
        match &self.transport {
            BrowsrTransport::Http(inner) => inner.get("/sessions").await,
        }
    }

    /// Create a new browser session.
    /// Returns the full session info including viewer_url, sse_url, and frame_url.
    pub async fn create_session(&self) -> Result<SessionCreated, ClientError> {
        match &self.transport {
            BrowsrTransport::Http(inner) => {
                inner.post("/sessions", &Value::Null).await
            }
        }
    }

    /// Destroy a browser session.
    pub async fn destroy_session(&self, session_id: &str) -> Result<(), ClientError> {
        match &self.transport {
            BrowsrTransport::Http(inner) => inner
                .delete(&format!("/sessions/{}", session_id))
                .await
                .map(|_: Value| ()),
        }
    }

    /// Create a new shell session.
    pub async fn create_shell_session(
        &self,
        request: ShellCreateSessionRequest,
    ) -> Result<ShellCreateSessionResponse, ClientError> {
        match &self.transport {
            BrowsrTransport::Http(inner) => inner.post("/shells", &request).await,
        }
    }

    /// List active shells.
    pub async fn list_shell_sessions(&self) -> Result<ShellListResponse, ClientError> {
        match &self.transport {
            BrowsrTransport::Http(inner) => inner.get("/shells").await,
        }
    }

    /// Terminate a shell session.
    pub async fn terminate_shell_session(
        &self,
        session_id: &str,
    ) -> Result<ShellTerminateResponse, ClientError> {
        match &self.transport {
            BrowsrTransport::Http(inner) => inner.delete(&format!("/shells/{}", session_id)).await,
        }
    }

    /// Execute a shell command in an existing shell session.
    pub async fn shell_exec(
        &self,
        request: ShellExecRequest,
    ) -> Result<ShellExecResponse, ClientError> {
        match &self.transport {
            BrowsrTransport::Http(inner) => inner.post("/shells/exec", &request).await,
        }
    }

    // ============================================================
    // Pool Management
    // ============================================================

    /// Get warm pool status.
    pub async fn pool_status(&self) -> Result<Value, ClientError> {
        match &self.transport {
            BrowsrTransport::Http(inner) => inner.get("/shells/pool/status").await,
        }
    }

    /// Probe health of warm containers.
    pub async fn pool_probe(&self) -> Result<Value, ClientError> {
        match &self.transport {
            BrowsrTransport::Http(inner) => inner.post("/shells/pool/probe", &json!({})).await,
        }
    }

    /// Warm N containers for an image.
    pub async fn pool_warm(&self, image: &str, count: usize) -> Result<Value, ClientError> {
        match &self.transport {
            BrowsrTransport::Http(inner) => {
                inner
                    .post("/shells/pool/warm", &json!({"image": image, "count": count}))
                    .await
            }
        }
    }

    /// Drain all idle containers for an image.
    pub async fn pool_drain(&self, image: &str) -> Result<Value, ClientError> {
        match &self.transport {
            BrowsrTransport::Http(inner) => {
                inner
                    .post("/shells/pool/drain", &json!({"image": image}))
                    .await
            }
        }
    }

    /// Proxy an HTTP GET request to a container port.
    pub async fn proxy_session_port(
        &self,
        session_id: &str,
        port: u16,
        path: &str,
    ) -> Result<String, ClientError> {
        match &self.transport {
            BrowsrTransport::Http(inner) => {
                let url = inner.url(&format!(
                    "/shells/{}/proxy?port={}&path={}",
                    session_id,
                    port,
                    path.trim_start_matches('/'),
                ));
                let resp = inner.client.get(&url).send().await?;
                if !resp.status().is_success() {
                    let text = resp.text().await.unwrap_or_default();
                    return Err(ClientError::InvalidResponse(text));
                }
                resp.text().await.map_err(Into::into)
            }
        }
    }

    // ============================================================
    // Command Execution
    // ============================================================

    /// Execute a list of browser commands.
    pub async fn execute_commands(
        &self,
        commands: Vec<Commands>,
        session_id: Option<String>,
        headless: Option<bool>,
        context: Option<BrowserContext>,
    ) -> Result<AutomateResponse, ClientError> {
        let payload = CommandsPayload {
            commands,
            session_id,
            headless: headless.or(self.config.headless),
            context,
        };

        match &self.transport {
            BrowsrTransport::Http(inner) => inner.post("/commands", &payload).await,
        }
    }

    /// Execute a single browser command.
    pub async fn execute_command(
        &self,
        command: Commands,
        session_id: Option<String>,
        headless: Option<bool>,
    ) -> Result<AutomateResponse, ClientError> {
        self.execute_commands(vec![command], session_id, headless, None)
            .await
    }

    // ============================================================
    // Convenience Methods for Common Commands
    // ============================================================

    /// Navigate to a URL.
    pub async fn navigate(
        &self,
        url: &str,
        session_id: Option<String>,
    ) -> Result<AutomateResponse, ClientError> {
        self.execute_command(
            Commands::NavigateTo {
                url: url.to_string(),
            },
            session_id,
            None,
        )
        .await
    }

    /// Click an element by selector.
    pub async fn click(
        &self,
        selector: &str,
        session_id: Option<String>,
    ) -> Result<AutomateResponse, ClientError> {
        self.execute_command(
            Commands::Click {
                selector: selector.to_string(),
            },
            session_id,
            None,
        )
        .await
    }

    /// Type text into an element.
    pub async fn type_text(
        &self,
        selector: &str,
        text: &str,
        clear: Option<bool>,
        session_id: Option<String>,
    ) -> Result<AutomateResponse, ClientError> {
        self.execute_command(
            Commands::TypeText {
                selector: selector.to_string(),
                text: text.to_string(),
                clear,
            },
            session_id,
            None,
        )
        .await
    }

    /// Wait for an element to appear.
    pub async fn wait_for_element(
        &self,
        selector: &str,
        timeout_ms: Option<u64>,
        session_id: Option<String>,
    ) -> Result<AutomateResponse, ClientError> {
        self.execute_command(
            Commands::WaitForElement {
                selector: selector.to_string(),
                timeout_ms,
                visible_only: None,
            },
            session_id,
            None,
        )
        .await
    }

    /// Take a screenshot.
    pub async fn screenshot(
        &self,
        full_page: bool,
        session_id: Option<String>,
    ) -> Result<AutomateResponse, ClientError> {
        self.execute_command(
            Commands::Screenshot {
                full_page: Some(full_page),
                path: None,
            },
            session_id,
            None,
        )
        .await
    }

    /// Get page title.
    pub async fn get_title(
        &self,
        session_id: Option<String>,
    ) -> Result<AutomateResponse, ClientError> {
        self.execute_command(Commands::GetTitle, session_id, None)
            .await
    }

    /// Get text content of an element.
    pub async fn get_text(
        &self,
        selector: &str,
        session_id: Option<String>,
    ) -> Result<AutomateResponse, ClientError> {
        self.execute_command(
            Commands::GetText {
                selector: selector.to_string(),
            },
            session_id,
            None,
        )
        .await
    }

    /// Get HTML content of an element or page.
    pub async fn get_content(
        &self,
        selector: Option<String>,
        session_id: Option<String>,
    ) -> Result<AutomateResponse, ClientError> {
        self.execute_command(
            Commands::GetContent {
                selector,
                kind: None,
            },
            session_id,
            None,
        )
        .await
    }

    /// Evaluate JavaScript expression.
    pub async fn evaluate(
        &self,
        expression: &str,
        session_id: Option<String>,
    ) -> Result<AutomateResponse, ClientError> {
        self.execute_command(
            Commands::Evaluate {
                expression: expression.to_string(),
            },
            session_id,
            None,
        )
        .await
    }

    // ============================================================
    // Structured Extraction
    // ============================================================

    /// Extract structured content from the current page using AI.
    ///
    /// # Arguments
    /// * `query` - Natural language description of what to extract
    /// * `schema` - Optional JSON schema for the output
    /// * `max_chars` - Optional maximum characters to process
    /// * `session_id` - Optional session ID
    ///
    /// # Example
    /// ```rust,ignore
    /// let data = client.extract_structured(
    ///     "Extract all product names and prices",
    ///     None,
    ///     None,
    /// ).await?;
    /// ```
    pub async fn extract_structured(
        &self,
        query: &str,
        schema: Option<serde_json::Value>,
        max_chars: Option<usize>,
        session_id: Option<String>,
    ) -> Result<AutomateResponse, ClientError> {
        self.execute_command(
            Commands::ExtractStructuredContent {
                query: query.to_string(),
                schema,
                max_chars,
            },
            session_id,
            None,
        )
        .await
    }

    // ============================================================
    // Observation
    // ============================================================

    /// Observe the current browser state (screenshot + DOM snapshot).
    pub async fn observe(
        &self,
        session_id: Option<String>,
        headless: Option<bool>,
        opts: ObserveOptions,
    ) -> Result<ObserveResponse, ClientError> {
        let payload = ObservePayload {
            session_id,
            headless: headless.or(self.config.headless),
            use_image: opts.use_image,
            full_page: opts.full_page,
            wait_ms: opts.wait_ms,
            include_content: opts.include_content,
        };

        match &self.transport {
            BrowsrTransport::Http(inner) => {
                let envelope: ObserveEnvelope = inner.post("/observe", &payload).await?;
                Ok(envelope.observation)
            }
        }
    }

    /// Execute a raw CDP request against an existing session.
    pub async fn cdp(
        &self,
        session_id: impl Into<String>,
        method: impl Into<String>,
        params: Option<Value>,
    ) -> Result<Value, ClientError> {
        let payload = json!({
            "session_id": session_id.into(),
            "method": method.into(),
            "params": params.unwrap_or_else(|| json!({})),
        });

        match &self.transport {
            BrowsrTransport::Http(inner) => inner.post("/cdp", &payload).await,
        }
    }

    /// List recent buffered relay events for a relay session.
    pub async fn relay_events(
        &self,
        session_id: &str,
        limit: Option<usize>,
    ) -> Result<RelayEventsResponse, ClientError> {
        let path = match limit {
            Some(limit) => format!("/relay/sessions/{}/events?limit={}", session_id, limit),
            None => format!("/relay/sessions/{}/events", session_id),
        };

        match &self.transport {
            BrowsrTransport::Http(inner) => inner.get(&path).await,
        }
    }

    /// List relay sessions visible to the current authenticated user.
    pub async fn list_relay_sessions(&self) -> Result<RelaySessionListResponse, ClientError> {
        match &self.transport {
            BrowsrTransport::Http(inner) => inner.get("/relay/sessions").await,
        }
    }

    /// Clear buffered relay events for a relay session.
    pub async fn clear_relay_events(&self, session_id: &str) -> Result<Value, ClientError> {
        let path = format!("/relay/sessions/{}/events", session_id);
        match &self.transport {
            BrowsrTransport::Http(inner) => inner.delete(&path).await,
        }
    }

    // ============================================================
    // Scraping (v1 API)
    // ============================================================

    /// Scrape a URL with full format options (v1 API).
    pub async fn scrape_v1(&self, request: ScrapeApiRequest) -> Result<ScrapeApiResponse, ClientError> {
        match &self.transport {
            BrowsrTransport::Http(inner) => inner.post("/v1/scrape", &request).await,
        }
    }

    /// Scrape a URL with default options (markdown output).
    pub async fn scrape_url(&self, url: &str) -> Result<ScrapeApiResponse, ClientError> {
        self.scrape_v1(ScrapeApiRequest::new(url)).await
    }

    /// Crawl a website starting from a URL.
    pub async fn crawl(&self, request: CrawlApiRequest) -> Result<CrawlApiResponse, ClientError> {
        match &self.transport {
            BrowsrTransport::Http(inner) => inner.post("/v1/crawl", &request).await,
        }
    }

    /// Crawl a URL with default options (markdown, 10 pages, depth 2).
    pub async fn crawl_url(&self, url: &str) -> Result<CrawlApiResponse, ClientError> {
        self.crawl(CrawlApiRequest::new(url)).await
    }

    // ============================================================
    // Search
    // ============================================================

    /// Perform a web search via the /v1/search endpoint.
    pub async fn search(&self, options: SearchOptions) -> Result<SearchResponse, ClientError> {
        match &self.transport {
            BrowsrTransport::Http(inner) => inner.post("/v1/search", &options).await,
        }
    }

    /// Search with a query string.
    pub async fn search_query(&self, query: &str) -> Result<SearchResponse, ClientError> {
        let options = SearchOptions {
            query: query.to_string(),
            limit: None,
        };
        self.search(options).await
    }

    // ============================================================
    // Browser Step (Agent Integration)
    // ============================================================

    /// Execute a browser step with commands and optional context.
    ///
    /// This is the primary method for agent integration, providing full control
    /// over browser automation with context for sequence persistence.
    ///
    /// # Arguments
    /// * `request` - Full browser step request with commands and context
    ///
    /// # Example
    /// ```rust,ignore
    /// use browsr_client::{BrowsrClient, BrowserStepRequest, BrowserStepInput};
    /// use browsr_types::Commands;
    ///
    /// let client = BrowsrClient::from_env();
    ///
    /// let input = BrowserStepInput::new(vec![
    ///     Commands::NavigateTo { url: "https://example.com".to_string() },
    ///     Commands::Screenshot { full_page: Some(false), path: None },
    /// ]);
    ///
    /// let request = BrowserStepRequest::new(input)
    ///     .with_session_id("my-session")
    ///     .with_thread_id("thread-123");
    ///
    /// let result = client.step(request).await?;
    /// println!("Success: {}, URL: {:?}", result.success, result.url);
    /// ```
    pub async fn step(&self, request: BrowserStepRequest) -> Result<BrowserStepResult, ClientError> {
        match &self.transport {
            BrowsrTransport::Http(inner) => inner.post("/browser_step", &request).await,
        }
    }

    /// Execute a browser step with just commands (simple usage).
    ///
    /// Use this for quick automation tasks without context tracking.
    ///
    /// # Example
    /// ```rust,ignore
    /// use browsr_client::BrowsrClient;
    /// use browsr_types::Commands;
    ///
    /// let client = BrowsrClient::from_env();
    ///
    /// let result = client.step_commands(vec![
    ///     Commands::NavigateTo { url: "https://example.com".to_string() },
    /// ]).await?;
    /// ```
    pub async fn step_commands(
        &self,
        commands: Vec<Commands>,
    ) -> Result<BrowserStepResult, ClientError> {
        let input = BrowserStepInput::new(commands);
        let request = BrowserStepRequest::new(input);
        self.step(request).await
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
struct CommandsPayload {
    commands: Vec<Commands>,
    #[serde(skip_serializing_if = "Option::is_none")]
    session_id: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    headless: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    context: Option<BrowserContext>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
struct ObservePayload {
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub session_id: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub headless: Option<bool>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub use_image: Option<bool>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub full_page: Option<bool>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub wait_ms: Option<u64>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub include_content: Option<bool>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
struct ObserveEnvelope {
    pub session_id: String,
    pub observation: ObserveResponse,
}

// ============================================================
// Error Types
// ============================================================

#[derive(Debug, Error)]
pub enum ClientError {
    #[error("http request failed: {0}")]
    Http(#[from] reqwest::Error),
    #[error("invalid response: {0}")]
    InvalidResponse(String),
    #[error("serialization error: {0}")]
    Serialization(#[from] serde_json::Error),
    #[error("io error: {0}")]
    Io(#[from] std::io::Error),
}

// ============================================================
// HTTP Transport
// ============================================================

#[derive(Debug, Clone)]
struct HttpTransport {
    base_url: String,
    client: reqwest::Client,
}

impl HttpTransport {
    fn new_with_client(base_url: impl Into<String>, client: reqwest::Client) -> Self {
        Self {
            base_url: base_url.into(),
            client,
        }
    }

    fn url(&self, path: &str) -> String {
        let base = self.base_url.trim_end_matches('/');
        let suffix = path.trim_start_matches('/');
        format!("{}/{}", base, suffix)
    }

    async fn get<T: DeserializeOwned>(&self, path: &str) -> Result<T, ClientError> {
        let resp = self.client.get(self.url(path)).send().await?;
        Self::handle_response(resp).await
    }

    async fn delete<T: DeserializeOwned>(&self, path: &str) -> Result<T, ClientError> {
        let resp = self.client.delete(self.url(path)).send().await?;
        Self::handle_response(resp).await
    }

    async fn post<T: DeserializeOwned, B: Serialize + ?Sized>(
        &self,
        path: &str,
        body: &B,
    ) -> Result<T, ClientError> {
        let resp = self.client.post(self.url(path)).json(body).send().await?;
        Self::handle_response(resp).await
    }

    async fn handle_response<T: DeserializeOwned>(
        resp: reqwest::Response,
    ) -> Result<T, ClientError> {
        let status = resp.status();
        if status == StatusCode::NO_CONTENT {
            let empty: Value = Value::Null;
            let value: T = serde_json::from_value(empty).map_err(ClientError::Serialization)?;
            return Ok(value);
        }

        let text = resp.text().await?;
        if !status.is_success() {
            return Err(ClientError::InvalidResponse(format!(
                "{}: {}",
                status, text
            )));
        }

        serde_json::from_str(&text).map_err(ClientError::Serialization)
    }
}

// ============================================================
// Legacy Helper Functions
// ============================================================

/// Derive a default base URL for HTTP transport from standard env vars.
pub fn default_base_url() -> Option<String> {
    if let Ok(url) = std::env::var("BROWSR_API_URL") {
        return Some(url);
    }
    if let Ok(url) = std::env::var("BROWSR_BASE_URL") {
        return Some(url);
    }
    if let Ok(port) = std::env::var("BROWSR_PORT") {
        let host = std::env::var("BROWSR_HOST").unwrap_or_else(|_| "127.0.0.1".to_string());
        return Some(format!("http://{}:{}", host, port));
    }
    // Return cloud default
    Some(DEFAULT_BASE_URL.to_string())
}

pub fn default_transport() -> TransportConfig {
    TransportConfig::Http {
        base_url: default_base_url().unwrap_or_else(|| DEFAULT_BASE_URL.to_string()),
    }
}
