use super::*;

pub(super) fn validate_component(
    component: &Map<String, Value>,
    catalog_id: &str,
) -> Result<(), String> {
    let id = required_string(component, "id")?;
    let kind = required_string(component, "component")?;
    let allowed = if catalog_id == CATALOG_ID {
        ds4_catalog()
            .pointer("/components")
            .and_then(Value::as_object)
            .is_some_and(|components| components.contains_key(kind))
    } else {
        BASIC_COMPONENTS.contains(&kind)
    };
    if !allowed {
        return Err(format!(
            "component `{id}` uses `{kind}`, which is not in catalog `{catalog_id}`"
        ));
    }
    let required: &[&str] = match kind {
        "Text" => &["text"],
        "Image" => &["url"],
        "Icon" => &["name"],
        "Video" | "AudioPlayer" => &["url"],
        "Row" | "Column" | "List" => &["children"],
        "Card" => &["child"],
        "Modal" => &["trigger", "content"],
        "Tabs" => &["tabs"],
        "Button" => &["child", "action"],
        "TextField" => &["label"],
        "CheckBox" => &["label", "value"],
        "Slider" => &["value", "max"],
        "DateTimeInput" => &["value"],
        "ChoicePicker" => &["options", "value"],
        "Chart" => &["chartType", "series"],
        "Table" => &["columns", "rows"],
        "Metric" => &["label", "value"],
        "Timeline" => &["events"],
        "Map" => &["locations"],
        "MindMap" => &["nodes"],
        "Form" => &["children"],
        _ => &[],
    };
    if let Some(field) = required
        .iter()
        .find(|field| !component.contains_key(**field))
    {
        return Err(format!("component `{id}` ({kind}) requires `{field}`"));
    }
    let common = ["id", "component", "accessibility", "weight", "checks"];
    let specific: &[&str] = match kind {
        "Text" => &["text", "variant"],
        "Image" => &["url", "description", "fit", "variant"],
        "Icon" => &["name"],
        "Video" => &["url", "posterUrl"],
        "AudioPlayer" => &["url", "description"],
        "Divider" => &["axis"],
        "Row" | "Column" => &["children", "justify", "align"],
        "List" => &["children", "direction", "align"],
        "Card" => &["child"],
        "Modal" => &["trigger", "content"],
        "Tabs" => &["tabs"],
        "Button" => &["child", "variant", "action"],
        "TextField" => &["label", "value", "placeholder", "variant"],
        "CheckBox" => &["label", "value"],
        "Slider" => &["label", "min", "max", "value", "steps"],
        "DateTimeInput" => &["label", "value", "enableDate", "enableTime", "min", "max"],
        "ChoicePicker" => &[
            "label",
            "variant",
            "options",
            "value",
            "displayStyle",
            "filterable",
        ],
        "Chart" => &["title", "chartType", "series"],
        "Table" => &["title", "columns", "rows"],
        "Metric" => &["label", "value", "detail", "trend"],
        "Timeline" => &["title", "events"],
        "Map" => &["title", "locations"],
        "MindMap" => &["title", "nodes"],
        "Form" => &["title", "children", "submitLabel", "action"],
        _ => &[],
    };
    if let Some(field) = component
        .keys()
        .find(|field| !common.contains(&field.as_str()) && !specific.contains(&field.as_str()))
    {
        return Err(format!(
            "component `{id}` ({kind}) contains unknown property `{field}`"
        ));
    }
    if let Some(accessibility) = component.get("accessibility") {
        let accessibility = object(Some(accessibility), "accessibility")?;
        for field in ["label", "description"] {
            validate_optional_dynamic(accessibility, field, Value::is_string, id)?;
        }
    }
    if component
        .get("weight")
        .is_some_and(|value| !value.is_number())
    {
        return Err(format!("component `{id}` weight must be a number"));
    }
    if component
        .get("title")
        .is_some_and(|value| !value.is_string())
    {
        return Err(format!("component `{id}` title must be a string"));
    }
    if catalog_id != CATALOG_ID
        && component.contains_key("checks")
        && !matches!(
            kind,
            "Button" | "TextField" | "CheckBox" | "ChoicePicker" | "Slider" | "DateTimeInput"
        )
    {
        return Err(format!("component `{id}` ({kind}) does not support checks"));
    }
    match kind {
        "Row" | "Column" | "List" | "Form" => {
            validate_children(component.get("children").unwrap(), id)?
        }
        "Card" | "Button" => expect_string(component, "child", id)?,
        "Modal" => {
            expect_string(component, "trigger", id)?;
            expect_string(component, "content", id)?;
        }
        "Tabs" => validate_tabs(component.get("tabs").unwrap(), id)?,
        "ChoicePicker" => validate_options(component.get("options").unwrap(), id)?,
        "Slider" => {
            expect_number(component, "max", id)?;
            if let Some(min) = component.get("min")
                && !min.is_number()
            {
                return Err(format!("component `{id}` min must be a number"));
            }
            if let Some(steps) = component.get("steps")
                && !steps.as_u64().is_some_and(|steps| steps > 0)
            {
                return Err(format!("component `{id}` steps must be a positive integer"));
            }
        }
        _ => {}
    }
    match kind {
        "Text" => validate_dynamic_field(component, "text", Value::is_string, id)?,
        "Image" | "Video" | "AudioPlayer" => {
            validate_dynamic_field(component, "url", Value::is_string, id)?;
            validate_optional_dynamic(component, "description", Value::is_string, id)?;
            validate_optional_dynamic(component, "posterUrl", Value::is_string, id)?;
        }
        "Icon" => validate_dynamic_field(component, "name", Value::is_string, id)?,
        "TextField" | "DateTimeInput" => {
            validate_optional_dynamic(component, "label", Value::is_string, id)?;
            validate_optional_dynamic(component, "value", Value::is_string, id)?;
        }
        "CheckBox" => {
            validate_dynamic_field(component, "label", Value::is_string, id)?;
            validate_dynamic_field(component, "value", Value::is_boolean, id)?;
        }
        "Slider" => validate_dynamic_field(component, "value", Value::is_number, id)?,
        "ChoicePicker" => {
            validate_optional_dynamic(component, "label", Value::is_string, id)?;
            validate_dynamic_field(
                component,
                "value",
                |value| {
                    value
                        .as_array()
                        .is_some_and(|values| values.iter().all(Value::is_string))
                },
                id,
            )?;
        }
        "Chart" | "Table" | "Timeline" | "Map" | "MindMap" => {
            for field in required {
                if *field != "chartType" {
                    validate_dynamic_field(component, field, Value::is_array, id)?;
                }
            }
        }
        _ => {}
    }
    match kind {
        "Text" => validate_enum(component, "variant", &["caption", "body"], id)?,
        "Image" => {
            validate_enum(
                component,
                "fit",
                &["contain", "cover", "fill", "none", "scaleDown"],
                id,
            )?;
            validate_enum(
                component,
                "variant",
                &[
                    "icon",
                    "avatar",
                    "smallFeature",
                    "mediumFeature",
                    "largeFeature",
                    "header",
                ],
                id,
            )?;
        }
        "Icon" if component.get("name").is_some_and(Value::is_string) => validate_enum(
            component,
            "name",
            &[
                "accountCircle",
                "add",
                "arrowBack",
                "arrowForward",
                "attachFile",
                "calendarToday",
                "call",
                "camera",
                "check",
                "close",
                "delete",
                "download",
                "edit",
                "event",
                "error",
                "fastForward",
                "favorite",
                "favoriteOff",
                "folder",
                "help",
                "home",
                "info",
                "locationOn",
                "lock",
                "lockOpen",
                "mail",
                "menu",
                "moreVert",
                "moreHoriz",
                "notificationsOff",
                "notifications",
                "pause",
                "payment",
                "person",
                "phone",
                "photo",
                "play",
                "print",
                "refresh",
                "rewind",
                "search",
                "send",
                "settings",
                "share",
                "shoppingCart",
                "skipNext",
                "skipPrevious",
                "star",
                "starHalf",
                "starOff",
                "stop",
                "upload",
                "visibility",
                "visibilityOff",
                "volumeDown",
                "volumeMute",
                "volumeOff",
                "volumeUp",
                "warning",
            ],
            id,
        )?,
        "Divider" => validate_enum(component, "axis", &["horizontal", "vertical"], id)?,
        "Row" | "Column" => {
            validate_enum(
                component,
                "justify",
                &[
                    "start",
                    "center",
                    "end",
                    "spaceBetween",
                    "spaceAround",
                    "spaceEvenly",
                    "stretch",
                ],
                id,
            )?;
            validate_enum(
                component,
                "align",
                &["start", "center", "end", "stretch"],
                id,
            )?;
        }
        "List" => {
            validate_enum(component, "direction", &["vertical", "horizontal"], id)?;
            validate_enum(
                component,
                "align",
                &["start", "center", "end", "stretch"],
                id,
            )?;
        }
        "Button" => validate_enum(
            component,
            "variant",
            &["default", "primary", "borderless"],
            id,
        )?,
        "TextField" => validate_enum(
            component,
            "variant",
            &["longText", "number", "shortText", "obscured"],
            id,
        )?,
        "ChoicePicker" => {
            validate_enum(
                component,
                "variant",
                &["multipleSelection", "mutuallyExclusive"],
                id,
            )?;
            validate_enum(component, "displayStyle", &["checkbox", "chips"], id)?;
            optional_bool(component, "filterable")?;
        }
        "DateTimeInput" => {
            optional_bool(component, "enableDate")?;
            optional_bool(component, "enableTime")?;
            validate_optional_dynamic(component, "min", Value::is_string, id)?;
            validate_optional_dynamic(component, "max", Value::is_string, id)?;
        }
        "Chart" => validate_enum(
            component,
            "chartType",
            &[
                "bar",
                "line",
                "area",
                "stackedBar",
                "pie",
                "donut",
                "heatmap",
            ],
            id,
        )?,
        _ => {}
    }
    if let Some(action) = component.get("action") {
        validate_action(action, id)?;
    }
    if let Some(checks) = component.get("checks") {
        let checks = checks
            .as_array()
            .ok_or_else(|| format!("component `{id}` checks must be an array"))?;
        for check in checks {
            let check = object(Some(check), "check")?;
            reject_unknown(check, &["condition", "message"], "check")?;
            let condition = check
                .get("condition")
                .ok_or_else(|| format!("component `{id}` check requires condition"))?;
            required_string(check, "message")?;
            validate_function(condition)?;
        }
    }
    validate_dynamic_values(component)?;
    Ok(())
}

fn validate_dynamic_field(
    object: &Map<String, Value>,
    field: &str,
    literal: impl Fn(&Value) -> bool,
    id: &str,
) -> Result<(), String> {
    let value = object
        .get(field)
        .ok_or_else(|| format!("component `{id}` requires `{field}`"))?;
    if literal(value) {
        return Ok(());
    }
    let dynamic = value
        .as_object()
        .ok_or_else(|| format!("component `{id}` {field} has the wrong literal or dynamic type"))?;
    if let Some(path) = dynamic.get("path") {
        if dynamic.len() == 1 && path.is_string() {
            return Ok(());
        }
        return Err(format!(
            "component `{id}` {field} has an invalid data binding"
        ));
    }
    validate_function(value).map_err(|error| format!("component `{id}` {field}: {error}"))
}

fn validate_optional_dynamic(
    object: &Map<String, Value>,
    field: &str,
    literal: impl Fn(&Value) -> bool,
    id: &str,
) -> Result<(), String> {
    if object.contains_key(field) {
        validate_dynamic_field(object, field, literal, id)
    } else {
        Ok(())
    }
}

pub(super) fn ds4_catalog() -> &'static Value {
    static CATALOG: OnceLock<Value> = OnceLock::new();
    CATALOG.get_or_init(|| {
        serde_json::from_str(CATALOG_JSON).expect("embedded DS4Server A2UI catalog must be valid")
    })
}

fn expect_string(object: &Map<String, Value>, field: &str, id: &str) -> Result<(), String> {
    if !object.get(field).is_some_and(Value::is_string) {
        return Err(format!("component `{id}` {field} must be a string"));
    }
    Ok(())
}

fn expect_number(object: &Map<String, Value>, field: &str, id: &str) -> Result<(), String> {
    if !object.get(field).is_some_and(Value::is_number) {
        return Err(format!("component `{id}` {field} must be a number"));
    }
    Ok(())
}

fn validate_enum(
    object: &Map<String, Value>,
    field: &str,
    allowed: &[&str],
    id: &str,
) -> Result<(), String> {
    if let Some(value) = object.get(field) {
        let value = value
            .as_str()
            .ok_or_else(|| format!("component `{id}` {field} must be a string"))?;
        if !allowed.contains(&value) {
            return Err(format!(
                "component `{id}` has invalid {field} `{value}`; expected one of: {}",
                allowed.join(", ")
            ));
        }
    }
    Ok(())
}

fn validate_children(value: &Value, id: &str) -> Result<(), String> {
    if value
        .as_array()
        .is_some_and(|children| children.iter().all(Value::is_string))
    {
        return Ok(());
    }
    let template = object(Some(value), "children")?;
    reject_unknown(template, &["componentId", "path"], "children template")?;
    required_string(template, "componentId")?;
    required_string(template, "path")?;
    if !required_string(template, "path")?.starts_with('/') {
        return Err(format!(
            "component `{id}` child template path must be a JSON Pointer"
        ));
    }
    Ok(())
}

fn validate_tabs(value: &Value, id: &str) -> Result<(), String> {
    let tabs = value
        .as_array()
        .filter(|tabs| !tabs.is_empty())
        .ok_or_else(|| format!("component `{id}` tabs must be a non-empty array"))?;
    for tab in tabs {
        let tab = object(Some(tab), "tab")?;
        reject_unknown(tab, &["title", "child"], "tab")?;
        validate_dynamic_field(tab, "title", Value::is_string, id)?;
        required_string(tab, "child")?;
    }
    Ok(())
}

fn validate_options(value: &Value, id: &str) -> Result<(), String> {
    let options = value
        .as_array()
        .ok_or_else(|| format!("component `{id}` options must be an array"))?;
    for option in options {
        let option = object(Some(option), "choice option")?;
        reject_unknown(option, &["label", "value"], "choice option")?;
        validate_dynamic_field(option, "label", Value::is_string, id)?;
        required_string(option, "value")?;
    }
    Ok(())
}

fn validate_action(value: &Value, id: &str) -> Result<(), String> {
    let action = object(Some(value), "action")?;
    if let Some(event) = action.get("event") {
        reject_unknown(action, &["event"], "action")?;
        let event = object(Some(event), "action.event")?;
        reject_unknown(
            event,
            &["name", "context", "wantResponse", "responsePath"],
            "action.event",
        )?;
        required_string(event, "name")?;
        if let Some(context) = event.get("context")
            && !context.is_object()
        {
            return Err(format!("component `{id}` action context must be an object"));
        }
        optional_bool(event, "wantResponse")?;
        if let Some(path) = event.get("responsePath")
            && !path.as_str().is_some_and(|path| path.starts_with('/'))
        {
            return Err(format!(
                "component `{id}` responsePath must be a JSON Pointer"
            ));
        }
        return Ok(());
    }
    reject_unknown(action, &["functionCall"], "action")?;
    validate_function(
        action
            .get("functionCall")
            .ok_or_else(|| format!("component `{id}` action requires event or functionCall"))?,
    )
}

fn validate_dynamic_values(value: &Map<String, Value>) -> Result<(), String> {
    for value in value.values() {
        if value.get("call").is_some() {
            validate_function(value)?;
        }
        match value {
            Value::Object(object) => validate_dynamic_values(object)?,
            Value::Array(values) => {
                for value in values {
                    if let Value::Object(object) = value {
                        validate_dynamic_values(object)?;
                    }
                }
            }
            _ => {}
        }
    }
    Ok(())
}

pub(super) fn validate_function(value: &Value) -> Result<(), String> {
    let function = object(Some(value), "function call")?;
    reject_unknown(function, &["call", "args"], "function call")?;
    let call = value
        .get("call")
        .and_then(Value::as_str)
        .ok_or_else(|| "function call requires `call`".to_owned())?;
    if !FUNCTIONS.contains(&call) {
        return Err(format!("function `{call}` is not declared by the catalog"));
    }
    if value.get("args").is_some_and(|args| !args.is_object()) {
        return Err(format!("function `{call}` requires object `args`"));
    }
    let args = value
        .get("args")
        .and_then(Value::as_object)
        .cloned()
        .unwrap_or_default();
    let allowed: &[&str] = match call {
        "required" | "email" | "formatString" | "not" => &["value"],
        "regex" => &["value", "pattern"],
        "length" | "numeric" => &["value", "min", "max"],
        "formatNumber" => &["value", "decimals", "grouping"],
        "formatCurrency" => &["value", "currency", "decimals", "grouping"],
        "formatDate" => &["value", "format"],
        "pluralize" => &["value", "zero", "one", "two", "few", "many", "other"],
        "openUrl" => &["url"],
        "and" | "or" => &["values"],
        "@index" => &["offset"],
        _ => &[],
    };
    reject_unknown(&args, allowed, &format!("function `{call}` args"))?;
    let required: &[&str] = match call {
        "required" | "regex" | "length" | "numeric" | "email" | "formatString" | "formatNumber"
        | "not" | "pluralize" => &["value"],
        "formatCurrency" => &["value", "currency"],
        "formatDate" => &["value", "format"],
        "openUrl" => &["url"],
        "and" | "or" => &["values"],
        "@index" => &[],
        _ => &[],
    };
    if let Some(field) = required.iter().find(|field| !args.contains_key(**field)) {
        return Err(format!("function `{call}` requires argument `{field}`"));
    }
    if call == "regex" && !args.get("pattern").is_some_and(Value::is_string) {
        return Err("function `regex` requires string argument `pattern`".into());
    }
    if matches!(call, "length" | "numeric")
        && !args.contains_key("min")
        && !args.contains_key("max")
    {
        return Err(format!("function `{call}` requires `min` or `max`"));
    }
    if call == "pluralize" && !args.contains_key("other") {
        return Err("function `pluralize` requires argument `other`".into());
    }
    if matches!(call, "and" | "or")
        && !args
            .get("values")
            .and_then(Value::as_array)
            .is_some_and(|values| values.len() >= 2)
    {
        return Err(format!("function `{call}` requires at least two values"));
    }
    match call {
        "regex" | "length" | "email" | "formatString" => {
            validate_dynamic_field(&args, "value", Value::is_string, call)?;
        }
        "numeric" | "formatNumber" | "formatCurrency" | "pluralize" => {
            validate_dynamic_field(&args, "value", Value::is_number, call)?;
        }
        "not" => validate_dynamic_field(&args, "value", Value::is_boolean, call)?,
        "formatDate" => validate_dynamic_field(&args, "format", Value::is_string, call)?,
        "openUrl" => {
            let url = required_string(&args, "url")?;
            url::Url::parse(url).map_err(|error| format!("invalid openUrl URL: {error}"))?;
        }
        "and" | "or" => {
            for item in args["values"].as_array().unwrap() {
                if !item.is_boolean() {
                    let item = item.as_object().ok_or_else(|| {
                        format!("function `{call}` values must be dynamic booleans")
                    })?;
                    if !item.contains_key("path") && !item.contains_key("call") {
                        return Err(format!("function `{call}` values must be dynamic booleans"));
                    }
                }
            }
        }
        _ => {}
    }
    for field in ["min", "max", "decimals", "offset"] {
        if args.contains_key(field) {
            validate_dynamic_field(&args, field, Value::is_number, call)?;
        }
    }
    if args.contains_key("grouping") {
        validate_dynamic_field(&args, "grouping", Value::is_boolean, call)?;
    }
    for field in ["currency", "zero", "one", "two", "few", "many", "other"] {
        if args.contains_key(field) {
            validate_dynamic_field(&args, field, Value::is_string, call)?;
        }
    }
    Ok(())
}
