// Package mgmtplane is the agent-side OBSERVER for the management-plane break-glass system (TASK G1).
//
// It does NOT heal anything — the healing is done by the dumb, agent-independent
// felhom-mgmt-watchdog timer (configs/felhom-mgmt-watchdog.{sh,service,timer}), precisely so the
// heal works when the agent is down. This package only READS host state each heartbeat and produces
// the report stanza the hub surfaces:
//
//   - /run/sshd present?            — OpenSSH's SHARED privsep dir; its absence is the KEXINIT-reset
//                                     lockout (SPIKE-felhom-sshd-2026-07-05 §8).
//   - stock sshd listener answers?  — a TCP connect to the sshd port (22 for G1; H1 passes the
//                                     discovered felhom-sshd port later).
//   - did the watchdog auto-heal?   — the watchdog writes an RFC3339 marker to /run when it heals;
//                                     its presence (+ timestamp) tells the hub a clobber recurred, so
//                                     the operator learns of a recurring cause BEFORE a lockout.
//
// Read-only and dependency-light: os.Stat + os.ReadFile + a short net.Dial, no exec, no sudo. Safe to
// run every heartbeat.
package mgmtplane

import (
	"context"
	"net"
	"os"
	"strings"
	"time"

	"gitea.dooplex.hu/admin/felhom-agent/internal/hub"
)

const (
	// DefaultPrivsepDir is OpenSSH's compiled-in privilege-separation directory (shared by every sshd
	// on the host). Its absence is the exact lockout G1 closes.
	DefaultPrivsepDir = "/run/sshd"
	// DefaultHealMarker is where felhom-mgmt-watchdog records a heal (RFC3339 UTC). On tmpfs, so it
	// clears on reboot — "healed since boot" is the intended semantics.
	DefaultHealMarker = "/run/felhom-mgmt-watchdog.healed"
	// DefaultSshdPort is the stock sshd port G1 observes (H1 will pass the felhom-sshd port).
	DefaultSshdPort = 22
	// dialTimeout bounds the sshd reachability probe (a local TCP connect is fast; never block a report).
	dialTimeout = 2 * time.Second
)

// Reporter observes the host management plane. All fields are injectable for tests.
type Reporter struct {
	privsepDir string
	marker     string
	sshdAddr   string                                  // host:port dialed for the reachability probe
	statDir    func(string) bool                       // dir-exists check (os.Stat wrapper; test seam)
	readMarker func(string) (string, bool)             // marker read → (timestamp, present)
	dialSSHD   func(ctx context.Context, addr string) bool
}

// NewReporter builds the production reporter over the real filesystem + a real TCP dial. sshdPort<=0
// falls back to DefaultSshdPort.
func NewReporter(privsepDir, marker string, sshdPort int) *Reporter {
	if privsepDir == "" {
		privsepDir = DefaultPrivsepDir
	}
	if marker == "" {
		marker = DefaultHealMarker
	}
	if sshdPort <= 0 {
		sshdPort = DefaultSshdPort
	}
	return &Reporter{
		privsepDir: privsepDir,
		marker:     marker,
		sshdAddr:   net.JoinHostPort("127.0.0.1", itoa(sshdPort)),
		statDir:    statIsDir,
		readMarker: readMarkerFile,
		dialSSHD:   dialTCP,
	}
}

// MgmtPlaneStatus builds the heartbeat stanza. Never errors — every probe degrades to a boolean; a
// read failure means "not ok", never a crash. Implements hub.MgmtPlaneReporter.
func (r *Reporter) MgmtPlaneStatus(ctx context.Context) *hub.MgmtPlaneStatus {
	st := &hub.MgmtPlaneStatus{
		PrivsepDirOK:  r.statDir(r.privsepDir),
		SshdReachable: r.dialSSHD(ctx, r.sshdAddr),
	}
	if ts, present := r.readMarker(r.marker); present {
		st.HealedRecently = true
		st.PrivsepHealedAt = ts
	}
	return st
}

// --- production probe impls (all replaceable in tests) ---

func statIsDir(path string) bool {
	fi, err := os.Stat(path)
	return err == nil && fi.IsDir()
}

func readMarkerFile(path string) (string, bool) {
	raw, err := os.ReadFile(path)
	if err != nil {
		return "", false
	}
	ts := strings.TrimSpace(string(raw))
	if ts == "" {
		return "", false // an empty marker is treated as absent (never report a heal we can't timestamp)
	}
	return ts, true
}

func dialTCP(ctx context.Context, addr string) bool {
	d := net.Dialer{Timeout: dialTimeout}
	conn, err := d.DialContext(ctx, "tcp", addr)
	if err != nil {
		return false
	}
	_ = conn.Close()
	return true
}

// itoa avoids importing strconv for one call.
func itoa(n int) string {
	if n == 0 {
		return "0"
	}
	neg := n < 0
	if neg {
		n = -n
	}
	var b [20]byte
	i := len(b)
	for n > 0 {
		i--
		b[i] = byte('0' + n%10)
		n /= 10
	}
	if neg {
		i--
		b[i] = '-'
	}
	return string(b[i:])
}
