use car_a2ui::{A2uiSurface, RenderReport};
use serde_json::{json, Value};

use crate::types::IntentSignature;

/// A pluggable rule mapping `(RenderReport, A2uiSurface)` to a list
/// of RFC 6902 JSON-Patch ops applied to the surface's component
/// tree. Implementations are deterministic and side-effect-free.
///
/// In the longer-term car-side architecture, strategies are entries
/// in a memgine skill graph keyed on `IntentSignature` via
/// `SkillTrigger.structured`. This trait is the interface; memgine
/// is the persistence + ranking layer in front of it.
pub trait RenderingStrategy: Send + Sync {
    /// Stable identifier persisted with success/failure feedback. Map
    /// 1:1 to memgine skill names.
    fn id(&self) -> &'static str;

    /// Return `None` (or an empty `Vec`) when this strategy has no
    /// opinion. Otherwise return the JSON-Patch ops the renderer
    /// should apply to the component tree. Always include a `test`
    /// precondition guarding the assumptions the strategy is making
    /// — protects against patch-stale-race when an array reshuffles
    /// between report-time and patch-time.
    fn proposes(
        &self,
        report: &RenderReport,
        signature: &IntentSignature,
        surface: &A2uiSurface,
    ) -> Option<Value>;
}

/// Built-in strategy library. Loaded on agent construction; can be
/// extended at runtime with caller-supplied strategies.
pub struct BuiltInStrategies;

impl BuiltInStrategies {
    pub fn default_set() -> Vec<Box<dyn RenderingStrategy>> {
        // DenseListForLargeCardinality first so a 50-item undomained
        // list gets density before domain — the bigger UX win.
        vec![
            Box::new(DenseListForLargeCardinality),
            Box::new(TagSmallListDomain),
            Box::new(CompactDenseColumn),
        ]
    }
}

/// When a List has > 20 items and no explicit density hint, propose
/// `density: "compact"`. The Swift renderer's `Layout.swift` reads
/// `x-renderer.density` and drops row height ~40%, fitting more on
/// screen without scrolling. Loop terminates when the agent sees the
/// hint on the next render.
pub struct DenseListForLargeCardinality;

impl RenderingStrategy for DenseListForLargeCardinality {
    fn id(&self) -> &'static str {
        "dense_list_for_large_cardinality"
    }

    fn proposes(
        &self,
        _report: &RenderReport,
        signature: &IntentSignature,
        surface: &A2uiSurface,
    ) -> Option<Value> {
        if signature.structure != "list" {
            return None;
        }
        let band = signature.cardinality_band.as_str();
        if band != "21-100" && band != "100+" {
            return None;
        }
        let root_id = root_id(surface)?;
        // If a density hint is already present on the root, decline
        // so we don't oscillate between bands.
        let existing = surface
            .components
            .get(&root_id)
            .and_then(|c| c.get("props"))
            .and_then(|p| p.get("x-renderer"))
            .and_then(|x| x.get("density"));
        if existing.is_some() {
            return None;
        }
        Some(json!([
            { "op": "test", "path": format!("/{}/component", root_id), "value": "List" },
            { "op": "add",
              "path": format!("/{}/props/x-renderer", root_id),
              "value": { "density": "compact" } }
        ]))
    }
}

/// When a small List (cardinality 2-5) has no `x-renderer.domain`
/// hint, propose tagging it as `"small_list"`. The renderer's
/// `IntentSignature::infer` reads the hint, so the next signature
/// includes `domain = "small_list"` and the agent stops proposing.
/// Loop converges in two render passes plus the stability window.
pub struct TagSmallListDomain;

impl RenderingStrategy for TagSmallListDomain {
    fn id(&self) -> &'static str {
        "tag_small_list_domain"
    }

    fn proposes(
        &self,
        _report: &RenderReport,
        signature: &IntentSignature,
        surface: &A2uiSurface,
    ) -> Option<Value> {
        if signature.structure != "list"
            || signature.cardinality_band != "2-5"
            || signature.domain.is_some()
        {
            return None;
        }
        let root_id = root_id(surface)?;
        // Test precondition guards against the agent's view going
        // stale between report-time and patch-time: if the root has
        // been swapped from a List in the meantime, abort.
        //
        // `component` is the field name car-a2ui validates and stores
        // (car-rs/crates/car-a2ui/src/lib.rs:586). The Swift renderer
        // reads the same key via its renderer-side normalization; the
        // two ends agree on stored shape even though older fixtures
        // shipped with `type` as the discriminator.
        Some(json!([
            { "op": "test", "path": format!("/{}/component", root_id), "value": "List" },
            { "op": "add",
              "path": format!("/{}/props/x-renderer", root_id),
              "value": { "domain": "small_list" } }
        ]))
    }
}

/// When a Column root has compact density on a mobile-class viewport,
/// propose marking it with `attention_budget: "scan"`. Next signature
/// includes the hint; agent stops proposing.
pub struct CompactDenseColumn;

impl RenderingStrategy for CompactDenseColumn {
    fn id(&self) -> &'static str {
        "compact_dense_column"
    }

    fn proposes(
        &self,
        report: &RenderReport,
        signature: &IntentSignature,
        surface: &A2uiSurface,
    ) -> Option<Value> {
        if signature.structure != "column"
            || report.density.as_deref() != Some("compact")
            || signature.attention_budget.is_some()
        {
            return None;
        }
        if !is_mobile_viewport(&report.viewport) {
            return None;
        }
        let root_id = root_id(surface)?;
        Some(json!([
            { "op": "test", "path": format!("/{}/component", root_id), "value": "Column" },
            { "op": "add",
              "path": format!("/{}/props/x-renderer", root_id),
              "value": { "attention_budget": "scan" } }
        ]))
    }
}

/// A2UI surfaces designate `"root"` as the entry point by convention.
/// Surfaces missing the `root` key are malformed; strategies decline
/// rather than silently target whatever sorts first in the components
/// map — that path produced "x-renderer hints get added to random
/// components" bugs in earlier iterations.
fn root_id(surface: &A2uiSurface) -> Option<String> {
    if surface.has_root() {
        Some("root".to_string())
    } else {
        tracing::debug!(
            surface_id = %surface.surface_id,
            "ui-agent: surface has no root component; declining"
        );
        None
    }
}

/// Viewport width < 640pt is "mobile" — matches the Swift renderer's
/// `RenderViewport.sizeClass` thresholds in
/// `a2ui-shader/Sources/A2UIShaderCore/Telemetry.swift`.
fn is_mobile_viewport(viewport: &Value) -> bool {
    viewport
        .get("width")
        .and_then(Value::as_f64)
        .map(|w| w < 640.0)
        .unwrap_or(false)
}
