use plushie_core::protocol::{PropMap, PropValue};

use crate::protocol::TreeNode;

const MAX_CANVAS_COORDINATE: f64 = 1_000_000.0;

pub(crate) fn validate_canvas_shape_tree(canvas: &TreeNode) -> Vec<String> {
    let mut validator = ShapeValidator {
        warnings: Vec::new(),
    };

    for child in &canvas.children {
        validator.validate_canvas_child(child, "canvas");
    }

    validator.warnings
}

struct ShapeValidator {
    warnings: Vec<String>,
}

impl ShapeValidator {
    fn validate_canvas_child(&mut self, node: &TreeNode, parent: &str) {
        if node.type_name == "__layer__" {
            self.validate_layer(node, parent);
        } else {
            self.validate_shape_node(node, parent);
        }
    }

    fn validate_layer(&mut self, node: &TreeNode, parent: &str) {
        self.validate_allowed_props(node, &["id", "name"]);

        for child in &node.children {
            self.validate_shape_node(child, &format!("{parent} > layer {}", node.id));
        }
    }

    fn validate_shape_node(&mut self, node: &TreeNode, parent: &str) {
        match node.type_name.as_str() {
            "rect" => {
                self.validate_allowed_props(
                    node,
                    &[
                        "id",
                        "x",
                        "y",
                        "w",
                        "h",
                        "fill",
                        "stroke",
                        "opacity",
                        "fill_rule",
                        "radius",
                    ],
                );
                self.validate_coordinates(node, &["x", "y"]);
                self.validate_non_negative_fields(node, &["w", "h"]);
                self.validate_common_shape_fields(node);
                self.validate_radius(node, "radius");
            }
            "circle" => {
                self.validate_allowed_props(
                    node,
                    &[
                        "id",
                        "x",
                        "y",
                        "r",
                        "fill",
                        "stroke",
                        "opacity",
                        "fill_rule",
                    ],
                );
                self.validate_coordinates(node, &["x", "y"]);
                self.validate_non_negative_fields(node, &["r"]);
                self.validate_common_shape_fields(node);
            }
            "line" => {
                self.validate_allowed_props(
                    node,
                    &["id", "x1", "y1", "x2", "y2", "stroke", "opacity"],
                );
                self.validate_coordinates(node, &["x1", "y1", "x2", "y2"]);
                self.validate_common_shape_fields(node);
            }
            "path" => {
                self.validate_allowed_props(
                    node,
                    &["id", "commands", "fill", "stroke", "opacity", "fill_rule"],
                );
                self.validate_path_commands(node);
                self.validate_common_shape_fields(node);
            }
            "text" => {
                self.validate_allowed_props(
                    node,
                    &[
                        "id", "x", "y", "content", "fill", "size", "font", "align_x", "align_y",
                        "opacity",
                    ],
                );
                self.validate_coordinates(node, &["x", "y"]);
                self.validate_non_negative_fields(node, &["size"]);
                self.validate_common_shape_fields(node);
            }
            "image" => {
                self.validate_allowed_props(
                    node,
                    &["id", "source", "x", "y", "w", "h", "rotation", "opacity"],
                );
                self.validate_coordinates(node, &["x", "y", "rotation"]);
                self.validate_non_negative_fields(node, &["w", "h"]);
                self.validate_source(node, "image");
                self.validate_common_shape_fields(node);
            }
            "svg" => {
                self.validate_allowed_props(node, &["id", "source", "x", "y", "w", "h"]);
                self.validate_coordinates(node, &["x", "y"]);
                self.validate_non_negative_fields(node, &["w", "h"]);
                self.validate_source(node, "svg");
            }
            "group" => {
                self.validate_allowed_props(
                    node,
                    &[
                        "id",
                        "transforms",
                        "clip",
                        "on_click",
                        "on_hover",
                        "draggable",
                        "drag_axis",
                        "drag_bounds",
                        "cursor",
                        "hit_rect",
                        "tooltip",
                        "hover_style",
                        "pressed_style",
                        "focus_style",
                        "show_focus_ring",
                        "focus_ring_radius",
                        "focusable",
                        "a11y",
                    ],
                );
                self.validate_group_fields(node);
                for child in &node.children {
                    self.validate_shape_node(child, &format!("{parent} > group {}", node.id));
                }
            }
            _ => self.warn(format!(
                "{parent} child '{}' has unknown canvas shape type '{}'",
                node.id, node.type_name
            )),
        }
    }

    fn validate_common_shape_fields(&mut self, node: &TreeNode) {
        self.validate_opacity(node);
        self.validate_stroke(node, "stroke");
    }

    fn validate_group_fields(&mut self, node: &TreeNode) {
        self.validate_transforms(node);
        self.validate_rect_object(node, "clip", true);
        self.validate_rect_object(node, "hit_rect", true);
        self.validate_drag_bounds(node);
        self.validate_non_negative_fields(node, &["focus_ring_radius"]);
        for key in ["hover_style", "pressed_style", "focus_style"] {
            self.validate_shape_style(node, key);
        }
    }

    fn validate_allowed_props(&mut self, node: &TreeNode, allowed: &[&str]) {
        for key in node.props.as_prop_map().keys() {
            if !allowed.contains(&key) {
                self.warn(format!(
                    "shape '{}' ({}) has unknown prop '{}'",
                    node.id, node.type_name, key
                ));
            }
        }
    }

    fn validate_coordinates(&mut self, node: &TreeNode, fields: &[&str]) {
        for field in fields {
            if let Some(value) = node.props.get(field) {
                self.validate_bounded_number(
                    value,
                    &format!("shape '{}' prop '{}'", node.id, field),
                );
            }
        }
    }

    fn validate_non_negative_fields(&mut self, node: &TreeNode, fields: &[&str]) {
        for field in fields {
            if let Some(value) = node.props.get(field) {
                self.validate_non_negative_number(
                    value,
                    &format!("shape '{}' prop '{}'", node.id, field),
                );
            }
        }
    }

    fn validate_opacity(&mut self, node: &TreeNode) {
        if let Some(value) = node.props.get("opacity") {
            self.validate_opacity_value(value, &format!("shape '{}' prop 'opacity'", node.id));
        }
    }

    fn validate_stroke(&mut self, node: &TreeNode, key: &str) {
        let Some(value) = node.props.get(key) else {
            return;
        };
        let Some(stroke) = value.as_object() else {
            return;
        };

        self.validate_stroke_object(node, key, stroke);
    }

    fn validate_stroke_object(&mut self, node: &TreeNode, key: &str, stroke: &PropMap) {
        self.validate_object_fields(
            stroke,
            &["color", "width", "cap", "join", "dash"],
            &format!("shape '{}' prop '{key}'", node.id),
        );

        if let Some(width) = stroke.get("width") {
            self.validate_non_negative_number(width, &format!("shape '{}' stroke width", node.id));
        }

        if let Some(dash) = stroke.get("dash") {
            self.validate_dash(node, dash);
        }
    }

    fn validate_dash(&mut self, node: &TreeNode, value: &PropValue) {
        let Some(dash) = value.as_object() else {
            return;
        };
        self.validate_object_fields(
            dash,
            &["segments", "offset"],
            &format!("shape '{}' stroke dash", node.id),
        );
        if let Some(offset) = dash.get("offset") {
            self.validate_bounded_number(
                offset,
                &format!("shape '{}' stroke dash offset", node.id),
            );
        }
        if let Some(segments) = dash.get("segments").and_then(PropValue::as_array) {
            for (idx, segment) in segments.iter().enumerate() {
                self.validate_non_negative_number(
                    segment,
                    &format!("shape '{}' stroke dash segment {idx}", node.id),
                );
            }
        }
    }

    fn validate_radius(&mut self, node: &TreeNode, key: &str) {
        let Some(value) = node.props.get(key) else {
            return;
        };
        match value {
            PropValue::Object(radius) => {
                self.validate_object_fields(
                    radius,
                    &["top_left", "top_right", "bottom_right", "bottom_left"],
                    &format!("shape '{}' prop '{key}'", node.id),
                );
                for field in ["top_left", "top_right", "bottom_right", "bottom_left"] {
                    if let Some(value) = radius.get(field) {
                        self.validate_non_negative_number(
                            value,
                            &format!("shape '{}' radius {field}", node.id),
                        );
                    }
                }
            }
            _ => self
                .validate_non_negative_number(value, &format!("shape '{}' prop '{key}'", node.id)),
        }
    }

    fn validate_source(&mut self, node: &TreeNode, kind: &str) {
        let Some(source) = node.props.get_str("source") else {
            return;
        };
        if source.contains("://") {
            self.warn(format!(
                "shape '{}' {kind} source uses URL-like value '{source}'",
                node.id
            ));
        }
    }

    fn validate_transforms(&mut self, node: &TreeNode) {
        let Some(value) = node.props.get("transforms") else {
            return;
        };
        let Some(transforms) = value.as_array() else {
            self.warn(format!("shape '{}' transforms must be an array", node.id));
            return;
        };

        for (idx, transform) in transforms.iter().enumerate() {
            let context = format!("shape '{}' transform {idx}", node.id);
            let Some(obj) = transform.as_object() else {
                self.warn(format!("{context} must be an object"));
                continue;
            };
            let Some(kind) = obj.get("type").and_then(PropValue::as_str) else {
                self.warn(format!("{context} must have a string type"));
                continue;
            };

            match kind {
                "translate" => {
                    self.validate_object_fields(obj, &["type", "x", "y"], &context);
                    self.validate_object_numbers(obj, &["x", "y"], &context, false);
                }
                "rotate" => {
                    self.validate_object_fields(obj, &["type", "angle"], &context);
                    if let Some(angle) = obj.get("angle") {
                        self.validate_bounded_number(angle, &format!("{context} angle"));
                    } else {
                        self.warn(format!("{context} missing angle"));
                    }
                }
                "scale" => {
                    self.validate_object_fields(obj, &["type", "x", "y", "factor"], &context);
                    if let Some(factor) = obj.get("factor") {
                        self.validate_bounded_number(factor, &format!("{context} factor"));
                    } else if obj.get("x").is_some() || obj.get("y").is_some() {
                        self.validate_object_numbers(obj, &["x", "y"], &context, false);
                        for field in ["x", "y"] {
                            if obj.get(field).is_none() {
                                self.warn(format!("{context} missing {field}"));
                            }
                        }
                    } else {
                        self.warn(format!("{context} missing scale values"));
                    }
                }
                _ => self.warn(format!("{context} has unknown type '{kind}'")),
            }
        }
    }

    fn validate_path_commands(&mut self, node: &TreeNode) {
        let Some(value) = node.props.get("commands") else {
            return;
        };
        let Some(commands) = value.as_array() else {
            self.warn(format!("shape '{}' commands must be an array", node.id));
            return;
        };

        for (idx, command) in commands.iter().enumerate() {
            let context = format!("shape '{}' path command {idx}", node.id);
            match command {
                PropValue::Str(name) if name == "close" => {}
                PropValue::Array(parts) => self.validate_path_command_array(parts, &context),
                PropValue::Object(obj) => self.validate_path_command_object(obj, &context),
                _ => self.warn(format!("{context} has invalid shape")),
            }
        }
    }

    fn validate_path_command_array(&mut self, parts: &[PropValue], context: &str) {
        let Some(name) = parts.first().and_then(PropValue::as_str) else {
            self.warn(format!("{context} must start with a command name"));
            return;
        };
        let numeric_indexes: &[usize] = match name {
            "move_to" | "line_to" => &[1, 2],
            "bezier_to" => &[1, 2, 3, 4, 5, 6],
            "quadratic_to" => &[1, 2, 3, 4],
            "arc" => &[1, 2, 3, 4, 5],
            "arc_to" => &[1, 2, 3, 4, 5],
            "ellipse" => &[1, 2, 3, 4, 5, 6, 7],
            "rounded_rect" => &[1, 2, 3, 4, 5],
            _ => {
                self.warn(format!("{context} has unknown command '{name}'"));
                return;
            }
        };
        for idx in numeric_indexes {
            if let Some(value) = parts.get(*idx) {
                let label = format!("{context} value {idx}");
                if radius_or_dimension_index(name, *idx) {
                    self.validate_non_negative_number(value, &label);
                } else {
                    self.validate_bounded_number(value, &label);
                }
            }
        }
    }

    fn validate_path_command_object(&mut self, obj: &PropMap, context: &str) {
        let Some(name) = obj.get("type").and_then(PropValue::as_str) else {
            self.warn(format!("{context} must have a string type"));
            return;
        };
        if name != "rounded_rect" {
            self.warn(format!("{context} has unknown command '{name}'"));
            return;
        }
        self.validate_object_fields(obj, &["type", "x", "y", "w", "h", "radius"], context);
        self.validate_object_numbers(obj, &["x", "y"], context, false);
        self.validate_object_numbers(obj, &["w", "h"], context, true);
        if let Some(radius) = obj.get("radius") {
            self.validate_radius_value(radius, &format!("{context} radius"));
        }
    }

    fn validate_shape_style(&mut self, node: &TreeNode, key: &str) {
        let Some(value) = node.props.get(key) else {
            return;
        };
        let Some(style) = value.as_object() else {
            return;
        };
        self.validate_object_fields(
            style,
            &["fill", "stroke", "opacity"],
            &format!("shape '{}' prop '{key}'", node.id),
        );
        if let Some(opacity) = style.get("opacity") {
            self.validate_opacity_value(opacity, &format!("shape '{}' {key} opacity", node.id));
        }
        if let Some(stroke) = style.get("stroke").and_then(PropValue::as_object) {
            self.validate_stroke_object(node, key, stroke);
        }
    }

    fn validate_rect_object(&mut self, node: &TreeNode, key: &str, dimensions_required: bool) {
        let Some(value) = node.props.get(key) else {
            return;
        };
        let Some(obj) = value.as_object() else {
            return;
        };
        self.validate_object_fields(
            obj,
            &["x", "y", "w", "h"],
            &format!("shape '{}' prop '{key}'", node.id),
        );
        self.validate_object_numbers(
            obj,
            &["x", "y"],
            &format!("shape '{}' {key}", node.id),
            false,
        );
        self.validate_object_numbers(
            obj,
            &["w", "h"],
            &format!("shape '{}' {key}", node.id),
            true,
        );
        if dimensions_required {
            for field in ["w", "h"] {
                if obj.get(field).is_none() {
                    self.warn(format!("shape '{}' {key} missing {field}", node.id));
                }
            }
        }
    }

    fn validate_drag_bounds(&mut self, node: &TreeNode) {
        let Some(value) = node.props.get("drag_bounds") else {
            return;
        };
        let Some(obj) = value.as_object() else {
            return;
        };
        self.validate_object_fields(
            obj,
            &["min_x", "max_x", "min_y", "max_y"],
            &format!("shape '{}' drag_bounds", node.id),
        );
        self.validate_object_numbers(
            obj,
            &["min_x", "max_x", "min_y", "max_y"],
            &format!("shape '{}' drag_bounds", node.id),
            false,
        );
    }

    fn validate_object_fields(&mut self, obj: &PropMap, allowed: &[&str], context: &str) {
        for key in obj.keys() {
            if !allowed.contains(&key) {
                self.warn(format!("{context} has unknown field '{key}'"));
            }
        }
    }

    fn validate_object_numbers(
        &mut self,
        obj: &PropMap,
        fields: &[&str],
        context: &str,
        non_negative: bool,
    ) {
        for field in fields {
            if let Some(value) = obj.get(field) {
                let label = format!("{context} field '{field}'");
                if non_negative {
                    self.validate_non_negative_number(value, &label);
                } else {
                    self.validate_bounded_number(value, &label);
                }
            }
        }
    }

    fn validate_radius_value(&mut self, value: &PropValue, context: &str) {
        match value {
            PropValue::Object(radius) => {
                self.validate_object_fields(
                    radius,
                    &["top_left", "top_right", "bottom_right", "bottom_left"],
                    context,
                );
                for field in ["top_left", "top_right", "bottom_right", "bottom_left"] {
                    if let Some(value) = radius.get(field) {
                        self.validate_non_negative_number(value, &format!("{context} {field}"));
                    }
                }
            }
            _ => self.validate_non_negative_number(value, context),
        }
    }

    fn validate_opacity_value(&mut self, value: &PropValue, context: &str) {
        if let Some(number) = self.number(value, context)
            && !(0.0..=1.0).contains(&number)
        {
            self.warn(format!("{context} must be between 0 and 1"));
        }
    }

    fn validate_non_negative_number(&mut self, value: &PropValue, context: &str) {
        if let Some(number) = self.number(value, context) {
            if number < 0.0 {
                self.warn(format!("{context} must be non-negative"));
            }
            if number.abs() > MAX_CANVAS_COORDINATE {
                self.warn(format!("{context} is outside the canvas coordinate limit"));
            }
        }
    }

    fn validate_bounded_number(&mut self, value: &PropValue, context: &str) {
        if let Some(number) = self.number(value, context)
            && number.abs() > MAX_CANVAS_COORDINATE
        {
            self.warn(format!("{context} is outside the canvas coordinate limit"));
        }
    }

    fn number(&mut self, value: &PropValue, context: &str) -> Option<f64> {
        let number = match value {
            PropValue::F64(value) => *value,
            PropValue::I64(value) => *value as f64,
            PropValue::U64(value) => *value as f64,
            _ => {
                self.warn(format!("{context} must be a finite number"));
                return None;
            }
        };

        if !number.is_finite() {
            self.warn(format!("{context} must be a finite number"));
            return None;
        }

        Some(number)
    }

    fn warn(&mut self, warning: String) {
        self.warnings.push(warning);
    }
}

fn radius_or_dimension_index(command: &str, idx: usize) -> bool {
    matches!(
        (command, idx),
        ("arc", 3)
            | ("arc_to", 5)
            | ("ellipse", 3)
            | ("ellipse", 4)
            | ("rounded_rect", 3)
            | ("rounded_rect", 4)
            | ("rounded_rect", 5)
    )
}

#[cfg(test)]
mod tests {
    use super::*;
    use plushie_core::protocol::Props;
    use serde_json::{Value, json};

    fn node(id: &str, type_name: &str, props: Value, children: Vec<TreeNode>) -> TreeNode {
        TreeNode {
            id: id.to_string(),
            type_name: type_name.to_string(),
            props: Props::from_json(props),
            children,
        }
    }

    fn shape(id: &str, type_name: &str, props: Value) -> TreeNode {
        node(id, type_name, props, Vec::new())
    }

    fn canvas(children: Vec<TreeNode>) -> TreeNode {
        node("canvas", "canvas", json!({}), children)
    }

    fn warnings_for(child: TreeNode) -> Vec<String> {
        validate_canvas_shape_tree(&canvas(vec![child]))
    }

    #[test]
    fn canvas_shape_validation_valid_direct_rect_and_layered_group() {
        let direct = canvas(vec![shape(
            "r",
            "rect",
            json!({"x": 1, "y": 2, "w": 10, "h": 20, "fill": "#ff0000"}),
        )]);
        assert!(validate_canvas_shape_tree(&direct).is_empty());

        let layered = canvas(vec![node(
            "layer",
            "__layer__",
            json!({"name": "main"}),
            vec![node(
                "g",
                "group",
                json!({"transforms": [{"type": "translate", "x": 5, "y": 6}]}),
                vec![shape("c", "circle", json!({"x": 1, "y": 2, "r": 3}))],
            )],
        )]);
        assert!(validate_canvas_shape_tree(&layered).is_empty());
    }

    #[test]
    fn canvas_shape_validation_unknown_shape_prop_warns() {
        let warnings = warnings_for(shape(
            "r",
            "rect",
            json!({"x": 0, "w": 1, "h": 1, "extra": true}),
        ));
        assert!(warnings.iter().any(|w| w.contains("unknown prop 'extra'")));
    }

    #[test]
    fn canvas_shape_validation_huge_coordinate_warns() {
        let warnings = warnings_for(shape(
            "r",
            "rect",
            json!({"x": 1_000_001, "y": 0, "w": 1, "h": 1}),
        ));
        assert!(
            warnings
                .iter()
                .any(|w| w.contains("outside the canvas coordinate limit"))
        );
    }

    #[test]
    fn canvas_shape_validation_negative_dimension_or_radius_warns() {
        let rect_warnings =
            warnings_for(shape("r", "rect", json!({"x": 0, "y": 0, "w": -1, "h": 1})));
        assert!(
            rect_warnings
                .iter()
                .any(|w| w.contains("must be non-negative"))
        );

        let circle_warnings = warnings_for(shape("c", "circle", json!({"x": 0, "y": 0, "r": -1})));
        assert!(
            circle_warnings
                .iter()
                .any(|w| w.contains("must be non-negative"))
        );
    }

    #[test]
    fn canvas_shape_validation_invalid_transform_object_warns() {
        let warnings = warnings_for(node(
            "g",
            "group",
            json!({"transforms": [{"type": "skew", "x": 1}, {"type": "scale"}]}),
            Vec::new(),
        ));
        assert!(warnings.iter().any(|w| w.contains("unknown type 'skew'")));
        assert!(warnings.iter().any(|w| w.contains("missing scale values")));
    }

    #[test]
    fn canvas_shape_validation_image_or_svg_url_source_warns() {
        let image_warnings = warnings_for(shape(
            "i",
            "image",
            json!({"source": "https://example.invalid/image.png", "x": 0, "y": 0, "w": 1, "h": 1}),
        ));
        assert!(image_warnings.iter().any(|w| w.contains("URL-like value")));

        let svg_warnings = warnings_for(shape(
            "s",
            "svg",
            json!({"source": "file://tmp/icon.svg", "x": 0, "y": 0, "w": 1, "h": 1}),
        ));
        assert!(svg_warnings.iter().any(|w| w.contains("URL-like value")));
    }

    #[test]
    fn canvas_shape_validation_unknown_child_type_warns_under_canvas_layer_and_group() {
        let warnings = validate_canvas_shape_tree(&canvas(vec![
            shape("bad-canvas", "not_shape", json!({})),
            node(
                "layer",
                "__layer__",
                json!({"name": "main"}),
                vec![shape("bad-layer", "not_shape", json!({}))],
            ),
            node(
                "g",
                "group",
                json!({}),
                vec![shape("bad-group", "not_shape", json!({}))],
            ),
        ]));

        assert!(warnings.iter().any(|w| w.contains("bad-canvas")));
        assert!(warnings.iter().any(|w| w.contains("bad-layer")));
        assert!(warnings.iter().any(|w| w.contains("bad-group")));
    }
}
