//! Plugin System Data Models — Phase-92 (T92.1)
//!
//! Core data structures for the plugin framework:
//! - `PluginManifest`: Static metadata declaring a plugin's identity and capabilities
//! - `PluginInfo`: Runtime state of a registered plugin
//! - `PluginType`: Classification of plugin functionality
//! - `PluginCapability`: Fine-grained capability declarations
//! - `PluginStatus`: Runtime health status

use std::collections::HashMap;
use std::fmt;

use serde::{Deserialize, Serialize};

// ============================================================================
// Error
// ============================================================================

/// Errors originating from the plugin subsystem.
#[derive(Debug, thiserror::Error)]
pub enum PluginError {
    /// Plugin not found in registry.
    #[error("Plugin not found: {0}")]
    NotFound(String),

    /// Plugin is currently unavailable (disconnected, unhealthy, disabled).
    #[error("Plugin unavailable: {0}")]
    Unavailable(String),

    /// Plugin endpoint returned an error.
    #[error("Plugin call failed: {0}")]
    CallFailed(String),

    /// HTTP transport error.
    #[error("HTTP error: {0}")]
    HttpError(String),

    /// Response parsing error.
    #[error("Invalid response: {0}")]
    InvalidResponse(String),

    /// Plugin version / API version mismatch.
    #[error("Version mismatch: {0}")]
    VersionMismatch(String),

    /// Configuration error.
    #[error("Config error: {0}")]
    ConfigError(String),

    /// Timeout.
    #[error("Plugin call timed out after {0}s")]
    Timeout(u64),

    /// Generic I/O error.
    #[error("IO error: {0}")]
    IoError(#[from] std::io::Error),

    /// JSON serialization error.
    #[error("JSON error: {0}")]
    JsonError(#[from] serde_json::Error),
}

pub type PluginResult<T> = Result<T, PluginError>;

// ============================================================================
// PluginType
// ============================================================================

/// Classification of plugin functionality.
///
/// Determines which protocol endpoints the plugin must implement
/// and how the plugin integrates with `ai_db` subsystems.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PluginType {
    /// Indexer: converts raw documents (PDF, DOCX, etc.) into tree structures.
    /// Required endpoints: `POST /api/v1/index/{format}`
    Indexer,

    /// Retriever: provides custom retrieval strategies.
    /// Required endpoints: `POST /api/v1/retrieve`
    Retriever,

    /// Processor: performs post-processing (summarization, TOC repair, etc.).
    /// Required endpoints: `POST /api/v1/process/{action}`
    Processor,

    /// Custom: user-defined plugin type with arbitrary endpoints.
    Custom,
}

impl fmt::Display for PluginType {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Indexer => write!(f, "indexer"),
            Self::Retriever => write!(f, "retriever"),
            Self::Processor => write!(f, "processor"),
            Self::Custom => write!(f, "custom"),
        }
    }
}

impl Default for PluginType {
    fn default() -> Self {
        Self::Custom
    }
}

// ============================================================================
// PluginCapability
// ============================================================================

/// Fine-grained capability declaration.
///
/// Plugins declare their capabilities so that `ai_db` can route
/// requests to the appropriate plugin and provide capability discovery.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PluginCapability {
    /// Can index PDF documents into tree structures.
    PdfIndexing,
    /// Can index Markdown documents into tree structures.
    MarkdownIndexing,
    /// Can generate section summaries.
    SummaryGeneration,
    /// Can verify and repair document TOC.
    TocVerification,
    /// Can process images / visual content.
    Vision,
    /// Can process audio content.
    Audio,
    /// Can perform OCR on scanned documents.
    Ocr,
    /// Custom capability (user-defined string).
    Other(String),
}

impl fmt::Display for PluginCapability {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::PdfIndexing => write!(f, "pdf_indexing"),
            Self::MarkdownIndexing => write!(f, "markdown_indexing"),
            Self::SummaryGeneration => write!(f, "summary_generation"),
            Self::TocVerification => write!(f, "toc_verification"),
            Self::Vision => write!(f, "vision"),
            Self::Audio => write!(f, "audio"),
            Self::Ocr => write!(f, "ocr"),
            Self::Other(s) => write!(f, "{}", s),
        }
    }
}

// ============================================================================
// PluginManifest
// ============================================================================

/// Static metadata that a plugin declares about itself.
///
/// Retrieved from the plugin's `GET /api/v1/capabilities` endpoint
/// or specified in `.plugins.json`.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PluginManifest {
    /// Unique plugin name (e.g., "pageindex-aifastdb").
    pub name: String,

    /// Plugin version (semver, e.g., "1.2.0").
    pub version: String,

    /// API protocol version this plugin implements (e.g., "1.0").
    /// Used for compatibility checks.
    pub api_version: String,

    /// Plugin type classification.
    pub plugin_type: PluginType,

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

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

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

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

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

impl PluginManifest {
    /// Create a minimal manifest.
    pub fn new(name: impl Into<String>, version: impl Into<String>, plugin_type: PluginType) -> Self {
        Self {
            name: name.into(),
            version: version.into(),
            api_version: "1.0".to_string(),
            plugin_type,
            capabilities: Vec::new(),
            description: None,
            author: None,
            homepage: None,
            extra: HashMap::new(),
        }
    }

    /// Add a capability.
    pub fn with_capability(mut self, cap: PluginCapability) -> Self {
        self.capabilities.push(cap);
        self
    }

    /// Set description.
    pub fn with_description(mut self, desc: impl Into<String>) -> Self {
        self.description = Some(desc.into());
        self
    }

    /// Check if the plugin has a specific capability.
    pub fn has_capability(&self, cap: &PluginCapability) -> bool {
        self.capabilities.contains(cap)
    }
}

// ============================================================================
// PluginStatus
// ============================================================================

/// Runtime health status of a plugin.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PluginStatus {
    /// Plugin is healthy and responding.
    Connected,

    /// Plugin was healthy but last health check failed.
    Disconnected,

    /// Plugin returned an error response.
    Error,

    /// Plugin has been manually disabled by the administrator.
    Disabled,

    /// Initial state before first health check.
    Unknown,
}

impl fmt::Display for PluginStatus {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Connected => write!(f, "connected"),
            Self::Disconnected => write!(f, "disconnected"),
            Self::Error => write!(f, "error"),
            Self::Disabled => write!(f, "disabled"),
            Self::Unknown => write!(f, "unknown"),
        }
    }
}

impl Default for PluginStatus {
    fn default() -> Self {
        Self::Unknown
    }
}

// ============================================================================
// PluginInfo
// ============================================================================

/// Runtime information about a registered plugin.
///
/// Combines the static `PluginManifest` with dynamic state
/// (status, health check results, call statistics).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PluginInfo {
    /// Static metadata from the plugin.
    pub manifest: PluginManifest,

    /// HTTP endpoint URL (e.g., "http://localhost:8800").
    pub endpoint: String,

    /// Current runtime status.
    pub status: PluginStatus,

    /// Whether this plugin is enabled (can be toggled by admin).
    pub enabled: bool,

    /// Last successful health check timestamp (Unix seconds).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub last_health_check: Option<u64>,

    /// Error message from last failed health check.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub last_error: Option<String>,

    /// Total number of successful calls.
    #[serde(default)]
    pub call_count: u64,

    /// Total number of failed calls.
    #[serde(default)]
    pub error_count: u64,
}

impl PluginInfo {
    /// Create a new PluginInfo from a manifest and endpoint.
    pub fn new(manifest: PluginManifest, endpoint: impl Into<String>) -> Self {
        Self {
            manifest,
            endpoint: endpoint.into(),
            status: PluginStatus::Unknown,
            enabled: true,
            last_health_check: None,
            last_error: None,
            call_count: 0,
            error_count: 0,
        }
    }

    /// Check if the plugin is available for calls (connected + enabled).
    pub fn is_available(&self) -> bool {
        self.enabled && self.status == PluginStatus::Connected
    }

    /// Record a successful call.
    pub fn record_success(&mut self) {
        self.call_count += 1;
    }

    /// Record a failed call.
    pub fn record_failure(&mut self, error: impl Into<String>) {
        self.error_count += 1;
        self.last_error = Some(error.into());
    }

    /// Produce a summary for API responses.
    pub fn to_summary(&self) -> serde_json::Value {
        serde_json::json!({
            "name": self.manifest.name,
            "version": self.manifest.version,
            "type": self.manifest.plugin_type,
            "status": self.status,
            "enabled": self.enabled,
            "endpoint": self.endpoint,
            "capabilities": self.manifest.capabilities,
            "call_count": self.call_count,
            "error_count": self.error_count,
            "last_health_check": self.last_health_check,
            "last_error": self.last_error,
        })
    }
}

// ============================================================================
// PluginCallOptions
// ============================================================================

/// Options for a single plugin call.
#[derive(Debug, Clone)]
pub struct PluginCallOptions {
    /// Request timeout in seconds (default: 30).
    pub timeout_secs: u64,

    /// Maximum number of retries on transient failures (default: 2).
    pub max_retries: u32,

    /// Additional HTTP headers to include.
    pub headers: HashMap<String, String>,
}

impl Default for PluginCallOptions {
    fn default() -> Self {
        Self {
            timeout_secs: 30,
            max_retries: 2,
            headers: HashMap::new(),
        }
    }
}

impl PluginCallOptions {
    /// Create with a custom timeout.
    pub fn with_timeout(mut self, secs: u64) -> Self {
        self.timeout_secs = secs;
        self
    }

    /// Create with custom retries.
    pub fn with_retries(mut self, retries: u32) -> Self {
        self.max_retries = retries;
        self
    }
}

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

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

    #[test]
    fn test_plugin_manifest_creation() {
        let manifest = PluginManifest::new("test-plugin", "1.0.0", PluginType::Indexer)
            .with_capability(PluginCapability::PdfIndexing)
            .with_capability(PluginCapability::MarkdownIndexing)
            .with_description("A test plugin");

        assert_eq!(manifest.name, "test-plugin");
        assert_eq!(manifest.version, "1.0.0");
        assert_eq!(manifest.api_version, "1.0");
        assert_eq!(manifest.plugin_type, PluginType::Indexer);
        assert_eq!(manifest.capabilities.len(), 2);
        assert!(manifest.has_capability(&PluginCapability::PdfIndexing));
        assert!(!manifest.has_capability(&PluginCapability::Vision));
    }

    #[test]
    fn test_plugin_info_availability() {
        let manifest = PluginManifest::new("p1", "1.0.0", PluginType::Processor);
        let mut info = PluginInfo::new(manifest, "http://localhost:8800");

        // Initially unknown + enabled → not available
        assert!(!info.is_available());

        // Connected + enabled → available
        info.status = PluginStatus::Connected;
        assert!(info.is_available());

        // Connected + disabled → not available
        info.enabled = false;
        assert!(!info.is_available());

        // Disconnected + enabled → not available
        info.enabled = true;
        info.status = PluginStatus::Disconnected;
        assert!(!info.is_available());
    }

    #[test]
    fn test_plugin_info_call_recording() {
        let manifest = PluginManifest::new("p1", "1.0.0", PluginType::Custom);
        let mut info = PluginInfo::new(manifest, "http://localhost:8800");

        info.record_success();
        info.record_success();
        info.record_failure("timeout");

        assert_eq!(info.call_count, 2);
        assert_eq!(info.error_count, 1);
        assert_eq!(info.last_error.as_deref(), Some("timeout"));
    }

    #[test]
    fn test_plugin_manifest_serialization() {
        let manifest = PluginManifest::new("pageindex", "2.0.0", PluginType::Indexer)
            .with_capability(PluginCapability::PdfIndexing);

        let json = serde_json::to_string(&manifest).unwrap();
        let deserialized: PluginManifest = serde_json::from_str(&json).unwrap();

        assert_eq!(deserialized.name, "pageindex");
        assert_eq!(deserialized.plugin_type, PluginType::Indexer);
        assert_eq!(deserialized.capabilities.len(), 1);
    }

    #[test]
    fn test_plugin_type_display() {
        assert_eq!(PluginType::Indexer.to_string(), "indexer");
        assert_eq!(PluginType::Retriever.to_string(), "retriever");
        assert_eq!(PluginType::Processor.to_string(), "processor");
        assert_eq!(PluginType::Custom.to_string(), "custom");
    }

    #[test]
    fn test_plugin_status_display() {
        assert_eq!(PluginStatus::Connected.to_string(), "connected");
        assert_eq!(PluginStatus::Disconnected.to_string(), "disconnected");
        assert_eq!(PluginStatus::Disabled.to_string(), "disabled");
    }

    #[test]
    fn test_plugin_call_options_default() {
        let opts = PluginCallOptions::default();
        assert_eq!(opts.timeout_secs, 30);
        assert_eq!(opts.max_retries, 2);
    }

    #[test]
    fn test_plugin_info_summary() {
        let manifest = PluginManifest::new("test", "1.0.0", PluginType::Indexer);
        let info = PluginInfo::new(manifest, "http://localhost:9000");
        let summary = info.to_summary();

        assert_eq!(summary["name"], "test");
        assert_eq!(summary["endpoint"], "http://localhost:9000");
    }
}
