use super::*;
use crate::a2ui::{Surface, binding_path, bound_value_at, display_value};
use iced::widget::{Column, Row, column, radio, responsive, text_editor};
use serde_json::Value;
use std::collections::{BTreeSet, HashMap, HashSet};
use time::{Month, OffsetDateTime};

impl App {
    pub(super) fn a2ui_detail(&self) -> Element<'_, Message> {
        let content = self.displayed_a2ui_surface().unwrap_or_else(|| {
            container(
                column![
                    text("No A2UI surface").size(24),
                    text("Interactive surfaces from DS4 will appear here.")
                        .size(14)
                        .color(muted_text()),
                ]
                .spacing(8)
                .align_x(Alignment::Center),
            )
            .center_x(Length::Fill)
            .center_y(Length::Fill)
            .into()
        });
        container(content)
            .center_x(Length::Fill)
            .height(Length::Fill)
            .padding(24)
            .into()
    }

    pub(crate) fn sync_a2ui_renderer_state(&mut self) {
        let mut markdown = HashMap::new();
        let mut editors = HashMap::new();
        let mut filters = HashSet::new();
        if let Some(surface) = self.displayed_a2ui_store().active_surface() {
            collect_renderer_state(
                surface,
                "root",
                &surface.data,
                None,
                BTreeSet::new(),
                &mut markdown,
                &mut editors,
                &mut filters,
            );
        }
        self.a2ui_markdown = markdown
            .into_iter()
            .map(|(key, value)| (key, markdown::Content::parse(&value)))
            .collect();
        self.a2ui_editors.retain(|key, _| editors.contains_key(key));
        for (key, value) in editors {
            match self.a2ui_editors.entry(key) {
                std::collections::hash_map::Entry::Occupied(mut entry)
                    if entry.get().text() != value =>
                {
                    entry.insert(iced::widget::text_editor::Content::with_text(&value));
                }
                std::collections::hash_map::Entry::Vacant(entry) => {
                    entry.insert(iced::widget::text_editor::Content::with_text(&value));
                }
                _ => {}
            }
        }
        self.a2ui_choice_filters
            .retain(|key, _| filters.contains(key));
    }

    pub(super) fn displayed_a2ui_surface(&self) -> Option<Element<'_, Message>> {
        self.displayed_a2ui_store()
            .active_surface()
            .map(|surface| self.a2ui_surface(surface))
    }

    fn a2ui_surface<'a>(&'a self, surface: &'a Surface) -> Element<'a, Message> {
        let agent = surface
            .surface_properties
            .get("agentDisplayName")
            .and_then(Value::as_str)
            .unwrap_or("A2UI");
        let content: Element<'_, Message> = if surface.components.contains_key("root") {
            responsive(move |size| {
                scrollable(self.a2ui_component(
                    surface,
                    "root",
                    &surface.data,
                    None,
                    BTreeSet::new(),
                    size.height,
                ))
                .height(Length::Fill)
                .into()
            })
            .into()
        } else {
            text("Building interactive surface…")
                .size(13)
                .color(muted_text())
                .into()
        };
        let dismiss = action_button(text("Dismiss").size(11)).padding([5, 9]);
        let dismiss = if self.generating || self.a2ui_history_index.is_some() {
            dismiss
        } else {
            dismiss.on_press(Message::RequestA2uiDismiss(surface.id.clone()))
        };
        let previous = action_button(icon(ICON_ARROW_LEFT, 15)).padding(5);
        let previous = if self.has_previous_a2ui_surface() {
            previous.on_press(Message::A2uiPreviousSurface)
        } else {
            previous
        };
        let next = action_button(icon(ICON_ARROW_RIGHT, 15)).padding(5);
        let next = if self.has_next_a2ui_surface() {
            next.on_press(Message::A2uiNextSurface)
        } else {
            next
        };
        container(
            column![
                row![
                    previous,
                    next,
                    text(agent).size(10).color(muted_text()),
                    Space::new().width(Length::Fill),
                    text(&surface.id).size(10).color(muted_text()),
                    dismiss,
                ]
                .spacing(8)
                .align_y(Alignment::Center),
                content,
            ]
            .height(Length::Fill)
            .spacing(10),
        )
        .padding(14)
        .width(Length::Fill)
        .height(Length::Fill)
        .style(preference_group_style)
        .into()
    }

    fn a2ui_component<'a>(
        &'a self,
        surface: &'a Surface,
        id: &str,
        context: &'a Value,
        context_path: Option<String>,
        mut ancestors: BTreeSet<String>,
        surface_height: f32,
    ) -> Element<'a, Message> {
        if !ancestors.insert(id.to_owned()) {
            return text(format!("Cyclic component reference: {id}"))
                .style(iced::widget::text::danger)
                .into();
        }
        let Some(component) = surface.components.get(id).and_then(Value::as_object) else {
            return text(format!("Waiting for component `{id}`…"))
                .size(12)
                .color(muted_text())
                .into();
        };
        let kind = component
            .get("component")
            .and_then(Value::as_str)
            .unwrap_or("Unknown");
        match kind {
            "Text" => {
                let value = display_value(&bound_value_at(
                    component.get("text"),
                    &surface.data,
                    context,
                ));
                let size = match component.get("variant").and_then(Value::as_str) {
                    Some("caption") => 11,
                    _ => 14,
                };
                let key = (
                    surface.id.clone(),
                    id.to_owned(),
                    context_path.clone().unwrap_or_default(),
                );
                self.a2ui_markdown.get(&key).map_or_else(
                    || text(value).size(size).into(),
                    |content| {
                        markdown::view(
                            content.items(),
                            markdown::Settings::with_text_size(
                                size,
                                markdown::Style::from_palette(app_theme().palette()),
                            ),
                        )
                        .map(Message::OpenLink)
                    },
                )
            }
            "Image" => {
                let url = display_value(&bound_value_at(
                    component.get("url"),
                    &surface.data,
                    context,
                ));
                if let Some(handle) = self.a2ui_images.get(&url) {
                    let description = display_value(&bound_value_at(
                        component.get("description"),
                        &surface.data,
                        context,
                    ));
                    let variant = component.get("variant").and_then(Value::as_str);
                    let size = match variant {
                        Some("icon") => 24,
                        Some("avatar") => 40,
                        Some("smallFeature") => 100,
                        Some("largeFeature") => 300,
                        Some("header") => 200,
                        _ => 200,
                    };
                    let mut rendered = image(handle.clone())
                        .width(
                            if matches!(variant, Some("icon" | "avatar" | "smallFeature")) {
                                Length::Fixed(size as f32)
                            } else {
                                Length::Fill
                            },
                        )
                        .height(size)
                        .content_fit(match component.get("fit").and_then(Value::as_str) {
                            Some("cover") => iced::ContentFit::Cover,
                            Some("fill") => iced::ContentFit::Fill,
                            Some("none") => iced::ContentFit::None,
                            Some("scaleDown") => iced::ContentFit::ScaleDown,
                            _ => iced::ContentFit::Contain,
                        });
                    if variant == Some("avatar") {
                        rendered = rendered.border_radius(20);
                    }
                    let mut content = Column::new().push(rendered);
                    if !description.is_empty() {
                        content = content.push(text(description).size(11).color(muted_text()));
                    }
                    content.spacing(5).into()
                } else {
                    image_link(component, &surface.data, context)
                }
            }
            "Video" | "AudioPlayer" => self.media_player(component, kind, &surface.data, context),
            "Icon" => text(icon_glyph(&display_value(&bound_value_at(
                component.get("name"),
                &surface.data,
                context,
            ))))
            .size(19)
            .into(),
            "Divider" => {
                if component.get("axis").and_then(Value::as_str) == Some("vertical") {
                    rule::vertical(1).into()
                } else {
                    rule::horizontal(1).into()
                }
            }
            "Row" | "Column" => self.a2ui_children(
                surface,
                component.get("children"),
                context,
                context_path,
                ancestors,
                Some((component, kind == "Row")),
                surface_height,
            ),
            "List" => {
                let horizontal =
                    component.get("direction").and_then(Value::as_str) == Some("horizontal");
                let content = self.a2ui_children(
                    surface,
                    component.get("children"),
                    context,
                    context_path,
                    ancestors,
                    Some((component, horizontal)),
                    surface_height,
                );
                let list = scrollable(content).height(280);
                if horizontal {
                    list.direction(iced::widget::scrollable::Direction::Horizontal(
                        Default::default(),
                    ))
                    .into()
                } else {
                    list.into()
                }
            }
            "Card" => {
                let child = component.get("child").and_then(Value::as_str).unwrap_or("");
                container(self.a2ui_component(
                    surface,
                    child,
                    context,
                    context_path,
                    ancestors,
                    surface_height,
                ))
                .padding(14)
                .width(Length::Fill)
                .style(overview_style)
                .into()
            }
            "Tabs" => {
                let tabs = component
                    .get("tabs")
                    .and_then(Value::as_array)
                    .map(Vec::as_slice)
                    .unwrap_or(&[]);
                let selected = self
                    .a2ui_tabs
                    .get(&(surface.id.clone(), id.to_owned()))
                    .copied()
                    .unwrap_or(0)
                    .min(tabs.len().saturating_sub(1));
                let mut labels = Row::new().spacing(6);
                for (index, tab) in tabs.iter().enumerate() {
                    let title =
                        display_value(&bound_value_at(tab.get("title"), &surface.data, context));
                    let mut tab_button = action_button(text(title).size(12));
                    if index != selected {
                        tab_button = tab_button.on_press(Message::A2uiSelectTab(
                            surface.id.clone(),
                            id.to_owned(),
                            index,
                        ));
                    }
                    labels = labels.push(tab_button);
                }
                let child = tabs
                    .get(selected)
                    .and_then(|tab| tab.get("child"))
                    .and_then(Value::as_str)
                    .unwrap_or("");
                column![
                    labels,
                    self.a2ui_component(
                        surface,
                        child,
                        context,
                        context_path,
                        ancestors,
                        surface_height,
                    )
                ]
                .spacing(10)
                .into()
            }
            "Modal" => {
                let trigger = component
                    .get("trigger")
                    .and_then(Value::as_str)
                    .unwrap_or("");
                let content = component
                    .get("content")
                    .and_then(Value::as_str)
                    .unwrap_or("");
                let key = (surface.id.clone(), id.to_owned());
                let trigger = mouse_area(self.a2ui_component(
                    surface,
                    trigger,
                    context,
                    context_path.clone(),
                    ancestors.clone(),
                    surface_height,
                ))
                .on_press(Message::A2uiToggleModal(key.0.clone(), key.1.clone()));
                let _ = (content, context_path, ancestors);
                trigger.into()
            }
            "Button" => {
                let child = component.get("child").and_then(Value::as_str).unwrap_or("");
                let content = self.a2ui_component(
                    surface,
                    child,
                    context,
                    context_path.clone(),
                    ancestors,
                    surface_height,
                );
                let button =
                    if component.get("variant").and_then(Value::as_str) == Some("borderless") {
                        button(content).style(button::text)
                    } else {
                        action_button(content)
                    };
                if crate::a2ui::first_failed_check(component, &surface.data).is_some() {
                    button.into()
                } else if let Some(url) = component
                    .get("action")
                    .and_then(|action| action.get("functionCall"))
                    .and_then(|call| call.get("args"))
                    .and_then(|args| args.get("url"))
                    .map(|url| display_value(&bound_value_at(Some(url), &surface.data, context)))
                    .filter(|url| {
                        url::Url::parse(url)
                            .is_ok_and(|url| matches!(url.scheme(), "http" | "https"))
                    })
                {
                    button.on_press(Message::OpenLink(url)).into()
                } else {
                    button
                        .on_press(Message::A2uiAction(
                            surface.id.clone(),
                            id.to_owned(),
                            context_path,
                        ))
                        .into()
                }
            }
            "TextField" => {
                let binding = component.get("value");
                let value = display_value(&bound_value_at(binding, &surface.data, context));
                let label = display_value(&bound_value_at(
                    component.get("label"),
                    &surface.data,
                    context,
                ));
                let placeholder = component
                    .get("placeholder")
                    .map(|value| {
                        display_value(&bound_value_at(Some(value), &surface.data, context))
                    })
                    .filter(|value| !value.is_empty())
                    .unwrap_or_else(|| label.clone());
                let path = local_path(binding_path(binding), context_path.as_deref());
                let variant = component.get("variant").and_then(Value::as_str);
                let input: Element<'_, Message> = if variant == Some("longText") {
                    let key = path
                        .as_ref()
                        .map(|path| (surface.id.clone(), id.to_owned(), path.clone()));
                    if let Some((key, content)) = key
                        .as_ref()
                        .and_then(|key| self.a2ui_editors.get(key).map(|content| (key, content)))
                    {
                        let mut editor = text_editor(content)
                            .placeholder(placeholder.clone())
                            .height(120)
                            .padding(9);
                        if path.is_some() {
                            let surface_id = surface.id.clone();
                            let component_id = id.to_owned();
                            let path = key.2.clone();
                            editor = editor.on_action(move |action| {
                                Message::A2uiEditorAction(
                                    surface_id.clone(),
                                    component_id.clone(),
                                    path.clone(),
                                    action,
                                )
                            });
                        }
                        editor.into()
                    } else {
                        text_input(&placeholder, &value).padding(9).into()
                    }
                } else {
                    let mut input = text_input(&placeholder, &value)
                        .secure(variant == Some("obscured"))
                        .padding(9);
                    if let Some(path) = path {
                        let surface_id = surface.id.clone();
                        input = input.on_input(move |value| {
                            if variant == Some("number") && !valid_number_edit(&value) {
                                Message::Noop
                            } else {
                                Message::A2uiDataChanged(
                                    surface_id.clone(),
                                    path.clone(),
                                    Value::String(value),
                                )
                            }
                        });
                    }
                    input.into()
                };
                let mut field = column![text(label).size(12), input].spacing(5);
                if let Some(error) = crate::a2ui::first_failed_check(component, &surface.data) {
                    field = field.push(text(error).size(11).style(iced::widget::text::danger));
                }
                field.into()
            }
            "DateTimeInput" => date_time_input(component, surface, context, context_path),
            "CheckBox" => {
                let binding = component.get("value");
                let checked = bound_value_at(binding, &surface.data, context)
                    .as_bool()
                    .unwrap_or(false);
                let label = display_value(&bound_value_at(
                    component.get("label"),
                    &surface.data,
                    context,
                ));
                let control = toggle(checked).label(label);
                if let Some(path) = local_path(binding_path(binding), context_path.as_deref()) {
                    let surface_id = surface.id.clone();
                    control
                        .on_toggle(move |value| {
                            Message::A2uiDataChanged(
                                surface_id.clone(),
                                path.clone(),
                                Value::Bool(value),
                            )
                        })
                        .into()
                } else {
                    control.into()
                }
            }
            "Slider" => {
                let binding = component.get("value");
                let current = bound_value_at(binding, &surface.data, context)
                    .as_f64()
                    .unwrap_or(0.0) as f32;
                let min = component.get("min").and_then(Value::as_f64).unwrap_or(0.0) as f32;
                let max = component
                    .get("max")
                    .and_then(Value::as_f64)
                    .unwrap_or(100.0) as f32;
                if let Some(path) = local_path(binding_path(binding), context_path.as_deref()) {
                    let surface_id = surface.id.clone();
                    let mut control = slider(min..=max, current.clamp(min, max), move |value| {
                        Message::A2uiDataChanged(
                            surface_id.clone(),
                            path.clone(),
                            serde_json::Number::from_f64(value as f64)
                                .map(Value::Number)
                                .unwrap_or(Value::Null),
                        )
                    });
                    if let Some(steps) = component.get("steps").and_then(Value::as_u64)
                        && steps > 0
                    {
                        control = control.step((max - min) / steps as f32);
                    }
                    column![
                        text(display_value(&bound_value_at(
                            component.get("label"),
                            &surface.data,
                            context,
                        )))
                        .size(12),
                        row![control, text(format!("{current}"))]
                            .spacing(8)
                            .align_y(Alignment::Center)
                    ]
                    .spacing(5)
                    .into()
                } else {
                    progress_bar(min..=max, current.clamp(min, max)).into()
                }
            }
            "ChoicePicker" => {
                let binding = component.get("value");
                let selected = bound_value_at(binding, &surface.data, context);
                let multiple =
                    component.get("variant").and_then(Value::as_str) == Some("multipleSelection");
                let path = local_path(binding_path(binding), context_path.as_deref());
                let filter_key = (
                    surface.id.clone(),
                    id.to_owned(),
                    context_path.clone().unwrap_or_default(),
                );
                let filter = self
                    .a2ui_choice_filters
                    .get(&filter_key)
                    .map_or("", String::as_str);
                let normalized_filter = filter.to_lowercase();
                let options = component
                    .get("options")
                    .and_then(Value::as_array)
                    .map(Vec::as_slice)
                    .unwrap_or(&[]);
                let selected_index = options.iter().position(|option| {
                    option.get("value").is_some_and(|value| {
                        selected
                            .as_array()
                            .is_some_and(|values| values.contains(value))
                    })
                });
                let display_style = component
                    .get("displayStyle")
                    .and_then(Value::as_str)
                    .unwrap_or("checkbox");
                let mut choices: Column<'_, Message> = Column::new().spacing(6);
                let mut chips: Row<'_, Message> = Row::new().spacing(6);
                for (option_index, option) in options.iter().enumerate() {
                    let label =
                        display_value(&bound_value_at(option.get("label"), &surface.data, context));
                    if !normalized_filter.is_empty()
                        && !label.to_lowercase().contains(&normalized_filter)
                    {
                        continue;
                    }
                    let option_value = option.get("value").cloned().unwrap_or(Value::Null);
                    let is_selected = selected
                        .as_array()
                        .is_some_and(|values| values.contains(&option_value));
                    let next_value =
                        choice_selection(&selected, &option_value, multiple, !is_selected);
                    if display_style == "chips" {
                        let mut choice = action_button(text(if is_selected {
                            format!("✓ {label}")
                        } else {
                            label
                        }));
                        if let Some(path) = path.clone() {
                            choice = choice.on_press(Message::A2uiDataChanged(
                                surface.id.clone(),
                                path,
                                next_value,
                            ));
                        }
                        chips = chips.push(choice);
                    } else if multiple {
                        let mut choice = toggle(is_selected).label(label);
                        if let Some(path) = path.clone() {
                            let surface_id = surface.id.clone();
                            let option_value = option_value.clone();
                            let selected = selected.clone();
                            choice = choice.on_toggle(move |checked| {
                                Message::A2uiDataChanged(
                                    surface_id.clone(),
                                    path.clone(),
                                    choice_selection(&selected, &option_value, true, checked),
                                )
                            });
                        }
                        choices = choices.push(choice);
                    } else {
                        let surface_id = surface.id.clone();
                        let message_path = path.clone();
                        choices =
                            choices.push(radio(label, option_index, selected_index, move |_| {
                                message_path.clone().map_or(Message::Noop, |path| {
                                    Message::A2uiDataChanged(
                                        surface_id.clone(),
                                        path,
                                        next_value.clone(),
                                    )
                                })
                            }));
                    }
                }
                let mut picker = Column::new().spacing(5).push(
                    text(display_value(&bound_value_at(
                        component.get("label"),
                        &surface.data,
                        context,
                    )))
                    .size(12),
                );
                if component.get("filterable") == Some(&Value::Bool(true)) {
                    let key = filter_key.clone();
                    picker =
                        picker.push(text_input("Filter choices…", filter).padding(7).on_input(
                            move |value| {
                                Message::A2uiChoiceFilterChanged(
                                    key.0.clone(),
                                    key.1.clone(),
                                    key.2.clone(),
                                    value,
                                )
                            },
                        ));
                }
                let choices: Element<'_, Message> = if display_style == "chips" {
                    chips.wrap().into()
                } else {
                    choices.into()
                };
                picker.push(choices).into()
            }
            "Chart" => research_chart(component, &surface.data, context, surface_height),
            "Table" => research_table(component, &surface.data, context),
            "Metric" => research_metric(component, &surface.data, context),
            "Timeline" => research_timeline(component, &surface.data, context),
            "Map" => research_map(component, &surface.data, context),
            "MindMap" => research_mind_map(component, &surface.data, context),
            "Form" => {
                let children = self.a2ui_children(
                    surface,
                    component.get("children"),
                    context,
                    context_path.clone(),
                    ancestors,
                    None,
                    surface_height,
                );
                let label = component
                    .get("submitLabel")
                    .and_then(Value::as_str)
                    .unwrap_or("Submit");
                let button = action_button(label);
                let button = if component.get("action").is_some() {
                    button.on_press(Message::A2uiAction(
                        surface.id.clone(),
                        id.to_owned(),
                        context_path,
                    ))
                } else {
                    button
                };
                column![
                    component
                        .get("title")
                        .and_then(Value::as_str)
                        .map(|title| text(title.to_owned()).size(18))
                        .unwrap_or_else(|| text("")),
                    children,
                    button,
                ]
                .spacing(10)
                .into()
            }
            _ => text(format!("Unsupported component `{kind}`"))
                .style(iced::widget::text::danger)
                .into(),
        }
    }

    #[allow(clippy::too_many_arguments)]
    fn a2ui_children<'a>(
        &'a self,
        surface: &'a Surface,
        children: Option<&'a Value>,
        context: &'a Value,
        context_path: Option<String>,
        ancestors: BTreeSet<String>,
        layout: Option<(&serde_json::Map<String, Value>, bool)>,
        surface_height: f32,
    ) -> Element<'a, Message> {
        let horizontal = layout.is_some_and(|(_, horizontal)| horizontal);
        let layout = layout.map(|(component, _)| component);
        let mut elements = Vec::new();
        if let Some(children) = children.and_then(Value::as_array) {
            for child in children.iter().filter_map(Value::as_str) {
                elements.push((
                    self.a2ui_component(
                        surface,
                        child,
                        context,
                        context_path.clone(),
                        ancestors.clone(),
                        surface_height,
                    ),
                    component_weight(surface, child),
                ));
            }
        } else if let Some(template) = children.and_then(Value::as_object) {
            let component_id = template
                .get("componentId")
                .and_then(Value::as_str)
                .unwrap_or("");
            let path = template.get("path").and_then(Value::as_str).unwrap_or("/");
            if let Some(items) = surface.data.pointer(path).and_then(Value::as_array) {
                for (index, item) in items.iter().enumerate() {
                    elements.push((
                        crate::a2ui::with_template_index(index, || {
                            self.a2ui_component(
                                surface,
                                component_id,
                                item,
                                Some(format!("{}/{index}", path.trim_end_matches('/'))),
                                ancestors.clone(),
                                surface_height,
                            )
                        }),
                        component_weight(surface, component_id),
                    ));
                }
            }
        }
        let align_name = layout
            .and_then(|component| component.get("align"))
            .and_then(Value::as_str);
        let align = match align_name {
            Some("center") => Alignment::Center,
            Some("end") => Alignment::End,
            _ => Alignment::Start,
        };
        let justify = layout
            .and_then(|component| component.get("justify"))
            .and_then(Value::as_str);
        let expands_main_axis = elements.iter().any(|(_, weight)| weight.is_some())
            || matches!(
                justify,
                Some("center" | "end" | "spaceBetween" | "spaceAround" | "spaceEvenly" | "stretch")
            );
        if horizontal {
            let mut row = Row::new().spacing(10).align_y(align);
            if matches!(justify, Some("spaceAround" | "spaceEvenly")) {
                row = row.push(Space::new().width(Length::FillPortion(1)));
            }
            let count = elements.len();
            for (index, (element, weight)) in elements.into_iter().enumerate() {
                if index > 0
                    && matches!(
                        justify,
                        Some("spaceBetween" | "spaceAround" | "spaceEvenly")
                    )
                {
                    row = row.push(Space::new().width(Length::FillPortion(
                        if justify == Some("spaceAround") { 2 } else { 1 },
                    )));
                }
                let width = weight
                    .or((justify == Some("stretch")).then_some(1))
                    .map_or(Length::Shrink, Length::FillPortion);
                let mut child = container(element).width(width);
                if align_name == Some("stretch") {
                    child = child.height(Length::Fill);
                }
                row = row.push(child);
            }
            if count > 0 && matches!(justify, Some("spaceAround" | "spaceEvenly")) {
                row = row.push(Space::new().width(Length::FillPortion(1)));
            }
            if align_name == Some("stretch") {
                row = row.height(Length::Fill);
            }
            let row = container(row).width(Length::Fill);
            match justify {
                Some("center") => row.align_x(Alignment::Center).into(),
                Some("end") => row.align_x(Alignment::End).into(),
                _ => row.into(),
            }
        } else {
            let mut column = Column::new().spacing(10).align_x(align);
            if matches!(justify, Some("spaceAround" | "spaceEvenly")) {
                column = column.push(Space::new().height(Length::FillPortion(1)));
            }
            let count = elements.len();
            for (index, (element, weight)) in elements.into_iter().enumerate() {
                if index > 0
                    && matches!(
                        justify,
                        Some("spaceBetween" | "spaceAround" | "spaceEvenly")
                    )
                {
                    column = column.push(Space::new().height(Length::FillPortion(
                        if justify == Some("spaceAround") { 2 } else { 1 },
                    )));
                }
                let height = weight
                    .or((justify == Some("stretch")).then_some(1))
                    .map_or(Length::Shrink, Length::FillPortion);
                let mut child = container(element).height(height);
                if align_name == Some("stretch") {
                    child = child.width(Length::Fill);
                }
                column = column.push(child);
            }
            if count > 0 && matches!(justify, Some("spaceAround" | "spaceEvenly")) {
                column = column.push(Space::new().height(Length::FillPortion(1)));
            }
            if align_name == Some("stretch") {
                column = column.width(Length::Fill);
            }
            let mut column = container(column);
            if expands_main_axis {
                column = column.height(Length::Fill);
            }
            match justify {
                Some("center") => column.align_y(Alignment::Center).into(),
                Some("end") => column.align_y(Alignment::End).into(),
                _ => column.into(),
            }
        }
    }

    fn media_player<'a>(
        &'a self,
        component: &'a serde_json::Map<String, Value>,
        kind: &str,
        data: &Value,
        context: &Value,
    ) -> Element<'a, Message> {
        let url = display_value(&bound_value_at(component.get("url"), data, context));
        let description =
            display_value(&bound_value_at(component.get("description"), data, context));
        let title = if description.is_empty() {
            url::Url::parse(&url)
                .ok()
                .and_then(|url| {
                    url.path_segments()
                        .and_then(Iterator::last)
                        .filter(|name| !name.is_empty())
                        .map(str::to_owned)
                })
                .unwrap_or_else(|| kind.to_owned())
        } else {
            description.clone()
        };
        let video = kind == "Video";
        let mut content = Column::new().spacing(10);
        if video {
            let poster = display_value(&bound_value_at(component.get("posterUrl"), data, context));
            if let Some(handle) = self.a2ui_images.get(&poster) {
                content = content.push(
                    image(handle.clone())
                        .width(Length::Fill)
                        .height(240)
                        .content_fit(iced::ContentFit::Cover),
                );
            } else if !poster.is_empty() {
                content = content.push(
                    container(text("Loading video poster…").color(muted_text()))
                        .center(Length::Fill)
                        .height(180),
                );
            }
        }
        let label = if video {
            "▶ Play video"
        } else {
            "▶ Play audio"
        };
        let mut play = action_button(text(label).size(12));
        if url::Url::parse(&url).is_ok_and(|url| matches!(url.scheme(), "http" | "https")) {
            play = play.on_press(Message::A2uiPlayMedia(url.clone(), title.clone(), video));
        }
        content = content.push(
            row![
                column![text(title).size(13), text(url).size(10).color(muted_text())]
                    .spacing(3)
                    .width(Length::Fill),
                play,
            ]
            .spacing(10)
            .align_y(Alignment::Center),
        );
        container(content)
            .padding(12)
            .width(Length::Fill)
            .style(overview_style)
            .into()
    }

    pub(super) fn a2ui_modal_panel(&self) -> Option<Element<'_, Message>> {
        let (surface_id, component_id) = self.a2ui_modals.iter().next()?;
        let surface = self.displayed_a2ui_store().surface(surface_id)?;
        let component = surface.components.get(component_id)?.as_object()?;
        let content_id = component.get("content")?.as_str()?;
        let content = self.a2ui_component(
            surface,
            content_id,
            &surface.data,
            None,
            BTreeSet::new(),
            480.0,
        );
        let dialog = container(
            column![
                row![
                    text("Dialog").size(12),
                    Space::new().width(Length::Fill),
                    action_button("Close").on_press(Message::A2uiToggleModal(
                        surface_id.clone(),
                        component_id.clone(),
                    )),
                ],
                content,
            ]
            .spacing(12),
        )
        .padding(22)
        .width(560)
        .style(overview_style);
        Some(opaque(
            container(dialog)
                .center_x(Length::Fill)
                .center_y(Length::Fill)
                .style(|_| {
                    container::Style::default().background(Color::from_rgba8(0, 0, 0, 0.68))
                }),
        ))
    }
}

#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
struct DateTimeParts {
    year: i32,
    month: u8,
    day: u8,
    hour: u8,
    minute: u8,
    second: u8,
}

fn component_weight(surface: &Surface, id: &str) -> Option<u16> {
    let weight = surface.components.get(id)?.get("weight")?.as_f64()?;
    (weight > 0.0).then(|| (weight * 1_000.0).round().clamp(1.0, u16::MAX as f64) as u16)
}

#[derive(Clone, Copy)]
enum DatePart {
    Year,
    Month,
    Day,
    Hour,
    Minute,
}

#[derive(Clone)]
struct DateControl {
    surface_id: String,
    path: String,
    value: DateTimeParts,
    enable_date: bool,
    enable_time: bool,
    min: Option<DateTimeParts>,
    max: Option<DateTimeParts>,
}

impl DateControl {
    fn message(&self, part: DatePart, value: String) -> Message {
        let mut parts = self.value;
        match part {
            DatePart::Year => {
                let Ok(value) = value.parse() else {
                    return Message::Noop;
                };
                parts.year = value;
            }
            DatePart::Month => parts.month = value.parse().unwrap_or(parts.month),
            DatePart::Day => parts.day = value.parse().unwrap_or(parts.day),
            DatePart::Hour => parts.hour = value.parse().unwrap_or(parts.hour),
            DatePart::Minute => parts.minute = value.parse().unwrap_or(parts.minute),
        }
        parts.day = parts.day.min(days_in_month(parts.year, parts.month));
        if let Some(min) = self.min {
            parts = parts.max(min);
        }
        if let Some(max) = self.max {
            parts = parts.min(max);
        }
        Message::A2uiDataChanged(
            self.surface_id.clone(),
            self.path.clone(),
            Value::String(format_date_time(parts, self.enable_date, self.enable_time)),
        )
    }
}

fn date_time_input<'a>(
    component: &serde_json::Map<String, Value>,
    surface: &Surface,
    context: &Value,
    context_path: Option<String>,
) -> Element<'a, Message> {
    let label = display_value(&bound_value_at(
        component.get("label"),
        &surface.data,
        context,
    ));
    let binding = component.get("value");
    let enable_date = component
        .get("enableDate")
        .and_then(Value::as_bool)
        .unwrap_or(false);
    let enable_time = component
        .get("enableTime")
        .and_then(Value::as_bool)
        .unwrap_or(false);
    let now = OffsetDateTime::now_utc();
    let fallback = DateTimeParts {
        year: now.year(),
        month: now.month() as u8,
        day: now.day(),
        hour: now.hour(),
        minute: now.minute(),
        second: now.second(),
    };
    let raw = display_value(&bound_value_at(binding, &surface.data, context));
    let mut value = parse_date_time(&raw, enable_date, enable_time, fallback).unwrap_or(fallback);
    let min = component
        .get("min")
        .map(|value| display_value(&bound_value_at(Some(value), &surface.data, context)))
        .and_then(|value| parse_date_time(&value, enable_date, enable_time, fallback));
    let max = component
        .get("max")
        .map(|value| display_value(&bound_value_at(Some(value), &surface.data, context)))
        .and_then(|value| parse_date_time(&value, enable_date, enable_time, fallback));
    if let Some(min) = min {
        value = value.max(min);
    }
    if let Some(max) = max {
        value = value.min(max);
    }
    let Some(path) = local_path(binding_path(binding), context_path.as_deref()) else {
        return column![text(label).size(12), text(raw)].spacing(5).into();
    };
    if !enable_date && !enable_time {
        return column![text(label).size(12), text(raw)].spacing(5).into();
    }
    let control = DateControl {
        surface_id: surface.id.clone(),
        path,
        value,
        enable_date,
        enable_time,
        min,
        max,
    };
    let mut inputs = Row::new().spacing(6).align_y(Alignment::Center);
    if enable_date {
        let first_year = min
            .map_or(value.year.saturating_sub(100), |parts| parts.year)
            .clamp(0, 9999);
        let last_year = max
            .map_or(value.year.saturating_add(100), |parts| parts.year)
            .clamp(0, 9999);
        let years = (first_year.min(last_year)..=first_year.max(last_year))
            .map(|year| format!("{year:04}"))
            .collect::<Vec<_>>();
        let update = control.clone();
        inputs = inputs
            .push(pick_list(
                years,
                Some(format!("{:04}", value.year)),
                move |value| update.message(DatePart::Year, value),
            ))
            .push(text("–").color(muted_text()));
        let update = control.clone();
        inputs = inputs
            .push(pick_list(
                (1..=12)
                    .map(|value| format!("{value:02}"))
                    .collect::<Vec<_>>(),
                Some(format!("{:02}", value.month)),
                move |value| update.message(DatePart::Month, value),
            ))
            .push(text("–").color(muted_text()));
        let update = control.clone();
        inputs = inputs.push(pick_list(
            (1..=days_in_month(value.year, value.month))
                .map(|value| format!("{value:02}"))
                .collect::<Vec<_>>(),
            Some(format!("{:02}", value.day)),
            move |value| update.message(DatePart::Day, value),
        ));
    }
    if enable_date && enable_time {
        inputs = inputs.push(text("at").size(12).color(muted_text()));
    }
    if enable_time {
        let update = control.clone();
        inputs = inputs
            .push(pick_list(
                (0..24)
                    .map(|value| format!("{value:02}"))
                    .collect::<Vec<_>>(),
                Some(format!("{:02}", value.hour)),
                move |value| update.message(DatePart::Hour, value),
            ))
            .push(text(":").color(muted_text()));
        inputs = inputs.push(pick_list(
            (0..60)
                .map(|value| format!("{value:02}"))
                .collect::<Vec<_>>(),
            Some(format!("{:02}", value.minute)),
            move |value| control.message(DatePart::Minute, value),
        ));
    }
    let mut field = column![text(label).size(12), inputs].spacing(5);
    if let Some(error) = crate::a2ui::first_failed_check(component, &surface.data) {
        field = field.push(text(error).size(11).style(iced::widget::text::danger));
    }
    field.into()
}

fn parse_date_time(
    value: &str,
    enable_date: bool,
    enable_time: bool,
    mut parts: DateTimeParts,
) -> Option<DateTimeParts> {
    let (date, time) = match (enable_date, enable_time) {
        (true, true) => value.split_once('T')?,
        (true, false) => (value, ""),
        (false, true) => ("", value),
        (false, false) => return Some(parts),
    };
    if enable_date {
        let mut values = date.split('-');
        parts.year = values.next()?.parse().ok()?;
        parts.month = values.next()?.parse().ok()?;
        parts.day = values.next()?.parse().ok()?;
        if values.next().is_some()
            || !(0..=9999).contains(&parts.year)
            || !(1..=12).contains(&parts.month)
            || parts.day == 0
            || parts.day > days_in_month(parts.year, parts.month)
        {
            return None;
        }
    }
    if enable_time {
        let time = time
            .trim_end_matches('Z')
            .split(['+', '-'])
            .next()
            .unwrap_or(time)
            .split('.')
            .next()
            .unwrap_or(time);
        let mut values = time.split(':');
        parts.hour = values.next()?.parse().ok()?;
        parts.minute = values.next()?.parse().ok()?;
        parts.second = values.next().unwrap_or("0").parse().ok()?;
        if values.next().is_some() || parts.hour > 23 || parts.minute > 59 || parts.second > 59 {
            return None;
        }
    }
    Some(parts)
}

fn format_date_time(parts: DateTimeParts, enable_date: bool, enable_time: bool) -> String {
    match (enable_date, enable_time) {
        (true, true) => format!(
            "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}",
            parts.year, parts.month, parts.day, parts.hour, parts.minute, parts.second
        ),
        (true, false) => format!("{:04}-{:02}-{:02}", parts.year, parts.month, parts.day),
        (false, true) => format!("{:02}:{:02}:{:02}", parts.hour, parts.minute, parts.second),
        (false, false) => String::new(),
    }
}

fn days_in_month(year: i32, month: u8) -> u8 {
    Month::try_from(month)
        .map(|month| month.length(year))
        .unwrap_or(31)
}

fn valid_number_edit(value: &str) -> bool {
    matches!(value, "" | "+" | "-" | "." | "+." | "-.") || value.parse::<f64>().is_ok()
}

fn choice_selection(selected: &Value, option: &Value, multiple: bool, checked: bool) -> Value {
    let mut values = selected.as_array().cloned().unwrap_or_default();
    if multiple {
        values.retain(|value| value != option);
        if checked {
            values.push(option.clone());
        }
    } else {
        values = vec![option.clone()];
    }
    Value::Array(values)
}

#[allow(clippy::too_many_arguments)]
fn collect_renderer_state(
    surface: &Surface,
    id: &str,
    context: &Value,
    context_path: Option<String>,
    mut ancestors: BTreeSet<String>,
    markdown: &mut HashMap<(String, String, String), String>,
    editors: &mut HashMap<(String, String, String), String>,
    filters: &mut HashSet<(String, String, String)>,
) {
    if !ancestors.insert(id.to_owned()) {
        return;
    }
    let Some(component) = surface.components.get(id).and_then(Value::as_object) else {
        return;
    };
    let context_key = context_path.clone().unwrap_or_default();
    match component.get("component").and_then(Value::as_str) {
        Some("Text") => {
            markdown.insert(
                (surface.id.clone(), id.to_owned(), context_key),
                display_value(&bound_value_at(
                    component.get("text"),
                    &surface.data,
                    context,
                )),
            );
        }
        Some("TextField")
            if component.get("variant").and_then(Value::as_str) == Some("longText") =>
        {
            if let Some(path) = local_path(
                binding_path(component.get("value")),
                context_path.as_deref(),
            ) {
                editors.insert(
                    (surface.id.clone(), id.to_owned(), path),
                    display_value(&bound_value_at(
                        component.get("value"),
                        &surface.data,
                        context,
                    )),
                );
            }
        }
        Some("ChoicePicker") if component.get("filterable") == Some(&Value::Bool(true)) => {
            filters.insert((surface.id.clone(), id.to_owned(), context_key));
        }
        _ => {}
    }
    match component.get("component").and_then(Value::as_str) {
        Some("Row" | "Column" | "List" | "Form") => collect_children_state(
            surface,
            component.get("children"),
            context,
            context_path,
            ancestors,
            markdown,
            editors,
            filters,
        ),
        Some("Card" | "Button") => {
            if let Some(child) = component.get("child").and_then(Value::as_str) {
                collect_renderer_state(
                    surface,
                    child,
                    context,
                    context_path,
                    ancestors,
                    markdown,
                    editors,
                    filters,
                );
            }
        }
        Some("Tabs") => {
            for child in component
                .get("tabs")
                .and_then(Value::as_array)
                .into_iter()
                .flatten()
                .filter_map(|tab| tab.get("child").and_then(Value::as_str))
            {
                collect_renderer_state(
                    surface,
                    child,
                    context,
                    context_path.clone(),
                    ancestors.clone(),
                    markdown,
                    editors,
                    filters,
                );
            }
        }
        Some("Modal") => {
            for child in ["trigger", "content"]
                .into_iter()
                .filter_map(|field| component.get(field).and_then(Value::as_str))
            {
                collect_renderer_state(
                    surface,
                    child,
                    context,
                    context_path.clone(),
                    ancestors.clone(),
                    markdown,
                    editors,
                    filters,
                );
            }
        }
        _ => {}
    }
}

#[allow(clippy::too_many_arguments)]
fn collect_children_state(
    surface: &Surface,
    children: Option<&Value>,
    context: &Value,
    context_path: Option<String>,
    ancestors: BTreeSet<String>,
    markdown: &mut HashMap<(String, String, String), String>,
    editors: &mut HashMap<(String, String, String), String>,
    filters: &mut HashSet<(String, String, String)>,
) {
    if let Some(children) = children.and_then(Value::as_array) {
        for child in children.iter().filter_map(Value::as_str) {
            collect_renderer_state(
                surface,
                child,
                context,
                context_path.clone(),
                ancestors.clone(),
                markdown,
                editors,
                filters,
            );
        }
    } else if let Some(template) = children.and_then(Value::as_object) {
        let component_id = template
            .get("componentId")
            .and_then(Value::as_str)
            .unwrap_or("");
        let path = template.get("path").and_then(Value::as_str).unwrap_or("/");
        for (index, item) in surface
            .data
            .pointer(path)
            .and_then(Value::as_array)
            .into_iter()
            .flatten()
            .enumerate()
        {
            collect_renderer_state(
                surface,
                component_id,
                item,
                Some(format!("{}/{index}", path.trim_end_matches('/'))),
                ancestors.clone(),
                markdown,
                editors,
                filters,
            );
        }
    }
}

fn title<'a>(component: &serde_json::Map<String, Value>) -> Option<iced::widget::Text<'a>> {
    component
        .get("title")
        .and_then(Value::as_str)
        .map(|title| text(title.to_owned()).size(17))
}

fn image_link<'a>(
    component: &serde_json::Map<String, Value>,
    data: &Value,
    context: &Value,
) -> Element<'a, Message> {
    let url = display_value(&bound_value_at(component.get("url"), data, context));
    let button = action_button(text("Open image").size(12));
    if url::Url::parse(&url).is_ok_and(|url| matches!(url.scheme(), "http" | "https")) {
        button.on_press(Message::OpenLink(url)).into()
    } else {
        column![button, text(url).size(11).color(muted_text())]
            .spacing(5)
            .into()
    }
}

fn research_chart<'a>(
    component: &serde_json::Map<String, Value>,
    data: &Value,
    context: &Value,
    surface_height: f32,
) -> Element<'a, Message> {
    let series = bound_value_at(component.get("series"), data, context);
    let series = series.as_array().map(Vec::as_slice).unwrap_or(&[]);
    let chart_type = component
        .get("chartType")
        .and_then(Value::as_str)
        .unwrap_or("bar");
    if matches!(chart_type, "pie" | "donut") {
        return research_pie_chart(component, series, chart_type == "donut", surface_height);
    }
    if chart_type == "heatmap" {
        return research_heatmap(component, series);
    }
    let max = series
        .iter()
        .filter_map(|point| point.get("value").and_then(Value::as_f64))
        .fold(0.0_f64, f64::max)
        .max(1.0) as f32;
    let mut rows = Column::new().spacing(7);
    if let Some(title) = title(component) {
        rows = rows.push(title);
    }
    rows = rows.push(text(chart_type.to_uppercase()).size(10).color(muted_text()));
    for point in series {
        let label = point.get("label").map(display_value).unwrap_or_default();
        let value = point.get("value").and_then(Value::as_f64).unwrap_or(0.0) as f32;
        rows = rows.push(
            row![
                text(label).size(11).width(110),
                progress_bar(0.0..=max, value.clamp(0.0, max)),
                text(format!("{value}")).size(11).width(55),
            ]
            .spacing(8)
            .align_y(Alignment::Center),
        );
        if let Some(segments) = point.get("segments").and_then(Value::as_array) {
            rows = rows.push(
                text(
                    segments
                        .iter()
                        .map(|segment| {
                            format!(
                                "{} {}",
                                segment.get("label").map(display_value).unwrap_or_default(),
                                segment.get("value").map(display_value).unwrap_or_default()
                            )
                        })
                        .collect::<Vec<_>>()
                        .join(" · "),
                )
                .size(10)
                .color(muted_text()),
            );
        }
    }
    rows.into()
}

#[derive(Debug, PartialEq)]
struct HeatmapRow {
    label: String,
    cells: Vec<f64>,
}

#[derive(Debug, PartialEq)]
struct HeatmapData {
    columns: Vec<String>,
    rows: Vec<HeatmapRow>,
    max: f64,
}

fn heatmap_data(series: &[Value]) -> HeatmapData {
    let rows = series
        .iter()
        .filter(|entry| {
            entry
                .get("segments")
                .and_then(Value::as_array)
                .is_some_and(|segments| !segments.is_empty())
        })
        .collect::<Vec<_>>();
    let mut columns = Vec::new();
    let mut max = 0.0_f64;
    for entry in &rows {
        for segment in entry["segments"].as_array().into_iter().flatten() {
            if let Some(label) = segment.get("label").and_then(Value::as_str)
                && !columns.iter().any(|column| column == label)
            {
                columns.push(label.to_owned());
            }
            if let Some(value) = segment.get("value").and_then(Value::as_f64) {
                max = max.max(value);
            }
        }
    }
    let rows = rows
        .into_iter()
        .map(|entry| {
            let mut values = HashMap::new();
            for segment in entry["segments"].as_array().into_iter().flatten() {
                if let (Some(label), Some(value)) = (
                    segment.get("label").and_then(Value::as_str),
                    segment.get("value").and_then(Value::as_f64),
                ) {
                    values.insert(label, value);
                }
            }
            HeatmapRow {
                label: entry.get("label").map(display_value).unwrap_or_default(),
                cells: columns
                    .iter()
                    .map(|column| values.get(column.as_str()).copied().unwrap_or(0.0))
                    .collect(),
            }
        })
        .collect();
    HeatmapData { columns, rows, max }
}

fn research_heatmap<'a>(
    component: &serde_json::Map<String, Value>,
    series: &[Value],
) -> Element<'a, Message> {
    let heatmap = heatmap_data(series);
    let mut chart = Column::new().spacing(8).width(Length::Fill);
    if let Some(title) = title(component) {
        chart = chart.push(title);
    }
    chart = chart.push(text("HEATMAP").size(10).color(muted_text()));
    if heatmap.rows.is_empty() || heatmap.columns.is_empty() {
        return chart.into();
    }
    chart
        .push(responsive(move |size| {
            let row_label_width = size.width.min(80.0);
            let gap = 2.0;
            let cell_side = ((size.width - row_label_width - gap * heatmap.columns.len() as f32)
                / heatmap.columns.len() as f32)
                .max(14.0);
            let mut grid = Column::new().spacing(gap);
            let mut header = Row::new()
                .spacing(gap)
                .push(Space::new().width(row_label_width).height(14));
            for label in &heatmap.columns {
                header = header.push(
                    container(text(label.clone()).size(11).color(muted_text()))
                        .width(cell_side)
                        .align_x(Alignment::Center),
                );
            }
            grid = grid.push(header);
            for row in &heatmap.rows {
                let mut cells = Row::new().spacing(gap).align_y(Alignment::Center).push(
                    container(text(row.label.clone()).size(11).color(muted_text()))
                        .width(row_label_width)
                        .padding([0, 4])
                        .align_x(Alignment::End),
                );
                for value in &row.cells {
                    let value = *value;
                    let (background, foreground) = heatmap_cell_colors(value, heatmap.max);
                    cells = cells.push(
                        container(
                            text(if value > 0.0 {
                                format_chart_value(value)
                            } else {
                                String::new()
                            })
                            .size(10),
                        )
                        .width(cell_side)
                        .height(cell_side)
                        .center_x(Length::Fill)
                        .center_y(Length::Fill)
                        .style(move |_| container::Style {
                            text_color: Some(foreground),
                            background: Some(background.into()),
                            border: Border {
                                radius: 2.0.into(),
                                ..Border::default()
                            },
                            ..container::Style::default()
                        }),
                    );
                }
                grid = grid.push(cells);
            }
            grid.into()
        }))
        .into()
}

fn heatmap_cell_colors(value: f64, max: f64) -> (Color, Color) {
    let intensity = if max > 0.0 { value / max } else { 0.0 };
    if intensity <= 0.0 {
        return (Color::TRANSPARENT, app_theme().palette().text);
    }
    let intensity = intensity as f32;
    let red = (53.0 + (183.0 - 53.0) * intensity).round();
    let green = (117.0 + (72.0 - 117.0) * intensity).round();
    let blue = (56.0 + (72.0 - 56.0) * intensity).round();
    let opacity = 0.25 + intensity * 0.75;
    let effective_red = red * opacity + 30.0 * (1.0 - opacity);
    let effective_green = green * opacity + 30.0 * (1.0 - opacity);
    let effective_blue = blue * opacity + 30.0 * (1.0 - opacity);
    let foreground =
        if 0.299 * effective_red + 0.587 * effective_green + 0.114 * effective_blue > 140.0 {
            Color::BLACK
        } else {
            Color::WHITE
        };
    (
        Color::from_rgba8(red as u8, green as u8, blue as u8, opacity),
        foreground,
    )
}

fn research_pie_chart<'a>(
    component: &serde_json::Map<String, Value>,
    series: &[Value],
    donut: bool,
    surface_height: f32,
) -> Element<'a, Message> {
    let slices = series
        .iter()
        .filter_map(|point| {
            let value = point.get("value")?.as_f64()?;
            (value > 0.0).then(|| {
                (
                    point.get("label").map(display_value).unwrap_or_default(),
                    value,
                    point.get("value").map(display_value).unwrap_or_default(),
                )
            })
        })
        .collect::<Vec<_>>();
    let values = slices
        .iter()
        .map(|(_, value, _)| *value)
        .collect::<Vec<_>>();
    let mut chart = Column::new().spacing(8).width(Length::Fill);
    if let Some(title) = title(component) {
        chart = chart.push(title);
    }
    chart = chart.push(
        text(if donut { "DONUT" } else { "PIE" })
            .size(10)
            .color(muted_text()),
    );
    if slices.is_empty() {
        return chart
            .push(text("No positive values").size(11).color(muted_text()))
            .into();
    }
    let handle = svg::Handle::from_memory(pie_chart_svg(&values, donut).into_bytes());
    let total = values.iter().sum::<f64>();
    chart = chart.push(
        responsive(move |size| {
            let (body_height, side, rows_per_column) = pie_chart_layout(size.width, surface_height);
            let mut legend = Row::new().spacing(14);
            for (column_index, entries) in slices.chunks(rows_per_column).enumerate() {
                let mut column = Column::new().spacing(6);
                for (row_index, (label, _, value)) in entries.iter().enumerate() {
                    let index = column_index * rows_per_column + row_index;
                    let color = chart_color(index);
                    column = column.push(
                        row![
                            container(Space::new().width(10).height(10))
                                .style(move |_| { container::Style::default().background(color) }),
                            text(format!(
                                "{label}  {value} · {:.0}%",
                                values[index] / total * 100.0
                            ))
                            .size(11),
                        ]
                        .spacing(5)
                        .align_y(Alignment::Center),
                    );
                }
                legend = legend.push(column);
            }
            row![
                container(svg(handle.clone()).width(side).height(side))
                    .width(side)
                    .align_x(Alignment::Center),
                container(legend).width(Length::Fill),
            ]
            .height(body_height)
            .spacing(18)
            .align_y(Alignment::Center)
            .into()
        })
        .height(Length::Shrink),
    );
    chart.into()
}

fn pie_chart_layout(width: f32, surface_height: f32) -> (f32, f32, usize) {
    let body_height = (surface_height - 80.0).max(0.0);
    (
        body_height,
        (width * 0.68).min(body_height).min(560.0),
        ((body_height + 6.0) / 20.0).max(1.0) as usize,
    )
}

fn pie_chart_svg(values: &[f64], donut: bool) -> String {
    let total = values
        .iter()
        .copied()
        .filter(|value| *value > 0.0)
        .sum::<f64>();
    if total <= 0.0 {
        return String::new();
    }
    let mut current = 0.0;
    let mut slices = String::new();
    for (index, value) in values.iter().copied().enumerate() {
        if value <= 0.0 {
            continue;
        }
        let fraction = value / total;
        let color = hex(chart_color(index));
        if donut {
            let circumference = 2.0 * std::f64::consts::PI * 46.0;
            slices.push_str(&format!(
                r#"<circle cx="70" cy="70" r="46" fill="none" stroke="{color}" stroke-width="24" stroke-dasharray="{:.3} {:.3}" stroke-dashoffset="{:.3}" transform="rotate(-90 70 70)"/>"#,
                fraction * circumference,
                (1.0 - fraction) * circumference,
                -current * circumference,
            ));
        } else if fraction >= 0.999_999 {
            slices.push_str(&format!(
                r#"<circle cx="70" cy="70" r="56" fill="{color}"/>"#
            ));
        } else {
            let start = current * std::f64::consts::TAU - std::f64::consts::FRAC_PI_2;
            let end = (current + fraction) * std::f64::consts::TAU - std::f64::consts::FRAC_PI_2;
            let (x1, y1) = (70.0 + 56.0 * start.cos(), 70.0 + 56.0 * start.sin());
            let (x2, y2) = (70.0 + 56.0 * end.cos(), 70.0 + 56.0 * end.sin());
            let large = u8::from(fraction > 0.5);
            slices.push_str(&format!(
                r##"<path d="M70 70 L{x1:.3} {y1:.3} A56 56 0 {large} 1 {x2:.3} {y2:.3} Z" fill="{color}" stroke="#1f1f21" stroke-width="1"/>"##
            ));
        }
        current += fraction;
    }
    let center = donut.then(|| {
        format!(
            r#"<text x="70" y="70" text-anchor="middle" dominant-baseline="central" fill="{}" font-family="sans-serif" font-size="16" font-weight="600">{}</text>"#,
            hex(app_theme().palette().text),
            format_chart_value(total),
        )
    });
    format!(
        r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 140 140">{slices}{}</svg>"#,
        center.unwrap_or_default()
    )
}

fn chart_color(index: usize) -> Color {
    [
        Color::from_rgb8(117, 190, 255),
        Color::from_rgb8(137, 209, 133),
        Color::from_rgb8(209, 134, 22),
        Color::from_rgb8(241, 76, 76),
        Color::from_rgb8(177, 128, 215),
        Color::from_rgb8(226, 196, 64),
    ][index % 6]
}

fn format_chart_value(value: f64) -> String {
    if value.fract() == 0.0 {
        format!("{value:.0}")
    } else {
        format!("{value:.2}")
    }
}

fn research_table<'a>(
    component: &serde_json::Map<String, Value>,
    data: &Value,
    context: &Value,
) -> Element<'a, Message> {
    let columns = bound_value_at(component.get("columns"), data, context);
    let rows = bound_value_at(component.get("rows"), data, context);
    let mut table = Column::new().spacing(0);
    if let Some(title) = title(component) {
        table = table.push(container(title).padding(8));
    }
    if let Some(columns) = columns.as_array() {
        table = table
            .push(table_row(columns, true))
            .push(rule::horizontal(1));
    }
    if let Some(rows) = rows.as_array() {
        for (index, row) in rows.iter().filter_map(Value::as_array).enumerate() {
            if index > 0 {
                table = table.push(rule::horizontal(1));
            }
            table = table.push(table_row(row, false));
        }
    }
    container(table)
        .width(Length::Fill)
        .style(overview_style)
        .into()
}

fn table_row<'a>(values: &[Value], header: bool) -> Element<'a, Message> {
    values
        .iter()
        .fold(Row::new().spacing(8), |row, value| {
            row.push(
                text(display_value(value))
                    .size(if header { 12 } else { 11 })
                    .width(Length::Fill),
            )
        })
        .padding(8)
        .into()
}

fn research_metric<'a>(
    component: &serde_json::Map<String, Value>,
    data: &Value,
    context: &Value,
) -> Element<'a, Message> {
    column![
        text(display_value(&bound_value_at(
            component.get("label"),
            data,
            context
        )))
        .size(11)
        .color(muted_text()),
        text(display_value(&bound_value_at(
            component.get("value"),
            data,
            context
        )))
        .size(28),
        text(display_value(&bound_value_at(
            component.get("detail"),
            data,
            context
        )))
        .size(11)
        .color(muted_text()),
    ]
    .spacing(4)
    .into()
}

fn research_timeline<'a>(
    component: &serde_json::Map<String, Value>,
    data: &Value,
    context: &Value,
) -> Element<'a, Message> {
    let events = bound_value_at(component.get("events"), data, context);
    let mut timeline = Column::new().spacing(8);
    if let Some(title) = title(component) {
        timeline = timeline.push(title);
    }
    for event in events.as_array().map(Vec::as_slice).unwrap_or(&[]) {
        timeline = timeline.push(
            row![
                text("●").size(12),
                column![
                    text(event.get("title").map(display_value).unwrap_or_default()).size(13),
                    text(format!(
                        "{}{}",
                        event.get("time").map(display_value).unwrap_or_default(),
                        event
                            .get("description")
                            .map(|value| format!(" · {}", display_value(value)))
                            .unwrap_or_default()
                    ))
                    .size(11)
                    .color(muted_text()),
                ]
                .spacing(2),
            ]
            .spacing(8),
        );
    }
    timeline.into()
}

fn research_map<'a>(
    component: &serde_json::Map<String, Value>,
    data: &Value,
    context: &Value,
) -> Element<'a, Message> {
    let locations = bound_value_at(component.get("locations"), data, context);
    let mut map = Column::new().spacing(7);
    if let Some(title) = title(component) {
        map = map.push(title);
    }
    for location in locations.as_array().map(Vec::as_slice).unwrap_or(&[]) {
        let label = location.get("label").map(display_value).unwrap_or_default();
        let latitude = location
            .get("latitude")
            .map(display_value)
            .unwrap_or_default();
        let longitude = location
            .get("longitude")
            .map(display_value)
            .unwrap_or_default();
        map = map.push(
            row![
                text("⌖").size(18),
                column![
                    text(label).size(13),
                    text(format!("{latitude}, {longitude}"))
                        .size(11)
                        .color(muted_text()),
                ]
            ]
            .spacing(8),
        );
    }
    container(map)
        .padding(10)
        .width(Length::Fill)
        .style(overview_style)
        .into()
}

fn research_mind_map<'a>(
    component: &serde_json::Map<String, Value>,
    data: &Value,
    context: &Value,
) -> Element<'a, Message> {
    let nodes = bound_value_at(component.get("nodes"), data, context);
    let nodes = nodes.as_array().map(Vec::as_slice).unwrap_or(&[]);
    let mut map = Column::new().spacing(6);
    if let Some(title) = title(component) {
        map = map.push(title);
    }
    if let Some(root) = nodes.first() {
        map = map.push(mind_node(nodes, root, 0, BTreeSet::new()));
    }
    map.into()
}

fn mind_node<'a>(
    nodes: &[Value],
    node: &Value,
    depth: usize,
    mut seen: BTreeSet<String>,
) -> Element<'a, Message> {
    let id = node.get("id").map(display_value).unwrap_or_default();
    if !seen.insert(id) {
        return text("Cycle").size(11).color(muted_text()).into();
    }
    let mut branch = Column::new().spacing(4).push(
        row![
            Space::new().width((depth * 18) as f32),
            text(if depth == 0 { "◆" } else { "↳" }).size(12),
            text(node.get("label").map(display_value).unwrap_or_default()).size(13),
        ]
        .spacing(6),
    );
    for child in node
        .get("children")
        .and_then(Value::as_array)
        .into_iter()
        .flatten()
    {
        if let Some(child) = nodes.iter().find(|node| node.get("id") == Some(child)) {
            branch = branch.push(mind_node(nodes, child, depth + 1, seen.clone()));
        }
    }
    branch.into()
}

fn local_path(path: Option<&str>, context_path: Option<&str>) -> Option<String> {
    let path = path?;
    if path.starts_with('/') {
        Some(path.to_owned())
    } else {
        Some(format!(
            "{}/{}",
            context_path.unwrap_or("").trim_end_matches('/'),
            path
        ))
    }
}

fn icon_glyph(name: &str) -> &'static str {
    match name {
        "accountCircle" | "person" => "●",
        "add" => "+",
        "arrowBack" => "←",
        "arrowForward" => "→",
        "attachFile" => "⌕",
        "calendarToday" | "event" => "▣",
        "call" | "phone" => "☎",
        "camera" => "◉",
        "check" => "✓",
        "close" => "×",
        "delete" => "⌫",
        "download" => "⇩",
        "edit" => "✎",
        "error" => "⊗",
        "fastForward" => "≫",
        "favorite" => "♥",
        "favoriteOff" => "♡",
        "folder" => "▰",
        "help" => "?",
        "home" => "⌂",
        "warning" => "⚠",
        "info" => "ⓘ",
        "locationOn" => "⌖",
        "lock" => "▣",
        "lockOpen" => "□",
        "search" => "⌕",
        "mail" => "✉",
        "menu" => "☰",
        "moreVert" => "⋮",
        "moreHoriz" => "…",
        "notifications" => "◈",
        "notificationsOff" => "◇",
        "pause" => "Ⅱ",
        "payment" => "¤",
        "photo" => "▧",
        "play" => "▶",
        "print" => "▤",
        "refresh" => "↻",
        "rewind" => "≪",
        "send" => "➤",
        "settings" => "⚙",
        "share" => "↗",
        "shoppingCart" => "⌑",
        "skipNext" => "▸|",
        "skipPrevious" => "|◂",
        "star" => "★",
        "starHalf" => "☆",
        "starOff" => "☆",
        "stop" => "■",
        "upload" => "⇧",
        "visibility" => "◉",
        "visibilityOff" => "○",
        "volumeDown" => "◖",
        "volumeMute" => "◁",
        "volumeOff" => "×",
        "volumeUp" => "◀",
        _ => "•",
    }
}

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

    fn parts() -> DateTimeParts {
        DateTimeParts {
            year: 2024,
            month: 1,
            day: 31,
            hour: 12,
            minute: 30,
            second: 45,
        }
    }

    #[test]
    fn renderer_input_helpers_cover_dates_numbers_choices_and_weights() {
        let fallback = parts();
        let leap = parse_date_time("2024-02-29T23:59:58Z", true, true, fallback).unwrap();
        assert_eq!(format_date_time(leap, true, true), "2024-02-29T23:59:58");
        assert!(parse_date_time("2023-02-29", true, false, fallback).is_none());
        assert_eq!(
            format_date_time(
                parse_date_time("08:09+02:00", false, true, fallback).unwrap(),
                false,
                true
            ),
            "08:09:00"
        );
        assert!(valid_number_edit("-."));
        assert!(valid_number_edit("1.25e-3"));
        assert!(!valid_number_edit("one"));

        assert_eq!(
            choice_selection(&json!(["a"]), &json!("b"), true, true),
            json!(["a", "b"])
        );
        assert_eq!(
            choice_selection(&json!(["a", "b"]), &json!("a"), true, false),
            json!(["b"])
        );
        assert_eq!(
            choice_selection(&json!(["a"]), &json!("b"), false, true),
            json!(["b"])
        );

        let surface = Surface {
            id: "surface".into(),
            catalog_id: crate::a2ui::CATALOG_ID.into(),
            surface_properties: json!({}),
            send_data_model: true,
            components: [("child".into(), json!({"weight": 1.5}))].into(),
            data: json!({}),
            owner_message_id: 1,
        };
        assert_eq!(component_weight(&surface, "child"), Some(1_500));

        let control = DateControl {
            surface_id: "surface".into(),
            path: "/when".into(),
            value: parts(),
            enable_date: true,
            enable_time: false,
            min: None,
            max: None,
        };
        let Message::A2uiDataChanged(_, _, Value::String(value)) =
            control.message(DatePart::Month, "2".into())
        else {
            panic!("date control did not update its binding");
        };
        assert_eq!(value, "2024-02-29");

        let pie = pie_chart_svg(&[60.0, 25.0, 15.0], false);
        assert_eq!(pie.matches("<path").count(), 3);
        let donut = pie_chart_svg(&[41.0], true);
        assert!(donut.contains("stroke-dasharray"));
        assert!(donut.contains(">41</text>"));
        assert_eq!(pie_chart_layout(1_000.0, 400.0), (320.0, 320.0, 16));
        assert_eq!(pie_chart_layout(1_000.0, 700.0), (620.0, 560.0, 31));

        let series = json!([
            {"label":"2024","value":99,"segments":[{"label":"Jan","value":2},{"label":"Feb","value":4}]},
            {"label":"plain","value":50},
            {"label":"2025","segments":[{"label":"Feb","value":8},{"label":"Mar","value":1}]}
        ]);
        let heatmap = heatmap_data(series.as_array().unwrap());
        assert_eq!(heatmap.columns, ["Jan", "Feb", "Mar"]);
        assert_eq!(heatmap.max, 8.0);
        assert_eq!(
            heatmap.rows,
            [
                HeatmapRow {
                    label: "2024".into(),
                    cells: vec![2.0, 4.0, 0.0],
                },
                HeatmapRow {
                    label: "2025".into(),
                    cells: vec![0.0, 8.0, 1.0],
                },
            ]
        );
        assert_eq!(heatmap_cell_colors(0.0, heatmap.max).0, Color::TRANSPARENT);
        assert_eq!(
            heatmap_cell_colors(heatmap.max, heatmap.max),
            (Color::from_rgb8(183, 72, 72), Color::WHITE)
        );
    }
}
