//! Infrastructure state service — NATS subscriber + SurrealDB writer.
//!
//! Bridges the provisioning DAG event stream to the `NS infra DB provisioning`
//! schema. Three responsibilities:
//!
//! 1. **Step tracking** — translates `provisioning.dag.formula.*` events into
//!    `dag_run` + `step_state` records in SurrealDB.
//! 2. **DAG run creation** — on `provisioning.infra.dag_run.create` (published
//!    by radicle-nats-adapter on patch merge), creates the `dag_run` record and
//!    maps the workflow_id for subsequent step events.
//! 3. **Reconciler** — on `provisioning.infra.reconcile.workspace`, compares
//!    SurrealDB `resource` records against the live Hetzner API and marks
//!    divergent resources as `drifted`.

use std::collections::HashMap;
use std::sync::Arc;

use chrono::Utc;
use futures::StreamExt;
use platform_db::SurrealPool;
use platform_nats::NatsBridge;
use reqwest::Client as HttpClient;
use serde::{Deserialize, Serialize};
use tokio::sync::RwLock;
use tracing::{error, info, warn};

// ── NATS subjects ──────────────────────────────────────────────────────────────

const SUBJ_FORMULA_WILDCARD: &str = "provisioning.dag.formula.*";
const SUBJ_DAG_RUN_CREATE: &str = "provisioning.infra.dag_run.create";
const SUBJ_RECONCILE: &str = "provisioning.infra.reconcile.workspace";

// ── NATS payload types ─────────────────────────────────────────────────────────

#[derive(Debug, Deserialize)]
struct FormulaEvent {
    workflow_id: String,
    formula_id: String,
    workspace: Option<String>,
    error: Option<String>,
}

#[derive(Debug, Deserialize)]
pub(crate) struct DagRunCreatePayload {
    pub(crate) workspace: String,
    pub(crate) trigger: String,
    pub(crate) trigger_ref: Option<String>,
    pub(crate) actor: Option<String>,
    /// Pre-assigned workflow_id so formula events can be correlated immediately.
    pub(crate) workflow_id: Option<String>,
}

#[derive(Debug, Deserialize)]
struct ReconcilePayload {
    workspace: String,
}

// ── Hetzner API response shapes (minimal) ──────────────────────────────────────

#[derive(Debug, Deserialize)]
struct HetznerServer {
    id: u64,
    name: String,
    status: String,
}

#[derive(Debug, Deserialize)]
struct HetznerFip {
    id: u64,
    name: String,
    ip: String,
    server: Option<u64>,
}

#[derive(Debug, Deserialize)]
struct HetznerServersResponse {
    servers: Vec<HetznerServer>,
}

#[derive(Debug, Deserialize)]
struct HetznerFipsResponse {
    floating_ips: Vec<HetznerFip>,
}

#[derive(Debug, Serialize, Deserialize)]
struct ResourceRecord {
    workspace: String,
    kind: String,
    name: String,
    provider_id: Option<String>,
    state: String,
    actual: Option<serde_json::Value>,
    last_synced: Option<chrono::DateTime<Utc>>,
}

// ── Service ────────────────────────────────────────────────────────────────────

/// Runs the three infra-state bridges against a shared SurrealDB connection.
pub struct InfraStateService {
    db: SurrealPool,
    nats: Arc<NatsBridge>,
    http: HttpClient,
    hetzner_token: String,
    /// workflow_id → dag_run SurrealDB key
    run_map: Arc<RwLock<HashMap<String, String>>>,
}

impl InfraStateService {
    pub fn new(db: SurrealPool, nats: Arc<NatsBridge>, hetzner_token: impl Into<String>) -> Self {
        Self {
            db,
            nats,
            http: HttpClient::new(),
            hetzner_token: hetzner_token.into(),
            run_map: Arc::new(RwLock::new(HashMap::new())),
        }
    }

    /// Subscribe to all three subjects concurrently.
    /// Returns when any subscriber closes — caller should restart.
    pub async fn serve(self: Arc<Self>) -> anyhow::Result<()> {
        let s1 = self.clone();
        let s2 = self.clone();
        let s3 = self.clone();

        let mut h1 = tokio::spawn(async move { s1.subscribe_formula_events().await });
        let mut h2 = tokio::spawn(async move { s2.subscribe_dag_run_create().await });
        let mut h3 = tokio::spawn(async move { s3.subscribe_reconcile().await });

        let result = tokio::select! {
            r = &mut h1 => r.map_err(|e| anyhow::anyhow!("formula events panicked: {e}"))?,
            r = &mut h2 => r.map_err(|e| anyhow::anyhow!("dag_run_create panicked: {e}"))?,
            r = &mut h3 => r.map_err(|e| anyhow::anyhow!("reconcile panicked: {e}"))?,
        };

        h1.abort();
        h2.abort();
        h3.abort();

        result
    }

    // ── Bridge 1: formula events → step_state ─────────────────────────────────

    async fn subscribe_formula_events(self: Arc<Self>) -> anyhow::Result<()> {
        let mut sub = self
            .nats
            .subscribe(SUBJ_FORMULA_WILDCARD)
            .await
            .map_err(|e| anyhow::anyhow!("{e}"))?;

        info!("InfraState: listening on {SUBJ_FORMULA_WILDCARD}");

        while let Some(msg) = sub.next().await {
            let event_type = msg
                .subject
                .strip_prefix("provisioning.dag.formula.")
                .unwrap_or("unknown");

            let event: FormulaEvent = match serde_json::from_slice(&msg.payload) {
                Ok(e) => e,
                Err(e) => {
                    warn!(error = %e, subject = %msg.subject, "cannot decode FormulaEvent");
                    continue;
                }
            };

            // Bootstrap a dag_run on first `started` for a new workflow_id.
            if event_type == "started" {
                let has_run = self.run_map.read().await.contains_key(&event.workflow_id);
                if !has_run {
                    if let Some(ref ws) = event.workspace {
                        match self
                            .create_dag_run(ws, "manual", None, "daemon", Some(&event.workflow_id))
                            .await
                        {
                            Ok(k) => {
                                self.run_map
                                    .write()
                                    .await
                                    .insert(event.workflow_id.clone(), k);
                            }
                            Err(e) => {
                                error!(error = %e, "failed to create dag_run");
                            }
                        }
                    }
                }
            }

            let status = match event_type {
                "started" => "running",
                "completed" => "completed",
                "failed" => "failed",
                _ => continue,
            };

            let now = Utc::now();
            let record = serde_json::json!({
                "workflow_id": event.workflow_id,
                "formula_id": event.formula_id,
                "status": status,
                "started_at": if event_type == "started" { Some(now) } else { None::<chrono::DateTime<Utc>> },
                "ended_at": if event_type != "started" { Some(now) } else { None::<chrono::DateTime<Utc>> },
                "error": event.error,
            });

            let key = format!("{}_{}", event.workflow_id, event.formula_id);
            self.upsert_record("step_state", &key, record).await;

            // On terminal events: close the dag_run and prune the run_map entry.
            if event_type == "completed" || event_type == "failed" {
                if let Some(run_key) = self.run_map.read().await.get(&event.workflow_id).cloned() {
                    let update = serde_json::json!({
                        "status": status,
                        "ended_at": now,
                    });
                    let close_result = self
                        .db
                        .db()
                        .query("USE NS infra DB provisioning; UPDATE type::thing('dag_run', $key) MERGE $update")
                        .bind(("key", run_key))
                        .bind(("update", update))
                        .await
                        .map_err(|e| anyhow::anyhow!("{e}"))
                        .and_then(|r| r.check().map_err(|e| anyhow::anyhow!("{e}")));

                    match close_result {
                        Ok(_) => { self.run_map.write().await.remove(&event.workflow_id); }
                        Err(e) => error!(error = %e, workflow_id = %event.workflow_id, "failed to close dag_run"),
                    }
                }
            }
        }

        Err(anyhow::anyhow!("{SUBJ_FORMULA_WILDCARD} subscription closed"))
    }

    // ── Bridge 2: dag_run.create → dag_run record ─────────────────────────────

    async fn subscribe_dag_run_create(self: Arc<Self>) -> anyhow::Result<()> {
        let mut sub = self
            .nats
            .subscribe(SUBJ_DAG_RUN_CREATE)
            .await
            .map_err(|e| anyhow::anyhow!("{e}"))?;

        info!("InfraState: listening on {SUBJ_DAG_RUN_CREATE}");

        while let Some(msg) = sub.next().await {
            let payload: DagRunCreatePayload = match serde_json::from_slice(&msg.payload) {
                Ok(p) => p,
                Err(e) => {
                    warn!(error = %e, "cannot decode DagRunCreatePayload");
                    continue;
                }
            };

            info!(
                workspace = %payload.workspace,
                trigger = %payload.trigger,
                trigger_ref = ?payload.trigger_ref,
                "creating dag_run from external trigger"
            );

            match self
                .create_dag_run(
                    &payload.workspace,
                    &payload.trigger,
                    payload.trigger_ref.as_deref(),
                    payload.actor.as_deref().unwrap_or("unknown"),
                    payload.workflow_id.as_deref(),
                )
                .await
            {
                Ok(k) => {
                    if let Some(wf_id) = payload.workflow_id {
                        self.run_map.write().await.insert(wf_id, k);
                    }
                }
                Err(e) => error!(error = %e, "failed to create dag_run"),
            }
        }

        Err(anyhow::anyhow!("{SUBJ_DAG_RUN_CREATE} subscription closed"))
    }

    // ── Bridge 3: reconcile.workspace → resource drift detection ─────────────

    async fn subscribe_reconcile(self: Arc<Self>) -> anyhow::Result<()> {
        let mut sub = self
            .nats
            .subscribe(SUBJ_RECONCILE)
            .await
            .map_err(|e| anyhow::anyhow!("{e}"))?;

        info!("InfraState: listening on {SUBJ_RECONCILE}");

        while let Some(msg) = sub.next().await {
            let payload: ReconcilePayload = match serde_json::from_slice(&msg.payload) {
                Ok(p) => p,
                Err(e) => {
                    warn!(error = %e, "cannot decode ReconcilePayload");
                    continue;
                }
            };

            if let Err(e) = self.reconcile_workspace(&payload.workspace).await {
                error!(workspace = %payload.workspace, error = %e, "reconcile failed");
            }
        }

        Err(anyhow::anyhow!("{SUBJ_RECONCILE} subscription closed"))
    }

    // ── SurrealDB helpers ─────────────────────────────────────────────────────

    // USE NS is inlined in each query via semicolon chaining so no extra round-trip
    // is needed before each operation and the connection can be freely shared.

    async fn upsert_record(&self, table: &str, key: &str, record: serde_json::Value) {
        let query = format!(
            "USE NS infra DB provisioning; UPSERT type::thing('{table}', $key) MERGE $record"
        );
        match self
            .db
            .db()
            .query(query)
            .bind(("key", key.to_string()))
            .bind(("record", record))
            .await
        {
            Ok(resp) => {
                if let Err(e) = resp.check() {
                    error!(table = %table, key = %key, error = %e, "upsert statement failed");
                }
            }
            Err(e) => error!(table = %table, key = %key, error = %e, "upsert transport failed"),
        }
    }

    /// Create a dag_run record. Returns the SurrealDB record key on success.
    async fn create_dag_run(
        &self,
        workspace: &str,
        trigger: &str,
        trigger_ref: Option<&str>,
        actor: &str,
        workflow_id: Option<&str>,
    ) -> anyhow::Result<String> {
        let key = workflow_id
            .map(|id| id.to_string())
            .unwrap_or_else(|| uuid::Uuid::new_v4().to_string());

        let record = serde_json::json!({
            "workspace": workspace,
            "trigger": trigger,
            "trigger_ref": trigger_ref,
            "actor": actor,
            "status": "running",
            "started_at": Utc::now(),
            "ended_at": null,
        });

        self.db
            .db()
            .query("USE NS infra DB provisioning; UPSERT type::thing('dag_run', $key) CONTENT $record")
            .bind(("key", key.clone()))
            .bind(("record", record))
            .await
            .map_err(|e| anyhow::anyhow!("dag_run transport error: {e}"))?
            .check()
            .map_err(|e| anyhow::anyhow!("dag_run statement error: {e}"))?;

        info!(key = %key, workspace = %workspace, trigger = %trigger, "dag_run created");
        Ok(key)
    }

    /// Upsert a resource record (present / absent / drifted / unknown).
    pub async fn upsert_resource(
        &self,
        workspace: &str,
        kind: &str,
        name: &str,
        provider_id: Option<&str>,
        state: &str,
        actual: Option<serde_json::Value>,
    ) -> anyhow::Result<()> {
        let key = format!("{workspace}/{kind}/{name}");
        let record = serde_json::json!({
            "workspace": workspace,
            "kind": kind,
            "name": name,
            "provider_id": provider_id,
            "state": state,
            "actual": actual,
            "last_synced": Utc::now(),
        });

        self.db
            .db()
            .query("USE NS infra DB provisioning; UPSERT type::thing('resource', $key) MERGE $record")
            .bind(("key", key))
            .bind(("record", record))
            .await
            .map_err(|e| anyhow::anyhow!("resource transport error: {e}"))?
            .check()
            .map_err(|e| anyhow::anyhow!("resource statement error: {e}"))?;

        Ok(())
    }

    // ── Reconciler ────────────────────────────────────────────────────────────

    async fn reconcile_workspace(&self, workspace: &str) -> anyhow::Result<()> {
        info!(workspace = %workspace, "reconciling resource state");

        let raw: Vec<serde_json::Value> = self
            .db
            .db()
            .query("USE NS infra DB provisioning; SELECT * FROM resource WHERE workspace = $ws")
            .bind(("ws", workspace.to_string()))
            .await
            .map_err(|e| anyhow::anyhow!("resource SELECT failed: {e}"))?
            .take(1)
            .map_err(|e| anyhow::anyhow!("resource take failed: {e}"))?;

        let tracked: Vec<ResourceRecord> = raw
            .into_iter()
            .filter_map(|v| serde_json::from_value(v).ok())
            .collect();

        let live_servers = self.hetzner_list_servers().await.unwrap_or_else(|e| {
            warn!(error = %e, "failed to list Hetzner servers");
            vec![]
        });
        let live_fips = self.hetzner_list_fips().await.unwrap_or_else(|e| {
            warn!(error = %e, "failed to list Hetzner FIPs");
            vec![]
        });

        let live_server_names: std::collections::HashSet<&str> =
            live_servers.iter().map(|s| s.name.as_str()).collect();
        let live_fip_names: std::collections::HashSet<&str> =
            live_fips.iter().map(|f| f.name.as_str()).collect();

        for resource in &tracked {
            let live_present = match resource.kind.as_str() {
                "server" => live_server_names.contains(resource.name.as_str()),
                "fip" | "floating_ip" => live_fip_names.contains(resource.name.as_str()),
                _ => true,
            };

            let new_state = if live_present { "present" } else { "drifted" };

            if new_state == resource.state {
                continue;
            }

            info!(
                workspace = %workspace,
                kind = %resource.kind,
                name = %resource.name,
                old = %resource.state,
                new = %new_state,
                "drift detected"
            );

            let actual = match resource.kind.as_str() {
                "server" => live_servers
                    .iter()
                    .find(|s| s.name == resource.name)
                    .map(|s| serde_json::json!({"id": s.id, "name": s.name, "status": s.status})),
                "fip" | "floating_ip" => live_fips
                    .iter()
                    .find(|f| f.name == resource.name)
                    .map(|f| serde_json::json!({"id": f.id, "name": f.name, "ip": f.ip, "server": f.server})),
                _ => None,
            };

            if let Err(e) = self
                .upsert_resource(
                    &resource.workspace,
                    &resource.kind,
                    &resource.name,
                    resource.provider_id.as_deref(),
                    new_state,
                    actual,
                )
                .await
            {
                error!(error = %e, "failed to update resource state after drift");
            }
        }

        info!(workspace = %workspace, total = tracked.len(), "reconcile complete");
        Ok(())
    }

    // ── Hetzner API calls ─────────────────────────────────────────────────────

    async fn hetzner_list_servers(&self) -> anyhow::Result<Vec<HetznerServer>> {
        let resp = self
            .http
            .get("https://api.hetzner.cloud/v1/servers")
            .bearer_auth(&self.hetzner_token)
            .send()
            .await?
            .error_for_status()?
            .json::<HetznerServersResponse>()
            .await?;
        Ok(resp.servers)
    }

    async fn hetzner_list_fips(&self) -> anyhow::Result<Vec<HetznerFip>> {
        let resp = self
            .http
            .get("https://api.hetzner.cloud/v1/floating_ips")
            .bearer_auth(&self.hetzner_token)
            .send()
            .await?
            .error_for_status()?
            .json::<HetznerFipsResponse>()
            .await?;
        Ok(resp.floating_ips)
    }
}
