package networking

import (
	"context"
	"fmt"
	"os/exec"
	"time"

	"nadir/internal/oscmd"
)

// backend is the write-side abstraction. Each host network manager (nmcli,
// networkd, ifupdown) implements this. Reads go through `ip -j` and
// /etc/resolv.conf regardless - they are backend-agnostic (see read.go).
//
// When detect() finds no backend, Module.be is nil and all write endpoints
// return 501 Not Implemented. Reads still work.
// Methods that shell out take a context so a request that is cancelled (client
// disconnect, timeout) kills the slow command (e.g. `nmcli con up` waiting on
// DHCP). The timer-driven auto-revert, which must finish even with no client,
// passes context.Background().
type backend interface {
	// Name returns the backend identifier ("nmcli", "networkd", "ifupdown").
	Name() string

	// Snapshot captures the current IPv4 configuration of iface so it can be
	// restored on rollback. The returned IfaceConfig is backend-specific:
	// nmcli reads from NM's connection, networkd/ifupdown read their config
	// files, falling back to live `ip` output when no managed file exists
	// (in which case Method is "dhcp" - safest revert assumption).
	Snapshot(ctx context.Context, iface string) (IfaceConfig, error)

	// Apply replaces the interface's IPv4 configuration with cfg. It is the
	// caller's responsibility to have taken a Snapshot first (the rollback
	// mechanism does this). Apply must be idempotent: calling it with the
	// same cfg twice should leave the system in the same state.
	Apply(ctx context.Context, iface string, cfg IfaceConfig) error

	// SetLinkUp brings the interface up.
	SetLinkUp(ctx context.Context, iface string) error

	// SetLinkDown takes the interface down.
	SetLinkDown(ctx context.Context, iface string) error
}

// detect probes the host for a supported network manager, in priority order:
//
//  1. nmcli  (NetworkManager)  - the majority of desktop and modern server installs
//  2. networkctl (systemd-networkd) - common on minimal/container hosts
//  3. ifup/ifdown (ifupdown)        - classic Debian/Ubuntu servers
//
// Returns nil when none is found. The order matters: some distros ship both NM
// and networkd; NM wins because it's the active manager in that case.
func detect() backend {
	if _, err := exec.LookPath("nmcli"); err == nil {
		if _, err := oscmd.Run("nmcli", "general", "status"); err == nil {
			return &nmcliBackend{}
		}
	}
	if _, err := exec.LookPath("networkctl"); err == nil {
		if _, err := oscmd.Run("systemctl", "is-active", "--quiet", "systemd-networkd"); err == nil {
			return &networkdBackend{}
		}
	}
	if _, err := exec.LookPath("ifup"); err == nil {
		if _, err := exec.LookPath("ifdown"); err == nil {
			return &ifupdownBackend{}
		}
	}
	return nil
}

// pendingChange tracks a single in-flight change that has been applied but not
// yet confirmed. revert undoes it (re-apply the prior config, or bring a
// downed link back up). If the timer fires before confirmation, revert runs -
// protecting against lock-yourself-out mistakes.
//
// ponytail: one slot for the whole module, not per-interface. An admin makes one
// change at a time; a concurrent change to another iface is rejected with a 409
// that says so. Key it by iface (a map) if multi-interface concurrency is ever
// needed.
type pendingChange struct {
	Iface    string       // interface that was changed
	revert   func() error // undoes the change, for rollback
	Timer    *time.Timer  // fires the auto-revert
	Deadline time.Time    // when the timer will fire (for the status endpoint)
}

// errNoBackend is the 501 returned when no write backend was detected.
var errNoBackend = fmt.Errorf("no supported network backend detected (tried nmcli, networkctl, ifup/ifdown)")
