//! Mizan Tauri adapter — typed RPC dispatch over Tauri's IPC.
//!
//! Ships as a Tauri plugin. The consumer installs it with one line:
//!
//! ```ignore
//! tauri::Builder::default()
//!     .plugin(mizan_tauri::init())
//!     .run(tauri::generate_context!())
//!     .expect("error while running tauri application");
//! ```
//!
//! The plugin exposes a single command `mizan_invoke` (full Tauri name
//! `plugin:mizan|mizan_invoke`). The JS-side `@mizan/tauri-transport`
//! sends call/fetch envelopes to it; the dispatch routes through
//! `mizan-core`'s FUNCTIONS / CONTEXTS registries — the same
//! linkme-backed distributed slices the HTTP adapter (mizan-rust-axum)
//! consumes. There is no per-function tauri::command; the registry IS
//! the dispatch table.
//!
//! Wire envelope:
//!
//! ```json
//! { "op": "call",  "fn": "list_sessions", "args": {} }
//! { "op": "fetch", "context": "session",  "params": {} }
//! ```
//!
//! Response shapes mirror POST /call/ and GET /ctx/.../ from
//! mizan-rust-axum:
//!
//! * `call`  → `{ result, invalidate, merge? }`
//! * `fetch` → `{ <fnName>: <result>, ... }` (a flat bundle)
//!
//! Error responses come back as the `Err` variant of the Tauri command's
//! `Result`, which Tauri serializes into the JS-side `Promise.reject`.
//! The TS-side transport re-wraps it into a `MizanError` so consumers
//! see one error surface regardless of transport.

use mizan_core::{
    compute_invalidation, compute_merges, lookup_context, lookup_function,
    FunctionSpec, InvalidationTarget, MergeEntry, MizanError, RequestHandle, FUNCTIONS,
};
use serde::{Deserialize, Serialize};
use serde_json::{json, Map, Value};
use tauri::{
    plugin::{Builder, TauriPlugin},
    Runtime,
};

/// Build the Mizan Tauri plugin. Install with `.plugin(mizan_tauri::init())`
/// on the `tauri::Builder`. The plugin name is `mizan`; the dispatch
/// command is reachable from JS as `plugin:mizan|mizan_invoke`.
pub fn init<R: Runtime>() -> TauriPlugin<R> {
    Builder::<R>::new("mizan")
        .invoke_handler(tauri::generate_handler![mizan_invoke])
        .build()
}

// === Wire envelope ===

/// One Mizan request. The JS-side transport sends `{ envelope: ... }`;
/// Tauri's serde deserializer pulls this struct out of the `envelope`
/// field of the invoke payload.
#[derive(Debug, Deserialize)]
#[serde(tag = "op")]
pub enum Envelope {
    #[serde(rename = "call")]
    Call {
        /// Wire-level function name — registered name on the Rust side.
        #[serde(rename = "fn")]
        function_name: String,
        #[serde(default)]
        args: Map<String, Value>,
    },
    #[serde(rename = "fetch")]
    Fetch {
        context: String,
        #[serde(default)]
        params: Map<String, Value>,
    },
}

/// Error payload returned to the frontend. Mirrors the HTTP adapter's
/// `{"code", "message", "details?"}` shape; the TS-side transport reads
/// this and constructs a `MizanError`.
#[derive(Debug, Serialize)]
pub struct ErrorPayload {
    pub code: &'static str,
    pub message: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub details: Option<Value>,
}

impl From<MizanError> for ErrorPayload {
    fn from(e: MizanError) -> Self {
        let details = if let MizanError::ValidationFailed { details, .. } = &e {
            Some(details.clone())
        } else {
            None
        };
        Self {
            code: e.code(),
            message: e.message().to_string(),
            details,
        }
    }
}

// === Dispatch ===

/// The single Mizan dispatch command. Registered on the plugin's invoke
/// handler — the consumer never wires it directly.
///
/// `app: AppHandle` is auto-injected by Tauri; the function body borrows
/// it into a `RequestHandle` so `#[mizan::client]` functions can
/// `req.downcast::<tauri::AppHandle>()` for app-managed state or event
/// emission. Stateless functions ignore the handle.
#[tauri::command]
async fn mizan_invoke<R: Runtime>(
    app: tauri::AppHandle<R>,
    envelope: Envelope,
) -> Result<Value, ErrorPayload> {
    match envelope {
        Envelope::Call {
            function_name,
            args,
        } => handle_call(&app, &function_name, args).await,
        Envelope::Fetch { context, params } => handle_fetch(&app, &context, params).await,
    }
}

async fn handle_call<R: Runtime>(
    app: &tauri::AppHandle<R>,
    fn_name: &str,
    args: Map<String, Value>,
) -> Result<Value, ErrorPayload> {
    let fn_spec = lookup_function(fn_name).ok_or_else(|| {
        ErrorPayload::from(MizanError::NotFound(format!(
            "function {fn_name:?} not registered"
        )))
    })?;

    let req = RequestHandle::new(app);
    let result = fn_spec
        .dispatch(req, Value::Object(args.clone()))
        .await
        .map_err(ErrorPayload::from)?;

    let invalidate: Vec<Value> = compute_invalidation(fn_spec, &args)
        .iter()
        .map(InvalidationTarget::to_json)
        .collect();
    let merges = compute_merges(fn_spec, &args, &result);
    let merge_payload: Option<Vec<Value>> = if merges.is_empty() {
        None
    } else {
        Some(merges.iter().map(MergeEntry::to_json).collect())
    };

    let mut payload = json!({
        "result": result,
        "invalidate": invalidate,
    });
    if let Some(merge) = merge_payload {
        payload
            .as_object_mut()
            .expect("payload is a JSON object")
            .insert("merge".into(), Value::Array(merge));
    }
    Ok(payload)
}

async fn handle_fetch<R: Runtime>(
    app: &tauri::AppHandle<R>,
    context_name: &str,
    params: Map<String, Value>,
) -> Result<Value, ErrorPayload> {
    if lookup_context(context_name).is_none() {
        return Err(ErrorPayload::from(MizanError::NotFound(format!(
            "context {context_name:?} not registered"
        ))));
    }

    let members: Vec<&dyn FunctionSpec> = FUNCTIONS
        .iter()
        .copied()
        .filter(|f| f.context() == Some(context_name))
        .collect();
    if members.is_empty() {
        return Err(ErrorPayload::from(MizanError::NotFound(format!(
            "context {context_name:?} has no registered members"
        ))));
    }

    let mut bundled = Map::new();
    for fn_spec in &members {
        let args = filter_args(*fn_spec, &params);
        let req = RequestHandle::new(app);
        let result = fn_spec
            .dispatch(req, Value::Object(args))
            .await
            .map_err(ErrorPayload::from)?;
        bundled.insert(fn_spec.name().to_string(), result);
    }

    Ok(Value::Object(bundled))
}

/// Filter the envelope's params down to keys this function declares as
/// input. The HTTP/axum adapter coerces string-typed query params to
/// JSON primitives in the equivalent step; the Tauri arg channel already
/// carries typed JSON, so the filter is sufficient on its own.
fn filter_args(fn_spec: &dyn FunctionSpec, params: &Map<String, Value>) -> Map<String, Value> {
    let mut out = Map::new();
    for ip in fn_spec.input_params() {
        if let Some(v) = params.get(ip.name) {
            out.insert(ip.name.into(), v.clone());
        }
    }
    out
}
