//! Phase orchestration wiring: `StageRunner` + `ActionGraph` + Cedar + Vault.
//!
//! `PhaseOrchestrator` listens for syntaxis phase transitions and runs the
//! corresponding `ActionGraph` pipeline via `stratum-orchestrator`.

use std::{path::PathBuf, sync::Arc};

use anyhow::Result;
use serde::{Deserialize, Serialize};
use stratum_graph::ActionGraph;
use stratum_orchestrator::{
    auth::{CedarAuthorizer, VaultClient, VaultConfig},
    context::PipelineContext,
    executor::NuExecutor,
    graph::load_graph_from_dir,
    runner::StageRunner,
};
use stratum_state::{InMemoryStateTracker, PipelineStatus, StateTracker};
use tokio::sync::RwLock;
use tracing::info;

/// Configuration for `PhaseOrchestrator`.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OrchestratorConfig {
    /// Directory containing NuShell step scripts.
    pub scripts_base: PathBuf,
    /// Directory containing `.ncl` ActionGraph node definitions.
    pub graphs_dir: PathBuf,
    /// Directory containing capability `.ncl` schema files (used by `PipelineContext`).
    pub schema_dir: PathBuf,
    /// Enforce Cedar authorization on every pipeline node execution.
    pub cedar_required: bool,
    /// Directory containing `.cedar` policy files.
    ///
    /// Required only when `cedar_required = true`. When `cedar_required = false`
    /// a permissive allow-all policy is written to a temp directory automatically.
    pub cedar_policy_dir: Option<PathBuf>,
    /// HashiCorp Vault URL for credential injection.
    ///
    /// If empty and `cedar_required = false`, the Vault client is never invoked
    /// because no nodes are dispatched without a defined graph.
    #[serde(default)]
    pub vault_url: String,
    /// Vault token.
    #[serde(default)]
    pub vault_token: String,
}

impl OrchestratorConfig {
    /// In-process default for tests: ephemeral directories, no Cedar, no Vault calls.
    pub fn for_testing() -> Self {
        Self {
            scripts_base: std::env::temp_dir().join("syntaxis-scripts"),
            graphs_dir: std::env::temp_dir().join("syntaxis-graphs"),
            schema_dir: std::env::temp_dir().join("syntaxis-schema"),
            cedar_required: false,
            cedar_policy_dir: None,
            vault_url: String::new(),
            vault_token: String::new(),
        }
    }
}

/// Orchestrator that maps syntaxis phase transitions to `StageRunner` pipelines.
pub struct PhaseOrchestrator {
    runner: StageRunner,
    schema_dir: PathBuf,
}

impl std::fmt::Debug for PhaseOrchestrator {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("PhaseOrchestrator")
            .field("schema_dir", &self.schema_dir)
            .finish_non_exhaustive()
    }
}

impl PhaseOrchestrator {
    /// Construct from config, building all internal components.
    ///
    /// When `cedar_required = false` and `cedar_policy_dir` is absent, a
    /// permissive allow-all `.cedar` policy is written to a temp directory so
    /// `CedarAuthorizer::load_from_dir` can succeed without a real policy set.
    pub async fn from_config(
        cfg: &OrchestratorConfig,
        state: Arc<dyn StateTracker>,
    ) -> Result<Self> {
        // Load ActionGraph (empty dir produces an empty graph with a warning).
        let graph = if cfg.graphs_dir.exists() {
            load_graph_from_dir(&cfg.graphs_dir)?
        } else {
            ActionGraph::from_nodes(vec![])?
        };

        // Build Cedar authorizer.
        let cedar_dir = match &cfg.cedar_policy_dir {
            Some(dir) => dir.clone(),
            None => {
                // Write a permissive allow-all policy to a stable temp path.
                let tmp = std::env::temp_dir().join("syntaxis-cedar-permissive");
                std::fs::create_dir_all(&tmp)?;
                let policy_path = tmp.join("allow-all.cedar");
                std::fs::write(
                    &policy_path,
                    r#"permit(principal, action, resource);"#,
                )?;
                tmp
            }
        };

        let cedar = CedarAuthorizer::load_from_dir(&cedar_dir)?;

        let vault = VaultClient::new(VaultConfig {
            url: cfg.vault_url.clone(),
            token: cfg.vault_token.clone(),
        });

        // Ensure script base dir exists so NuExecutor can locate scripts.
        std::fs::create_dir_all(&cfg.scripts_base)?;
        std::fs::create_dir_all(&cfg.schema_dir)?;

        let runner = StageRunner {
            graph: Arc::new(RwLock::new(graph)),
            executor: Arc::new(NuExecutor::new(cfg.scripts_base.clone())),
            state,
            auth: Arc::new(cedar),
            vault: Arc::new(vault),
            cedar_required: cfg.cedar_required,
        };

        Ok(Self {
            runner,
            schema_dir: cfg.schema_dir.clone(),
        })
    }

    /// Build with an `InMemoryStateTracker` — intended for tests.
    pub async fn for_testing() -> Result<Self> {
        let cfg = OrchestratorConfig::for_testing();
        let state = Arc::new(InMemoryStateTracker::new()) as Arc<dyn StateTracker>;
        Self::from_config(&cfg, state).await
    }

    /// Called on every successful syntaxis phase transition.
    ///
    /// Publishes to subject `syntaxis.phase.<from>.<to>`, deposits `project_id`
    /// as the trigger payload, and runs the matching pipeline.
    ///
    /// Returns `PipelineStatus::Success` when there are no graph nodes to
    /// execute (empty or unmatched graph).
    pub async fn on_phase_transition(
        &self,
        project_id: &str,
        from_phase: &str,
        to_phase: &str,
    ) -> Result<PipelineStatus> {
        let subject = format!("syntaxis.phase.{from_phase}.{to_phase}");
        let payload = serde_json::json!({ "project_id": project_id });

        info!(
            project_id,
            from_phase,
            to_phase,
            subject = %subject,
            "phase transition — dispatching pipeline"
        );

        let ctx = Arc::new(
            PipelineContext::new(
                subject,
                payload,
                Arc::clone(&self.runner.state),
                self.schema_dir.clone(),
            )
            .await?,
        );

        self.runner.run_pipeline(ctx).await
    }
}

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

    #[tokio::test]
    async fn test_orchestrator_empty_graph_returns_success() {
        let orch = PhaseOrchestrator::for_testing()
            .await
            .expect("orchestrator construction");

        let status = orch
            .on_phase_transition("proj-001", "create", "devel")
            .await
            .expect("phase transition");

        assert_eq!(status, PipelineStatus::Success);
    }

    #[tokio::test]
    async fn test_orchestrator_custom_state_tracker() {
        let cfg = OrchestratorConfig::for_testing();
        let state = Arc::new(InMemoryStateTracker::new()) as Arc<dyn StateTracker>;
        let orch = PhaseOrchestrator::from_config(&cfg, Arc::clone(&state))
            .await
            .expect("orchestrator from config");

        let status = orch
            .on_phase_transition("proj-002", "devel", "publish")
            .await
            .expect("phase transition");

        assert_eq!(status, PipelineStatus::Success);
    }
}
