//! Mediated agent input (cadrage « entrée médiée par IdeA », lot C1/C2).
//!
//! Every input to an agent — **human** keystrokes *and* **inter-agent** delegations
//! — converges on **one FIFO per agent**, with `enqueue` (Envoyer) and `preempt`
//! (Interrompre) kept distinct. This module owns the pure value objects
//! ([`InputSource`], [`AgentBusyState`]) and the [`InputMediator`] port.
//!
//! The mediator **absorbs** [`crate::mailbox::AgentMailbox`]: the existing mailbox
//! (FIFO + one-shot reply) is reused as the *correlation engine* inside the infra
//! adapter (`MediatedInbox`), rather than spawning a second concurrent queue. The
//! delegation mailbox is thus just **one input source among two**.
//!
//! Pure (no I/O): the FIFO, locks and busy bookkeeping live in the infra adapter.

use serde::{Deserialize, Serialize};

use crate::ids::AgentId;
use crate::mailbox::{PendingReply, Ticket, TicketId};
use crate::ports::PtyHandle;

/// Where a queued input came from.
///
/// Replaces the free-form `requester: String` as the **source of truth** (the
/// string remains a derived display label). Lets IdeA propagate the requester's
/// identity and feed the wait-for graph (cycle detection).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", tag = "kind")]
pub enum InputSource {
    /// The human operator (a `SubmitHumanInput` use case).
    Human,
    /// Another agent delegating via `idea_ask_agent`.
    #[serde(rename_all = "camelCase")]
    Agent {
        /// The delegating agent.
        agent_id: AgentId,
    },
}

impl InputSource {
    /// Convenience constructor for an agent source.
    #[must_use]
    pub const fn agent(agent_id: AgentId) -> Self {
        Self::Agent { agent_id }
    }

    /// Returns the [`AgentId`] when this source is an agent.
    #[must_use]
    pub const fn as_agent(&self) -> Option<AgentId> {
        match self {
            Self::Agent { agent_id } => Some(*agent_id),
            Self::Human => None,
        }
    }

    /// Whether the source is the human operator.
    #[must_use]
    pub const fn is_human(&self) -> bool {
        matches!(self, Self::Human)
    }
}

/// Whether an agent is currently processing a task.
///
/// Derived state, published to the front (`AgentBusyChanged`). An agent goes `Busy`
/// on the enqueue that **starts a turn**, and returns `Idle` on prompt-ready OR an
/// explicit signal (cf. cadrage §6). In doubt it stays `Busy`, but the FIFO keeps
/// accepting enqueues (forward, never reject the sender).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", tag = "state")]
pub enum AgentBusyState {
    /// No turn in flight; the next enqueued ticket can start.
    Idle,
    /// A turn is in flight.
    #[serde(rename_all = "camelCase")]
    Busy {
        /// The ticket whose turn is running.
        ticket: TicketId,
        /// When the turn started (epoch millis), for UI age display.
        since_ms: u64,
    },
}

/// Whether an agent currently shows **signs of life** during a turn (lot 2,
/// chantier readiness/heartbeat — détection « Stalled »).
///
/// Orthogonal to [`AgentBusyState`]: an agent can be `Busy` **and** `Alive` (it is
/// working, producing deltas/heartbeats) or `Busy` **and** `Stalled` (no proof of
/// liveness for longer than its profile's `stall_after_ms`). Only meaningful while
/// `Busy`; an `Idle` agent is considered `Alive` (its turn ended cleanly).
///
/// Published to the front (`AgentLivenessChanged`) **once per transition** so the UI
/// can badge a frozen agent without event spam. Derived from the per-agent
/// `last_seen_ms` heartbeat, never from parsing the model output.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", tag = "liveness")]
pub enum AgentLiveness {
    /// The agent is producing proof of liveness (deltas / tool activity /
    /// heartbeats) within its `stall_after_ms` window — or is idle.
    Alive,
    /// No proof of liveness for longer than the profile's `stall_after_ms`: the
    /// agent is presumed frozen. Advisory only — the FIFO and the turn keep running
    /// (a late heartbeat flips it back to [`Self::Alive`]).
    Stalled,
}

impl AgentBusyState {
    /// Whether a turn is currently in flight.
    #[must_use]
    pub const fn is_busy(&self) -> bool {
        matches!(self, Self::Busy { .. })
    }

    /// The in-flight ticket, if any.
    #[must_use]
    pub const fn ticket(&self) -> Option<TicketId> {
        match self {
            Self::Busy { ticket, .. } => Some(*ticket),
            Self::Idle => None,
        }
    }
}

/// Per-agent submit configuration (ARCHITECTURE §20.3), resolved from the target
/// agent's profile and carried to the [`InputMediator`] at bind time so it can be
/// echoed on a [`crate::events::DomainEvent::DelegationReady`].
///
/// Both fields stay `Option`: the **default** (`"\r"`, ~60 ms) is applied at the
/// point of use (the frontend write-portal), never hard-coded in the domain — the
/// domain only transports the declared values.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct SubmitConfig {
    /// The profile's `submit_sequence` (e.g. `"\r"`). `None` ⇒ front default.
    pub sequence: Option<String>,
    /// The profile's `submit_delay_ms`. `None` ⇒ front default (~60 ms).
    pub delay_ms: Option<u32>,
}

impl SubmitConfig {
    /// Builds a submit config from the two optional profile fields.
    #[must_use]
    pub const fn new(sequence: Option<String>, delay_ms: Option<u32>) -> Self {
        Self { sequence, delay_ms }
    }
}

/// The single convergence point of **all** of an agent's input (one FIFO/agent),
/// with `enqueue` (Envoyer) and `preempt` (Interrompre) kept **distinct**, plus the
/// busy state.
///
/// Object-safe (held as `Arc<dyn InputMediator>`). The infra adapter `MediatedInbox`
/// composes the existing [`crate::mailbox::AgentMailbox`] (correlation by ticket) +
/// turn locks + busy bookkeeping — it does **not** create a second queue.
pub trait InputMediator: Send + Sync {
    /// Envoyer = enqueue: appends `ticket` to `agent`'s FIFO and returns the
    /// [`PendingReply`] to await (the mailbox reply type, reused).
    ///
    /// **Delivery** (ARCHITECTURE §20.2/§20.3): the mediator **no longer writes the
    /// turn into the PTY** (the `\n` band-aid is gone — it never submitted in raw mode
    /// and produced a "double chat"). Instead, on the enqueue that **starts a turn**
    /// (Idle→Busy), the adapter publishes a [`crate::events::DomainEvent::DelegationReady`]
    /// carrying the task text + ticket + the target's [`SubmitConfig`]; the frontend
    /// cell (sole owner of the terminal) runs the write-portal handshake and writes the
    /// text + submit sequence through the single PTY writer. The mediator stays the
    /// authority of the FIFO/busy state and correlation only.
    fn enqueue(&self, agent: AgentId, ticket: Ticket) -> PendingReply;

    /// Headless/system enqueue: appends `ticket` to the same FIFO and marks the agent
    /// busy, but does **not** deliver any text to the human terminal surface.
    ///
    /// This is for inter-agent structured/headless turns where the real transport is
    /// [`crate::ports::AgentSession::send`] and the answer is its final response. The
    /// mailbox ticket remains useful for queue/workstate accounting and cancellation,
    /// but publishing [`crate::events::DomainEvent::DelegationReady`] here would leak
    /// the same task into the user's CLI cell.
    ///
    /// Default keeps compatibility for simple mediators; production overrides it to
    /// suppress delivery.
    fn enqueue_silent(&self, agent: AgentId, ticket: Ticket) -> PendingReply {
        self.enqueue(agent, ticket)
    }

    /// Registers (or refreshes) the live input [`PtyHandle`] of `agent` so a later
    /// [`InputMediator::enqueue`] delivers the turn through it (cadrage C3 §5.2).
    ///
    /// Keyed by **agent** (one live input stream per agent — «1 agent = 1 employee»);
    /// the orchestrator calls this once it has resolved/launched the agent's live
    /// session for the target conversation. Default: no-op (a mediator that does not
    /// own the delivery write).
    fn bind_handle(&self, _agent: AgentId, _handle: PtyHandle) {}

    /// Like [`InputMediator::bind_handle`], but also records the target's
    /// [`SubmitConfig`] (ARCHITECTURE §20.3) so the adapter can echo
    /// `submit_sequence`/`submit_delay_ms` on the
    /// [`crate::events::DomainEvent::DelegationReady`] published when a turn starts.
    /// The bind is the natural carrier: the submit config is per-agent profile data the
    /// orchestrator resolves at bind time, so it travels with the handle (no extra
    /// ticket field, no second resolve).
    ///
    /// Turn-end detection is **no longer** armed here: the dead PTY prompt-ready sniff
    /// was replaced by the transcript [`crate::ports::TurnWatcher`] (armed at the
    /// composition root, once per live session) which calls
    /// [`InputMediator::turn_ended`].
    ///
    /// Default: delegates to [`InputMediator::bind_handle`], ignoring the submit config.
    /// The infra adapter overrides it to stash the submit config.
    fn bind_handle_with_submit(&self, agent: AgentId, handle: PtyHandle, _submit: SubmitConfig) {
        self.bind_handle(agent, handle);
    }

    /// **Deprecated since ARCHITECTURE §20**: the mediator never writes the turn into
    /// the PTY anymore — the **frontend** always delivers (via the write-portal). Kept
    /// for source compatibility; it now always returns `false`. Callers should stop
    /// branching on it (the orchestrator no longer falls back to its own PTY write).
    #[must_use]
    fn delivers_turn(&self, _agent: AgentId) -> bool {
        false
    }

    /// Déclare qu'`agent` vient d'être **lancé à froid** : la livraison de son tout
    /// premier tour doit être *gatée* sur la readiness MCP (la `DelegationReady` est
    /// différée jusqu'à la connexion du pont MCP du CLI), pour éviter d'écrire la tâche
    /// avant que le CLI ait fini de booter (premier tour perdu sinon).
    ///
    /// À n'appeler **que** lorsqu'un signal de readiness le libérera (le profil porte un
    /// pont MCP ⇒ [`InputMediator::release_cold_start`]). Sans signal pour le libérer,
    /// gater le premier tour le bloquerait indéfiniment ; dans ce cas l'orchestrateur ne
    /// doit **pas** appeler `mark_starting` et l'`enqueue` livre la tâche immédiatement
    /// (chemin chaud, fallback sûr, zéro régression).
    ///
    /// Default: no-op (a mediator that does not gate cold starts).
    fn mark_starting(&self, _agent: AgentId) {}

    /// Signal de **readiness de démarrage** : le pont MCP de `agent` vient de se
    /// connecter (son CLI est up et a chargé les outils `idea_*`). Si un premier tour
    /// a été différé par [`InputMediator::mark_starting`] (démarrage à froid), c'est le
    /// moment de le livrer ⇒ draine le `DelegationReady` retenu. **Contrairement à
    /// `turn_ended`, aucun repli `mark_idle`** : c'est un signal de DÉMARRAGE, pas de
    /// fin de tour. Idempotent : un second appel ne trouve rien et est un no-op. C'est
    /// désormais le **seul** drain du tour différé (le signal MCP-initialize), le
    /// watcher prompt-ready PTY ayant été supprimé.
    ///
    /// Default: no-op (médiateur qui ne gate pas les démarrages à froid).
    fn release_cold_start(&self, _agent: AgentId) {}

    /// Déclare si une **cellule terminal du frontend** est montée pour `agent`
    /// (`true` au montage du write-portal, `false` au démontage). C'est le frontend
    /// qui possède l'écriture physique du PTY *quand une cellule existe* (cas d'un
    /// agent épinglé dans le layout) ; un agent **délégué en arrière-plan n'a aucune
    /// cellule**, donc personne ne consomme `DelegationReady` côté UI. Le médiateur
    /// utilise cet état pour décider, à la livraison d'un tour, entre **publier
    /// l'événement** (cellule présente ⇒ le front écrit) et **écrire lui-même** la
    /// tâche dans le PTY (agent headless ⇒ sinon le tour est perdu).
    ///
    /// Default: no-op (médiateur sans notion de cellule front — tout passe par
    /// l'événement, comportement historique).
    fn set_front_attached(&self, _agent: AgentId, _attached: bool) {}

    /// Interrompre = preempt: signals the running turn to stop (Échap/stop). This
    /// is **not** an enqueue and correlates **no** ticket.
    fn preempt(&self, agent: AgentId);

    /// Marks `agent` free (explicit signal) so its FIFO advances.
    fn mark_idle(&self, agent: AgentId);

    /// **End-of-turn** signal: the agent's transcript just recorded a completed turn
    /// (the [`crate::ports::TurnWatcher`] fired), and **no** `idea_reply` carried a
    /// result. This is the no-reply backstop trigger: the adapter captures the active
    /// ticket, marks the agent `Idle` (advancing the FIFO), then arms a short **grace**
    /// window — if no `idea_reply` correlates the ticket by then, it completes the turn
    /// "without reply" (waking a parked caller with a typed error instead of leaving it
    /// blocked until the long timeout). A late `idea_reply` within the grace wins.
    ///
    /// Replaces the former prompt-ready watcher branch verbatim; only the **trigger**
    /// changed (transcript `turn_duration` instead of a PTY prompt sigil). Default:
    /// [`InputMediator::mark_idle`] (a mediator with no mailbox/grace just advances).
    fn turn_ended(&self, agent: AgentId) {
        self.mark_idle(agent);
    }

    /// Records a **proof of liveness** (« battement ») for `agent` — called on every
    /// non-terminal turn event (text delta, tool activity, [`crate::ports::ReplyEvent::Heartbeat`])
    /// by the drain loop. Refreshes the per-agent `last_seen` timestamp so the stall
    /// detector ([`crate::events::DomainEvent::AgentLivenessChanged`], lot 2) knows the
    /// agent is still working; a battement arriving after a `Stalled` transition flips it
    /// back to [`AgentLiveness::Alive`].
    ///
    /// Default: no-op (a mediator that does not track liveness). The infra adapter
    /// `MediatedInbox` overrides it to refresh `last_seen` and publish an
    /// `AgentLivenessChanged{Stalled→Alive}` recovery on the first late battement.
    fn mark_alive(&self, _agent: AgentId) {}

    /// Declares the agent's **stall threshold** (its profile's
    /// [`crate::profile::LivenessStrategy::stall_after_ms`]) so the stall detector knows
    /// how long without a battement means "frozen" (lot 2). The orchestrator resolves it
    /// from the target's profile and calls this **before** the enqueue that starts a
    /// turn; the adapter stashes it and arms a fresh liveness window when the turn
    /// begins. `None` ⇒ no stall detection for this agent (legacy / no `liveness` block —
    /// zero regression).
    ///
    /// Default: no-op (a mediator that does not track liveness).
    fn set_stall_threshold(&self, _agent: AgentId, _stall_after_ms: Option<u32>) {}

    /// The current [`AgentBusyState`] of `agent`.
    fn busy_state(&self, agent: AgentId) -> AgentBusyState;
}

#[cfg(test)]
mod tests {
    use super::*;

    fn agent(n: u128) -> AgentId {
        AgentId::from_uuid(uuid::Uuid::from_u128(n))
    }

    fn ticket(n: u128) -> TicketId {
        TicketId::from_uuid(uuid::Uuid::from_u128(n))
    }

    #[test]
    fn input_source_human_vs_agent() {
        assert!(InputSource::Human.is_human());
        assert_eq!(InputSource::Human.as_agent(), None);
        let s = InputSource::agent(agent(1));
        assert!(!s.is_human());
        assert_eq!(s.as_agent(), Some(agent(1)));
    }

    #[test]
    fn busy_state_transitions_and_accessors() {
        let idle = AgentBusyState::Idle;
        assert!(!idle.is_busy());
        assert_eq!(idle.ticket(), None);

        let busy = AgentBusyState::Busy {
            ticket: ticket(7),
            since_ms: 1000,
        };
        assert!(busy.is_busy());
        assert_eq!(busy.ticket(), Some(ticket(7)));
        assert_ne!(idle, busy);
    }
}
