use dioxus::prelude::*;
use jacquard::common::types::ident::AtIdentifier;
use jacquard::common::types::recordkey::Rkey;
use jacquard::common::types::string::AtUri;
use jacquard::common::types::value::from_data;
use jacquard_common::xrpc::XrpcClient;
use polymodel_api::space_polymodel::library;

use crate::Route;
use crate::client::PolymodelClient;
use crate::profile::profile_href;
use crate::viewer::{AssetViewer, ViewerFallbackImage, demo_models};

#[derive(Clone, Debug, PartialEq, Eq)]
enum DetailState {
    Loading,
    InvalidRoute(String),
    Error(String),
    Populated(Box<ThingDetailData>),
}

#[derive(Clone, Debug, PartialEq, Eq)]
struct ThingDetailData {
    thing: library::ThingView,
    thing_record: library::thing::Thing,
    models: Vec<ModelDetailSummary>,
    selected_model: Option<SelectedModelDetail>,
}

#[derive(Clone, Debug, PartialEq, Eq)]
struct ModelDetailSummary {
    view: library::ModelView,
    record: library::model::Model,
}

#[derive(Clone, Debug, PartialEq, Eq)]
struct SelectedModelDetail {
    model: library::ModelView,
    record: library::model::Model,
    parts: Vec<PartDetail>,
}

#[derive(Clone, Debug, PartialEq, Eq)]
struct PartDetail {
    view: library::PartView,
    record: library::part::Part,
}

fn detail_state(pending: bool, result: Option<&Result<ThingDetailData, String>>) -> DetailState {
    if pending && result.is_none() {
        return DetailState::Loading;
    }

    match result {
        Some(Ok(data)) => DetailState::Populated(Box::new(data.clone())),
        Some(Err(error)) if error.starts_with("Invalid route:") => {
            DetailState::InvalidRoute(error.clone())
        }
        Some(Err(error)) => DetailState::Error(error.clone()),
        None => DetailState::Loading,
    }
}

pub(crate) fn thing_detail_href(uri: &AtUri) -> Option<String> {
    let path = uri.path()?;
    if path.collection.as_str() != "space.polymodel.library.thing" {
        return None;
    }
    let repo = uri.authority();
    let rkey = path.rkey?;
    Some(format!("/{}/thing/{}", repo.as_str(), rkey.as_ref()))
}

fn thing_uri_from_route(repo: &str, rkey: &str) -> Result<AtUri, String> {
    let repo: AtIdentifier<String> = AtIdentifier::new_owned(repo)
        .map_err(|error| format!("Invalid route: unsupported repository identifier: {error}"))?;
    let rkey: Rkey<String> = Rkey::new_owned(rkey)
        .map_err(|error| format!("Invalid route: unsupported record key: {error}"))?;
    AtUri::new_owned(format!(
        "at://{}/space.polymodel.library.thing/{}",
        repo.as_str(),
        rkey.as_str()
    ))
    .map_err(|error| format!("Invalid route: could not build thing URI: {error}"))
}

fn thing_record(view: &library::ThingView) -> Result<library::thing::Thing, String> {
    from_data(&view.record).map_err(|error| format!("Thing record decode failed: {error}"))
}

fn model_record(view: &library::ModelView) -> Result<library::model::Model, String> {
    from_data(&view.record).map_err(|error| format!("Model record decode failed: {error}"))
}

fn part_record(view: &library::PartView) -> Result<library::part::Part, String> {
    from_data(&view.record).map_err(|error| format!("Part record decode failed: {error}"))
}

async fn load_thing_detail(
    client: PolymodelClient,
    route_uri: Result<AtUri, String>,
    selected_model_uri: Option<AtUri>,
) -> Result<ThingDetailData, String> {
    let uri = route_uri?;
    let thing_response = client
        .send(library::get_thing::GetThing::new().uri(uri).build())
        .await
        .map_err(|error| format!("Thing request failed: {error}"))?;
    let output = thing_response
        .into_output()
        .map_err(|error| format!("Thing decode failed: {error}"))?;
    let thing_record = thing_record(&output.thing)?;
    let models = output
        .models
        .unwrap_or_default()
        .into_iter()
        .map(|view| model_record(&view).map(|record| ModelDetailSummary { view, record }))
        .collect::<Result<Vec<_>, _>>()?;
    let selected_uri =
        selected_model_uri.or_else(|| models.first().map(|model| model.view.uri.clone()));
    let selected_model = if let Some(uri) = selected_uri {
        let response = client
            .send(library::get_model::GetModel::new().uri(uri).build())
            .await
            .map_err(|error| format!("Model request failed: {error}"))?;
        let output = response
            .into_output()
            .map_err(|error| format!("Model decode failed: {error}"))?;
        let record = model_record(&output.model)?;
        let parts = output
            .parts
            .unwrap_or_default()
            .into_iter()
            .map(|view| part_record(&view).map(|record| PartDetail { view, record }))
            .collect::<Result<Vec<_>, _>>()?;
        Some(SelectedModelDetail {
            model: output.model,
            record,
            parts,
        })
    } else {
        None
    };

    Ok(ThingDetailData {
        thing: output.thing,
        thing_record,
        models,
        selected_model,
    })
}

fn file_size_label(size: i64) -> String {
    if size >= 1_000_000 {
        format!("{:.1} MB", size as f64 / 1_000_000.0)
    } else if size >= 1_000 {
        format!("{:.0} KB", size as f64 / 1_000.0)
    } else {
        format!("{size} B")
    }
}

#[allow(clippy::useless_format)]
#[component]
pub(crate) fn ThingDetail(repo: String, rkey: String) -> Element {
    let client = use_context::<PolymodelClient>();
    let selected_model_uri = use_signal(|| None::<AtUri>);
    let route_uri = thing_uri_from_route(&repo, &rkey);

    let detail = use_resource(move || {
        let client = client.clone();
        let route_uri = route_uri.clone();
        let selected_model_uri = selected_model_uri.read().clone();
        async move { load_thing_detail(client, route_uri, selected_model_uri).await }
    });

    let detail_result = detail.read();
    let state = detail_state(detail.pending(), detail_result.as_ref());

    rsx! {
        main { class: "thing-detail-page product-shell",
            nav { class: "detail-nav", aria_label: "Thing detail navigation",
                Link { to: Route::Browse {}, "← Back to browse" }
            }
            match state {
                DetailState::Loading => rsx! { DetailLoading {} },
                DetailState::InvalidRoute(message) => rsx! { DetailError { title: "Thing route unavailable".to_string(), message } },
                DetailState::Error(message) => rsx! { DetailError { title: "Thing unavailable".to_string(), message } },
                DetailState::Populated(data) => rsx! { DetailPopulated { data: *data, selected_model_uri } },
            }
        }
    }
}

#[allow(clippy::useless_format)]
#[component]
fn DetailLoading() -> Element {
    rsx! {
        section { class: "thing-detail-shell blueprint-panel", aria_label: "Thing detail loading",
            div { class: "state-card loading-state",
                span { class: "status-pill", "Loading" }
                div { class: "skeleton-line skeleton-wide" }
                div { class: "skeleton-line" }
                div { class: "skeleton-box" }
            }
        }
    }
}

#[allow(clippy::useless_format)]
#[component]
fn DetailError(title: String, message: String) -> Element {
    rsx! {
        section { class: "thing-detail-shell blueprint-panel", aria_label: "Thing detail error",
            div { class: "state-card error-state",
                span { class: "status-pill status-error", "Error" }
                h1 { "{title}" }
                p { "{message}" }
                Link { class: "button button-secondary", to: Route::Browse {}, "Back to browse" }
            }
        }
    }
}

#[allow(clippy::useless_format)]
#[component]
fn DetailPopulated(
    data: ThingDetailData,
    mut selected_model_uri: Signal<Option<AtUri>>,
) -> Element {
    let ThingDetailData {
        thing,
        thing_record,
        models,
        selected_model,
    } = data;
    let title = clean_text(Some(thing.name.as_ref()))
        .unwrap_or("Untitled thing")
        .to_string();
    let creator = actor_label(&thing.author).to_string();
    let summary = clean_text(thing.summary.as_ref().map(AsRef::as_ref))
        .or_else(|| clean_text(thing_record.summary.as_ref().map(AsRef::as_ref)))
        .map(ToString::to_string);
    let media = select_detail_media(thing.cover.as_deref(), thing.previews.as_deref(), &title);
    let selected_uri = selected_model
        .as_ref()
        .map(|selected| selected.model.uri.clone());
    let tags = thing
        .tags
        .as_deref()
        .or(thing_record.tags.as_deref())
        .unwrap_or_default()
        .iter()
        .filter_map(|tag| clean_text(Some(tag.as_ref())).map(ToString::to_string))
        .collect::<Vec<_>>();
    let license = thing.license.to_string();
    let model_count = count_label(thing.model_count, "model");
    let part_count = count_label(thing.part_count, "part");
    let like_count = count_label(thing.like_count, "like");
    let save_count = count_label(thing.save_count, "save");
    let instructions = thing_record.instructions;

    rsx! {
        header { class: "thing-detail-header",
            div { class: "product-kicker", "Thing detail" }
            div { class: "thing-detail-title-row",
                div {
                    h1 { "{title}" }
                    p { class: "thing-detail-creator",
                        if let Some(href) = profile_href(&thing.author) {
                            a { class: "thing-detail-creator-link", href: "{href}", "by {creator}" }
                        } else {
                            "by {creator}"
                        }
                    }
                }
                dl { class: "thing-detail-stats", aria_label: "Thing stats",
                    StatItem { label: "Models".to_string(), value: model_count }
                    StatItem { label: "Parts".to_string(), value: part_count }
                    StatItem { label: "Likes".to_string(), value: like_count }
                    StatItem { label: "Saves".to_string(), value: save_count }
                }
            }
        }

        section { class: "thing-detail-shell blueprint-panel", aria_label: "Thing overview",
            div { class: "thing-detail-viewer",
                AssetViewer { initial_model: demo_models()[0], fallback: media.map(|media| ViewerFallbackImage { url: media.url, alt: media.alt }) }
            }

            div { class: "thing-detail-summary",
                div { class: "thing-detail-copy",
                    if let Some(summary) = summary {
                        p { class: "product-lede", "{summary}" }
                    } else {
                        p { class: "product-lede", "No summary is published for this thing yet." }
                    }
                    div { class: "thing-detail-chips", aria_label: "Thing tags and license",
                        span { class: "status-pill", "{license}" }
                        for tag in tags {
                            span { class: "status-pill status-muted", "#{tag}" }
                        }
                    }
                }

                div { class: "thing-detail-actions", aria_label: "Thing actions",
                    button { class: "button button-primary", "Download files" }
                    button { class: "button button-secondary", disabled: true, aria_label: "Save disabled until sign in is available", "Save" }
                }
            }
        }

        section { class: "thing-detail-grid", aria_label: "Thing detail sections",
            DetailPanel { title: "Overview".to_string(),
                InstructionList { instructions, empty: "No thing-level instructions are published yet.".to_string() }
            }

            DetailPanel { title: "Models".to_string(),
                if models.is_empty() {
                    p { class: "detail-muted", "This thing does not publish any model variants yet." }
                } else {
                    div { class: "model-list", role: "list", aria_label: "Model variants",
                        for model in models {
                            {
                                let row_selected = selected_uri.as_ref() == Some(&model.view.uri);
                                let uri_for_select = model.view.uri.clone();
                                rsx! {
                            ModelRow {
                                key: "{model.view.uri}",
                                model,
                                selected: row_selected,
                                onselect: move |_| selected_model_uri.set(Some(uri_for_select.clone()))
                            }
                                }
                            }
                        }
                    }
                }
            }

            if let Some(selected) = selected_model {
                {
                    let SelectedModelDetail { model, record, parts } = selected;
                    rsx! {
                article { class: "detail-panel model-detail-panel card-surface",
                    SelectedModelPanel { model, record }
                }
                DetailPanel { title: "Parts & files".to_string(),
                    PartsPanel { parts }
                }
                    }
                }
            } else {
                DetailPanel { title: "Parts & files".to_string(),
                    p { class: "detail-muted", "Select a model to load its ordered parts and files." }
                }
            }
        }
    }
}

#[allow(clippy::useless_format)]
#[component]
fn DetailPanel(title: String, children: Element) -> Element {
    rsx! {
        article { class: "detail-panel card-surface",
            div { class: "detail-panel-heading",
                h2 { "{title}" }
            }
            {children}
        }
    }
}

#[allow(clippy::useless_format)]
#[component]
fn StatItem(label: String, value: String) -> Element {
    rsx! {
        div {
            dt { "{label}" }
            dd { "{value}" }
        }
    }
}

#[allow(clippy::useless_format)]
#[component]
fn InstructionList(
    instructions: Option<Vec<jacquard_common::DefaultStr>>,
    empty: String,
) -> Element {
    let instructions = instructions.unwrap_or_default();
    let items = instructions
        .iter()
        .filter_map(|item| clean_text(Some(item.as_ref())).map(ToString::to_string))
        .collect::<Vec<_>>();

    rsx! {
        if items.is_empty() {
            p { class: "detail-muted", "{empty}" }
        } else {
            ol { class: "instruction-list",
                for (index, item) in items.iter().enumerate() {
                    li { key: "instruction-{index}", "{item}" }
                }
            }
        }
    }
}

#[allow(clippy::useless_format)]
#[component]
fn ModelRow(
    model: ModelDetailSummary,
    selected: bool,
    onselect: EventHandler<MouseEvent>,
) -> Element {
    let name = clean_text(Some(model.view.name.as_ref()))
        .unwrap_or("Untitled model")
        .to_string();
    let summary = clean_text(model.view.summary.as_ref().map(AsRef::as_ref))
        .or_else(|| clean_text(model.record.summary.as_ref().map(AsRef::as_ref)))
        .map(ToString::to_string);
    let class = if selected {
        "model-row is-selected"
    } else {
        "model-row"
    };
    let license = model
        .record
        .license
        .as_ref()
        .map(AsRef::as_ref)
        .and_then(|value| clean_text(Some(value)))
        .unwrap_or("inherits thing license")
        .to_string();

    rsx! {
        button { class, role: "listitem", onclick: move |event| onselect.call(event),
            span { class: "model-row-title", "{name}" }
            span { class: "model-row-meta", "{model.view.part_count} parts · {license}" }
            if let Some(summary) = summary {
                span { class: "model-row-summary", "{summary}" }
            }
        }
    }
}

#[allow(clippy::useless_format)]
#[component]
fn SelectedModelPanel(model: library::ModelView, record: library::model::Model) -> Element {
    let name = clean_text(Some(model.name.as_ref()))
        .unwrap_or("Untitled model")
        .to_string();
    let summary = clean_text(model.summary.as_ref().map(AsRef::as_ref))
        .or_else(|| clean_text(record.summary.as_ref().map(AsRef::as_ref)))
        .map(ToString::to_string);
    let instructions = record.instructions;
    let tags = record
        .tags
        .as_deref()
        .unwrap_or_default()
        .iter()
        .filter_map(|tag| clean_text(Some(tag.as_ref())).map(ToString::to_string))
        .collect::<Vec<_>>();

    rsx! {
        div { class: "selected-model-panel",
            div { class: "selected-model-heading",
                span { class: "product-kicker", "Current model" }
                h2 { "{name}" }
            }
            if let Some(summary) = summary {
                p { "{summary}" }
            }
            InstructionList { instructions, empty: "No model-specific instructions are published yet.".to_string() }
            if !tags.is_empty() {
                div { class: "thing-detail-chips", aria_label: "Selected model tags",
                    for tag in tags {
                        span { class: "status-pill status-muted", "#{tag}" }
                    }
                }
            }
        }
    }
}

#[allow(clippy::useless_format)]
#[component]
fn PartsPanel(parts: Vec<PartDetail>) -> Element {
    rsx! {
        if parts.is_empty() {
            p { class: "detail-muted", "This model does not publish any ordered part files yet." }
        } else {
            div { class: "parts-list", role: "list", aria_label: "Ordered part files",
                for (index, part) in parts.iter().enumerate() {
                    PartRow { key: "{part.view.uri}", position: index + 1, part: part.clone() }
                }
            }
        }
    }
}

#[allow(clippy::useless_format)]
#[component]
fn PartRow(position: usize, part: PartDetail) -> Element {
    let name = clean_text(Some(part.view.name.as_ref()))
        .unwrap_or("Untitled part")
        .to_string();
    let format = part
        .view
        .format
        .as_ref()
        .or(part.record.format.as_ref())
        .map(AsRef::as_ref)
        .and_then(|value| clean_text(Some(value)))
        .unwrap_or("file")
        .to_string();
    let size = file_size_label(part.view.file.size);
    let dimensions = part.record.dimensions.as_ref().map(dimensions_label);
    let notes = clean_text(part.record.notes.as_ref().map(AsRef::as_ref)).map(ToString::to_string);
    let print_settings = part
        .record
        .print_settings
        .as_deref()
        .unwrap_or_default()
        .iter()
        .filter_map(|setting| clean_text(Some(setting.as_ref())).map(ToString::to_string))
        .collect::<Vec<_>>();

    rsx! {
        article { class: "part-row", role: "listitem",
            div { class: "part-row-index", aria_label: "Part position", "{position}" }
            div { class: "part-row-body",
                h3 { "{name}" }
                dl { class: "part-row-meta", aria_label: "Part file metadata",
                    StatItem { label: "Format".to_string(), value: format }
                    StatItem { label: "Size".to_string(), value: size }
                    if let Some(dimensions) = dimensions {
                        StatItem { label: "Dimensions".to_string(), value: dimensions }
                    }
                }
                if let Some(notes) = notes {
                    p { class: "part-notes", "{notes}" }
                }
                if !print_settings.is_empty() {
                    ul { class: "print-settings", aria_label: "Print settings",
                        for setting in print_settings {
                            li { "{setting}" }
                        }
                    }
                }
            }
        }
    }
}

#[allow(clippy::useless_format)]
#[component]
fn BlueprintPlaceholder(label: String, detail: String) -> Element {
    rsx! {
        div { class: "thing-card-placeholder blueprint-media", role: "img", aria_label: "Blueprint placeholder for missing preview media",
            span { class: "status-pill status-muted", "{label}" }
            span { class: "blueprint-axis blueprint-axis-x", "X" }
            span { class: "blueprint-axis blueprint-axis-y", "Y" }
            span { class: "blueprint-dimension", "{detail}" }
        }
    }
}

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

fn select_detail_media(
    cover: Option<&[library::ImageView]>,
    previews: Option<&[library::ImageView]>,
    title: &str,
) -> Option<DetailMedia> {
    first_detail_image(cover, title).or_else(|| first_detail_image(previews, title))
}

fn first_detail_image(images: Option<&[library::ImageView]>, title: &str) -> Option<DetailMedia> {
    images?.iter().find_map(|image| {
        let url = image.fullsize.as_ref().to_string();
        if !usable_image_url(&url) {
            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(DetailMedia { url, alt })
    })
}

fn usable_image_url(url: &str) -> bool {
    url.starts_with("http://")
        || url.starts_with("https://")
        || url.starts_with('/')
        || url.starts_with("data:image/")
}

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

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

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

fn dimensions_label(dimensions: &library::Bbox) -> String {
    let unit = dimensions
        .unit
        .as_ref()
        .map(AsRef::as_ref)
        .and_then(|value| clean_text(Some(value)))
        .unwrap_or("mm");
    let x: &str = dimensions.x.as_ref();
    let y: &str = dimensions.y.as_ref();
    let z: &str = dimensions.z.as_ref();
    format!("{x} × {y} × {z} {unit}")
}
