// attach_dispatch.rs --- Post-init attach dispatcher (T M5.6g).

//! Post-init attach dispatcher.
//!
//! After `init.lua` runs, [`crate::editor::EditorState`] reads the
//! [`crate::lua_bindings::RequestedAttach`] slot. This module decides
//! what the resulting program should do with a stored attach request:
//!
//! * `None` (init.lua never called `pmacs.attach{...}`) — run the
//!   local TUI as usual.
//! * `Some(LocalSocket(_))` — hand off to [`crate::attach::run_attach`]
//!   against that socket. The local terminal becomes a frontend
//!   talking to the named daemon.
//! * `Some(Ssh|Tls|Custom)` — error. These transports parse and store
//!   in v0.1 (per the spec's "validate locally, defer activation"
//!   rule) but their activation pathway hasn't shipped yet. The
//!   dispatcher surfaces a workaround-pointing error rather than
//!   silently downgrading to local mode.
//!
//! # Threading
//!
//! Pure function. Called once per process from `editor::run` *before*
//! the local [`crate::frontend::Frontend`] is constructed, so a
//! handed-off attach doesn't fight the local-TUI pathway for the
//! terminal.

use std::path::PathBuf;

use crate::protocol::AttachTarget;

/// What the post-init dispatcher decided to do with the requested
/// attach.
///
/// The `editor::run` caller matches on this and either continues the
/// local TUI, hands off to `attach::run_attach`, or surfaces an error
/// pointing at the milestone where the requested transport ships.
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum AttachDispatch {
    /// No attach was requested in init.lua. Run the local TUI as
    /// usual.
    RunLocal,
    /// Hand off to `attach::run_attach(socket)`. The local frontend
    /// will talk to the daemon at this socket path.
    RunAttachLocalSocket(PathBuf),
    /// Hand off to `attach::run_attach_ssh(target)`. The local
    /// frontend will spawn `ssh ... pmacs --daemon-attach ...` and
    /// drive the attach pump over the SSH child's stdio.
    RunAttachSsh(AttachTarget),
    /// The requested transport is recognized and validated, but its
    /// activation pathway isn't shipped in v0.1. The dispatcher
    /// surfaces a structured deferral; `editor::run` formats it into
    /// a workaround-pointing error message at the boundary.
    DeferredInV01 {
        /// Lower-case transport name: `"tls"`, `"custom"` (SSH used
        /// to defer here pre-M5.7e; now activates via
        /// [`Self::RunAttachSsh`]).
        kind: &'static str,
        /// Target milestone for activation: `"v0.2"` for tls/custom.
        /// Combined with `kind` to form the user-visible error
        /// message.
        milestone: &'static str,
    },
}

impl AttachDispatch {
    /// Render `DeferredInV01` as a single error line. Pulled into a
    /// method so `editor::run` and the acceptance tests share one
    /// canonical message shape.
    ///
    /// Returns `None` for `RunLocal` and `RunAttachLocalSocket` —
    /// only the deferral case has a message.
    #[must_use]
    pub fn deferred_message(&self) -> Option<String> {
        match self {
            Self::DeferredInV01 { kind, milestone } => Some(format!(
                "{kind} attach is not yet implemented in v0.1 (planned for {milestone}); \
                 remove the `pmacs.attach{{...}}` call from init.lua and restart, \
                 or pass `--attach --socket NAME` for a local-socket attach"
            )),
            _ => None,
        }
    }
}

/// Decide what to do with the (optional) attach request consumed
/// from `init.lua`'s [`pmacs::lua_bindings::RequestedAttach`] slot.
///
/// Pure function: takes the parsed target, returns the dispatch
/// outcome. The wrapping `editor::run` is responsible for actually
/// running the chosen pathway.
#[must_use]
pub fn dispatch_attach(target: Option<AttachTarget>) -> AttachDispatch {
    match target {
        None => AttachDispatch::RunLocal,
        Some(AttachTarget::LocalSocket(path)) => AttachDispatch::RunAttachLocalSocket(path),
        Some(t @ AttachTarget::Ssh { .. }) => AttachDispatch::RunAttachSsh(t),
        Some(AttachTarget::Tls { .. }) => AttachDispatch::DeferredInV01 {
            kind: "tls",
            milestone: "v0.2",
        },
        Some(AttachTarget::Custom { .. }) => AttachDispatch::DeferredInV01 {
            kind: "custom",
            milestone: "v0.2",
        },
    }
}

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

    #[test]
    fn no_request_runs_local() {
        assert_eq!(dispatch_attach(None), AttachDispatch::RunLocal);
    }

    #[test]
    fn local_socket_dispatches_to_attach_run() {
        let p = PathBuf::from("/run/pmacs/work.sock");
        assert_eq!(
            dispatch_attach(Some(AttachTarget::LocalSocket(p.clone()))),
            AttachDispatch::RunAttachLocalSocket(p),
        );
    }

    #[test]
    fn ssh_target_dispatches_to_run_attach_ssh() {
        // M5.7e activates SSH: the dispatcher routes SSH targets
        // straight to `RunAttachSsh` so `attach::run_attach_ssh` can
        // spawn the subprocess. The fields round-trip unchanged.
        let target = AttachTarget::Ssh {
            host: "host".into(),
            user: Some("alice".into()),
            instance_name: Some("research".into()),
        };
        match dispatch_attach(Some(target.clone())) {
            AttachDispatch::RunAttachSsh(round_tripped) => {
                assert_eq!(round_tripped, target);
            }
            other => panic!("expected RunAttachSsh, got {other:?}"),
        }
    }

    #[test]
    fn tls_target_defers_to_v0_2() {
        let target = AttachTarget::Tls {
            endpoint: "example:9999".into(),
            cert: PathBuf::from("/etc/p.crt"),
        };
        match dispatch_attach(Some(target)) {
            AttachDispatch::DeferredInV01 { kind, milestone } => {
                assert_eq!(kind, "tls");
                assert_eq!(milestone, "v0.2");
            }
            other => panic!("expected DeferredInV01, got {other:?}"),
        }
    }

    #[test]
    fn custom_target_defers_to_v0_2() {
        let target = AttachTarget::Custom {
            command: vec!["docker".into(), "exec".into()],
        };
        match dispatch_attach(Some(target)) {
            AttachDispatch::DeferredInV01 { kind, milestone } => {
                assert_eq!(kind, "custom");
                assert_eq!(milestone, "v0.2");
            }
            other => panic!("expected DeferredInV01, got {other:?}"),
        }
    }

    #[test]
    fn deferred_message_present_only_on_deferred_variant() {
        assert!(AttachDispatch::RunLocal.deferred_message().is_none());
        assert!(
            AttachDispatch::RunAttachLocalSocket(PathBuf::from("/x"))
                .deferred_message()
                .is_none(),
        );
        assert!(
            AttachDispatch::RunAttachSsh(AttachTarget::Ssh {
                host: "h".into(),
                user: None,
                instance_name: None,
            })
            .deferred_message()
            .is_none(),
        );
        let m = AttachDispatch::DeferredInV01 {
            kind: "tls",
            milestone: "v0.2",
        }
        .deferred_message();
        let m = m.expect("deferred variant has a message");
        assert!(m.contains("tls attach"), "{m}");
        assert!(m.contains("v0.2"), "{m}");
        assert!(
            m.contains("--attach"),
            "message must point at workaround: {m}"
        );
    }

    #[test]
    fn deferred_message_for_tls_names_v0_2_and_pointer_to_workaround() {
        let m = AttachDispatch::DeferredInV01 {
            kind: "tls",
            milestone: "v0.2",
        }
        .deferred_message()
        .unwrap();
        assert!(m.contains("tls"));
        assert!(m.contains("v0.2"));
        assert!(m.contains("init.lua"));
        assert!(m.contains("--attach"));
    }

    #[test]
    fn each_deferred_variant_yields_distinct_kind() {
        // Defends against a copy-paste bug that would map two
        // deferred variants to the same kind tag. Post-M5.7e only
        // tls and custom defer; ssh now activates.
        let kinds: std::collections::HashSet<&'static str> = [
            AttachTarget::Tls {
                endpoint: "e".into(),
                cert: PathBuf::from("/x"),
            },
            AttachTarget::Custom {
                command: vec!["c".into()],
            },
        ]
        .into_iter()
        .map(|t| match dispatch_attach(Some(t)) {
            AttachDispatch::DeferredInV01 { kind, .. } => kind,
            other => panic!("expected DeferredInV01, got {other:?}"),
        })
        .collect();
        assert_eq!(kinds.len(), 2, "kind tags must be distinct: {kinds:?}");
    }
}
