//! Response transform pipeline for stripping noise from API responses.
//!
//! Transforms sit between the executor response and the MCP response,
//! configured per-capability in the YAML `transform` section.
//!
//! Pipeline order (fixed): **project -> rename -> redact -> format**
//!
//! ```text
//! Executor Response
//!       │
//!       ▼
//! ┌─────────────┐
//! │  Transform  │──▶ project ──▶ rename ──▶ redact ──▶ format
//! │  Pipeline   │
//! └─────────────┘
//!       │
//!       ▼
//! MCP Response (lean)
//! ```

use std::collections::HashMap;

use serde::{Deserialize, Serialize};
use serde_json::Value;

// ============================================================================
// Configuration types (deserialized from YAML)
// ============================================================================

/// Complete transform configuration for a capability.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct TransformConfig {
    /// Field projection (allowlist of JSON paths to keep).
    #[serde(default)]
    pub project: Vec<String>,

    /// Field renaming map (`old_path` -> `new_name`).
    #[serde(default)]
    pub rename: HashMap<String, String>,

    /// PII/sensitive data redaction rules.
    #[serde(default)]
    pub redact: Vec<RedactRule>,

    /// Output format conversion.
    #[serde(default)]
    pub format: Option<FormatConfig>,
}

impl TransformConfig {
    /// Returns `true` when the config contains no transform operations.
    ///
    /// Used as a `skip_serializing_if` predicate so empty transform sections
    /// are omitted from serialised capability YAML.
    #[must_use]
    pub fn is_empty(&self) -> bool {
        self.project.is_empty()
            && self.rename.is_empty()
            && self.redact.is_empty()
            && self.format.is_none()
    }
}

/// A single redaction rule.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RedactRule {
    /// Regex pattern to match.
    pub pattern: String,
    /// Replacement string.
    pub replacement: String,
}

/// Output format configuration.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FormatConfig {
    /// Format type.
    #[serde(rename = "type")]
    pub format_type: FormatType,
    /// Template string (for `type=template`).
    #[serde(default)]
    pub template: Option<String>,
}

/// Supported format transformations.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum FormatType {
    /// Flatten nested objects to dot-separated top-level keys.
    Flat,
    /// Keep nested structure (default, noop).
    Nested,
    /// Apply a `{{var}}` template.
    Template,
}

// ============================================================================
// JSON path types (shared with playbook module)
// ============================================================================

/// A single segment in a parsed JSON path.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum JsonPathSegment {
    /// Object key: `"foo"`.
    Key(String),
    /// Array wildcard: `"[]"`.
    ArrayWildcard,
    /// Array index: `"[0]"`.
    ArrayIndex(usize),
}

/// A parsed JSON path like `"web.results[].title"`.
pub type JsonPath = Vec<JsonPathSegment>;

/// Parse a dot-separated JSON path string into segments.
///
/// Supports:
/// - `"foo.bar"` -> `[Key("foo"), Key("bar")]`
/// - `"results[].title"` -> `[Key("results"), ArrayWildcard, Key("title")]`
/// - `"items[0].name"` -> `[Key("items"), ArrayIndex(0), Key("name")]`
#[must_use]
pub fn parse_json_path(path: &str) -> JsonPath {
    let mut segments = Vec::new();
    for part in path.split('.') {
        if part.is_empty() {
            continue;
        }
        if let Some(before_bracket) = part.strip_suffix("[]") {
            if !before_bracket.is_empty() {
                segments.push(JsonPathSegment::Key(before_bracket.to_string()));
            }
            segments.push(JsonPathSegment::ArrayWildcard);
        } else if let Some(idx_start) = part.find('[') {
            let key = &part[..idx_start];
            if !key.is_empty() {
                segments.push(JsonPathSegment::Key(key.to_string()));
            }
            let idx_str = &part[idx_start + 1..part.len() - 1];
            if let Ok(idx) = idx_str.parse::<usize>() {
                segments.push(JsonPathSegment::ArrayIndex(idx));
            }
        } else {
            segments.push(JsonPathSegment::Key(part.to_string()));
        }
    }
    segments
}

/// Resolve a parsed JSON path against a value, collecting all matched leaf values.
///
/// Array wildcards expand into every element of the matched array.
#[must_use]
pub fn resolve_path(value: &Value, path: &[JsonPathSegment]) -> Vec<Value> {
    if path.is_empty() {
        return vec![value.clone()];
    }

    match &path[0] {
        JsonPathSegment::Key(key) => match value.get(key.as_str()) {
            Some(child) => resolve_path(child, &path[1..]),
            None => vec![],
        },
        JsonPathSegment::ArrayWildcard => match value.as_array() {
            Some(arr) => arr
                .iter()
                .flat_map(|v| resolve_path(v, &path[1..]))
                .collect(),
            None => vec![],
        },
        JsonPathSegment::ArrayIndex(idx) => match value.as_array() {
            Some(arr) => match arr.get(*idx) {
                Some(child) => resolve_path(child, &path[1..]),
                None => vec![],
            },
            None => vec![],
        },
    }
}

/// Resolve a path to a single value (first match), returning `Value::Null` if none.
#[must_use]
pub fn resolve_path_single(value: &Value, path: &[JsonPathSegment]) -> Value {
    let results = resolve_path(value, path);
    if results.len() == 1 {
        results.into_iter().next().unwrap_or(Value::Null)
    } else if results.is_empty() {
        Value::Null
    } else {
        Value::Array(results)
    }
}

pub mod pipeline;
pub use pipeline::TransformPipeline;
