//! Wire types shared across all provisioning surfaces (CLI, HTTP, MCP).

use serde::{Deserialize, Serialize};

// ── Pagination ────────────────────────────────────────────────────────────────

#[derive(Debug, Serialize, Deserialize)]
pub struct Listing<T> {
    pub items: Vec<T>,
    pub total: usize,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub cursor: Option<String>,
}

impl<T> Listing<T> {
    pub fn new(items: Vec<T>) -> Self {
        let total = items.len();
        Self { items, total, cursor: None }
    }

    pub fn with_cursor(items: Vec<T>, total: usize, cursor: Option<String>) -> Self {
        Self { items, total, cursor }
    }
}

// ── Single resource ───────────────────────────────────────────────────────────

#[derive(Debug, Serialize, Deserialize)]
pub struct ItemResponse<T> {
    #[serde(flatten)]
    pub inner: T,
}

impl<T> ItemResponse<T> {
    pub fn new(inner: T) -> Self {
        Self { inner }
    }
}

impl<T> From<T> for ItemResponse<T> {
    fn from(inner: T) -> Self {
        Self { inner }
    }
}

// ── Errors ────────────────────────────────────────────────────────────────────

#[derive(Debug, Serialize, Deserialize, thiserror::Error)]
#[error("{error} [{code}]")]
pub struct ErrorResponse {
    pub error: String,
    pub code: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub hint: Option<String>,
}

impl ErrorResponse {
    pub fn new(code: impl Into<String>, error: impl Into<String>) -> Self {
        Self { error: error.into(), code: code.into(), hint: None }
    }

    pub fn with_hint(mut self, hint: impl Into<String>) -> Self {
        self.hint = Some(hint.into());
        self
    }
}

// ── Tool errors ───────────────────────────────────────────────────────────────

#[derive(Debug, thiserror::Error)]
pub enum ToolError {
    #[error("invalid parameter '{param}': {reason}")]
    InvalidParam { param: String, reason: String },

    #[error("backend error: {0}")]
    Backend(#[from] anyhow::Error),

    #[error("unauthorized: {0}")]
    Unauthorized(String),

    #[error("not found: {0}")]
    NotFound(String),

    #[error("tool invocation failed: {0}")]
    Invocation(String),
}

impl ToolError {
    pub fn invalid_param(param: impl Into<String>, reason: impl Into<String>) -> Self {
        Self::InvalidParam { param: param.into(), reason: reason.into() }
    }

    pub fn not_found(what: impl Into<String>) -> Self {
        Self::NotFound(what.into())
    }

    pub fn invocation(msg: impl Into<String>) -> Self {
        Self::Invocation(msg.into())
    }

    pub fn to_error_response(&self) -> ErrorResponse {
        let (code, hint) = match self {
            Self::InvalidParam { .. } => ("INVALID_PARAM", None),
            Self::Backend(_) => ("BACKEND_ERROR", None),
            Self::Unauthorized(_) => ("UNAUTHORIZED", Some("check your credentials or role")),
            Self::NotFound(_) => ("NOT_FOUND", None),
            Self::Invocation(_) => ("INVOCATION_ERROR", None),
        };
        let mut resp = ErrorResponse::new(code, self.to_string());
        if let Some(h) = hint {
            resp = resp.with_hint(h);
        }
        resp
    }
}

// ── Tool taxonomy ─────────────────────────────────────────────────────────────

/// Used by auth middleware (RBAC) to gate access without cedar.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ToolCategory {
    /// Safe reads — allowed to all authenticated roles.
    Read,
    /// State-changing but reversible operations (create, deploy).
    Mutation,
    /// Irreversible operations (delete, unseal, destroy).
    Destructive,
    /// Platform admin operations (config reload, policy change).
    Admin,
}

impl ToolCategory {
    /// Whether this category requires at least `operator` role.
    pub fn requires_operator(self) -> bool {
        matches!(self, Self::Mutation | Self::Destructive | Self::Admin)
    }

    /// Whether this category requires `admin` role exclusively.
    pub fn requires_admin(self) -> bool {
        matches!(self, Self::Destructive | Self::Admin)
    }
}

// ── Auth context ──────────────────────────────────────────────────────────────

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserContext {
    pub user_id: String,
    pub roles: Vec<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub tenant: Option<String>,
}

impl UserContext {
    pub fn has_role(&self, role: &str) -> bool {
        self.roles.iter().any(|r| r == role)
    }

    pub fn is_admin(&self) -> bool {
        self.has_role("admin")
    }

    pub fn is_operator(&self) -> bool {
        self.has_role("operator") || self.is_admin()
    }
}

/// Execution environment passed to every tool invocation.
#[derive(Debug, Clone, Default)]
pub struct Environment {
    /// Base URL of the running orchestrator HTTP API, e.g. `http://localhost:9011`.
    pub orchestrator_url: String,
    /// Root path of provisioning workspaces on disk.
    pub workspaces_root: std::path::PathBuf,
    /// Nickel cache directory (ncl-sync output).
    pub ncl_cache_dir: std::path::PathBuf,
    /// Root of the extensions tree (`provisioning/extensions/`).
    pub extensions_root: std::path::PathBuf,
    /// Root of the project-level ontology (`.ontology/` relative to project root).
    pub ontology_root: std::path::PathBuf,
}

// ── Human-readable summary ────────────────────────────────────────────────────

/// Implemented by response types to produce a one-line human-readable summary.
pub trait SmartResponse {
    fn summary(&self) -> String;
}

impl<T: SmartResponse> SmartResponse for Listing<T> {
    fn summary(&self) -> String {
        if self.items.is_empty() {
            return "no items".to_owned();
        }
        let first = self.items[0].summary();
        if self.total == 1 {
            return first;
        }
        format!("{first} (+{} more)", self.total - 1)
    }
}

// ── Tests ─────────────────────────────────────────────────────────────────────

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

    #[test]
    fn listing_roundtrip() {
        let l: Listing<String> = Listing::new(vec!["a".to_owned(), "b".to_owned()]);
        let json = serde_json::to_string(&l).unwrap();
        let parsed: Listing<String> = serde_json::from_str(&json).unwrap();
        assert_eq!(parsed.total, 2);
        assert_eq!(parsed.items, vec!["a", "b"]);
    }

    #[test]
    fn error_response_hint() {
        let e = ErrorResponse::new("NOT_FOUND", "workspace foo not found")
            .with_hint("run 'provisioning workspace register'");
        let json = serde_json::to_string(&e).unwrap();
        assert!(json.contains("hint"));
    }

    #[test]
    fn tool_error_to_response_unauthorized() {
        let e = ToolError::Unauthorized("read-only role".to_owned());
        let r = e.to_error_response();
        assert_eq!(r.code, "UNAUTHORIZED");
        assert!(r.hint.is_some());
    }

    #[test]
    fn tool_category_rbac() {
        assert!(!ToolCategory::Read.requires_operator());
        assert!(ToolCategory::Mutation.requires_operator());
        assert!(ToolCategory::Destructive.requires_admin());
        assert!(!ToolCategory::Mutation.requires_admin());
    }

    #[test]
    fn user_context_roles() {
        let u = UserContext {
            user_id: "u1".to_owned(),
            roles: vec!["operator".to_owned()],
            tenant: None,
        };
        assert!(u.is_operator());
        assert!(!u.is_admin());
    }
}
