//! Operator-declared tenant registry. Wave A of voice-multi-instance.
//!
//! Maps each tenant label (operator YAML `voice[].instance`) to its
//! `Arc<VoiceToolContext>` — one full pipeline per tenant, each
//! owning its own SQLite store + provider stack + breaker registry
//! + LRU audio cache. Mirrors the email plugin's `instance_registry`
//! shape so the daemon-side `register_instance_subprocess_factories`
//! helper can target voice too.
//!
//! Lives in a process-wide `OnceLock` so the boot loop, broker
//! subscribers, tool dispatcher, and admin RPC handlers all share
//! one map.

use std::sync::{Arc, OnceLock};

use dashmap::DashMap;

use crate::tools::VoiceToolContext;

static REGISTRY: OnceLock<Arc<DashMap<String, Arc<VoiceToolContext>>>> = OnceLock::new();
static CATCH_ALL: OnceLock<std::sync::RwLock<Option<String>>> = OnceLock::new();

fn map() -> Arc<DashMap<String, Arc<VoiceToolContext>>> {
    REGISTRY.get_or_init(|| Arc::new(DashMap::new())).clone()
}

fn catch_all_slot() -> &'static std::sync::RwLock<Option<String>> {
    CATCH_ALL.get_or_init(|| std::sync::RwLock::new(None))
}

/// Register `ctx` under `label`. Overwrites any prior entry —
/// callers that need diff-aware behaviour (the boot loop) compare
/// against [`entries`] before re-registering.
pub fn register(label: &str, ctx: Arc<VoiceToolContext>) {
    map().insert(label.to_string(), ctx);
}

/// Drop the entry for `label`. Returns the prior context so the
/// caller can flush its cache / close its DB before the Arc drops.
pub fn unregister(label: &str) -> Option<Arc<VoiceToolContext>> {
    map().remove(label).map(|(_, v)| v)
}

/// Look up the live context for `label`.
pub fn lookup(label: &str) -> Option<Arc<VoiceToolContext>> {
    map().get(label).map(|v| v.clone())
}

/// Snapshot every `(label, ctx)` pair. Used by the dispatcher's
/// "exactly-one-declared" compat shim + admin `list_instances`.
pub fn entries() -> Vec<(String, Arc<VoiceToolContext>)> {
    map()
        .iter()
        .map(|e| (e.key().clone(), e.value().clone()))
        .collect()
}

/// Number of declared tenants. O(1).
pub fn len() -> usize {
    map().len()
}

/// Drain every entry. Tests + the boot loop's transition path use
/// this; production callers prefer the diff path.
pub fn clear() {
    map().clear();
    *catch_all_slot().write().unwrap() = None;
}

/// Mark `label` as the catch-all instance — the one tool dispatch
/// returns when no other tenant claims the agent_id. Boot may set
/// at most one catch-all per `apply_configure` run.
pub fn set_catch_all(label: &str) {
    *catch_all_slot().write().unwrap() = Some(label.to_string());
}

/// Resolve an `agent_id` to its tenant context. Lookup order:
///
/// 1. Any tenant whose `allow_agents` list contains `agent_id`.
///    Mapping captured at register time (via the
///    [`set_agent_routes`] helper).
/// 2. The catch-all instance recorded via [`set_catch_all`].
/// 3. `None` — caller surfaces `Unavailable` to the host.
pub fn resolve(agent_id: Option<&str>) -> Option<Arc<VoiceToolContext>> {
    if let Some(id) = agent_id {
        if let Some(label) = AGENT_ROUTES
            .get_or_init(|| Arc::new(DashMap::new()))
            .get(id)
            .map(|v| v.clone())
        {
            if let Some(ctx) = lookup(&label) {
                return Some(ctx);
            }
        }
    }
    let fallback_label = catch_all_slot().read().unwrap().clone()?;
    lookup(&fallback_label)
}

static AGENT_ROUTES: OnceLock<Arc<DashMap<String, String>>> = OnceLock::new();

fn agent_routes() -> Arc<DashMap<String, String>> {
    AGENT_ROUTES
        .get_or_init(|| Arc::new(DashMap::new()))
        .clone()
}

/// Replace the agent_id → tenant_label routing table. Called by the
/// boot loop after diff/register so the map mirrors the freshly
/// applied operator config. Idempotent — caller clears first then
/// fills.
pub fn clear_agent_routes() {
    agent_routes().clear();
}

/// Add a single `agent_id → label` mapping. Last-write-wins when
/// the same agent_id is claimed by two tenants; boot rejects such
/// configs before calling this so the inconsistency never reaches
/// production.
pub fn add_agent_route(agent_id: &str, label: &str) {
    agent_routes().insert(agent_id.to_string(), label.to_string());
}
