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

use thiserror::Error;

use crate::agent::steering::TurnMailbox;
use crate::providers::ToolCall;

/// Presentation facts emitted while AgentLoop processes one model turn.
///
/// Events contain no persistence or channel-delivery decisions. Session owns
/// the lifecycle around these facts and reduces them into authoritative state.
#[derive(Debug, Clone)]
pub enum TurnEvent {
    ReasoningDelta {
        iteration: u32,
        delta: String,
    },
    TextDelta {
        iteration: u32,
        delta: String,
    },
    TextSegmentFinished {
        iteration: u32,
    },
    ToolStarted {
        iteration: u32,
        call: ToolCall,
    },
    ToolFinished {
        iteration: u32,
        call_id: String,
        success: bool,
        preview: Option<String>,
    },
}

#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum TurnEmitError {
    #[error("turn is no longer active")]
    Inactive,
    #[error("tool call {0} already exists in this turn")]
    DuplicateTool(String),
    #[error("tool call {0} does not exist in this turn")]
    UnknownTool(String),
}

type EmitFn = dyn Fn(TurnEvent) -> Result<(), TurnEmitError> + Send + Sync;

/// Cheap cloneable handle used by AgentLoop to report presentation facts.
#[derive(Clone)]
pub struct TurnEmitter {
    emit: Arc<EmitFn>,
    enabled: Arc<Mutex<bool>>,
}

/// Session-owned identity and emitter for one AgentLoop execution.
#[derive(Clone)]
pub struct AgentTurnContext {
    pub turn_id: String,
    pub message_id: String,
    pub emitter: TurnEmitter,
    /// Same-turn user input accepted while this turn is active.  Session owns
    /// the mailbox lifecycle; AgentLoop only drains it at safe boundaries.
    pub steering: Option<Arc<TurnMailbox>>,
}

impl AgentTurnContext {
    pub fn new(
        turn_id: impl Into<String>,
        message_id: impl Into<String>,
        emitter: TurnEmitter,
    ) -> Self {
        Self {
            turn_id: turn_id.into(),
            message_id: message_id.into(),
            emitter,
            steering: None,
        }
    }

    /// Construct a streaming context with a shared steering mailbox.
    pub fn new_with_steering(
        turn_id: impl Into<String>,
        message_id: impl Into<String>,
        emitter: TurnEmitter,
        steering: Arc<TurnMailbox>,
    ) -> Self {
        Self {
            turn_id: turn_id.into(),
            message_id: message_id.into(),
            emitter,
            steering: Some(steering),
        }
    }

    /// Attach a mailbox to an existing context.  This builder keeps the old
    /// `AgentTurnContext::new` call sites source-compatible.
    pub fn with_steering(mut self, steering: Arc<TurnMailbox>) -> Self {
        self.steering = Some(steering);
        self
    }

    /// Return a clone of the shared mailbox, if steering is enabled.
    pub fn steering(&self) -> Option<Arc<TurnMailbox>> {
        self.steering.clone()
    }
}

impl TurnEmitter {
    pub(crate) fn new<F>(emit: F) -> Self
    where
        F: Fn(TurnEvent) -> Result<(), TurnEmitError> + Send + Sync + 'static,
    {
        Self {
            emit: Arc::new(emit),
            enabled: Arc::new(Mutex::new(true)),
        }
    }

    pub fn emit(&self, event: TurnEvent) -> Result<(), TurnEmitError> {
        let enabled = self
            .enabled
            .lock()
            .unwrap_or_else(|poisoned| poisoned.into_inner());
        if !*enabled {
            return Ok(());
        }
        (self.emit)(event)
    }

    /// Stop forwarding new presentation facts without changing durable or
    /// terminal Turn status. Session uses this before invalidating a worker.
    pub fn deactivate(&self) {
        *self
            .enabled
            .lock()
            .unwrap_or_else(|poisoned| poisoned.into_inner()) = false;
    }
}
