//! `docs::` Rhai bridge — collection-scoped handle pattern, v1.1.2.
//!
//! ```rhai
//! let users = docs::collection("users");
//! let id = users.create(#{ name: "Alice", tier: "gold" });
//! let doc = users.get(id);                 // envelope or () if missing
//! let golds = users.find(#{ tier: "gold" });
//! let one  = users.find_one(#{ tier: "gold" });
//! users.update(id, #{ name: "Alice", tier: "platinum" });
//! let removed = users.delete(id);           // bool was-present
//! let page = users.list(#{ cursor: (), limit: 100 });
//! ```
//!
//! Mirrors `kv.rs`: `DocsHandle` captures the collection + service +
//! per-call cx; methods bind via `engine.register_fn` so scripts call
//! them with dot-notation. **The service derives `app_id` from
//! `cx.app_id` — never from any closure argument.** Cross-app
//! isolation boundary; same as KV.
//!
//! Doc shape returned by `get`/`find`/`find_one`/`list`: an envelope
//! `#{ id, data: #{...}, created_at, updated_at }`. Decision D in the
//! v1.1.2 plan — explicit metadata vs user-data separation.

use std::sync::Arc;

use picloud_shared::{DocId, DocRow, DocsService, GroupDocsService, SdkCallCx, Services};
use rhai::{Array, Dynamic, Engine as RhaiEngine, EvalAltResult, Map, Module};
use uuid::Uuid;

use super::bridge::{
    block_on, dynamic_to_json_capped, json_to_dynamic, MAX_JSON_MATERIALIZE_BYTES,
};

/// Per-call handle captured by the Rhai SDK. Cheap to clone (two Arcs
/// plus an owned string).
#[derive(Clone)]
pub struct DocsHandle {
    collection: String,
    service: Arc<dyn DocsService>,
    cx: Arc<SdkCallCx>,
}

/// §11.6 shared-collection handle, returned by `docs::shared_collection(name)`.
/// A distinct Rhai type from `DocsHandle` so a script's choice of
/// private-vs-shared scope is explicit. Routes through `GroupDocsService`, which
/// resolves the owning group from `cx.app_id` (the script never names a group).
#[derive(Clone)]
pub struct GroupDocsHandle {
    collection: String,
    service: Arc<dyn GroupDocsService>,
    cx: Arc<SdkCallCx>,
}

pub(super) fn register(engine: &mut RhaiEngine, services: &Services, cx: Arc<SdkCallCx>) {
    let docs_service = services.docs.clone();
    let group_docs_service = services.group_docs.clone();

    let mut module = Module::new();
    {
        let docs_service = docs_service.clone();
        let cx = cx.clone();
        module.set_native_fn(
            "collection",
            move |name: &str| -> Result<DocsHandle, Box<EvalAltResult>> {
                if name.is_empty() {
                    return Err("docs::collection name must not be empty".into());
                }
                Ok(DocsHandle {
                    collection: name.to_string(),
                    service: docs_service.clone(),
                    cx: cx.clone(),
                })
            },
        );
    }
    {
        let group_docs_service = group_docs_service.clone();
        let cx = cx.clone();
        module.set_native_fn(
            "shared_collection",
            move |name: &str| -> Result<GroupDocsHandle, Box<EvalAltResult>> {
                if name.is_empty() {
                    return Err("docs::shared_collection name must not be empty".into());
                }
                Ok(GroupDocsHandle {
                    collection: name.to_string(),
                    service: group_docs_service.clone(),
                    cx: cx.clone(),
                })
            },
        );
    }
    engine.register_static_module("docs", module.into());

    engine.register_type_with_name::<DocsHandle>("DocsHandle");

    register_create(engine);
    register_get(engine);
    register_find(engine);
    register_find_one(engine);
    register_update(engine);
    register_delete(engine);
    register_list(engine);

    // Same method names on GroupDocsHandle — Rhai dispatches by receiver type.
    engine.register_type_with_name::<GroupDocsHandle>("GroupDocsHandle");

    register_group_create(engine);
    register_group_get(engine);
    register_group_find(engine);
    register_group_find_one(engine);
    register_group_update(engine);
    register_group_delete(engine);
    register_group_list(engine);
}

fn register_create(engine: &mut RhaiEngine) {
    engine.register_fn(
        "create",
        |handle: &mut DocsHandle, data: Map| -> Result<String, Box<EvalAltResult>> {
            let h = handle.clone();
            let json = dynamic_to_json_capped(&Dynamic::from(data), MAX_JSON_MATERIALIZE_BYTES)?;
            let id = block_on("docs", async move {
                h.service.create(&h.cx, &h.collection, json).await
            })?;
            Ok(id.to_string())
        },
    );
}

fn register_get(engine: &mut RhaiEngine) {
    engine.register_fn(
        "get",
        |handle: &mut DocsHandle, id: &str| -> Result<Dynamic, Box<EvalAltResult>> {
            let h = handle.clone();
            let parsed_id = parse_doc_id(id)?;
            let row = block_on("docs", async move {
                h.service.get(&h.cx, &h.collection, parsed_id).await
            })?;
            Ok(row.map_or(Dynamic::UNIT, |d| Dynamic::from(doc_to_map(&d))))
        },
    );
}

fn register_find(engine: &mut RhaiEngine) {
    engine.register_fn(
        "find",
        |handle: &mut DocsHandle, filter: Map| -> Result<Array, Box<EvalAltResult>> {
            let h = handle.clone();
            let json = dynamic_to_json_capped(&Dynamic::from(filter), MAX_JSON_MATERIALIZE_BYTES)?;
            let rows = block_on("docs", async move {
                h.service.find(&h.cx, &h.collection, json).await
            })?;
            Ok(rows
                .iter()
                .map(|d| Dynamic::from(doc_to_map(d)))
                .collect::<Vec<Dynamic>>())
        },
    );
}

fn register_find_one(engine: &mut RhaiEngine) {
    engine.register_fn(
        "find_one",
        |handle: &mut DocsHandle, filter: Map| -> Result<Dynamic, Box<EvalAltResult>> {
            let h = handle.clone();
            let json = dynamic_to_json_capped(&Dynamic::from(filter), MAX_JSON_MATERIALIZE_BYTES)?;
            let row = block_on("docs", async move {
                h.service.find_one(&h.cx, &h.collection, json).await
            })?;
            Ok(row.map_or(Dynamic::UNIT, |d| Dynamic::from(doc_to_map(&d))))
        },
    );
}

fn register_update(engine: &mut RhaiEngine) {
    engine.register_fn(
        "update",
        |handle: &mut DocsHandle, id: &str, data: Map| -> Result<(), Box<EvalAltResult>> {
            let h = handle.clone();
            let parsed_id = parse_doc_id(id)?;
            let json = dynamic_to_json_capped(&Dynamic::from(data), MAX_JSON_MATERIALIZE_BYTES)?;
            block_on("docs", async move {
                h.service
                    .update(&h.cx, &h.collection, parsed_id, json)
                    .await
            })
        },
    );
}

fn register_delete(engine: &mut RhaiEngine) {
    engine.register_fn(
        "delete",
        |handle: &mut DocsHandle, id: &str| -> Result<bool, Box<EvalAltResult>> {
            let h = handle.clone();
            let parsed_id = parse_doc_id(id)?;
            block_on("docs", async move {
                h.service.delete(&h.cx, &h.collection, parsed_id).await
            })
        },
    );
}

fn register_list(engine: &mut RhaiEngine) {
    // Zero-arg form: full page from the start.
    engine.register_fn(
        "list",
        |handle: &mut DocsHandle| -> Result<Map, Box<EvalAltResult>> { list_call(handle, None, 0) },
    );
    // One-arg form: pass `#{ cursor, limit }` map. Either field is
    // optional; missing/unit → defaults.
    engine.register_fn(
        "list",
        |handle: &mut DocsHandle, args: Map| -> Result<Map, Box<EvalAltResult>> {
            let cursor = match args.get("cursor") {
                Some(d) if !d.is_unit() => {
                    Some(d.clone().into_string().map_err(|_| -> Box<EvalAltResult> {
                        "docs::list: 'cursor' must be a string or ()".into()
                    })?)
                }
                _ => None,
            };
            let limit = match args.get("limit") {
                Some(d) if !d.is_unit() => {
                    let n = d.as_int().map_err(|_| -> Box<EvalAltResult> {
                        "docs::list: 'limit' must be an integer".into()
                    })?;
                    u32::try_from(n.max(0)).unwrap_or(0)
                }
                _ => 0,
            };
            list_call(handle, cursor, limit)
        },
    );
}

fn list_call(
    handle: &DocsHandle,
    cursor: Option<String>,
    limit: u32,
) -> Result<Map, Box<EvalAltResult>> {
    let h = handle.clone();
    let page = block_on("docs", async move {
        h.service
            .list(&h.cx, &h.collection, cursor.as_deref(), limit)
            .await
    })?;
    let mut m = Map::new();
    let docs: Array = page
        .docs
        .iter()
        .map(|d| Dynamic::from(doc_to_map(d)))
        .collect();
    m.insert("docs".into(), docs.into());
    m.insert(
        "next_cursor".into(),
        page.next_cursor.map_or(Dynamic::UNIT, Dynamic::from),
    );
    Ok(m)
}

// --- GroupDocsHandle methods (§11.6 shared docs collections) ---------------

fn register_group_create(engine: &mut RhaiEngine) {
    engine.register_fn(
        "create",
        |handle: &mut GroupDocsHandle, data: Map| -> Result<String, Box<EvalAltResult>> {
            let h = handle.clone();
            let json = dynamic_to_json_capped(&Dynamic::from(data), MAX_JSON_MATERIALIZE_BYTES)?;
            let id = block_on("docs", async move {
                h.service.create(&h.cx, &h.collection, json).await
            })?;
            Ok(id.to_string())
        },
    );
}

fn register_group_get(engine: &mut RhaiEngine) {
    engine.register_fn(
        "get",
        |handle: &mut GroupDocsHandle, id: &str| -> Result<Dynamic, Box<EvalAltResult>> {
            let h = handle.clone();
            let parsed_id = parse_doc_id(id)?;
            let row = block_on("docs", async move {
                h.service.get(&h.cx, &h.collection, parsed_id).await
            })?;
            Ok(row.map_or(Dynamic::UNIT, |d| Dynamic::from(doc_to_map(&d))))
        },
    );
}

fn register_group_find(engine: &mut RhaiEngine) {
    engine.register_fn(
        "find",
        |handle: &mut GroupDocsHandle, filter: Map| -> Result<Array, Box<EvalAltResult>> {
            let h = handle.clone();
            let json = dynamic_to_json_capped(&Dynamic::from(filter), MAX_JSON_MATERIALIZE_BYTES)?;
            let rows = block_on("docs", async move {
                h.service.find(&h.cx, &h.collection, json).await
            })?;
            Ok(rows
                .iter()
                .map(|d| Dynamic::from(doc_to_map(d)))
                .collect::<Vec<Dynamic>>())
        },
    );
}

fn register_group_find_one(engine: &mut RhaiEngine) {
    engine.register_fn(
        "find_one",
        |handle: &mut GroupDocsHandle, filter: Map| -> Result<Dynamic, Box<EvalAltResult>> {
            let h = handle.clone();
            let json = dynamic_to_json_capped(&Dynamic::from(filter), MAX_JSON_MATERIALIZE_BYTES)?;
            let row = block_on("docs", async move {
                h.service.find_one(&h.cx, &h.collection, json).await
            })?;
            Ok(row.map_or(Dynamic::UNIT, |d| Dynamic::from(doc_to_map(&d))))
        },
    );
}

fn register_group_update(engine: &mut RhaiEngine) {
    engine.register_fn(
        "update",
        |handle: &mut GroupDocsHandle, id: &str, data: Map| -> Result<(), Box<EvalAltResult>> {
            let h = handle.clone();
            let parsed_id = parse_doc_id(id)?;
            let json = dynamic_to_json_capped(&Dynamic::from(data), MAX_JSON_MATERIALIZE_BYTES)?;
            block_on("docs", async move {
                h.service
                    .update(&h.cx, &h.collection, parsed_id, json)
                    .await
            })
        },
    );
}

fn register_group_delete(engine: &mut RhaiEngine) {
    engine.register_fn(
        "delete",
        |handle: &mut GroupDocsHandle, id: &str| -> Result<bool, Box<EvalAltResult>> {
            let h = handle.clone();
            let parsed_id = parse_doc_id(id)?;
            block_on("docs", async move {
                h.service.delete(&h.cx, &h.collection, parsed_id).await
            })
        },
    );
}

fn register_group_list(engine: &mut RhaiEngine) {
    engine.register_fn(
        "list",
        |handle: &mut GroupDocsHandle| -> Result<Map, Box<EvalAltResult>> {
            group_list_call(handle, None, 0)
        },
    );
    engine.register_fn(
        "list",
        |handle: &mut GroupDocsHandle, args: Map| -> Result<Map, Box<EvalAltResult>> {
            let cursor = match args.get("cursor") {
                Some(d) if !d.is_unit() => {
                    Some(d.clone().into_string().map_err(|_| -> Box<EvalAltResult> {
                        "docs::list: 'cursor' must be a string or ()".into()
                    })?)
                }
                _ => None,
            };
            let limit = match args.get("limit") {
                Some(d) if !d.is_unit() => {
                    let n = d.as_int().map_err(|_| -> Box<EvalAltResult> {
                        "docs::list: 'limit' must be an integer".into()
                    })?;
                    u32::try_from(n.max(0)).unwrap_or(0)
                }
                _ => 0,
            };
            group_list_call(handle, cursor, limit)
        },
    );
}

fn group_list_call(
    handle: &GroupDocsHandle,
    cursor: Option<String>,
    limit: u32,
) -> Result<Map, Box<EvalAltResult>> {
    let h = handle.clone();
    let page = block_on("docs", async move {
        h.service
            .list(&h.cx, &h.collection, cursor.as_deref(), limit)
            .await
    })?;
    let mut m = Map::new();
    let docs: Array = page
        .docs
        .iter()
        .map(|d| Dynamic::from(doc_to_map(d)))
        .collect();
    m.insert("docs".into(), docs.into());
    m.insert(
        "next_cursor".into(),
        page.next_cursor.map_or(Dynamic::UNIT, Dynamic::from),
    );
    Ok(m)
}

/// Build the `{ id, data, created_at, updated_at }` envelope per
/// Decision D. Scripts read user fields via `doc.data.<field>`; `id`
/// and timestamps are direct children of the envelope.
fn doc_to_map(doc: &DocRow) -> Map {
    let mut m = Map::new();
    m.insert("id".into(), doc.id.to_string().into());
    m.insert("data".into(), json_to_dynamic(doc.data.clone()));
    m.insert("created_at".into(), doc.created_at.to_rfc3339().into());
    m.insert("updated_at".into(), doc.updated_at.to_rfc3339().into());
    m
}

fn parse_doc_id(id: &str) -> Result<DocId, Box<EvalAltResult>> {
    Uuid::parse_str(id).map_err(|e| -> Box<EvalAltResult> {
        EvalAltResult::ErrorRuntime(
            format!("docs: invalid id '{id}': {e}").into(),
            rhai::Position::NONE,
        )
        .into()
    })
}
