package felhomsshd

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"log/slog"
	"net"
	"os"
	"os/exec"
	"path/filepath"
	"strconv"
	"strings"
	"time"

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

// AuthKeysUserPath is felhom-op's authorized_keys (the default operator identity — key OUTSIDE
// ~/.ssh so the customer's sshd never honours it [SF-3]). FIXED (sudoers install target).
const AuthKeysUserPath = AuthKeysDir + "/" + OperatorUser

const stagedConfName = "sshd_config"

// Manager renders + applies the felhom-sshd config and drives the unit through the narrow-sudoers
// runner. Config changes go write→`sshd -t`→`reload` (NEVER restart-on-change [SF-2]); a deliberate
// restart is `reset-failed`-then-`restart` [SF-5], rate-limited by a cooldown (no flap).
type Manager struct {
	runner   proxmox.Runner
	stateDir string
	logger   *slog.Logger

	// injectable seams (tests)
	isActive func(ctx context.Context) bool
	isFailed func(ctx context.Context) bool
	isFree   portProbe
	now      func() time.Time

	port          int       // the claimed port (0 until Apply claims it)
	hash          string    // sha256 of the last-applied config (steady-state zero-exec gate)
	akHash        string    // sha256 of the last-applied felhom-op authorized_keys
	lastRestartAt time.Time // heal cooldown (Part 4)
}

// restartCooldown bounds deliberate felhom-sshd restarts (heal path) — one attempt per window, so a
// persistently-broken instance is reported, not restart-stormed (spike §6 / notification-cooldown shape).
const restartCooldown = 10 * time.Minute

// NewManager builds a Manager. stateDir is the agent state dir (staged config lives under it).
func NewManager(runner proxmox.Runner, stateDir string, logger *slog.Logger) *Manager {
	if logger == nil {
		logger = slog.Default()
	}
	return &Manager{
		runner:   runner,
		stateDir: stateDir,
		logger:   logger,
		isActive: func(ctx context.Context) bool {
			out, _ := exec.CommandContext(ctx, "systemctl", "is-active", Unit).Output()
			return strings.TrimSpace(string(out)) == "active"
		},
		isFailed: func(ctx context.Context) bool {
			out, _ := exec.CommandContext(ctx, "systemctl", "is-failed", Unit).Output()
			return strings.TrimSpace(string(out)) == "failed"
		},
		isFree: probeFree,
		now:    time.Now,
	}
}

// Port returns the claimed port (0 before the first successful Apply).
func (m *Manager) Port() int { return m.port }

func (m *Manager) sshdDir() string        { return filepath.Join(m.stateDir, "felhom-sshd") }
func (m *Manager) stagedConfPath() string { return filepath.Join(m.sshdDir(), stagedConfName) }
func (m *Manager) portFilePath() string   { return filepath.Join(m.sshdDir(), "port") }

// readPort/writePort persist the claimed port in the AGENT-OWNED state dir (the agent is non-root and
// cannot write the root-owned /etc/felhom-sshd). A port is not a secret.
func (m *Manager) readPort() (int, bool) {
	raw, err := os.ReadFile(m.portFilePath())
	if err != nil {
		return 0, false
	}
	p, err := strconv.Atoi(strings.TrimSpace(string(raw)))
	if err != nil || p < 1 || p > 65535 {
		return 0, false
	}
	return p, true
}

func (m *Manager) writePort(port int) error {
	if err := os.MkdirAll(m.sshdDir(), 0o700); err != nil {
		return err
	}
	return os.WriteFile(m.portFilePath(), []byte(strconv.Itoa(port)+"\n"), 0o600)
}

// Apply claims the port, renders the config, reconciles the running unit, and (when the desired-state
// block carries it) installs the operator's authorized_keys. Idempotent: no change → at most an
// is-active check. block may be nil (no desired-state yet) — the instance still runs; only the
// operator login/belt inputs are skipped. Returns the claimed port (0 on a claim/exhaustion error) —
// the caller (belt sync) needs it.
func (m *Manager) Apply(ctx context.Context, block *hub.WireWireguard) (int, error) {
	port, err := claimPort(Candidates, m.isFree, m.readPort, m.writePort)
	if err != nil {
		m.logger.Error("felhomsshd: port claim failed", "err", err)
		return 0, err
	}
	m.port = port

	// Operator authorized_keys (from the hub-driven block) — written to felhom-op's dedicated file,
	// outside ~/.ssh [SF-3]. Independent of the config-reload path (a key change never reloads sshd).
	if block != nil {
		m.applyAuthorizedKeys(ctx, block.OOBOperatorSSHKey)
	}

	conf, err := renderConfig(port)
	if err != nil {
		m.logger.Error("felhomsshd: refusing to apply invalid config", "err", err)
		return port, err
	}
	sum := sha256.Sum256([]byte(conf))
	hash := hex.EncodeToString(sum[:])
	active := m.isActive(ctx)

	if m.hash == hash && active {
		return port, nil // steady state: zero execs
	}

	// Stage → validate → install → reload/enable. Never restart on a config change [SF-2].
	if err := os.MkdirAll(m.sshdDir(), 0o700); err != nil {
		m.logger.Error("felhomsshd: state dir", "err", err)
		return port, err
	}
	if err := os.WriteFile(m.stagedConfPath(), []byte(conf), 0o600); err != nil {
		m.logger.Error("felhomsshd: staging config", "err", err)
		return port, err
	}
	// Validate the STAGED config before it is installed — a bad render never reaches the live path.
	if _, errOut, err := m.runner.Run(ctx, "sshd", "-t", "-f", m.stagedConfPath()); err != nil {
		m.logger.Error("felhomsshd: staged config failed sshd -t — NOT installing", "err", err, "stderr", strings.TrimSpace(string(errOut)))
		return port, err
	}
	if _, errOut, err := m.runner.Run(ctx, "install", "-o", "root", "-g", "root", "-m", "0644", "--", m.stagedConfPath(), ConfPath); err != nil {
		m.logger.Error("felhomsshd: config install failed", "err", err, "stderr", strings.TrimSpace(string(errOut)))
		return port, err
	}
	verb := "reload"
	if !active {
		verb = "enable" // first apply / down → enable --now brings it up
	}
	if err := m.systemctl(ctx, verb); err != nil {
		return port, err
	}
	m.hash = hash
	m.logger.Info("felhomsshd: config applied", "port", port, "action", verb)
	return port, nil
}

// applyAuthorizedKeys installs (or clears) felhom-op's authorized_keys from the hub-delivered
// operator SSH key. Hash-gated (no churn); the key is public, never a secret. A write failure is
// logged, not fatal — the instance keeps running. NO sshd reload needed (sshd reads the file per-auth).
func (m *Manager) applyAuthorizedKeys(ctx context.Context, sshKey string) {
	content := ""
	if k := strings.TrimSpace(sshKey); k != "" {
		content = k + "\n"
	}
	sum := sha256.Sum256([]byte(content))
	h := hex.EncodeToString(sum[:])
	if h == m.akHash {
		return // unchanged
	}
	staged := filepath.Join(m.sshdDir(), "authorized_keys."+OperatorUser)
	if err := os.MkdirAll(m.sshdDir(), 0o700); err != nil {
		m.logger.Error("felhomsshd: state dir for authorized_keys", "err", err)
		return
	}
	if err := os.WriteFile(staged, []byte(content), 0o600); err != nil {
		m.logger.Error("felhomsshd: staging authorized_keys", "err", err)
		return
	}
	if _, errOut, err := m.runner.Run(ctx, "install", "-o", "root", "-g", "root", "-m", "0644", "--", staged, AuthKeysUserPath); err != nil {
		m.logger.Error("felhomsshd: authorized_keys install failed", "err", err, "stderr", strings.TrimSpace(string(errOut)))
		return
	}
	m.akHash = h
	m.logger.Info("felhomsshd: operator authorized_keys updated", "user", OperatorUser, "present", content != "")
}

// systemctl runs the reconcile verbs through the narrow runner. `enable` = `enable --now`; `reload`
// HUPs (config change, running instance survives a bad reload via the unit's ExecReload sshd -t gate).
func (m *Manager) systemctl(ctx context.Context, verb string) error {
	var args []string
	switch verb {
	case "enable":
		args = []string{"enable", "--now", Unit}
	case "reload", "restart":
		args = []string{verb, Unit}
	default:
		return nil
	}
	if _, errOut, err := m.runner.Run(ctx, "systemctl", args...); err != nil {
		m.logger.Error("felhomsshd: systemctl "+verb+" failed", "err", err, "stderr", strings.TrimSpace(string(errOut)))
		return err
	}
	return nil
}

// probeFree is the production port probe: nothing LISTENing (ss) AND a real bind succeeds (a bind
// that succeeds-then-closes proves the port is actually claimable, not just ss-silent).
func probeFree(port int) bool {
	p := strconv.Itoa(port)
	out, err := exec.Command("ss", "-Htln", "sport = :"+p).Output()
	if err == nil && strings.TrimSpace(string(out)) != "" {
		return false // something is listening
	}
	ln, err := net.Listen("tcp", "0.0.0.0:"+p)
	if err != nil {
		return false
	}
	_ = ln.Close()
	return true
}
