//! Deterministic local/demo sample data for appview read paths.
//!
//! This module writes projection-shaped SQLite rows only when explicitly enabled
//! and the local/demo database has no Polymodel things. It lets e2e and local
//! demo runs exercise the same `getFeed`, `getThing`, and `getModel` appview
//! contracts as production reads without UI-only fixtures.

use jacquard_common::types::blob::BlobRef;
use polymodel_api::space_polymodel::{
    actor::profile::Profile,
    library::{File, model::Model, part::Part, thing::Thing},
};
use serde::{Serialize, de::DeserializeOwned};
use serde_json::{Value, json};
use sqlx::{SqliteConnection, SqlitePool};

pub const SAMPLE_DID: &str = "did:plc:aaaaaaaaaaaaaaaaaaaaaaaa";
pub const SAMPLE_HANDLE: &str = "ari.model.tools";
pub const SAMPLE_THING_RKEY: &str = "parametric-enclosure";
pub const SAMPLE_THING_URI: &str =
    "at://did:plc:aaaaaaaaaaaaaaaaaaaaaaaa/space.polymodel.library.thing/parametric-enclosure";

const SAMPLE_TIME: &str = "2026-06-21T12:00:00.000Z";
const SAMPLE_TIME_MILLIS: i64 = 1_750_507_200_000;

// Additional demo makers so the discovery home shows a populated feed with
// mixed media states: one thing with a cover, two cover-less things that
// render the missing-media blueprint placeholder.
const MIRA_DID: &str = "did:plc:bbbbbbbbbbbbbbbbbbbbbbbb";
const MIRA_HANDLE: &str = "mira.tools";
const FOUNDRY_DID: &str = "did:plc:cccccccccccccccccccccccc";
const FOUNDRY_HANDLE: &str = "foundrylab.tools";

pub async fn seed_if_empty(pool: &SqlitePool) -> anyhow::Result<()> {
    let count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM things")
        .fetch_one(pool)
        .await?;
    if count > 0 {
        tracing::debug!(
            count,
            "sample data skipped because things table is populated"
        );
        return Ok(());
    }

    seed(pool).await?;
    tracing::info!(
        thing_uri = SAMPLE_THING_URI,
        "seeded local/demo Polymodel sample data"
    );
    Ok(())
}

async fn seed(pool: &SqlitePool) -> anyhow::Result<()> {
    let mut tx = pool.begin().await?;

    insert_identity(&mut tx, SAMPLE_DID, SAMPLE_HANDLE).await?;
    insert_profile(&mut tx).await?;

    let model_uris = [
        model_uri("enclosure-v1"),
        model_uri("panel-variant"),
        model_uri("ruggedized-lid"),
    ];
    let parts = sample_parts();
    for part in &parts {
        insert_part(&mut tx, part).await?;
    }

    insert_model(
        &mut tx,
        "enclosure-v1",
        "Snap-fit electronics enclosure",
        "Main printable enclosure with lid, body, buttons, and mounting hardware.",
        &parts[0..6],
    )
    .await?;
    insert_model(
        &mut tx,
        "panel-variant",
        "Panel-mount variant",
        "Alternate front panel for flush mounting sensors and displays.",
        &parts[6..12],
    )
    .await?;
    insert_model(
        &mut tx,
        "ruggedized-lid",
        "Ruggedized outdoor lid",
        "Weather-resistant lid and gasket parts for workshop deployments.",
        &parts[12..18],
    )
    .await?;

    let instructions = vec![
        "Print the body and lid in PETG or PLA+ with 0.2 mm layers.".to_string(),
        "Use the model selector to choose the standard, panel-mount, or ruggedized variant."
            .to_string(),
        "Press brass inserts into the corner bosses before final assembly.".to_string(),
    ];
    let tags = vec!["enclosure", "parametric", "print-in-place"];
    let thing_record = json!({
        "name": "Parametric enclosure kit",
        "summary": "A configurable electronics enclosure kit with three printable model variants and detailed part files.",
        "instructions": instructions,
        "license": "CC-BY-4.0",
        "tags": tags,
        "cover": [image("bafkreiathingcover", "Rendered preview of a parametric electronics enclosure")],
        "previews": [image("bafkreiathingpreview", "Exploded blueprint preview of the enclosure kit")],
        "models": model_uris.iter().map(|uri| strong_ref(uri)).collect::<Vec<_>>(),
        "createdAt": SAMPLE_TIME
    });

    let thing_record_json = typed_json::<Thing>(thing_record.clone())?;

    sqlx::query(
        r#"INSERT INTO things (did, rkey, uri, cid, name, summary, license, tags_json,
           tags_text, instructions_text, cover_json, derived_from_uri, created_at, indexed_at, record_json)
           VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?, ?)"#,
    )
    .bind(SAMPLE_DID)
    .bind(SAMPLE_THING_RKEY)
    .bind(SAMPLE_THING_URI)
    .bind("bafyreipolymodelsamplething")
    .bind("Parametric enclosure kit")
    .bind("A configurable electronics enclosure kit with three printable model variants and detailed part files.")
    .bind("CC-BY-4.0")
    .bind(serde_json::to_string(&tags)?)
    .bind(tags.join(" "))
    .bind(instructions.join("\n"))
    .bind(serde_json::to_string(&thing_record["cover"])? )
    .bind(SAMPLE_TIME_MILLIS)
    .bind(SAMPLE_TIME_MILLIS)
    .bind(thing_record_json)
    .execute(&mut *tx)
    .await?;

    for (position, model_uri) in model_uris.iter().enumerate() {
        sqlx::query("INSERT INTO thing_models (thing_uri, model_uri, position) VALUES (?, ?, ?)")
            .bind(SAMPLE_THING_URI)
            .bind(model_uri)
            .bind(position as i64)
            .execute(&mut *tx)
            .await?;
    }

    sqlx::query(
        "INSERT INTO content_stats (uri, like_count, save_count, tag_count) VALUES (?, 42, 12, 3)",
    )
    .bind(SAMPLE_THING_URI)
    .execute(&mut *tx)
    .await?;

    // Two more makers with cover-less things so the home feed is populated and
    // the missing-media placeholder path renders against real projected rows.
    insert_identity(&mut tx, MIRA_DID, MIRA_HANDLE).await?;
    insert_identity(&mut tx, FOUNDRY_DID, FOUNDRY_HANDLE).await?;
    insert_coverless_thing(
        &mut tx,
        MIRA_DID,
        "field-prototype",
        "Untitled thing",
        "A work-in-progress upload with no preview rendered yet.",
        0,
        0,
    )
    .await?;
    insert_coverless_thing(
        &mut tx,
        FOUNDRY_DID,
        "modular-calibration-tower",
        "Modular calibration tower",
        "A calibration tower for dialing in printer tolerances before a full run.",
        8,
        5,
    )
    .await?;

    tx.commit().await?;
    Ok(())
}

async fn insert_identity(
    conn: &mut SqliteConnection,
    did: &str,
    handle: &str,
) -> anyhow::Result<()> {
    sqlx::query("INSERT INTO identities (did, handle, updated_at) VALUES (?, ?, ?)")
        .bind(did)
        .bind(handle)
        .bind(SAMPLE_TIME_MILLIS)
        .execute(&mut *conn)
        .await?;
    Ok(())
}

async fn insert_profile(conn: &mut SqliteConnection) -> anyhow::Result<()> {
    let avatar = typed_json::<BlobRef>(blob("bafkreiariavatar", "image/jpeg", 2048))?;
    let profile_record = typed_json::<Profile>(json!({
        "displayName": "Ari Chen",
        "description": "Maker of practical parametric fixtures and electronics housings.",
        "avatar": serde_json::from_str::<Value>(&avatar)?,
        "pronouns": "she/her",
        "defaultLicense": "CC-BY-4.0"
    }))?;
    sqlx::query(
        r#"INSERT INTO profiles (did, display_name, description, avatar_json, default_license,
           pronouns, printers_json, links_json, record_json, indexed_at)
           VALUES (?, ?, ?, ?, ?, ?, NULL, NULL, ?, ?)"#,
    )
    .bind(SAMPLE_DID)
    .bind("Ari Chen")
    .bind("Maker of practical parametric fixtures and electronics housings.")
    .bind(&avatar)
    .bind("CC-BY-4.0")
    .bind("she/her")
    .bind(profile_record)
    .bind(SAMPLE_TIME_MILLIS)
    .execute(&mut *conn)
    .await?;
    Ok(())
}

async fn insert_model(
    conn: &mut SqliteConnection,
    rkey: &str,
    name: &str,
    summary: &str,
    parts: &[SamplePart],
) -> anyhow::Result<()> {
    let uri = model_uri(rkey);
    let record = typed_json::<Model>(json!({
        "name": name,
        "summary": summary,
        "instructions": ["Review the ordered part list before printing.", "Print fit-critical clips slowly for best tolerances."],
        "license": "CC-BY-4.0",
        "tags": ["variant", "enclosure"],
        "cover": [image("bafkreimodelcover", &format!("Preview of {name}"))],
        "previews": [image("bafkreimodelpreview", &format!("Blueprint preview of {name}"))],
        "parts": parts.iter().map(|part| strong_ref(&part_uri(part.rkey))).collect::<Vec<_>>(),
        "createdAt": SAMPLE_TIME
    }))?;

    sqlx::query(
        r#"INSERT INTO models (did, rkey, uri, cid, name, summary, created_at, indexed_at, record_json)
           VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"#,
    )
    .bind(SAMPLE_DID)
    .bind(rkey)
    .bind(&uri)
    .bind(format!("bafyreimodel{rkey}"))
    .bind(name)
    .bind(summary)
    .bind(SAMPLE_TIME_MILLIS)
    .bind(SAMPLE_TIME_MILLIS)
    .bind(record)
    .execute(&mut *conn)
    .await?;

    for (position, part) in parts.iter().enumerate() {
        sqlx::query("INSERT INTO model_parts (model_uri, part_uri, position) VALUES (?, ?, ?)")
            .bind(&uri)
            .bind(part_uri(part.rkey))
            .bind(position as i64)
            .execute(&mut *conn)
            .await?;
    }
    Ok(())
}

async fn insert_part(conn: &mut SqliteConnection, part: &SamplePart) -> anyhow::Result<()> {
    let file = file_manifest(part.file_size);
    let file_json = typed_json::<File>(file.clone())?;
    let record = typed_json::<Part>(json!({
        "name": part.name,
        "file": file,
        "format": "STL",
        "dimensions": { "x": part.dimensions.0, "y": part.dimensions.1, "z": part.dimensions.2, "unit": "mm" },
        "units": "mm",
        "notes": part.notes,
        "printSettings": ["0.2 mm layer height", "15% gyroid infill", "No supports required unless noted"],
        "previews": [image("bafkreipartpreview", &format!("Preview of {}", part.name))],
        "createdAt": SAMPLE_TIME
    }))?;

    sqlx::query(
        r#"INSERT INTO parts (did, rkey, uri, cid, name, format, file_json, created_at, indexed_at, record_json)
           VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"#,
    )
    .bind(SAMPLE_DID)
    .bind(part.rkey)
    .bind(part_uri(part.rkey))
    .bind(format!("bafyreipart{}", part.rkey))
    .bind(part.name)
    .bind("STL")
    .bind(file_json)
    .bind(SAMPLE_TIME_MILLIS)
    .bind(SAMPLE_TIME_MILLIS)
    .bind(record)
    .execute(&mut *conn)
    .await?;
    Ok(())
}

#[derive(Clone, Copy)]
struct SamplePart {
    rkey: &'static str,
    name: &'static str,
    file_size: i64,
    dimensions: (&'static str, &'static str, &'static str),
    notes: &'static str,
}

fn sample_parts() -> Vec<SamplePart> {
    [
        ("part-01", "Main enclosure body"),
        ("part-02", "Snap-fit lid"),
        ("part-03", "Button cap set"),
        ("part-04", "USB-C bezel"),
        ("part-05", "PCB mounting tray"),
        ("part-06", "Corner screw boss"),
        ("part-07", "Front sensor panel"),
        ("part-08", "Display window frame"),
        ("part-09", "Panel latch pair"),
        ("part-10", "Flush wall bracket"),
        ("part-11", "Cable strain relief"),
        ("part-12", "Label plate"),
        ("part-13", "Ruggedized lid shell"),
        ("part-14", "TPU gasket guide"),
        ("part-15", "Outdoor cable gland"),
        ("part-16", "Hinged dust cover"),
        ("part-17", "Drain slot insert"),
        ("part-18", "Mounting foot set"),
    ]
    .into_iter()
    .enumerate()
    .map(|(index, (rkey, name))| SamplePart {
        rkey,
        name,
        file_size: 48_000 + ((index + 1) as i64 * 2_048),
        dimensions: ("86.0", "54.0", "18.5"),
        notes: "Orient the visible face upward and verify first-layer adhesion around clips.",
    })
    .collect()
}

async fn insert_coverless_thing(
    conn: &mut SqliteConnection,
    did: &str,
    rkey: &str,
    name: &str,
    summary: &str,
    like_count: i64,
    save_count: i64,
) -> anyhow::Result<()> {
    let uri = thing_uri(did, rkey);
    let record = typed_json::<Thing>(json!({
        "name": name,
        "summary": summary,
        "license": "CC-BY-4.0",
        "createdAt": SAMPLE_TIME
    }))?;

    sqlx::query(
        r#"INSERT INTO things (did, rkey, uri, cid, name, summary, license, tags_json,
           tags_text, instructions_text, cover_json, derived_from_uri, created_at, indexed_at, record_json)
           VALUES (?, ?, ?, ?, ?, ?, ?, NULL, NULL, NULL, NULL, NULL, ?, ?, ?)"#,
    )
    .bind(did)
    .bind(rkey)
    .bind(&uri)
    .bind(format!("bafyreithing{}", rkey.replace('-', "")))
    .bind(name)
    .bind(summary)
    .bind("CC-BY-4.0")
    .bind(SAMPLE_TIME_MILLIS)
    .bind(SAMPLE_TIME_MILLIS)
    .bind(record)
    .execute(&mut *conn)
    .await?;

    if like_count > 0 || save_count > 0 {
        sqlx::query(
            "INSERT INTO content_stats (uri, like_count, save_count, tag_count) VALUES (?, ?, ?, 0)",
        )
        .bind(&uri)
        .bind(like_count)
        .bind(save_count)
        .execute(&mut *conn)
        .await?;
    }
    Ok(())
}

fn model_uri(rkey: &str) -> String {
    format!("at://{SAMPLE_DID}/space.polymodel.library.model/{rkey}")
}

fn part_uri(rkey: &str) -> String {
    format!("at://{SAMPLE_DID}/space.polymodel.library.part/{rkey}")
}

fn thing_uri(did: &str, rkey: &str) -> String {
    format!("at://{did}/space.polymodel.library.thing/{rkey}")
}

fn strong_ref(uri: &str) -> Value {
    json!({ "uri": uri, "cid": "bafyreicid" })
}

fn image(cid: &str, alt: &str) -> Value {
    json!({
        "alt": alt,
        "aspectRatio": { "width": 4, "height": 3 },
        "image": blob(cid, "image/jpeg", 1234)
    })
}

fn file_manifest(size: i64) -> Value {
    json!({
        "mimeType": "model/stl",
        "size": size,
        "chunks": [{ "blob": blob("bafkreistlchunk", "model/stl", size), "offset": 0, "size": size }]
    })
}

fn blob(cid: &str, mime_type: &str, size: i64) -> Value {
    json!({ "$type": "blob", "ref": { "$link": cid }, "mimeType": mime_type, "size": size })
}

fn typed_json<T>(value: Value) -> anyhow::Result<String>
where
    T: DeserializeOwned + Serialize,
{
    let typed: T = serde_json::from_value(value)?;
    Ok(serde_json::to_string(&typed)?)
}

#[cfg(test)]
mod tests {
    use std::str::FromStr;
    use std::sync::Arc;

    use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions};

    use super::*;
    use crate::appview::{state::AppState, views};

    async fn test_pool() -> SqlitePool {
        let options = SqliteConnectOptions::from_str("sqlite::memory:").unwrap();
        let pool = SqlitePoolOptions::new()
            .max_connections(1)
            .connect_with(options)
            .await
            .unwrap();
        sqlx::migrate!("./migrations").run(&pool).await.unwrap();
        pool
    }

    #[tokio::test]
    async fn seed_populates_feed_thing_and_model_read_paths() {
        let pool = test_pool().await;
        seed_if_empty(&pool).await.unwrap();
        seed_if_empty(&pool).await.unwrap();
        let bootstrap = crate::oauth::bootstrap_oauth(pool.clone(), Some("http://localhost"))
            .expect("ephemeral OAuth bootstrap for tests");
        let state = Arc::new(AppState::new(pool, bootstrap));

        let feed = views::feed_recent(&state, 10, None, None).await.unwrap();
        // Three seeded things: the model-rich parametric kit plus two cover-less
        // demo things that exercise the missing-media placeholder path.
        assert_eq!(feed.items.len(), 3);
        // feed_recent is rkey DESC, so "parametric-enclosure" sorts first.
        assert_eq!(
            feed.items[0].thing.name.as_str(),
            "Parametric enclosure kit"
        );
        assert_eq!(feed.items[0].thing.model_count, 3);
        assert_eq!(feed.items[0].thing.part_count, 18);
        let names: Vec<&str> = feed
            .items
            .iter()
            .map(|item| item.thing.name.as_str())
            .collect();
        assert!(names.contains(&"Untitled thing"));
        assert!(names.contains(&"Modular calibration tower"));

        let (thing, models) = views::get_thing(&state, SAMPLE_THING_URI, None)
            .await
            .unwrap();
        assert_eq!(
            thing.author.display_name.as_ref().unwrap().as_str(),
            "Ari Chen"
        );
        assert_eq!(models.len(), 3);

        let detail = views::get_model(&state, models[0].uri.as_ref(), None)
            .await
            .unwrap();
        assert_eq!(detail.parts.len(), 6);
        assert_eq!(detail.parts[0].name.as_str(), "Main enclosure body");
    }
}
