//! Hook type surface — serde-friendly so completion hooks can be
//! attached at dispatch time and round-trip through SQLite (67.F.3)
//! / NATS payloads.

use std::time::Duration;

use nexo_driver_claude::OriginChannel;
use nexo_driver_types::GoalId;
use serde::{Deserialize, Serialize};

/// Lifecycle transition that fires a hook.
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum HookTransition {
    Done,
    Failed,
    Cancelled,
    Progress,
}

impl HookTransition {
    pub fn as_str(self) -> &'static str {
        match self {
            HookTransition::Done => "done",
            HookTransition::Failed => "failed",
            HookTransition::Cancelled => "cancelled",
            HookTransition::Progress => "progress",
        }
    }
}

/// When a hook should fire.
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(tag = "on", rename_all = "snake_case")]
pub enum HookTrigger {
    Done,
    Failed,
    Cancelled,
    /// Fires after every Nth completed work-turn. `every_turns=0`
    /// disables the trigger (useful when an operator wants to keep
    /// the hook entry on the goal but suppress firing without
    /// removing it).
    Progress {
        every_turns: u32,
    },
}

impl HookTrigger {
    /// True when the trigger matches this transition.
    pub fn matches(&self, t: HookTransition) -> bool {
        match (self, t) {
            (HookTrigger::Done, HookTransition::Done) => true,
            (HookTrigger::Failed, HookTransition::Failed) => true,
            (HookTrigger::Cancelled, HookTransition::Cancelled) => true,
            (HookTrigger::Progress { every_turns }, HookTransition::Progress) => *every_turns > 0,
            _ => false,
        }
    }
}

fn default_hook_timeout() -> Duration {
    Duration::from_secs(30)
}

/// What the hook does when it fires.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum HookAction {
    /// Publish summary back to the chat that originated the goal.
    /// No-op when origin = console (CLI runs print to stdout via a
    /// separate path).
    NotifyOrigin,
    /// Publish summary to an explicit channel — useful to fan out
    /// progress / completion to a different chat than the
    /// dispatcher (e.g. ops on Telegram, customers on WhatsApp).
    NotifyChannel {
        plugin: String,
        instance: String,
        recipient: String,
    },
    /// Spawn another goal when this transition fires (chaining).
    /// Wired in 67.F.2.
    DispatchPhase {
        phase_id: String,
        /// Trigger guard — usually `done` so chaining only happens
        /// after a successful goal. `any` runs unconditionally.
        only_if: HookTrigger,
    },
    /// Spawn an audit goal that scans the parent goal's diff for
    /// bugs / incomplete follow-ups / missing tests, then reports
    /// findings via notify_origin (without fixing). Operator
    /// decides which findings to dispatch as fix-goals. Auto-
    /// attached by `program_phase` when
    /// `DispatchToolContext.audit_before_done` is true.
    DispatchAudit { only_if: HookTrigger },
    /// Publish the hook payload as JSON on a NATS subject.
    NatsPublish { subject: String },
    /// Run an arbitrary shell command. Gated by config
    /// `program_phase.allow_shell_hooks`. Wired in 67.F.4.
    Shell {
        cmd: String,
        #[serde(default = "default_hook_timeout", with = "humantime_serde")]
        timeout: Duration,
    },
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct CompletionHook {
    /// Stable id assigned at registration time so `remove_hook` can
    /// target a specific entry without ambiguity. Generated by the
    /// caller.
    pub id: String,
    pub on: HookTrigger,
    pub action: HookAction,
}

/// Payload handed to every hook. Keep it small — pre-rendered
/// summary, key identifiers, and the originating channel so
/// downstream actions don't need to reach back into the registry.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct HookPayload {
    pub goal_id: GoalId,
    pub phase_id: String,
    pub transition: HookTransition,
    pub summary: String,
    /// Bytes consumed in the run (rendered, e.g. "23m12s, 12 turns").
    #[serde(default)]
    pub elapsed: String,
    #[serde(default)]
    pub diff_stat: Option<String>,
    pub origin: Option<OriginChannel>,
}
