package ssh

import (
	"context"
	"errors"
	"fmt"
	"log/slog"
	"strconv"
	"strings"

	"github.com/dtoro/oikos/internal/core/ports"
)

// Provisioner implements ports.Provisioner over CommandExecutor: the
// pct create flow absorbed from httpapi's approved-execution path
// (Phase 7). It owns the SSH-side pre-flights — template-cache
// resolution, cluster-wide VMID collision guard, gateway reachability
// on the target bridge — and the pct create itself. Guest registration
// in the entity graph is ProvisioningService's job, not this adapter's.
type Provisioner struct {
	exec ports.CommandExecutor
}

var _ ports.Provisioner = (*Provisioner)(nil)

// NewProvisioner builds the provisioner over a command executor (the
// dial-pool executor; the target is the Proxmox host, reached directly).
func NewProvisioner(exec ports.CommandExecutor) *Provisioner {
	return &Provisioner{exec: exec}
}

func (p *Provisioner) target(in ports.LXCInput) ports.Target {
	return ports.Target{Host: in.HostAddr, User: in.HostUser}
}

// CreateLXC runs the full pct create: template pre-flight, VMID guard,
// gateway pre-flight, then create + start. Output streams to in.Sink.
func (p *Provisioner) CreateLXC(ctx context.Context, in ports.LXCInput) (ports.ProvisionResult, error) {
	// Template pre-flight: resolve against what the host actually has
	// cached. A hardcoded name (e.g. debian-13) fails opaquely with a raw
	// `pct` error when that exact file isn't present. List the cache, then
	// either validate the requested template or auto-pick the newest
	// debian one; on miss, fail early with the available list so the
	// operator/agent can retry with a real name.
	res := p.exec.Run(ctx, p.target(in),
		"ls -1 /var/lib/vz/template/cache/ 2>/dev/null | grep -E '\\.tar\\.(zst|gz|xz)$' || true",
		ports.ExecOpts{})
	if res.Err != nil {
		return ports.ProvisionResult{}, fmt.Errorf("list templates on %s: %w", in.HostSlug, res.Err)
	}
	available := []string{}
	for _, l := range strings.Split(strings.TrimSpace(res.Output), "\n") {
		if l = strings.TrimSpace(l); l != "" {
			available = append(available, l)
		}
	}
	in.Template = ResolveTemplate(in.Template, available)
	if in.Template == "" {
		return ports.ProvisionResult{}, fmt.Errorf(
			"no usable LXC template on %s; available: %v", in.HostSlug, available)
	}

	// VMID collision guard. Proxmox VMIDs are cluster-wide, so the model's
	// guess (e.g. 132) can collide with a container on another node — pct
	// create then fails with "CT N already exists on node X". Fetch the set
	// of in-use VMIDs across the cluster; if the requested id is taken (or
	// absent), fall back to the cluster's next free id so provisioning
	// still succeeds instead of dead-ending on the operator's approval.
	var vmidErr error
	in.VMID, vmidErr = p.resolveVMID(ctx, in)
	if vmidErr != nil {
		return ports.ProvisionResult{}, vmidErr
	}

	// Gateway pre-flight: for a static config, ping the gateway from the
	// target HOST, on the SPECIFIC BRIDGE being requested, before spending
	// 5+ minutes creating the container. Binding to the bridge
	// (`ping -I <bridge>`) matters: a bare `ping <gw>` from the host can
	// succeed via the host's own routing table even when the container —
	// which only gets a naive on-link default route via its bridge's veth —
	// can never ARP that gateway at all. Binding reproduces what the
	// container will actually experience.
	isStatic := in.IP != "" && !strings.EqualFold(in.IP, "dhcp")
	if isStatic && in.GW != "" {
		ping := p.exec.Run(ctx, p.target(in),
			fmt.Sprintf("ping -I %s -c1 -W2 %s >/dev/null 2>&1 && echo PREFLIGHT_OK || echo PREFLIGHT_FAIL", in.Bridge, in.GW),
			ports.ExecOpts{})
		if ping.Err != nil || !GatewayPreflightPassed(ping.Output) {
			return ports.ProvisionResult{}, fmt.Errorf(
				"gateway %s is not reachable from %s on bridge %s — this almost always means the bridge doesn't carry that subnet on this host (each bridge only reaches the network it's physically wired to). "+
					"Do not retry with a different gateway guess in the same subnet: find an existing LXC on this host with an IP in the same /28 and copy its exact bridge+gateway, or use DHCP instead",
				in.GW, in.HostSlug, in.Bridge)
		}
	}

	createCmd := BuildPctCreateCmd(in)

	slog.Info("provisioner: pct create running",
		"vmid", in.VMID, "hostname", in.Hostname, "cmd", createCmd)
	run := p.exec.Run(ctx, p.target(in), createCmd, ports.ExecOpts{Sink: in.Sink})
	if run.Err != nil {
		return ports.ProvisionResult{VMID: in.VMID, Template: in.Template, Output: run.Output}, run.Err
	}
	return ports.ProvisionResult{VMID: in.VMID, Template: in.Template, Output: run.Output}, nil
}

// resolveVMID returns the VMID to use, reassigning via the cluster's
// next free id when the request is absent or already taken.
func (p *Provisioner) resolveVMID(ctx context.Context, in ports.LXCInput) (int, error) {
	usedRaw := p.exec.Run(ctx, p.target(in),
		`pvesh get /cluster/resources --type vm --output-format json 2>/dev/null | grep -o '"vmid":[0-9]*' | grep -o '[0-9]*' || true`,
		ports.ExecOpts{})
	used := map[int]bool{}
	for _, l := range strings.Fields(usedRaw.Output) {
		if n, e := strconv.Atoi(strings.TrimSpace(l)); e == nil {
			used[n] = true
		}
	}
	if in.VMID != 0 && !used[in.VMID] {
		return in.VMID, nil
	}
	if in.VMID != 0 {
		slog.Info("provisioner: pct_create VMID reassigned", "requested", in.VMID)
	}
	nextRaw := p.exec.Run(ctx, p.target(in), `pvesh get /cluster/nextid 2>/dev/null`, ports.ExecOpts{})
	nextID, cerr := strconv.Atoi(strings.TrimSpace(nextRaw.Output))
	if nextRaw.Err != nil || cerr != nil || nextID == 0 {
		return 0, fmt.Errorf("VMID %d is already in use on the cluster and could not resolve a free id", in.VMID)
	}
	return nextID, nil
}

// CreateVM is reserved for the qm flow; no consumer exists yet.
func (p *Provisioner) CreateVM(_ context.Context, _ ports.VMInput) (ports.ProvisionResult, error) {
	return ports.ProvisionResult{}, errors.New("provisioner: VM creation via qm not implemented yet")
}

// BuildPctCreateCmd renders the pct create command from a
// fully-defaulted input. Pure; split out for tests.
func BuildPctCreateCmd(in ports.LXCInput) string {
	privFlag := "--unprivileged 1"
	if in.Privileged {
		privFlag = "--unprivileged 0"
	}

	features := []string{}
	if in.Nesting {
		features = append(features, "nesting=1")
	}
	if in.Privileged {
		features = append(features, "keyctl=1")
	}
	nestingFlag := ""
	if len(features) > 0 {
		nestingFlag = fmt.Sprintf(" --features %s", strings.Join(features, ","))
	}

	// net0: DHCP when no static IP is given (or ip=="dhcp"). Proxmox
	// rejects a gateway alongside ip=dhcp, so only add gw for a static IP.
	net0 := "name=eth0,bridge=" + in.Bridge + ","
	if in.IP == "" || strings.EqualFold(in.IP, "dhcp") {
		net0 += "ip=dhcp"
	} else {
		net0 += "ip=" + in.IP
		if in.GW != "" {
			net0 += ",gw=" + in.GW
		}
	}

	templatePath := fmt.Sprintf("/var/lib/vz/template/cache/%s", in.Template)
	cmd := fmt.Sprintf(
		"pct create %d %s --hostname %s --cores %d --memory %d --rootfs %s:%d %s --net0 %s%s --start 1",
		in.VMID, templatePath, in.Hostname, in.Cores, in.MemoryMB,
		in.Storage, in.DiskGB, privFlag, net0, nestingFlag)

	if in.Nameserver != "" {
		cmd += fmt.Sprintf(" --nameserver %s", in.Nameserver)
	}
	if in.Searchdomain != "" {
		cmd += fmt.Sprintf(" --searchdomain %s", in.Searchdomain)
	}

	for i, mp := range in.Mounts {
		if i < 10 { // pct supports up to mp9
			cmd += fmt.Sprintf(" --mp%d %s", i, mp)
		}
	}
	return cmd
}

// ResolveTemplate maps a requested template name to one actually present
// in the host's template cache. Exact match wins; a bare distro hint
// (e.g. "debian-13" or "debian") matches by prefix; empty picks the
// newest debian (falling back to any) template available. Returns ""
// when nothing fits.
func ResolveTemplate(requested string, available []string) string {
	if len(available) == 0 {
		return ""
	}
	if requested != "" {
		for _, a := range available {
			if a == requested {
				return a
			}
		}
		for _, a := range available {
			if strings.HasPrefix(a, requested) {
				return a
			}
		}
	}
	// Auto-pick: prefer debian, then the lexically-greatest (newest version).
	best := ""
	for _, a := range available {
		if strings.Contains(a, "debian") && a > best {
			best = a
		}
	}
	if best != "" {
		return best
	}
	for _, a := range available {
		if a > best {
			best = a
		}
	}
	return best
}

// GatewayPreflightPassed interprets the PREFLIGHT_OK/PREFLIGHT_FAIL
// markers from the gateway pre-flight check. Exact-match markers — a
// prior version checked for "REACHABLE", a substring of "UNREACHABLE",
// so the check could never actually fail. Exact match plus a test make
// that bug class structurally unable to recur silently.
func GatewayPreflightPassed(out string) bool {
	return strings.TrimSpace(out) == "PREFLIGHT_OK"
}
