use std::collections::HashMap;
use std::fmt;
use std::str::FromStr;
use std::time::Duration;

use serde::de::{DeserializeOwned, Error as DeError};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use serde_json::Value;
use strum::{Display, EnumString, IntoStaticStr};

use crate::{ExecOutputTail, FailureSignature, StageTiming, SystemActorKind};

pub trait OutcomeMeta:
    Default + Clone + Send + Sync + fmt::Debug + Serialize + DeserializeOwned + 'static
{
}

impl<T> OutcomeMeta for T where
    T: Default + Clone + Send + Sync + fmt::Debug + Serialize + DeserializeOwned + 'static
{
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StageOutcome {
    Succeeded,
    PartiallySucceeded,
    Failed { retry_requested: bool },
    Skipped,
}

impl StageOutcome {
    #[must_use]
    pub fn is_successful(self) -> bool {
        matches!(self, Self::Succeeded | Self::PartiallySucceeded)
    }

    #[must_use]
    pub fn is_failure(self) -> bool {
        matches!(self, Self::Failed { .. })
    }

    #[must_use]
    pub fn retry_requested(self) -> bool {
        matches!(self, Self::Failed {
            retry_requested: true,
        })
    }
}

impl fmt::Display for StageOutcome {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Succeeded => write!(f, "succeeded"),
            Self::PartiallySucceeded => write!(f, "partially_succeeded"),
            Self::Failed { .. } => write!(f, "failed"),
            Self::Skipped => write!(f, "skipped"),
        }
    }
}

impl FromStr for StageOutcome {
    type Err = String;

    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
        match s {
            "succeeded" => Ok(Self::Succeeded),
            "partially_succeeded" => Ok(Self::PartiallySucceeded),
            "failed" => Ok(Self::Failed {
                retry_requested: false,
            }),
            "skipped" => Ok(Self::Skipped),
            other => Err(format!("unknown stage outcome: {other}")),
        }
    }
}

impl Serialize for StageOutcome {
    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        serializer.serialize_str(&self.to_string())
    }
}

impl<'de> Deserialize<'de> for StageOutcome {
    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        let value = String::deserialize(deserializer)?;
        value.parse().map_err(DeError::custom)
    }
}

#[derive(
    Debug,
    Clone,
    Copy,
    PartialEq,
    Eq,
    Hash,
    Serialize,
    Deserialize,
    Display,
    EnumString,
    IntoStaticStr,
)]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]
pub enum StageState {
    Pending,
    Running,
    Retrying,
    Succeeded,
    PartiallySucceeded,
    Failed,
    Skipped,
    Cancelled,
}

impl StageState {
    #[must_use]
    pub fn is_terminal(self) -> bool {
        matches!(
            self,
            Self::Succeeded
                | Self::PartiallySucceeded
                | Self::Failed
                | Self::Skipped
                | Self::Cancelled
        )
    }
}

impl From<StageOutcome> for StageState {
    fn from(outcome: StageOutcome) -> Self {
        match outcome {
            StageOutcome::Succeeded => Self::Succeeded,
            StageOutcome::PartiallySucceeded => Self::PartiallySucceeded,
            StageOutcome::Failed {
                retry_requested: true,
            } => Self::Retrying,
            StageOutcome::Failed {
                retry_requested: false,
            } => Self::Failed,
            StageOutcome::Skipped => Self::Skipped,
        }
    }
}

#[cfg(test)]
mod stage_state_tests {
    use super::{StageOutcome, StageState};

    #[test]
    fn retry_requested_failure_projects_as_retrying() {
        assert_eq!(
            StageState::from(StageOutcome::Failed {
                retry_requested: true,
            }),
            StageState::Retrying
        );
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum FailureCategory {
    TransientInfra,
    Deterministic,
    BudgetExhausted,
    CompilationLoop,
    Canceled,
    Structural,
}

impl fmt::Display for FailureCategory {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let s = match self {
            Self::TransientInfra => "transient_infra",
            Self::Deterministic => "deterministic",
            Self::BudgetExhausted => "budget_exhausted",
            Self::CompilationLoop => "compilation_loop",
            Self::Canceled => "canceled",
            Self::Structural => "structural",
        };
        write!(f, "{s}")
    }
}

impl FromStr for FailureCategory {
    type Err = std::convert::Infallible;

    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
        let normalized = s.trim().to_lowercase();
        Ok(match normalized.as_str() {
            "transient_infra"
            | "transient"
            | "transient-infra"
            | "infra_transient"
            | "transient infra"
            | "infrastructure_transient"
            | "retryable"
            | "toolchain_workspace_io"
            | "toolchain-workspace-io"
            | "toolchain_or_dependency_registry_unavailable"
            | "toolchain-dependency-registry-unavailable" => Self::TransientInfra,
            "budget_exhausted" | "budget-exhausted" | "budget exhausted" | "budget" => {
                Self::BudgetExhausted
            }
            "compilation_loop" | "compilation-loop" | "compilation loop" | "compile_loop"
            | "compile-loop" => Self::CompilationLoop,
            "canceled" | "cancelled" => Self::Canceled,
            "structural" | "structure" | "scope_violation" | "write_scope_violation" => {
                Self::Structural
            }
            // "deterministic" and all unrecognized values
            _ => Self::Deterministic,
        })
    }
}

impl FailureCategory {
    pub fn is_signature_tracked(self) -> bool {
        matches!(self, Self::Deterministic | Self::Structural)
    }
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct FailureDetail {
    pub message:          String,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub causes:           Vec<String>,
    pub category:         FailureCategory,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub system_actor:     Option<SystemActorKind>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub signature:        Option<FailureSignature>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub exec_output_tail: Option<ExecOutputTail>,
}

impl FailureDetail {
    pub fn new(message: impl Into<String>, category: FailureCategory) -> Self {
        Self {
            message: message.into(),
            causes: Vec::new(),
            category,
            system_actor: None,
            signature: None,
            exec_output_tail: None,
        }
    }
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(bound = "M: OutcomeMeta")]
pub struct Outcome<M: OutcomeMeta = ()> {
    pub status:             StageOutcome,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub preferred_label:    Option<String>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub suggested_next_ids: Vec<String>,
    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
    pub context_updates:    HashMap<String, Value>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub jump_to_node:       Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub notes:              Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub failure:            Option<FailureDetail>,
    #[serde(default)]
    pub usage:              M,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub files_touched:      Vec<String>,
    /// Stage timing breakdown captured by the workflow engine.
    ///
    /// `None` until the stage produces a terminal outcome with a measured
    /// wall time. Stage handlers that perform no inference or tool work
    /// populate this with [`StageTiming::wall_only`].
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub timing:             Option<StageTiming>,
}

impl<M: OutcomeMeta> Default for Outcome<M> {
    fn default() -> Self {
        Self {
            status:             StageOutcome::Succeeded,
            preferred_label:    None,
            suggested_next_ids: Vec::new(),
            context_updates:    HashMap::new(),
            jump_to_node:       None,
            notes:              None,
            failure:            None,
            usage:              M::default(),
            files_touched:      Vec::new(),
            timing:             None,
        }
    }
}

impl<M: OutcomeMeta> Outcome<M> {
    pub fn success() -> Self {
        Self::default()
    }

    pub fn fail(message: &str) -> Self {
        Self {
            status: StageOutcome::Failed {
                retry_requested: false,
            },
            failure: Some(FailureDetail {
                message:          message.to_string(),
                causes:           Vec::new(),
                category:         FailureCategory::Deterministic,
                system_actor:     None,
                signature:        None,
                exec_output_tail: None,
            }),
            ..Self::default()
        }
    }

    pub fn skipped(reason: &str) -> Self {
        Self {
            status: StageOutcome::Skipped,
            notes: Some(reason.to_string()),
            ..Self::default()
        }
    }
}

#[cfg(test)]
mod tests {
    use serde_json::json;

    use super::{FailureCategory, FailureDetail, StageOutcome, StageState};

    #[test]
    fn stage_outcome_failed_serde_is_lossy_for_retry_intent() {
        assert_eq!(
            serde_json::to_value(StageOutcome::Failed {
                retry_requested: true,
            })
            .unwrap(),
            json!("failed")
        );
        assert_eq!(
            serde_json::from_value::<StageOutcome>(json!("failed")).unwrap(),
            StageOutcome::Failed {
                retry_requested: false,
            }
        );
    }

    #[test]
    fn stage_state_projects_terminal_outcomes() {
        assert_eq!(
            StageState::from(StageOutcome::Succeeded),
            StageState::Succeeded
        );
        assert_eq!(
            StageState::from(StageOutcome::PartiallySucceeded),
            StageState::PartiallySucceeded
        );
        assert_eq!(
            StageState::from(StageOutcome::Failed {
                retry_requested: true,
            }),
            StageState::Retrying
        );
        assert!(StageState::Cancelled.is_terminal());
        assert!(!StageState::Retrying.is_terminal());
        assert!(!StageState::Running.is_terminal());
    }

    #[test]
    fn failure_detail_serializes_rich_diagnostics() {
        use crate::{ExecOutputTail, FailureSignature};

        let mut failure = FailureDetail::new("ACP turn failed", FailureCategory::Deterministic);
        failure.causes = vec![
            "ACP protocol error".to_string(),
            "agent exited before initialize completed".to_string(),
        ];
        failure.signature = Some(FailureSignature("work|deterministic|acp".to_string()));
        failure.exec_output_tail = Some(ExecOutputTail {
            stdout:           None,
            stderr:           Some("redacted stderr tail".to_string()),
            stdout_truncated: false,
            stderr_truncated: true,
        });

        let value = serde_json::to_value(&failure).expect("failure detail should serialize");

        assert_eq!(value["message"], "ACP turn failed");
        assert_eq!(value["causes"][0], "ACP protocol error");
        assert_eq!(value["category"], "deterministic");
        assert_eq!(value["signature"], "work|deterministic|acp");
        assert_eq!(value["exec_output_tail"]["stderr"], "redacted stderr tail");
        assert_eq!(value["exec_output_tail"]["stderr_truncated"], true);
        assert!(value.get("failure_class").is_none());
        assert!(value.get("failure_signature").is_none());
    }
}

#[derive(Debug, Clone)]
pub struct NodeResult<M: OutcomeMeta = ()> {
    pub outcome:        Outcome<M>,
    /// Wall-clock time spent executing this node attempt (including handler
    /// internal waits). Independent of the `inference_time` / `tool_time`
    /// breakdown — those are work-only measurements.
    pub wall_time:      Duration,
    /// Sum of LLM request/stream elapsed time across this node attempt. Zero
    /// for non-LLM handlers.
    pub inference_time: Duration,
    /// Sum of tool/command execution elapsed time across this node attempt.
    /// Zero for handlers that do not invoke tools or commands.
    pub tool_time:      Duration,
    pub attempts:       u32,
    pub max_attempts:   u32,
}

impl<M: OutcomeMeta> NodeResult<M> {
    pub fn new(
        outcome: Outcome<M>,
        wall_time: Duration,
        inference_time: Duration,
        tool_time: Duration,
        attempts: u32,
        max_attempts: u32,
    ) -> Self {
        Self {
            outcome,
            wall_time,
            inference_time,
            tool_time,
            attempts,
            max_attempts,
        }
    }

    pub fn from_skip(outcome: Outcome<M>) -> Self {
        Self {
            outcome,
            wall_time: Duration::ZERO,
            inference_time: Duration::ZERO,
            tool_time: Duration::ZERO,
            attempts: 0,
            max_attempts: 0,
        }
    }
}
