//! Process role and placement — the master/worker contract (phase 79).
//!
//! Folk's runtime model is **fork-after-warm**: a single-threaded master
//! process warms PHP + bootstraps the framework, then `fork()`s N worker
//! processes. The master never executes PHP — it supervises (respawn,
//! per-worker memory, force-kill) and owns shared state (the metrics
//! shared-memory segment, the in-memory jobs queue). Each worker owns its
//! requests end-to-end and dispatches into the PHP of *its own* process.
//!
//! A plugin therefore needs to know two things:
//!
//! * **where it runs** — declared up front via [`Plugin::placement`](crate::Plugin::placement);
//!   the core only boots a plugin in the process(es) its placement names;
//! * **which process it is in right now** — observed at boot via
//!   [`PluginContext::role`](crate::PluginContext::role), so a `Both` plugin can
//!   branch on master vs worker behaviour.

/// Which process a plugin instance is executing in.
///
/// Supplied per-process in [`PluginContext::role`](crate::PluginContext::role):
/// the master gets [`PluginRole::Master`], each forked worker gets
/// [`PluginRole::Worker`] carrying its slot id.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PluginRole {
    /// The supervisor process. Holds shared state (metrics segment, in-memory
    /// jobs queue) and supervises the worker pool. **Does not execute PHP** —
    /// the `executor` in its [`PluginContext`](crate::PluginContext) is a
    /// [`NoopExecutor`](crate::NoopExecutor) and returns `Err` if called.
    Master,
    /// A forked worker process. Its `executor` dispatches into the PHP runtime
    /// of *this* process (zero-copy, one request at a time).
    Worker {
        /// Stable per-worker slot id (`0..count`). The master uses the same id
        /// when respawning a dead worker into the slot.
        worker_id: u32,
    },
}

impl PluginRole {
    /// `true` for [`PluginRole::Master`].
    pub fn is_master(&self) -> bool {
        matches!(self, PluginRole::Master)
    }

    /// `true` for [`PluginRole::Worker`].
    pub fn is_worker(&self) -> bool {
        matches!(self, PluginRole::Worker { .. })
    }

    /// The worker slot id, or `None` in the master.
    pub fn worker_id(&self) -> Option<u32> {
        match self {
            PluginRole::Worker { worker_id } => Some(*worker_id),
            PluginRole::Master => None,
        }
    }
}

/// Where a plugin should be booted in the fork-after-warm model.
///
/// Declared via [`Plugin::placement`](crate::Plugin::placement). The core boots
/// a plugin only in the process(es) named here, passing the matching
/// [`PluginRole`] in the [`PluginContext`](crate::PluginContext).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Placement {
    /// Boot only in the master (e.g. `process` supervisor, the `metrics`
    /// scrape server). The plugin never dispatches PHP.
    MasterOnly,
    /// Boot only in each worker (e.g. `http`/`grpc` listeners on
    /// `SO_REUSEPORT`, `jobs` competing-consumers). This is the default.
    WorkerOnly,
    /// Boot in both master and every worker; the plugin branches on
    /// [`PluginContext::role`](crate::PluginContext::role).
    Both,
}

impl Placement {
    /// Whether a plugin with this placement should be booted under `role`.
    pub fn includes(&self, role: PluginRole) -> bool {
        match self {
            Placement::MasterOnly => role.is_master(),
            Placement::WorkerOnly => role.is_worker(),
            Placement::Both => true,
        }
    }
}
