use dioxus::prelude::*;
#[cfg(test)]
use jacquard_common::types::{
    string::{AtUri, Datetime, Did, Handle},
    uri::UriValue,
};
use polymodel_api::space_polymodel::library;

#[cfg(test)]
const COMPLETE_PREVIEW_SVG: &str = "data:image/svg+xml,%3Csvg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 640 480\"%3E%3Crect width=\"640\" height=\"480\" fill=\"%23f4efe4\"/%3E%3Cg stroke=\"%232a6f97\" stroke-width=\"2\" fill=\"none\"%3E%3Cpath d=\"M64 96h512v288H64zM160 144h320v192H160zM256 192h128v96H256z\"/%3E%3Cpath d=\"M64 384 160 336M576 384 480 336M160 144 64 96M480 144 576 96\"/%3E%3C/g%3E%3Cg fill=\"%2366767f\" font-family=\"monospace\" font-size=\"24\"%3E%3Ctext x=\"72\" y=\"438\"%3Eparametric enclosure preview%3C/text%3E%3C/g%3E%3C/svg%3E";

#[derive(Clone, Debug, PartialEq)]
pub struct ThingCardMedia {
    pub url: String,
    pub alt: String,
}

#[cfg(test)]
pub fn thing_card_fixtures() -> Vec<library::ThingViewBasic> {
    vec![
        library::ThingViewBasic {
            author: actor(
                "did:plc:ari",
                "ari.model.tools",
                Some("Ari Chen"),
                Some("she/her"),
            ),
            cid: "bafyreicardcomplete".into(),
            cover: Some(vec![library::ImageView {
                alt: Some("Rendered preview of a parametric electronics enclosure".into()),
                aspect_ratio: None,
                fullsize: UriValue::new_owned(COMPLETE_PREVIEW_SVG).expect("fixture URI is valid"),
                thumb: UriValue::new_owned(COMPLETE_PREVIEW_SVG).expect("fixture URI is valid"),
                extra_data: None,
            }]),
            indexed_at: Datetime::try_from("2026-06-21T12:00:00Z".to_string())
                .expect("fixture datetime is valid"),
            like_count: 42,
            model_count: 3,
            name: "Parametric enclosure kit".into(),
            part_count: 18,
            previews: None,
            save_count: 12,
            tags: Some(vec![
                "enclosure".into(),
                "parametric".into(),
                "print-in-place".into(),
            ]),
            uri: AtUri::new_owned("at://did:plc:ari/space.polymodel.library.thing/complete")
                .expect("fixture AT URI is valid"),
            viewer: library::ViewerState {
                like: Some(
                    AtUri::new_owned("at://did:plc:viewer/space.polymodel.graph.like/one")
                        .expect("fixture AT URI is valid"),
                ),
                save: Some(
                    AtUri::new_owned("at://did:plc:viewer/space.polymodel.graph.save/one")
                        .expect("fixture AT URI is valid"),
                ),
                extra_data: None,
            },
            extra_data: None,
        },
        library::ThingViewBasic {
            author: actor("did:plc:mira", "mira.tools", None, None),
            cid: "bafyreicardsparse".into(),
            cover: None,
            indexed_at: Datetime::try_from("2026-06-21T12:05:00Z".to_string())
                .expect("fixture datetime is valid"),
            like_count: 0,
            model_count: 0,
            name: "Untitled thing".into(),
            part_count: 0,
            previews: None,
            save_count: 0,
            tags: None,
            uri: AtUri::new_owned("at://did:plc:mira/space.polymodel.library.thing/sparse")
                .expect("fixture AT URI is valid"),
            viewer: library::ViewerState::default(),
            extra_data: None,
        },
        library::ThingViewBasic {
            author: actor(
                "did:plc:foundry",
                "foundrylab.tools",
                Some("Foundry Lab"),
                Some("they/them"),
            ),
            cid: "bafyreicardmissingmedia".into(),
            cover: None,
            indexed_at: Datetime::try_from("2026-06-21T12:10:00Z".to_string())
                .expect("fixture datetime is valid"),
            like_count: 8,
            model_count: 1,
            name: "Modular calibration tower".into(),
            part_count: 6,
            previews: Some(vec![]),
            save_count: 5,
            tags: Some(vec!["calibration".into(), "fixture".into()]),
            uri: AtUri::new_owned(
                "at://did:plc:foundry/space.polymodel.library.thing/missing-media",
            )
            .expect("fixture AT URI is valid"),
            viewer: library::ViewerState {
                like: None,
                save: Some(
                    AtUri::new_owned("at://did:plc:viewer/space.polymodel.graph.save/two")
                        .expect("fixture AT URI is valid"),
                ),
                extra_data: None,
            },
            extra_data: None,
        },
    ]
}

#[allow(clippy::useless_format)]
#[component]
pub fn ThingCard(thing: library::ThingViewBasic, detail_href: Option<String>) -> Element {
    let title = thing_title(&thing);
    let creator = creator_label(&thing.author);
    let media = select_card_media(thing.cover.as_deref(), thing.previews.as_deref(), title);
    let stats = stats(&thing);
    let like_label = if thing.viewer.like.is_some() {
        "Liked"
    } else {
        "Like"
    };
    let save_label = if thing.viewer.save.is_some() {
        "Saved"
    } else {
        "Save"
    };

    rsx! {
        article { class: "thing-card card-surface", aria_label: "Thing card for {title}",
            div { class: "thing-card-media card-media",
                if let Some(media) = &media {
                    img { src: "{media.url}", alt: "{media.alt}" }
                } else {
                    div { class: "thing-card-placeholder blueprint-media", role: "img", aria_label: "Blueprint placeholder for missing preview media",
                        span { class: "status-pill status-muted", "No preview" }
                        span { class: "blueprint-axis blueprint-axis-x", "X" }
                        span { class: "blueprint-axis blueprint-axis-y", "Y" }
                        span { class: "blueprint-dimension", "preview pending" }
                    }
                }
            }
            div { class: "thing-card-body card-body",
                div { class: "thing-card-heading",
                    h2 { class: "card-title thing-card-title",
                        if let Some(href) = &detail_href {
                            a { class: "thing-card-link", href: "{href}", "{title}" }
                        } else {
                            "{title}"
                        }
                    }
                    p { class: "thing-card-creator", "by {creator}" }
                }
                if let Some(tags) = hydrated_tags(&thing) {
                    div { class: "thing-card-chips", aria_label: "Hydrated display tags",
                        for tag in tags.iter().take(3) {
                            span { class: "status-pill status-muted", "#{tag}" }
                        }
                    }
                }
                dl { class: "thing-card-stats", aria_label: "Thing stats",
                    for (label, value) in stats {
                        div {
                            dt { "{label}" }
                            dd { "{value}" }
                        }
                    }
                }
                div { class: "thing-card-actions", aria_label: "Thing card saved and liked state",
                    span { class: "thing-card-action", aria_label: like_label, "♡ {like_label}" }
                    span { class: "thing-card-action", aria_label: save_label, "☆ {save_label}" }
                }
            }
        }
    }
}

fn thing_title(thing: &library::ThingViewBasic) -> &str {
    clean_text(Some(thing.name.as_ref())).unwrap_or("Untitled thing")
}

fn clean_text(value: Option<&str>) -> Option<&str> {
    value.map(str::trim).filter(|value| !value.is_empty())
}

fn creator_label(actor: &library::Actor) -> &str {
    clean_text(actor.display_name.as_ref().map(AsRef::as_ref))
        .unwrap_or_else(|| actor.handle.as_ref())
}

fn hydrated_tags(thing: &library::ThingViewBasic) -> Option<&[jacquard_common::bos::DefaultStr]> {
    thing.tags.as_deref().filter(|tags| {
        tags.iter()
            .any(|tag| clean_text(Some(tag.as_ref())).is_some())
    })
}

fn stats(thing: &library::ThingViewBasic) -> [(&'static str, String); 4] {
    [
        ("Models", count_label(thing.model_count, "model")),
        ("Parts", count_label(thing.part_count, "part")),
        ("Likes", count_label(thing.like_count, "like")),
        ("Saves", count_label(thing.save_count, "save")),
    ]
}

fn count_label(value: i64, noun: &str) -> String {
    let suffix = if value == 1 { "" } else { "s" };
    format!("{value} {noun}{suffix}")
}

fn select_card_media(
    cover: Option<&[library::ImageView]>,
    previews: Option<&[library::ImageView]>,
    title: &str,
) -> Option<ThingCardMedia> {
    first_usable_image(cover, title).or_else(|| first_usable_image(previews, title))
}

fn first_usable_image(
    images: Option<&[library::ImageView]>,
    title: &str,
) -> Option<ThingCardMedia> {
    images?.iter().find_map(|image| {
        let url = image.thumb.as_ref().to_string();
        if !url.starts_with("http://")
            && !url.starts_with("https://")
            && !url.starts_with('/')
            && !url.starts_with("data:image/")
        {
            return None;
        }
        let alt = clean_text(image.alt.as_ref().map(AsRef::as_ref))
            .map(ToString::to_string)
            .unwrap_or_else(|| format!("Preview image for {title}"));
        Some(ThingCardMedia { url, alt })
    })
}

#[cfg(test)]
fn actor(
    did: &str,
    handle: &str,
    display_name: Option<&str>,
    pronouns: Option<&str>,
) -> library::Actor {
    library::Actor {
        avatar: None,
        description: None,
        did: Did::new_owned(did).expect("fixture DID is valid"),
        display_name: display_name.map(Into::into),
        follower_count: None,
        following_count: None,
        handle: Handle::new_owned(handle).expect("fixture handle is valid"),
        pronouns: pronouns.map(Into::into),
        thing_count: None,
        viewer: None,
        extra_data: None,
    }
}

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

    fn basic_view(overrides: impl FnOnce(&mut library::ThingViewBasic)) -> library::ThingViewBasic {
        let mut view = library::ThingViewBasic {
            author: actor(
                "did:plc:maker",
                "maker.tools",
                Some("Maker Name"),
                Some("he/him"),
            ),
            cid: "bafyreitest".into(),
            cover: Some(vec![library::ImageView {
                alt: Some("Cover alt".into()),
                aspect_ratio: None,
                fullsize: UriValue::new_owned(COMPLETE_PREVIEW_SVG).expect("fixture URI is valid"),
                thumb: UriValue::new_owned(COMPLETE_PREVIEW_SVG).expect("fixture URI is valid"),
                extra_data: None,
            }]),
            indexed_at: Datetime::try_from("2026-06-21T12:00:00Z".to_string())
                .expect("fixture datetime is valid"),
            like_count: 2,
            model_count: 1,
            name: "Named thing".into(),
            part_count: 4,
            previews: Some(vec![library::ImageView {
                alt: Some("Preview alt".into()),
                aspect_ratio: None,
                fullsize: UriValue::new_owned("/assets/preview.jpg").expect("fixture URI is valid"),
                thumb: UriValue::new_owned("/assets/preview.jpg").expect("fixture URI is valid"),
                extra_data: None,
            }]),
            save_count: 3,
            tags: Some(vec!["hydrated".into()]),
            uri: AtUri::new_owned("at://did:plc:maker/space.polymodel.library.thing/abc")
                .expect("fixture AT URI is valid"),
            viewer: library::ViewerState {
                like: Some(
                    AtUri::new_owned("at://did:plc:viewer/space.polymodel.graph.like/one")
                        .expect("fixture AT URI is valid"),
                ),
                save: None,
                extra_data: None,
            },
            extra_data: None,
        };
        overrides(&mut view);
        view
    }

    #[test]
    fn basic_view_contract_has_required_author_handle_and_counts() {
        let view = basic_view(|_| {});
        assert_eq!(view.author.handle.as_ref(), "maker.tools");
        assert_eq!(view.name.as_str(), "Named thing");
        assert_eq!(view.model_count, 1);
        assert_eq!(view.part_count, 4);
        assert_eq!(view.like_count, 2);
        assert_eq!(view.save_count, 3);
        assert!(view.viewer.like.is_some());
    }

    #[test]
    fn presentation_prefers_display_name_and_cover_media() {
        let view = basic_view(|_| {});
        let media = select_card_media(
            view.cover.as_deref(),
            view.previews.as_deref(),
            thing_title(&view),
        )
        .unwrap();
        assert_eq!(creator_label(&view.author), "Maker Name");
        assert_eq!(media.url, COMPLETE_PREVIEW_SVG);
        assert_eq!(media.alt, "Cover alt");
    }

    #[test]
    fn presentation_uses_required_handle_when_display_name_missing() {
        let view = basic_view(|view| {
            view.author.display_name = None;
            view.name = "  ".into();
            view.cover = None;
            view.previews = None;
        });

        assert_eq!(thing_title(&view), "Untitled thing");
        assert_eq!(creator_label(&view.author), "maker.tools");
        assert!(
            select_card_media(
                view.cover.as_deref(),
                view.previews.as_deref(),
                thing_title(&view)
            )
            .is_none()
        );
    }

    #[test]
    fn media_selection_skips_unusable_urls_and_falls_back_alt() {
        let view = basic_view(|view| {
            view.cover = Some(vec![library::ImageView {
                alt: Some("bad".into()),
                aspect_ratio: None,
                fullsize: UriValue::new_owned("notaurl").expect("fixture URI is accepted as Any"),
                thumb: UriValue::new_owned("notaurl").expect("fixture URI is accepted as Any"),
                extra_data: None,
            }]);
            view.previews = Some(vec![library::ImageView {
                alt: None,
                aspect_ratio: None,
                fullsize: UriValue::new_owned("https://cdn.example/preview.jpg")
                    .expect("fixture URI is valid"),
                thumb: UriValue::new_owned("https://cdn.example/preview.jpg")
                    .expect("fixture URI is valid"),
                extra_data: None,
            }]);
            view.name = "Fallback media".into();
        });

        let media = select_card_media(
            view.cover.as_deref(),
            view.previews.as_deref(),
            thing_title(&view),
        )
        .unwrap();
        assert_eq!(media.url, "https://cdn.example/preview.jpg");
        assert_eq!(media.alt, "Preview image for Fallback media");
    }

    #[test]
    fn stats_are_subordinate_display_labels() {
        let view = basic_view(|view| {
            view.model_count = 1;
            view.part_count = 2;
            view.like_count = 0;
            view.save_count = 1;
        });

        assert_eq!(
            stats(&view),
            [
                ("Models", "1 model".to_string()),
                ("Parts", "2 parts".to_string()),
                ("Likes", "0 likes".to_string()),
                ("Saves", "1 save".to_string())
            ]
        );
    }

    #[test]
    fn hydrated_tags_are_view_metadata() {
        let view = basic_view(|view| view.tags = Some(vec!["external".into(), "community".into()]));
        let tags = hydrated_tags(&view).unwrap();
        assert_eq!(tags[0].as_str(), "external");
        assert_eq!(tags[1].as_str(), "community");
    }
}
