//! `files::` Rhai bridge — collection-scoped handle pattern (v1.1.5).
//!
//! ```rhai
//! let avatars = files::collection("avatars");
//! let id = avatars.create(#{ name: "a.jpg", content_type: "image/jpeg", data: blob });
//! let meta  = avatars.head(id);   // metadata map or ()
//! let bytes = avatars.get(id);    // Blob or ()
//! avatars.update(id, #{ data: new_bytes });
//! let gone  = avatars.delete(id); // bool (was-present)
//! let page  = avatars.list();     // #{ files: [...], next_cursor: () }
//! ```
//!
//! The `FilesHandle` custom Rhai type captures the collection name once
//! and routes each call through the injected `Arc<dyn FilesService>`
//! with the per-call `Arc<SdkCallCx>`. **The service derives `app_id`
//! from `cx.app_id` — it never appears in any signature script-side,
//! preserving cross-app isolation.**
//!
//! Error convention (per `docs/sdk-shape.md`): `create`/`update`/
//! `delete` throw on failure; `get`/`head` return `()` for a missing
//! file; `delete` returns `bool` (was-present). The blob bytes are a
//! Rhai `Blob` (byte array) in both directions.

use std::sync::Arc;

use super::bridge::block_on;
use picloud_shared::{
    FileMeta, FileUpdate, FilesService, GroupFilesService, NewFile, SdkCallCx, Services,
};
use rhai::{Array, Dynamic, Engine as RhaiEngine, EvalAltResult, Map, Module};

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

/// §11.6 shared-collection handle, returned by `files::shared_collection(name)`.
/// Same method surface as `FilesHandle`, but routes through the
/// `GroupFilesService` — the owning group is resolved from `cx.app_id`'s
/// ancestor chain inside the service (the isolation boundary).
#[derive(Clone)]
pub struct GroupFilesHandle {
    collection: String,
    service: Arc<dyn GroupFilesService>,
    cx: Arc<SdkCallCx>,
}

pub(super) fn register(engine: &mut RhaiEngine, services: &Services, cx: Arc<SdkCallCx>) {
    let files_service = services.files.clone();
    let group_files_service = services.group_files.clone();

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

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

    register_create(engine);
    register_head(engine);
    register_get(engine);
    register_update(engine);
    register_delete(engine);
    register_list(engine);

    // Same method names on GroupFilesHandle — Rhai dispatches by receiver type.
    engine.register_type_with_name::<GroupFilesHandle>("GroupFilesHandle");
    register_group_create(engine);
    register_group_head(engine);
    register_group_get(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 FilesHandle, meta: Map| -> Result<String, Box<EvalAltResult>> {
            let name = require_string(&meta, "name")?;
            let content_type = require_string(&meta, "content_type")?;
            let data = require_blob(&meta, "data")?;
            let h = handle.clone();
            let new = NewFile {
                name,
                content_type,
                data,
            };
            let id = block_on("files", async move {
                h.service.create(&h.cx, &h.collection, new).await
            })?;
            Ok(id.to_string())
        },
    );
}

fn register_head(engine: &mut RhaiEngine) {
    engine.register_fn(
        "head",
        |handle: &mut FilesHandle, id: &str| -> Result<Dynamic, Box<EvalAltResult>> {
            let h = handle.clone();
            let id = id.to_string();
            let meta = block_on("files", async move {
                h.service.head(&h.cx, &h.collection, &id).await
            })?;
            Ok(meta.map_or(Dynamic::UNIT, |m| file_meta_to_map(&m).into()))
        },
    );
}

fn register_get(engine: &mut RhaiEngine) {
    engine.register_fn(
        "get",
        |handle: &mut FilesHandle, id: &str| -> Result<Dynamic, Box<EvalAltResult>> {
            let h = handle.clone();
            let id = id.to_string();
            let bytes = block_on("files", async move {
                h.service.get(&h.cx, &h.collection, &id).await
            })?;
            Ok(bytes.map_or(Dynamic::UNIT, Dynamic::from_blob))
        },
    );
}

fn register_update(engine: &mut RhaiEngine) {
    engine.register_fn(
        "update",
        |handle: &mut FilesHandle, id: &str, meta: Map| -> Result<(), Box<EvalAltResult>> {
            let data = require_blob(&meta, "data")?;
            let name = optional_string(&meta, "name")?;
            let content_type = optional_string(&meta, "content_type")?;
            let h = handle.clone();
            let id = id.to_string();
            let upd = FileUpdate {
                data,
                name,
                content_type,
            };
            block_on("files", async move {
                h.service.update(&h.cx, &h.collection, &id, upd).await
            })
        },
    );
}

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

fn register_list(engine: &mut RhaiEngine) {
    engine.register_fn(
        "list",
        |handle: &mut FilesHandle| -> Result<Map, Box<EvalAltResult>> {
            list_call(handle, None, 0)
        },
    );
    engine.register_fn(
        "list",
        |handle: &mut FilesHandle, cursor: &str| -> Result<Map, Box<EvalAltResult>> {
            list_call(handle, Some(cursor.to_string()), 0)
        },
    );
    engine.register_fn(
        "list",
        |handle: &mut FilesHandle, cursor: &str, limit: i64| -> Result<Map, Box<EvalAltResult>> {
            let limit = u32::try_from(limit.max(0)).unwrap_or(0);
            list_call(handle, Some(cursor.to_string()), limit)
        },
    );
    // `list(#{ cursor, limit })` — the map form documented in the brief.
    engine.register_fn(
        "list",
        |handle: &mut FilesHandle, opts: Map| -> Result<Map, Box<EvalAltResult>> {
            let cursor = match opts.get("cursor") {
                Some(v) if !v.is_unit() => {
                    Some(v.clone().into_string().map_err(|_| -> Box<EvalAltResult> {
                        "files: list cursor must be a string".into()
                    })?)
                }
                _ => None,
            };
            let limit = match opts.get("limit") {
                Some(v) if !v.is_unit() => {
                    u32::try_from(v.as_int().unwrap_or(0).max(0)).unwrap_or(0)
                }
                _ => 0,
            };
            list_call(handle, cursor, limit)
        },
    );
}

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

// --- GroupFilesHandle methods (§11.6 shared files collections) -------------
// Bodies mirror the app handle's; only the receiver type and service differ
// (Rhai dispatches `create`/`head`/... by the handle's concrete type).

fn register_group_create(engine: &mut RhaiEngine) {
    engine.register_fn(
        "create",
        |handle: &mut GroupFilesHandle, meta: Map| -> Result<String, Box<EvalAltResult>> {
            let name = require_string(&meta, "name")?;
            let content_type = require_string(&meta, "content_type")?;
            let data = require_blob(&meta, "data")?;
            let h = handle.clone();
            let new = NewFile {
                name,
                content_type,
                data,
            };
            let id = block_on("files", async move {
                h.service.create(&h.cx, &h.collection, new).await
            })?;
            Ok(id.to_string())
        },
    );
}

fn register_group_head(engine: &mut RhaiEngine) {
    engine.register_fn(
        "head",
        |handle: &mut GroupFilesHandle, id: &str| -> Result<Dynamic, Box<EvalAltResult>> {
            let h = handle.clone();
            let id = id.to_string();
            let meta = block_on("files", async move {
                h.service.head(&h.cx, &h.collection, &id).await
            })?;
            Ok(meta.map_or(Dynamic::UNIT, |m| file_meta_to_map(&m).into()))
        },
    );
}

fn register_group_get(engine: &mut RhaiEngine) {
    engine.register_fn(
        "get",
        |handle: &mut GroupFilesHandle, id: &str| -> Result<Dynamic, Box<EvalAltResult>> {
            let h = handle.clone();
            let id = id.to_string();
            let bytes = block_on("files", async move {
                h.service.get(&h.cx, &h.collection, &id).await
            })?;
            Ok(bytes.map_or(Dynamic::UNIT, Dynamic::from_blob))
        },
    );
}

fn register_group_update(engine: &mut RhaiEngine) {
    engine.register_fn(
        "update",
        |handle: &mut GroupFilesHandle, id: &str, meta: Map| -> Result<(), Box<EvalAltResult>> {
            let data = require_blob(&meta, "data")?;
            let name = optional_string(&meta, "name")?;
            let content_type = optional_string(&meta, "content_type")?;
            let h = handle.clone();
            let id = id.to_string();
            let upd = FileUpdate {
                data,
                name,
                content_type,
            };
            block_on("files", async move {
                h.service.update(&h.cx, &h.collection, &id, upd).await
            })
        },
    );
}

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

fn register_group_list(engine: &mut RhaiEngine) {
    engine.register_fn(
        "list",
        |handle: &mut GroupFilesHandle| -> Result<Map, Box<EvalAltResult>> {
            group_list_call(handle, None, 0)
        },
    );
    engine.register_fn(
        "list",
        |handle: &mut GroupFilesHandle, cursor: &str| -> Result<Map, Box<EvalAltResult>> {
            group_list_call(handle, Some(cursor.to_string()), 0)
        },
    );
    engine.register_fn(
        "list",
        |handle: &mut GroupFilesHandle,
         cursor: &str,
         limit: i64|
         -> Result<Map, Box<EvalAltResult>> {
            let limit = u32::try_from(limit.max(0)).unwrap_or(0);
            group_list_call(handle, Some(cursor.to_string()), limit)
        },
    );
    engine.register_fn(
        "list",
        |handle: &mut GroupFilesHandle, opts: Map| -> Result<Map, Box<EvalAltResult>> {
            let cursor = match opts.get("cursor") {
                Some(v) if !v.is_unit() => {
                    Some(v.clone().into_string().map_err(|_| -> Box<EvalAltResult> {
                        "files: list cursor must be a string".into()
                    })?)
                }
                _ => None,
            };
            let limit = match opts.get("limit") {
                Some(v) if !v.is_unit() => {
                    u32::try_from(v.as_int().unwrap_or(0).max(0)).unwrap_or(0)
                }
                _ => 0,
            };
            group_list_call(handle, cursor, limit)
        },
    );
}

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

/// Render a `FileMeta` into the Rhai map shape scripts see from
/// `head` / `list`.
fn file_meta_to_map(meta: &FileMeta) -> Map {
    let mut m = Map::new();
    m.insert("id".into(), meta.id.to_string().into());
    m.insert("collection".into(), meta.collection.clone().into());
    m.insert("name".into(), meta.name.clone().into());
    m.insert("content_type".into(), meta.content_type.clone().into());
    m.insert(
        "size".into(),
        i64::try_from(meta.size).unwrap_or(i64::MAX).into(),
    );
    m.insert("checksum".into(), meta.checksum.clone().into());
    m.insert("created_at".into(), meta.created_at.to_rfc3339().into());
    m.insert("updated_at".into(), meta.updated_at.to_rfc3339().into());
    m
}

/// Pull a required string field out of a Rhai map; throw naming the
/// field if it's absent or not a string.
fn require_string(meta: &Map, field: &'static str) -> Result<String, Box<EvalAltResult>> {
    match meta.get(field) {
        Some(v) if v.is_string() => Ok(v.clone().into_string().unwrap_or_default()),
        Some(_) => Err(format!("files::create: field '{field}' must be a string").into()),
        None => Err(format!("files::create: missing required field '{field}'").into()),
    }
}

/// Pull an optional string field; `None` when the key is absent or unit.
fn optional_string(meta: &Map, field: &'static str) -> Result<Option<String>, Box<EvalAltResult>> {
    match meta.get(field) {
        None => Ok(None),
        Some(v) if v.is_unit() => Ok(None),
        Some(v) if v.is_string() => Ok(Some(v.clone().into_string().unwrap_or_default())),
        Some(_) => Err(format!("files::update: field '{field}' must be a string").into()),
    }
}

/// Pull a required blob (`data`) out of a Rhai map; throw naming the
/// field if it's absent or not a blob.
fn require_blob(meta: &Map, field: &'static str) -> Result<Vec<u8>, Box<EvalAltResult>> {
    match meta.get(field) {
        Some(v) if v.is_blob() => Ok(v.clone().into_blob().unwrap_or_default()),
        Some(_) => Err(format!("files: field '{field}' must be a Blob (byte array)").into()),
        None => Err(format!("files: missing required field '{field}'").into()),
    }
}
