package installers

import (
	"fmt"
	"io"
	"os"
	"path/filepath"
)

// SNI router installer (feat-124, stealth TURN-over-443).
//
// Unlike the binary installers (Caddy, ntfy), the orama-sni-router binary is
// built and shipped to the node by `orama build` / the install tarball — this
// installer only writes the router's YAML config and the systemd unit, and
// drives the unit's lifecycle (install+enable+start when enabled,
// stop+disable when not).

const (
	// SNIRouterListenAddr is the public port the router binds. It owns :443 so
	// Caddy is moved to CaddyHTTPSPortBehindSNI (see caddy.go).
	SNIRouterListenAddr = ":443"

	// SNIRouterServiceName is the systemd unit name.
	SNIRouterServiceName = "orama-sni-router.service"

	// SNIRouterConfigName is the router config filename (resolved under
	// <oramaDir>/configs by the binary's config.DefaultPath lookup).
	SNIRouterConfigName = "sni-router.yaml"

	// sniRouterRescanInterval is how often the router rescans the namespaces
	// directory for per-namespace TURNS listeners. Matches the library default
	// (sniproxy.DefaultDiscoveryRescanInterval); kept as a literal here to avoid
	// importing the runtime package into the installer.
	sniRouterRescanInterval = "30s"

	// sniRouterClientHelloTimeout / sniRouterBackendDialTimeout bound the
	// per-connection ClientHello peek and backend dial (slowloris / dead-backend
	// protection). Mirror the sniproxy server defaults.
	sniRouterClientHelloTimeout = "5s"
	sniRouterBackendDialTimeout = "5s"

	// sniRouterMaxConcurrentConns caps in-flight connections on the public
	// :443 listener (DoS guard); mirrors the sniproxy server default.
	sniRouterMaxConcurrentConns = 10000

	// sniRouterSystemdUnitPath is where the unit file is written.
	sniRouterSystemdUnitPath = "/etc/systemd/system/" + SNIRouterServiceName

	// sniRouterBinaryPath is the installed binary path on the node.
	sniRouterBinaryPath = "/opt/orama/bin/orama-sni-router"
)

// SNIRouterInstaller writes the orama-sni-router config + systemd unit and
// manages the unit lifecycle. The caddy fallback port matches
// CaddyHTTPSPortBehindSNI so unmatched SNIs (regular HTTPS) reach the moved
// Caddy listener.
type SNIRouterInstaller struct {
	*BaseInstaller
	oramaDir string // e.g. "/opt/orama/.orama"
}

// NewSNIRouterInstaller creates an installer. oramaDir is the node's .orama
// data root (where configs/ and data/namespaces live).
func NewSNIRouterInstaller(arch string, logWriter io.Writer, oramaDir string) *SNIRouterInstaller {
	return &SNIRouterInstaller{
		BaseInstaller: NewBaseInstaller(arch, logWriter),
		oramaDir:      oramaDir,
	}
}

// configPath returns the absolute path the router config is written to and the
// binary resolves to via its DefaultPath lookup (<oramaDir>/configs/<name>).
func (si *SNIRouterInstaller) configPath() string {
	return filepath.Join(si.oramaDir, "configs", SNIRouterConfigName)
}

// namespacesDir returns the per-namespace config root the router scans for
// TURNS listeners.
func (si *SNIRouterInstaller) namespacesDir() string {
	return filepath.Join(si.oramaDir, "data", "namespaces")
}

// Configure writes the router YAML config. baseDomain drives the stealth and
// "turn.ns-*" SNI hostnames the router derives during discovery. Idempotent.
func (si *SNIRouterInstaller) Configure(baseDomain string) error {
	if baseDomain == "" {
		return fmt.Errorf("sni-router: base domain must not be empty")
	}

	configDir := filepath.Dir(si.configPath())
	if err := os.MkdirAll(configDir, 0755); err != nil {
		return fmt.Errorf("sni-router: create config dir %s: %w", configDir, err)
	}

	content := si.generateConfig(baseDomain)
	if err := os.WriteFile(si.configPath(), []byte(content), 0644); err != nil {
		return fmt.Errorf("sni-router: write config %s: %w", si.configPath(), err)
	}
	return nil
}

// generateConfig renders the sni-router.yaml. The fallback is Caddy on
// CaddyHTTPSPortBehindSNI; turn_discovery scans the node's namespaces dir so
// per-namespace TURNS routes appear without a router restart. No static routes
// are emitted — every TURNS route is auto-discovered.
func (si *SNIRouterInstaller) generateConfig(baseDomain string) string {
	return fmt.Sprintf(`# Orama SNI router config (feat-124, stealth TURN-over-443).
# Generated by the installer — re-running install/upgrade overwrites this file.
#
# The router owns :443, peeks each connection's TLS ClientHello SNI, and
# forwards the raw (still-encrypted) stream to a backend. TLS is NOT terminated
# here. Unmatched SNIs (regular HTTPS) go to the fallback (Caddy on :%[2]d).
listen: "%[1]s"
client_hello_timeout: %[3]s
backend_dial_timeout: %[4]s
max_concurrent_conns: %[5]d

fallback:
  name: caddy
  addr: "127.0.0.1:%[2]d"

# Per-namespace stealth-TURN routes are auto-discovered by scanning
# <namespaces_dir>/*/configs/turn-*.yaml every rescan_interval. Each namespace
# with a TURNS listener gets two routes (the bland stealth host and a
# turn.ns-<namespace>.<base_domain> alias) forwarding to its local TURNS port.
turn_discovery:
  namespaces_dir: %[6]q
  base_domain: %[7]q
  rescan_interval: %[8]s

# No static routes: every TURNS route comes from turn_discovery above.
routes: []
`,
		SNIRouterListenAddr,
		CaddyHTTPSPortBehindSNI,
		sniRouterClientHelloTimeout,
		sniRouterBackendDialTimeout,
		sniRouterMaxConcurrentConns,
		si.namespacesDir(),
		baseDomain,
		sniRouterRescanInterval,
	)
}

// generateSystemdUnit renders /etc/systemd/system/orama-sni-router.service.
// Runs as the orama user with CAP_NET_BIND_SERVICE so it can bind :443 without
// root. Ordered Before=caddy.service so the router is ready before Caddy
// switches to :8443. Restart=on-failure.
func (si *SNIRouterInstaller) generateSystemdUnit() string {
	return fmt.Sprintf(`[Unit]
Description=Orama SNI Router (TLS-level :443 → backend forwarder)
Documentation=https://github.com/DeBrosOfficial/network
After=network.target
Before=caddy.service
PartOf=orama-node.service

[Service]
Type=simple
WorkingDirectory=/opt/orama
EnvironmentFile=-/opt/orama/.orama/data/sni-router.env
ExecStart=%s --config %s

# Bind privileged ports (:80, :443) without running as root.
AmbientCapabilities=CAP_NET_BIND_SERVICE
CapabilityBoundingSet=CAP_NET_BIND_SERVICE

User=orama
Group=orama
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
LimitNOFILE=65536

TimeoutStopSec=15s
KillMode=mixed
KillSignal=SIGTERM

Restart=on-failure
RestartSec=5s

StandardOutput=journal
StandardError=journal
SyslogIdentifier=orama-sni-router

[Install]
WantedBy=multi-user.target
`, sniRouterBinaryPath, si.configPath())
}

// WriteSystemdUnit writes the unit file. Idempotent.
func (si *SNIRouterInstaller) WriteSystemdUnit() error {
	if err := os.WriteFile(sniRouterSystemdUnitPath, []byte(si.generateSystemdUnit()), 0644); err != nil {
		return fmt.Errorf("sni-router: write systemd unit %s: %w", sniRouterSystemdUnitPath, err)
	}
	return nil
}

// IsInstalled reports whether the router binary is present on the node.
func (si *SNIRouterInstaller) IsInstalled() bool {
	_, err := os.Stat(sniRouterBinaryPath)
	return err == nil
}
