use dioxus::prelude::*;
use jacquard_common::types::string::Cid;
#[cfg(test)]
use jacquard_common::types::{
    string::{AtUri, Datetime, Did, Handle},
    uri::UriValue,
};
use jacquard_common::xrpc::XrpcClient;
use polymodel_api::com_atproto::repo::strong_ref::StrongRef;
use polymodel_api::space_polymodel::graph::{
    create_like::CreateLike, create_save::CreateSave, delete_like::DeleteLike,
    delete_save::DeleteSave,
};
use polymodel_api::space_polymodel::library;

use crate::client::PolymodelClient;
use crate::session::SessionIdentity;

#[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>,
    #[props(default)] creator_href: Option<String>,
) -> Element {
    let client = use_context::<PolymodelClient>();
    let session = use_context::<Signal<SessionIdentity>>();
    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 mut like_uri = use_signal(|| thing.viewer.like.clone());
    let mut save_uri = use_signal(|| thing.viewer.save.clone());
    let mut like_count = use_signal(|| thing.like_count);
    let mut save_count = use_signal(|| thing.save_count);
    let mut action_pending = use_signal(|| false);
    let mut action_error = use_signal(|| None::<String>);
    let like_label = if like_uri.read().is_some() {
        "Liked"
    } else {
        "Like"
    };
    let save_label = if save_uri.read().is_some() {
        "Saved"
    } else {
        "Save"
    };
    let can_write = matches!(&*session.read(), SessionIdentity::Authenticated(_));
    let subject = StrongRef {
        uri: thing.uri.clone(),
        cid: Cid::new_owned(thing.cid.as_str().as_bytes()).expect("thing cid is server-validated"),
        extra_data: None,
    };
    let like_client = client.clone();
    let like_subject = subject.clone();
    let save_client = client.clone();
    let save_subject = subject.clone();

    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: "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",
                        if let Some(href) = &creator_href {
                            a { class: "thing-card-creator-link", href: "{href}", "by {creator}" }
                        } else {
                            "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_with_social_counts(&stats, *like_count.read(), *save_count.read()) {
                        div {
                            dt { "{label}" }
                            dd { "{value}" }
                        }
                    }
                }
                div { class: "thing-card-actions", aria_label: "Thing card saved and liked state",
                    if can_write {
                        button {
                            class: "thing-card-action",
                            disabled: *action_pending.read(),
                            aria_pressed: "{like_uri.read().is_some()}",
                            aria_label: "{like_label} {title}",
                            onclick: move |_| {
                                let client = like_client.clone();
                                let subject = like_subject.clone();
                                let current = like_uri.read().clone();
                                action_pending.set(true);
                                action_error.set(None);
                                spawn(async move {
                                    let result = if let Some(uri) = current {
                                        match client.send(DeleteLike { uri: Some(uri), subject: None, extra_data: None }).await {
                                            Ok(response) => response.into_output().map(|_| None).map_err(|error| error.to_string()),
                                            Err(error) => Err(error.to_string()),
                                        }
                                    } else {
                                        match client.send(CreateLike { subject, extra_data: None }).await {
                                            Ok(response) => response.into_output().map(|output| Some(output.uri)).map_err(|error| error.to_string()),
                                            Err(error) => Err(error.to_string()),
                                        }
                                    };
                                    match result {
                                        Ok(next) => {
                                            let was_liked = like_uri.read().is_some();
                                            like_uri.set(next);
                                            let current_count = *like_count.read();
                                            let is_liked = like_uri.read().is_some();
                                            like_count.set(adjust_count(current_count, was_liked, is_liked));
                                        }
                                        Err(error) => action_error.set(Some(format!("Like failed: {error}"))),
                                    }
                                    action_pending.set(false);
                                });
                            },
                            "♡ {like_label}"
                        }
                        button {
                            class: "thing-card-action",
                            disabled: *action_pending.read(),
                            aria_pressed: "{save_uri.read().is_some()}",
                            aria_label: "{save_label} {title}",
                            onclick: move |_| {
                                let client = save_client.clone();
                                let subject = save_subject.clone();
                                let current = save_uri.read().clone();
                                action_pending.set(true);
                                action_error.set(None);
                                spawn(async move {
                                    let result = if let Some(uri) = current {
                                        match client.send(DeleteSave { uri: Some(uri), subject: None, extra_data: None }).await {
                                            Ok(response) => response.into_output().map(|_| None).map_err(|error| error.to_string()),
                                            Err(error) => Err(error.to_string()),
                                        }
                                    } else {
                                        match client.send(CreateSave { subject, note: None, extra_data: None }).await {
                                            Ok(response) => response.into_output().map(|output| Some(output.uri)).map_err(|error| error.to_string()),
                                            Err(error) => Err(error.to_string()),
                                        }
                                    };
                                    match result {
                                        Ok(next) => {
                                            let was_saved = save_uri.read().is_some();
                                            save_uri.set(next);
                                            let current_count = *save_count.read();
                                            let is_saved = save_uri.read().is_some();
                                            save_count.set(adjust_count(current_count, was_saved, is_saved));
                                        }
                                        Err(error) => action_error.set(Some(format!("Save failed: {error}"))),
                                    }
                                    action_pending.set(false);
                                });
                            },
                            "☆ {save_label}"
                        }
                    } else {
                        a { class: "thing-card-action", href: "/oauth/start", aria_label: "Sign in to like {title}", "♡ Like" }
                        a { class: "thing-card-action", href: "/oauth/start", aria_label: "Sign in to save {title}", "☆ Save" }
                    }
                    if let Some(error) = action_error.read().as_ref() {
                        span { class: "thing-card-action-error", role: "status", "{error}" }
                    }
                }
            }
        }
    }
}

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 stats_with_social_counts(
    stats: &[(&'static str, String); 4],
    like_count: i64,
    save_count: i64,
) -> [(&'static str, String); 4] {
    [
        stats[0].clone(),
        stats[1].clone(),
        ("Likes", count_label(like_count, "like")),
        ("Saves", count_label(save_count, "save")),
    ]
}

fn adjust_count(current: i64, was_active: bool, is_active: bool) -> i64 {
    match (was_active, is_active) {
        (false, true) => current + 1,
        (true, false) => current.saturating_sub(1),
        _ => current,
    }
}

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");
    }
}
