//! Plugin HTTP Communication Protocol — Phase-92 (T92.3)
//!
//! Defines the request/response models for the plugin communication protocol.
//!
//! # Required Plugin Endpoints
//!
//! All plugins MUST implement:
//! - `GET /api/v1/health` → `HealthResponse`
//! - `GET /api/v1/capabilities` → `CapabilitiesResponse`
//!
//! Indexer plugins MUST also implement:
//! - `POST /api/v1/index/{format}` → `IndexResponse`
//!
//! Processor plugins MUST also implement:
//! - `POST /api/v1/process/{action}` → `ProcessResponse`
//!
//! # API Version Negotiation
//!
//! The `api_version` field in responses is checked against the host's
//! supported API version. Incompatible versions are rejected.

use serde::{Deserialize, Serialize};
use std::collections::HashMap;

use super::types::{PluginCapability, PluginManifest, PluginType};

/// Current API protocol version supported by the host.
pub const SUPPORTED_API_VERSION: &str = "1.0";

// ============================================================================
// Health Endpoint
// ============================================================================

/// Response from `GET /api/v1/health`.
///
/// This is the lightest-weight probe. If this endpoint returns 200,
/// the plugin is considered alive.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HealthResponse {
    /// Plugin name.
    pub name: String,

    /// Plugin version.
    pub version: String,

    /// API protocol version.
    pub api_version: String,

    /// Health status: "ok", "degraded", or "error".
    pub status: String,

    /// Optional details about current state.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub message: Option<String>,

    /// Uptime in seconds (optional).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub uptime_secs: Option<f64>,
}

impl HealthResponse {
    /// Check if the plugin reports itself as healthy.
    pub fn is_healthy(&self) -> bool {
        self.status == "ok" || self.status == "degraded"
    }

    /// Check API version compatibility.
    pub fn is_compatible(&self) -> bool {
        // Major version must match
        let host_major = SUPPORTED_API_VERSION.split('.').next().unwrap_or("1");
        let plugin_major = self.api_version.split('.').next().unwrap_or("0");
        host_major == plugin_major
    }
}

// ============================================================================
// Capabilities Endpoint
// ============================================================================

/// Response from `GET /api/v1/capabilities`.
///
/// Provides full plugin manifest including capabilities list.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CapabilitiesResponse {
    /// Plugin name.
    pub name: String,

    /// Plugin version.
    pub version: String,

    /// API protocol version.
    pub api_version: String,

    /// Plugin type.
    pub plugin_type: PluginType,

    /// List of capabilities.
    pub capabilities: Vec<PluginCapability>,

    /// Human-readable description.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub description: Option<String>,

    /// Author.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub author: Option<String>,

    /// Homepage URL.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub homepage: Option<String>,

    /// Additional metadata.
    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
    pub extra: HashMap<String, serde_json::Value>,
}

impl CapabilitiesResponse {
    /// Convert to a `PluginManifest`.
    pub fn to_manifest(&self) -> PluginManifest {
        PluginManifest {
            name: self.name.clone(),
            version: self.version.clone(),
            api_version: self.api_version.clone(),
            plugin_type: self.plugin_type,
            capabilities: self.capabilities.clone(),
            description: self.description.clone(),
            author: self.author.clone(),
            homepage: self.homepage.clone(),
            extra: self.extra.clone(),
        }
    }
}

// ============================================================================
// Index Endpoint (Indexer plugins)
// ============================================================================

/// Request body for `POST /api/v1/index/{format}`.
///
/// The `{format}` path parameter specifies the input format (e.g., "pdf", "md").
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IndexRequest {
    /// Raw document content (base64-encoded for binary formats like PDF).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub content: Option<String>,

    /// File path (alternative to content, for local files).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub file_path: Option<String>,

    /// Document name / identifier.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub document_name: Option<String>,

    /// Configuration overrides for this indexing request.
    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
    pub config: HashMap<String, serde_json::Value>,
}

/// Response from `POST /api/v1/index/{format}`.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IndexResponse {
    /// Whether indexing succeeded.
    pub success: bool,

    /// The resulting tree structure (as JSON, compatible with `TreeDocument`).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub tree: Option<serde_json::Value>,

    /// Document metadata.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub metadata: Option<serde_json::Value>,

    /// Error message if indexing failed.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub error: Option<String>,

    /// Processing duration in milliseconds.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub duration_ms: Option<u64>,
}

// ============================================================================
// Process Endpoint (Processor plugins)
// ============================================================================

/// Request body for `POST /api/v1/process/{action}`.
///
/// The `{action}` path parameter specifies the processing action
/// (e.g., "summarize", "verify_toc", "repair_toc").
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProcessRequest {
    /// The tree structure to process (as JSON).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub tree: Option<serde_json::Value>,

    /// Raw text content to process.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub content: Option<String>,

    /// Configuration overrides.
    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
    pub config: HashMap<String, serde_json::Value>,
}

/// Response from `POST /api/v1/process/{action}`.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProcessResponse {
    /// Whether processing succeeded.
    pub success: bool,

    /// The resulting processed data.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub result: Option<serde_json::Value>,

    /// Error message if processing failed.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub error: Option<String>,

    /// Processing duration in milliseconds.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub duration_ms: Option<u64>,
}

// ============================================================================
// Generic Plugin Call
// ============================================================================

/// Generic request for custom plugin actions.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PluginCallRequest {
    /// Action name.
    pub action: String,

    /// Arbitrary payload.
    #[serde(default)]
    pub payload: serde_json::Value,
}

/// Generic response from custom plugin actions.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PluginCallResponse {
    /// Whether the call succeeded.
    pub success: bool,

    /// Result payload.
    #[serde(default)]
    pub data: serde_json::Value,

    /// Error message if the call failed.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub error: Option<String>,
}

// ============================================================================
// Protocol URL helpers
// ============================================================================

/// Build a health check URL from a plugin endpoint.
pub fn health_url(endpoint: &str) -> String {
    format!("{}/api/v1/health", endpoint.trim_end_matches('/'))
}

/// Build a capabilities URL from a plugin endpoint.
pub fn capabilities_url(endpoint: &str) -> String {
    format!("{}/api/v1/capabilities", endpoint.trim_end_matches('/'))
}

/// Build an index URL from a plugin endpoint and format.
pub fn index_url(endpoint: &str, format: &str) -> String {
    format!(
        "{}/api/v1/index/{}",
        endpoint.trim_end_matches('/'),
        format
    )
}

/// Build a process URL from a plugin endpoint and action.
pub fn process_url(endpoint: &str, action: &str) -> String {
    format!(
        "{}/api/v1/process/{}",
        endpoint.trim_end_matches('/'),
        action
    )
}

// ============================================================================
// Tests
// ============================================================================

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

    #[test]
    fn test_health_url() {
        assert_eq!(health_url("http://localhost:8800"), "http://localhost:8800/api/v1/health");
        assert_eq!(health_url("http://localhost:8800/"), "http://localhost:8800/api/v1/health");
    }

    #[test]
    fn test_capabilities_url() {
        assert_eq!(
            capabilities_url("http://localhost:8800"),
            "http://localhost:8800/api/v1/capabilities"
        );
    }

    #[test]
    fn test_index_url() {
        assert_eq!(
            index_url("http://localhost:8800", "pdf"),
            "http://localhost:8800/api/v1/index/pdf"
        );
    }

    #[test]
    fn test_process_url() {
        assert_eq!(
            process_url("http://localhost:8800", "summarize"),
            "http://localhost:8800/api/v1/process/summarize"
        );
    }

    #[test]
    fn test_health_response_compatibility() {
        let resp = HealthResponse {
            name: "test".to_string(),
            version: "1.0.0".to_string(),
            api_version: "1.0".to_string(),
            status: "ok".to_string(),
            message: None,
            uptime_secs: Some(42.0),
        };
        assert!(resp.is_healthy());
        assert!(resp.is_compatible());
    }

    #[test]
    fn test_health_response_incompatible_version() {
        let resp = HealthResponse {
            name: "test".to_string(),
            version: "1.0.0".to_string(),
            api_version: "2.0".to_string(),
            status: "ok".to_string(),
            message: None,
            uptime_secs: None,
        };
        assert!(!resp.is_compatible());
    }

    #[test]
    fn test_capabilities_to_manifest() {
        let caps = CapabilitiesResponse {
            name: "pageindex".to_string(),
            version: "2.0.0".to_string(),
            api_version: "1.0".to_string(),
            plugin_type: PluginType::Indexer,
            capabilities: vec![PluginCapability::PdfIndexing],
            description: Some("PDF indexer".to_string()),
            author: None,
            homepage: None,
            extra: HashMap::new(),
        };

        let manifest = caps.to_manifest();
        assert_eq!(manifest.name, "pageindex");
        assert_eq!(manifest.plugin_type, PluginType::Indexer);
        assert!(manifest.has_capability(&PluginCapability::PdfIndexing));
    }

    #[test]
    fn test_index_request_serialization() {
        let req = IndexRequest {
            content: Some("# Hello\nWorld".to_string()),
            file_path: None,
            document_name: Some("test.md".to_string()),
            config: HashMap::new(),
        };
        let json = serde_json::to_string(&req).unwrap();
        assert!(json.contains("test.md"));
    }
}
