use super::common;
use crate::{
    selected_source, source_label, Message, OperationKind, SoftwareApp, ICON_SIZE_DETAIL,
};
use cosmic::iced::{Alignment, Length};
use cosmic::{theme, widget, Element};
use rakuos_packages::{NativeApp, SourceOption};
use rakuos_reviews::Review;
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use std::path::{Path, PathBuf};
use std::process::Command;

const REVIEWS_PER_PAGE: usize = 5;

pub fn view<'a>(app_state: &'a SoftwareApp, app: &'a NativeApp, width: usize) -> Element<'a, Message> {
    let spacing = theme::active().cosmic().spacing;
    let source = selected_source(app, app_state.selected_source_index);
    let current = display_app(app, &source);
    let installed = source.installed;
    let reviews_allowed = supports_reviews(&source.source);

    let icon = common::app_icon(&current.icon_path, &current.icon_url, ICON_SIZE_DETAIL);
    let mut header_controls: Vec<Element<_>> = Vec::new();
    if app.sources.len() > 1 {
        let source_labels: Vec<String> = app
            .sources
            .iter()
            .map(|source| {
                if source.label.is_empty() {
                    source_label(&source.source)
                } else {
                    source.label.clone()
                }
            })
            .collect();
        let selected_index = (app_state.selected_source_index < source_labels.len())
            .then_some(app_state.selected_source_index)
            .or(Some(0));
        let selected_label = selected_index
            .and_then(|index| source_labels.get(index))
            .cloned()
            .unwrap_or_else(|| "Source".to_string());

        header_controls.push(
            widget::button::standard(selected_label)
                .trailing_icon(widget::icon::from_name("pan-down-symbolic").size(16))
                .on_press(Message::ToggleSourceMenu)
            .into(),
        );

        if app_state.source_menu_open {
            let options: Vec<Element<_>> = source_labels
                .iter()
                .enumerate()
                .map(|(index, label)| {
                    if Some(index) == selected_index {
                        widget::button::suggested(label.clone())
                            .width(Length::Fill)
                            .on_press(Message::SelectSource(index))
                            .into()
                    } else {
                        widget::button::standard(label.clone())
                            .width(Length::Fill)
                            .on_press(Message::SelectSource(index))
                            .into()
                    }
                })
                .collect();

            header_controls.push(
                widget::container(
                    widget::column::with_children(options)
                        .spacing(spacing.space_xxs)
                        .width(Length::Fill),
                )
                .width(Length::Fixed(190.0))
                .padding(spacing.space_xxs)
                .class(theme::Container::Card)
                .into(),
            );
        }
    }

    let overlay_locked = app_state.overlay_update_running && is_repo_source(&source.source);

    if app_state.operation_running {
        header_controls.push(widget::text::caption("Working...").into());
    } else if overlay_locked {
        header_controls.push(widget::button::suggested("Install").into());
        header_controls.push(
            widget::text::caption("Package database update in progress").into(),
        );
    } else if installed {
        if app.source == "webapp" {
            header_controls.push(
                widget::button::suggested("Launch")
                    .on_press(Message::LaunchWebApp(app.id.clone()))
                    .into(),
            );
        }
        header_controls.push(
            widget::button::destructive("Remove")
                .on_press(Message::Operation(OperationKind::Remove))
                .into(),
        );
    } else {
        header_controls.push(
            widget::button::suggested("Install")
                .on_press(Message::Operation(OperationKind::Install))
                .into(),
        );
    }

    if !app_state.addon_list.is_empty() {
        header_controls.push(
            widget::button::standard("Add-ons")
                .trailing_icon(widget::icon::from_name(
                    if app_state.addon_dialog_open { "pan-up-symbolic" } else { "pan-down-symbolic" }
                ).size(16))
                .on_press(Message::ToggleAddonsDialog)
                .into(),
        );
    }

    let mut column = widget::column::with_capacity(8)
        .padding([0, spacing.space_s, spacing.space_m, spacing.space_s])
        .spacing(spacing.space_m)
        .width(Length::Fill);

    column = column.push(
        widget::button::text("Back")
            .leading_icon(widget::icon::from_name("go-previous-symbolic").size(16))
            .on_press(Message::SelectNone),
    );

    column = column.push(
        widget::row::with_children(vec![
            icon,
            widget::column::with_children(vec![
                widget::text::title2(current.name.clone()).into(),
                widget::text::body(current.summary.clone()).into(),
                widget::space::vertical().height(spacing.space_xs).into(),
                widget::row::with_children(header_links(&current))
                    .spacing(spacing.space_s)
                    .into(),
                if reviews_allowed {
                    hero_rating(app_state).into()
                } else {
                    widget::space::vertical().height(Length::Fixed(1.0)).into()
                },
            ])
            .width(Length::Fill)
            .into(),
            widget::column::with_children(header_controls)
                .spacing(spacing.space_xs)
                .align_x(Alignment::End)
                .into(),
        ])
        .spacing(spacing.space_m)
        .align_y(Alignment::Center),
    );

    column = column.push(detail_meta_row(current.clone()));

    if !current.screenshots.is_empty() {
        column = column.push(screenshot_carousel(
            current.screenshots.clone(),
            app_state.screenshot_index,
            width,
        ));
    }

    if !current.description.trim().is_empty() {
        column = column
            .push(widget::text::title4("About this app"))
            .push(widget::text::body(clean_description(&current.description)));
    }

    if app.source == "appimage" {
        column = column.push(appimage_update_settings(app_state));
    }

    if reviews_allowed {
        column = column.push(reviews_section(app_state));
    }

    if !app_state.status.is_empty() {
        column = column.push(widget::text::caption(&app_state.status));
    }

    column.into()
}

fn supports_reviews(source: &str) -> bool {
    matches!(source, "native" | "terra" | "flatpak")
}

fn is_repo_source(source: &str) -> bool {
    !matches!(source, "flatpak" | "appimage" | "webapp")
}

fn appimage_update_settings(app_state: &SoftwareApp) -> Element<'static, Message> {
    let spacing = theme::active().cosmic().spacing;
    let source = app_state.appimage_update_source.as_str();
    let fields_visible = source != "none";

    let mut content: Vec<Element<_>> = vec![
        widget::text::title4("Update Settings").into(),
        widget::text::caption("Configure how RakuOS checks this AppImage for updates.").into(),
        widget::row::with_children(vec![
            widget::text::body("Source").width(Length::Fixed(80.0)).into(),
            appimage_source_button("none", source),
            appimage_source_button("github", source),
            appimage_source_button("gitlab", source),
            appimage_source_button("url", source),
        ])
        .spacing(spacing.space_xs)
        .align_y(Alignment::Center)
        .into(),
    ];

    if fields_visible {
        content.push(
            widget::row::with_children(vec![
                widget::text::body(match source {
                    "github" => "owner/repo",
                    "gitlab" => "namespace/repo",
                    _ => "URL",
                })
                .width(Length::Fixed(120.0))
                .into(),
                widget::text_input::text_input(
                    match source {
                        "github" => "e.g. owner/myapp",
                        "gitlab" => "e.g. group/myapp",
                        _ => "https://example.com/releases/latest",
                    },
                    app_state.appimage_update_url.clone(),
                )
                .on_input(Message::AppImageUpdateUrlChanged)
                .into(),
            ])
            .spacing(spacing.space_xs)
            .align_y(Alignment::Center)
            .into(),
        );
        content.push(
            widget::row::with_children(vec![
                widget::text::body("Pattern")
                    .width(Length::Fixed(120.0))
                    .into(),
                widget::text_input::text_input(
                    "e.g. *.AppImage",
                    app_state.appimage_update_pattern.clone(),
                )
                .on_input(Message::AppImageUpdatePatternChanged)
                .into(),
            ])
            .spacing(spacing.space_xs)
            .align_y(Alignment::Center)
            .into(),
        );
    }

    content.push(
        widget::row::with_children(vec![
            widget::button::suggested("Save Settings")
                .on_press(Message::SaveAppImageSettings)
                .into(),
            if app_state.appimage_settings_status.is_empty() {
                widget::space::horizontal().width(Length::Shrink).into()
            } else {
                widget::text::caption(app_state.appimage_settings_status.clone()).into()
            },
        ])
        .spacing(spacing.space_s)
        .align_y(Alignment::Center)
        .into(),
    );

    widget::container(
        widget::column::with_children(content)
            .spacing(spacing.space_s)
            .width(Length::Fill),
    )
    .padding(spacing.space_s)
    .width(Length::Fill)
    .class(theme::Container::Card)
    .into()
}

fn appimage_source_button(label: &'static str, current: &str) -> Element<'static, Message> {
    if current == label {
        widget::button::suggested(label)
            .on_press(Message::AppImageUpdateSourceChanged(label.to_string()))
            .into()
    } else {
        widget::button::standard(label)
            .on_press(Message::AppImageUpdateSourceChanged(label.to_string()))
            .into()
    }
}

fn hero_rating(app_state: &SoftwareApp) -> Element<'static, Message> {
    if app_state.reviews_loading {
        return widget::text::caption("Loading reviews...").into();
    }

    let (avg, count) = rakuos_reviews::aggregate(&app_state.reviews);
    if count == 0 {
        return widget::text::caption("No reviews yet").into();
    }

    widget::row::with_children(vec![
        widget::text::body(stars_from_avg(avg)).into(),
        widget::text::caption(format!("({count})")).into(),
    ])
    .spacing(theme::active().cosmic().spacing.space_xxs)
    .align_y(Alignment::Center)
    .into()
}

#[derive(Clone)]
struct DisplayApp {
    name: String,
    summary: String,
    description: String,
    version: String,
    label: String,
    icon_path: String,
    icon_url: String,
    screenshots: Vec<String>,
    developer: String,
    license: String,
    url_homepage: String,
    url_bugtracker: String,
    url_donation: String,
    url_help: String,
    url_faq: String,
    url_vcs_browser: String,
    url_contribute: String,
}

fn display_app(app: &NativeApp, source: &SourceOption) -> DisplayApp {
    DisplayApp {
        name: app.name.clone(),
        summary: prefer(&source.summary, &app.summary),
        description: prefer(&source.description, &app.description),
        version: prefer(&source.version, &app.version),
        label: if source.label.is_empty() {
            source_label(&app.source)
        } else {
            source.label.clone()
        },
        icon_path: prefer(&source.icon_path, &app.icon_path),
        icon_url: prefer(&source.icon_url, &app.icon_url),
        screenshots: if source.screenshots.is_empty() {
            app.screenshots.clone()
        } else {
            source.screenshots.clone()
        },
        developer: prefer(&source.developer, &app.developer),
        license: prefer(&source.license, &app.license),
        url_homepage: first_nonempty(&[&source.url_homepage, &app.url_homepage]),
        url_bugtracker: first_nonempty(&[&source.url_bugtracker, &app.url_bugtracker]),
        url_donation: first_nonempty(&[&source.url_donation, &app.url_donation]),
        url_help: first_nonempty(&[&source.url_help, &app.url_help]),
        url_faq: first_nonempty(&[&source.url_faq, &app.url_faq]),
        url_vcs_browser: first_nonempty(&[&source.url_vcs_browser, &app.url_vcs_browser]),
        url_contribute: first_nonempty(&[&source.url_contribute, &app.url_contribute]),
    }
}

fn prefer(primary: &str, fallback: &str) -> String {
    if primary.is_empty() {
        fallback.to_string()
    } else {
        primary.to_string()
    }
}

fn first_nonempty(values: &[&String]) -> String {
    values
        .iter()
        .find(|value| !value.is_empty())
        .map(|value| (*value).clone())
        .unwrap_or_default()
}

fn detail_meta_row(app: DisplayApp) -> Element<'static, Message> {
    let spacing = theme::active().cosmic().spacing;
    let mut items: Vec<Element<_>> = vec![meta_block("Source", app.label)];
    if !app.version.is_empty() {
        items.push(meta_block("Version", app.version));
    }
    if !app.developer.is_empty() {
        items.push(meta_block("Developer", app.developer));
    }
    if !app.license.is_empty() {
        items.push(meta_block("License", app.license));
    }

    widget::column::with_children(vec![
        widget::divider::horizontal::default().into(),
        widget::row::with_children(items)
            .spacing(spacing.space_s)
            .align_y(Alignment::Center)
            .into(),
        widget::divider::horizontal::default().into(),
    ])
    .spacing(spacing.space_xxs)
    .into()
}

fn meta_block(label: &'static str, value: String) -> Element<'static, Message> {
    widget::column::with_children(vec![
        widget::text::heading(value).into(),
        widget::text::caption(label).into(),
    ])
    .align_x(Alignment::Center)
    .width(Length::Fill)
    .into()
}

fn header_links(app: &DisplayApp) -> Vec<Element<'static, Message>> {
    let mut links = Vec::new();
    push_link(&mut links, "Website", &app.url_homepage);
    push_link(&mut links, "Donate", &app.url_donation);
    push_link(&mut links, "Issues", &app.url_bugtracker);
    push_link(&mut links, "Help", &app.url_help);
    push_link(&mut links, "FAQ", &app.url_faq);
    push_link(&mut links, "Source", &app.url_vcs_browser);
    push_link(&mut links, "Contribute", &app.url_contribute);
    links
}

fn push_link<'a>(links: &mut Vec<Element<'a, Message>>, label: &'static str, url: &str) {
    if url.is_empty() {
        return;
    }
    let url = url.to_string();
    links.push(
        widget::button::link(label)
            .padding(0)
            .on_press(Message::LaunchUrl(url))
            .into(),
    );
}

fn reviews_section(app_state: &SoftwareApp) -> Element<'static, Message> {
    let spacing = theme::active().cosmic().spacing;
    let total = app_state.reviews.len();
    let total_pages = total.div_ceil(REVIEWS_PER_PAGE).max(1);
    let page = app_state.reviews_page.min(total_pages.saturating_sub(1));
    let start = page * REVIEWS_PER_PAGE;
    let end = (start + REVIEWS_PER_PAGE).min(total);

    let mut content: Vec<Element<_>> = Vec::new();
    content.push(
        widget::row::with_children(vec![
            widget::text::title4("Reviews").into(),
            widget::space::horizontal().width(Length::Fill).into(),
            widget::button::standard("Write Review")
                .on_press(Message::OpenReviewPopup)
                .into(),
        ])
        .align_y(Alignment::Center)
        .into(),
    );

    if app_state.reviews_loading {
        content.push(widget::text::caption("Loading reviews...").into());
    } else if total == 0 {
        content.push(widget::text::caption("No reviews yet.").into());
    } else {
        for review in &app_state.reviews[start..end] {
            content.push(review_card(review));
        }

        if total > REVIEWS_PER_PAGE {
            let previous: Element<_> = if page > 0 {
                widget::button::standard("Previous")
                    .on_press(Message::ReviewsPagePrev)
                    .into()
            } else {
                widget::button::standard("Previous").into()
            };
            let next: Element<_> = if page + 1 < total_pages {
                widget::button::standard("Next")
                    .on_press(Message::ReviewsPageNext)
                    .into()
            } else {
                widget::button::standard("Next").into()
            };

            content.push(
                widget::row::with_children(vec![
                    previous,
                    widget::text::caption(format!("Page {} of {}", page + 1, total_pages)).into(),
                    next,
                ])
                .spacing(spacing.space_s)
                .align_y(Alignment::Center)
                .into(),
            );
        }
    }

    widget::column::with_children(content)
        .spacing(spacing.space_s)
        .into()
}

fn review_card(review: &Review) -> Element<'static, Message> {
    let spacing = theme::active().cosmic().spacing;
    let stars = stars_from_rating(review.rating);
    let user = review
        .user_display
        .clone()
        .unwrap_or_else(|| "Anonymous".to_string());

    let mut rows: Vec<Element<_>> = vec![
        widget::row::with_children(vec![
            widget::text::caption(stars).into(),
            widget::text::caption(user).into(),
            widget::space::horizontal().width(Length::Fill).into(),
            widget::text::caption(review.version.clone()).into(),
        ])
        .spacing(spacing.space_xs)
        .align_y(Alignment::Center)
        .into(),
    ];

    if !review.summary.trim().is_empty() {
        rows.push(widget::text::heading(review.summary.clone()).into());
    }
    if !review.description.trim().is_empty() {
        rows.push(widget::text::body(review.description.clone()).into());
    }

    rows.push(
        widget::row::with_children(vec![
            widget::button::link(format!("Up {}", review.karma_up))
                .on_press(Message::VoteReview(review.review_id, true))
                .into(),
            widget::button::link(format!("Down {}", review.karma_down))
                .on_press(Message::VoteReview(review.review_id, false))
                .into(),
        ])
        .spacing(spacing.space_s)
        .into(),
    );

    widget::container(
        widget::column::with_children(rows)
            .spacing(spacing.space_xxs)
            .width(Length::Fill),
    )
    .padding(spacing.space_s)
    .class(theme::Container::Card)
    .width(Length::Fill)
    .into()
}

pub(crate) fn write_review_popup(app_state: &SoftwareApp) -> Element<'static, Message> {
    let spacing = theme::active().cosmic().spacing;
    let mut stars: Vec<Element<_>> = Vec::new();
    for rating in 1..=5 {
        let label = if rating <= app_state.review_rating {
            "★"
        } else {
            "☆"
        };
        stars.push(
            widget::button::link(label)
                .padding(0)
                .on_press(Message::ReviewRating(rating))
                .into(),
        );
    }

    let mut rows: Vec<Element<_>> = vec![
        widget::row::with_children(vec![
            widget::text::title4("Write a Review").into(),
            widget::space::horizontal().width(Length::Fill).into(),
            widget::button::icon(widget::icon::from_name("window-close-symbolic"))
                .on_press(Message::CloseReviewPopup)
                .into(),
        ])
        .align_y(Alignment::Center)
        .into(),
        widget::text::caption("Reviews are shared publicly with other Linux software centers through the Open Desktop Ratings Service.").into(),
        widget::text_input::text_input(
            "Optional — leave blank to post as Anonymous",
            app_state.review_name.clone(),
        )
        .on_input(Message::ReviewNameChanged)
        .into(),
        widget::row::with_children(stars)
            .spacing(spacing.space_xs)
            .align_y(Alignment::Center)
            .into(),
        widget::text_input::text_input("One-line summary", app_state.review_summary.clone())
            .on_input(Message::ReviewSummaryChanged)
            .into(),
        widget::text_input::text_input("Tell others what you think...", app_state.review_body.clone())
            .on_input(Message::ReviewBodyChanged)
            .into(),
    ];

    if !app_state.review_status.is_empty() {
        rows.push(widget::text::caption(app_state.review_status.clone()).into());
    }

    let submit: Element<_> = if app_state.review_submit_running {
        widget::button::suggested("Submitting...").into()
    } else {
        widget::button::suggested("Submit")
            .on_press(Message::SubmitReview)
            .into()
    };

    rows.push(
        widget::row::with_children(vec![
            widget::button::standard("Cancel")
                .on_press(Message::CloseReviewPopup)
                .into(),
            widget::space::horizontal().width(Length::Fill).into(),
            submit,
        ])
        .spacing(spacing.space_s)
        .align_y(Alignment::Center)
        .into(),
    );

    widget::container(
        widget::column::with_children(rows)
            .spacing(spacing.space_s)
            .width(Length::Fill),
    )
    .padding(spacing.space_m)
    .width(Length::Fixed(520.0))
    .class(theme::Container::Dialog)
    .into()
}

fn stars_from_avg(avg: f32) -> String {
    let full = (avg.round() as usize).clamp(0, 5);
    "★".repeat(full) + &"☆".repeat(5 - full)
}

fn stars_from_rating(rating: i32) -> String {
    let full = ((rating as f32 / 20.0).round() as usize).clamp(1, 5);
    "★".repeat(full) + &"☆".repeat(5 - full)
}

fn screenshot_carousel(
    screenshots: Vec<String>,
    selected_index: usize,
    width: usize,
) -> Element<'static, Message> {
    let spacing = theme::active().cosmic().spacing;
    let last_index = screenshots.len().saturating_sub(1);
    let index = selected_index.min(last_index);
    let shot = screenshots.get(index).cloned().unwrap_or_default();
    let image_handle = screenshot_image_handle(&shot);
    let image_height = if width < 800 { Length::Fixed(320.0) } else { Length::Fixed(420.0) };

    let previous: Element<_> = if index > 0 {
        widget::button::standard("‹")
            .on_press(Message::ScreenshotPrev)
            .into()
    } else {
        widget::button::standard("‹").into()
    };

    let next: Element<_> = if index < last_index {
        widget::button::standard("›")
            .on_press(Message::ScreenshotNext)
            .into()
    } else {
        widget::button::standard("›").into()
    };

    let mut dots = widget::row().spacing(spacing.space_xxs).align_y(Alignment::Center);
    for dot_index in 0..screenshots.len() {
        let label = if dot_index == index { "●" } else { "○" };
        dots = dots.push(
            widget::button::link(label)
                .padding(0)
                .on_press(Message::SelectScreenshot(dot_index)),
        );
    }

    let image_element: Element<_> = if let Some(handle) = image_handle {
        widget::container(widget::image(handle))
            .center_x(Length::Fill)
            .center_y(image_height)
            .into()
    } else {
        widget::space::horizontal()
            .width(Length::Fill)
            .height(image_height)
            .into()
    };

    widget::column::with_children(vec![
        widget::row::with_children(vec![
            previous,
            image_element,
            next,
        ])
        .spacing(spacing.space_s)
        .align_y(Alignment::Center)
        .into(),
        widget::container(dots)
            .width(Length::Fill)
            .center_x(Length::Fill)
            .into(),
    ])
    .spacing(spacing.space_xxs)
    .into()
}

fn screenshot_image_handle(value: &str) -> Option<widget::image::Handle> {
    let path = if value.starts_with("http://") || value.starts_with("https://") {
        cache_remote_screenshot(value)?
    } else {
        PathBuf::from(value)
    };

    if path.exists() {
        std::fs::read(path)
            .ok()
            .map(widget::image::Handle::from_bytes)
    } else {
        None
    }
}

fn cache_remote_screenshot(url: &str) -> Option<PathBuf> {
    let cache_dir = std::env::var_os("XDG_CACHE_HOME")
        .map(PathBuf::from)
        .or_else(|| std::env::var_os("HOME").map(|home| PathBuf::from(home).join(".cache")))
        .unwrap_or_else(|| PathBuf::from("/tmp"))
        .join("rakuos/software/screenshots");
    let _ = std::fs::create_dir_all(&cache_dir);

    let mut hasher = DefaultHasher::new();
    url.hash(&mut hasher);
    let extension = screenshot_extension(url);
    let path = cache_dir.join(format!("{:016x}.{}", hasher.finish(), extension));
    if path.exists() {
        return Some(path);
    }

    let output = Command::new("curl")
        .args(["-fsSL", "--max-time", "15", url])
        .output()
        .ok()?;
    if !output.status.success() || output.stdout.is_empty() {
        return None;
    }

    std::fs::write(&path, output.stdout).ok()?;
    if Path::new(&path).exists() {
        Some(path)
    } else {
        None
    }
}

fn screenshot_extension(url: &str) -> &'static str {
    let clean = url.split(['?', '#']).next().unwrap_or(url).to_lowercase();
    if clean.ends_with(".jpg") || clean.ends_with(".jpeg") {
        "jpg"
    } else if clean.ends_with(".webp") {
        "webp"
    } else {
        "png"
    }
}

fn clean_description(text: &str) -> String {
    let mut out = text
        .replace("<br>", "\n")
        .replace("<br/>", "\n")
        .replace("<br />", "\n")
        .replace("</p>", "\n\n");
    while let Some(start) = out.find('<') {
        let Some(end) = out[start..].find('>') else {
            break;
        };
        out.replace_range(start..=start + end, "");
    }
    out.replace("&nbsp;", " ")
        .replace("&amp;", "&")
        .replace("&lt;", "<")
        .replace("&gt;", ">")
        .replace("&quot;", "\"")
        .replace("&#39;", "'")
        .trim()
        .to_string()
}

pub(crate) fn addons_popup(app_state: &SoftwareApp) -> Element<'static, Message> {
    let spacing = theme::active().cosmic().spacing;
    let operation_running = app_state.operation_running;

    let title_row: Element<_> = widget::row::with_children(vec![
        widget::text::title3("Add-ons").into(),
        widget::space::horizontal().width(Length::Fill).into(),
        widget::button::icon(widget::icon::from_name("window-close-symbolic"))
            .on_press(Message::ToggleAddonsDialog)
            .into(),
    ])
    .align_y(Alignment::Center)
    .into();

    let mut addon_rows: Vec<Element<_>> = Vec::new();
    for addon in &app_state.addon_list {
        let name = addon["name"].as_str().unwrap_or("Unknown").to_string();
        let summary = addon["summary"].as_str().unwrap_or("").to_string();
        let installed = addon["installed"].as_bool().unwrap_or(false);
        let id = addon["id"].as_str().unwrap_or("").to_string();
        let src = addon["source"].as_str().unwrap_or("").to_string();

        let action_btn: Element<_> = if operation_running {
            widget::text::caption("Working...").into()
        } else if installed {
            widget::button::destructive("Remove")
                .on_press(Message::AddonOperation(OperationKind::Remove, id, src))
                .into()
        } else {
            widget::button::suggested("Install")
                .on_press(Message::AddonOperation(OperationKind::Install, id, src))
                .into()
        };

        addon_rows.push(
            widget::row::with_children(vec![
                widget::column::with_children(vec![
                    widget::text::body(name).into(),
                    widget::text::caption(summary).into(),
                ])
                .width(Length::Fill)
                .into(),
                action_btn,
            ])
            .align_y(Alignment::Center)
            .spacing(spacing.space_s)
            .into(),
        );
    }

    let close_row: Element<_> = widget::row::with_children(vec![
        widget::space::horizontal().width(Length::Fill).into(),
        widget::button::standard("Close")
            .on_press(Message::ToggleAddonsDialog)
            .into(),
    ])
    .into();

    widget::container(
        widget::column::with_children(vec![
            title_row,
            widget::scrollable(
                widget::column::with_children(addon_rows)
                    .spacing(spacing.space_s)
                    .width(Length::Fill),
            )
            .height(Length::Fill)
            .into(),
            close_row,
        ])
        .spacing(spacing.space_s)
        .width(Length::Fill),
    )
    .padding(spacing.space_m)
    .class(theme::Container::Dialog)
    .width(Length::Fixed(460.0))
    .height(Length::Fixed(500.0))
    .into()
}
