//! [`AppError`] — the single error type returned by every use case.
//!
//! Per-port errors from the domain ([`domain::ports::FsError`],
//! [`domain::ports::StoreError`], …) are mapped into this application-level
//! error so that the presentation layer (Tauri commands) only ever has to deal
//! with one error shape when building its `ErrorDTO`.

use domain::ports::{
    AgentSessionError, EmbedderError, FsError, GitError, MemoryError, ModelServerError,
    ProcessError, PtyError, RemoteError, RuntimeError, StoreError,
};
use domain::{AgentId, NodeId};
use domain::{IssueStoreError, SprintStoreError};

/// Errors surfaced by application use cases.
///
/// Each variant carries a stable, machine-readable `code` (see [`AppError::code`])
/// so the presentation layer can map it to an `ErrorDTO` without string matching.
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum AppError {
    /// A requested resource was not found.
    #[error("not found: {0}")]
    NotFound(String),

    /// The input failed a domain/application invariant.
    #[error("invalid input: {0}")]
    Invalid(String),

    /// A filesystem operation failed.
    #[error("filesystem error: {0}")]
    FileSystem(String),

    /// A persistence (store) operation failed.
    #[error("store error: {0}")]
    Store(String),

    /// A process/PTY/runtime operation failed.
    #[error("process error: {0}")]
    Process(String),

    /// A local model server could not be prepared for launch.
    #[error("model server error ({code}): {message}")]
    ModelServer {
        /// Stable model-server error code.
        code: String,
        /// Human-readable message.
        message: String,
    },

    /// A git operation failed.
    #[error("git error: {0}")]
    Git(String),

    /// A remote (SSH/WSL) operation failed.
    #[error("remote error: {0}")]
    Remote(String),

    /// An agent is already running in a live cell and cannot be launched again
    /// (the "one live session per agent" invariant). Reuse goes through
    /// templates (instantiate → distinct agents); IdeA never multi-instances a
    /// single agent.
    #[error("agent {agent_id} is already running in cell {node_id}")]
    AgentAlreadyRunning {
        /// The agent that is already live.
        agent_id: AgentId,
        /// The layout cell (leaf) currently hosting that agent's live session.
        node_id: NodeId,
    },

    /// The delegation target finished its turn and **returned to its prompt without
    /// calling `idea_reply`**, so the rendezvous could produce no answer. Surfaced
    /// promptly (after a short grace window) instead of blocking the caller until the
    /// long safety timeout. **Retryable**: the caller may re-ask, ideally reminding the
    /// target to render its result via `idea_reply` (never plain text). Carries the
    /// target's display name.
    #[error(
        "agent {0} returned to its prompt without calling idea_reply (no answer was \
         rendered); retry the request and ensure the agent replies via idea_reply"
    )]
    TargetReturnedNoReply(String),

    /// The delegation target is **still actively working** (its transcript kept
    /// growing across liveness probes) but the rendezvous **absolute ceiling** was
    /// reached, so the synchronous wait was abandoned. **Distinct** from
    /// [`Self::TargetReturnedNoReply`]: the target is *not* silent, so a blind retry
    /// would stack a second heavy turn on a target already mid-task. **Not** a mute
    /// timeout — the caller should check the target's in-progress work/branch (it may
    /// still finish and call `idea_reply`) before re-soliciting lightly. Carries the
    /// target's display name.
    #[error(
        "target {0} is still actively working but the rendezvous ceiling was reached; \
         do not retry blindly — check the target's in-progress work/branch (it may still \
         finish and call idea_reply), then re-solicit lightly if needed"
    )]
    TargetCeilingActive(String),

    /// An unexpected internal error.
    #[error("internal error: {0}")]
    Internal(String),
}

impl AppError {
    /// A stable, machine-readable code for this error, intended for the
    /// `ErrorDTO` so the frontend can branch without parsing messages.
    #[must_use]
    pub fn code(&self) -> &'static str {
        match self {
            Self::NotFound(_) => "NOT_FOUND",
            Self::Invalid(_) => "INVALID",
            Self::FileSystem(_) => "FILESYSTEM",
            Self::Store(_) => "STORE",
            Self::Process(_) => "PROCESS",
            Self::ModelServer { .. } => "MODEL_SERVER",
            Self::Git(_) => "GIT",
            Self::Remote(_) => "REMOTE",
            Self::AgentAlreadyRunning { .. } => "AGENT_ALREADY_RUNNING",
            Self::TargetReturnedNoReply(_) => "TARGET_RETURNED_NO_REPLY",
            Self::TargetCeilingActive(_) => "RENDEZVOUS_CEILING_ACTIVE",
            Self::Internal(_) => "INTERNAL",
        }
    }
}

impl From<FsError> for AppError {
    fn from(e: FsError) -> Self {
        match e {
            FsError::NotFound(p) => Self::NotFound(p),
            other => Self::FileSystem(other.to_string()),
        }
    }
}

impl From<StoreError> for AppError {
    fn from(e: StoreError) -> Self {
        match e {
            StoreError::NotFound => Self::NotFound("store item".to_owned()),
            other => Self::Store(other.to_string()),
        }
    }
}

impl From<IssueStoreError> for AppError {
    fn from(e: IssueStoreError) -> Self {
        match e {
            IssueStoreError::NotFound => Self::NotFound("issue".to_owned()),
            IssueStoreError::VersionConflict { .. } => Self::Invalid(e.to_string()),
            IssueStoreError::Invalid(message) => Self::Invalid(message),
            IssueStoreError::Store(message) => Self::Store(message),
        }
    }
}

impl From<SprintStoreError> for AppError {
    fn from(e: SprintStoreError) -> Self {
        match e {
            SprintStoreError::NotFound => Self::NotFound("sprint".to_owned()),
            SprintStoreError::VersionConflict { .. } => Self::Invalid(e.to_string()),
            SprintStoreError::Invalid(message) => Self::Invalid(message),
            SprintStoreError::Store(message) => Self::Store(message),
        }
    }
}

impl From<MemoryError> for AppError {
    fn from(e: MemoryError) -> Self {
        match e {
            MemoryError::NotFound => Self::NotFound("memory note".to_owned()),
            MemoryError::Frontmatter(m) => Self::Invalid(m),
            other => Self::Store(other.to_string()),
        }
    }
}

impl From<EmbedderError> for AppError {
    /// Maps to [`AppError::Store`] — an embedder is a *derived* recall detail; its
    /// failure must degrade the recall (fallback to naïve), never fail hard. This
    /// mapping exists for completeness; recall adapters degrade *before* an
    /// embedder error ever reaches a use case.
    fn from(e: EmbedderError) -> Self {
        Self::Store(e.to_string())
    }
}

impl From<PtyError> for AppError {
    fn from(e: PtyError) -> Self {
        Self::Process(e.to_string())
    }
}

impl From<ProcessError> for AppError {
    fn from(e: ProcessError) -> Self {
        Self::Process(e.to_string())
    }
}

impl From<ModelServerError> for AppError {
    fn from(e: ModelServerError) -> Self {
        Self::ModelServer {
            code: model_server_error_code(&e).to_owned(),
            message: e.to_string(),
        }
    }
}

fn model_server_error_code(err: &ModelServerError) -> &'static str {
    match err {
        ModelServerError::NotConfigured => "not_configured",
        ModelServerError::Invalid(_) => "invalid",
        ModelServerError::PermissionDenied(_) => "permission_denied",
        ModelServerError::PathNotAccessible(_) => "path_not_accessible",
        ModelServerError::PortOccupied(_) => "port_occupied",
        ModelServerError::InUse(_) => "model_server_in_use",
        ModelServerError::Probe(_) => "probe",
        ModelServerError::Process(_) => "process",
        ModelServerError::Store(_) => "store",
        ModelServerError::Cancelled => "cancelled",
        ModelServerError::Timeout => "timeout",
    }
}

impl From<RuntimeError> for AppError {
    fn from(e: RuntimeError) -> Self {
        Self::Process(e.to_string())
    }
}

impl From<GitError> for AppError {
    fn from(e: GitError) -> Self {
        Self::Git(e.to_string())
    }
}

impl From<RemoteError> for AppError {
    fn from(e: RemoteError) -> Self {
        Self::Remote(e.to_string())
    }
}

impl From<AgentSessionError> for AppError {
    /// Maps a structured [`AgentSessionError`] (ARCHITECTURE §17.1) onto the single
    /// application error shape. `Start`/`Io`/`Decode`/`Timeout` are all execution
    /// failures of a live structured session (a process/SDK conversation), so they
    /// fold into [`AppError::Process`] — coherent with [`PtyError`]/[`ProcessError`],
    /// the byte-stream twins. The raw CLI JSON never travels through `Decode` (the
    /// adapter already redacted it), so this is safe to surface.
    fn from(e: AgentSessionError) -> Self {
        Self::Process(e.to_string())
    }
}

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

    /// La cible-active-au-plafond porte un code **stable et distinct** du no-reply
    /// (l'appelant peut brancher sans parser le message) et un message « ne retente
    /// pas à l'aveugle » différent du retour-au-prompt-sans-reply.
    #[test]
    fn ceiling_active_code_is_stable_and_distinct() {
        let ceiling = AppError::TargetCeilingActive("architect".to_owned());
        assert_eq!(ceiling.code(), "RENDEZVOUS_CEILING_ACTIVE");

        let no_reply = AppError::TargetReturnedNoReply("architect".to_owned());
        assert_eq!(no_reply.code(), "TARGET_RETURNED_NO_REPLY");

        // Deux issues TYPÉES distinctes (le cœur du cadrage Architect) : un plafond
        // atteint sur cible active n'est jamais confondu avec un silence/no-reply.
        assert_ne!(ceiling.code(), no_reply.code());
        // Et distinct d'un timeout muet (`Process` via AgentSessionError::Timeout).
        assert_ne!(
            ceiling.code(),
            AppError::from(AgentSessionError::Timeout).code()
        );
        // Le message guide explicitement vers « ne pas retenter à l'aveugle ».
        assert!(ceiling.to_string().contains("do not retry blindly"));
    }
}
