package provision

import (
	"context"
	"fmt"
	"log/slog"
	"os"
	"path/filepath"
	"strconv"

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

// TokenMinter mints a per-guest local-API token, persisting only its hash. Satisfied by
// *localapi.TokenStore.
type TokenMinter interface {
	Mint(vmid int) (string, error)
}

// defaults for the config-mount layout.
const (
	// DefaultGuestPath is where the config mount appears INSIDE the guest (matches the
	// controller's bootstrap.DefaultMountPath dir + the golden bootstrap unit).
	DefaultGuestPath = "/etc/felhom-bootstrap"
	// DefaultMountIndex is the mpN slot used for the config mount. It is intentionally high so
	// it never collides with a bring-up data mount (mp0, mp1, …).
	DefaultMountIndex = 9
	// bootstrapFile is the file name inside the config mount.
	bootstrapFile = "bootstrap.json"
	// mappedRoot is the unprivileged-LXC host uid/gid that maps to the guest's root (spike
	// gotcha 1): files chowned to this appear as root:root 0600 inside the guest.
	mappedRoot = "100000:100000"
)

// BackHalf populates a guest's bootstrap config mount host-side (F3). It mints the per-guest
// token, renders bootstrap.json, writes it 0600, chowns it to the mapped guest-root, and attaches
// it as a read-only bind mount via `pct set`. The bind-mount attach + chown are host-root ops
// (NOT API ops and NOT one of proxmox.Privileged's 3 exceptions) — they run through the shared
// Runner (direct as root, or `sudo -n` with the configs/felhom-agent.sudoers PROVISION entries).
type BackHalf struct {
	tokens   TokenMinter
	runner   proxmox.Runner
	stateDir string // agent state dir; the per-guest config dir lives under <stateDir>/guests/<vmid>/bootstrap
	logger   *slog.Logger
}

// NewBackHalf builds the back-half. stateDir defaults to /var/lib/felhom-agent when empty.
func NewBackHalf(tokens TokenMinter, runner proxmox.Runner, stateDir string, logger *slog.Logger) *BackHalf {
	if stateDir == "" {
		stateDir = "/var/lib/felhom-agent"
	}
	if logger == nil {
		logger = slog.Default()
	}
	return &BackHalf{tokens: tokens, runner: runner, stateDir: stateDir, logger: logger}
}

// Input is everything the back-half needs that is NOT secret. The per-guest token is minted here,
// never supplied by the caller.
type Input struct {
	VMID        int
	Customer    DocCustomer
	Hub         DocHub
	Endpoint    string // local-api bridge IP:port
	Fingerprint string // agent leaf-cert SHA-256 (hex)
	GuestPath   string // in-guest mount path; "" → DefaultGuestPath
	MountIndex  int    // mpN slot; 0 → DefaultMountIndex (note: mp0 is a valid slot but reserved for data)
}

// Result reports the placement of the config mount. It deliberately contains NO token (the secret
// lives only in the 0600 file + the token store's hash).
type Result struct {
	VMID      int
	HostDir   string // agent-owned host dir backing the bind mount
	GuestPath string // in-guest mount path
	MountKey  string // mpN key used
}

// Provision runs the back-half for one already-brought-up guest. Order: mint → render → write →
// chown → attach. On any failure the partial host dir is left for inspection (it holds the 0600
// token file; it is not world-readable) and the error is returned. The token plaintext is NEVER
// logged and NEVER returned.
func (b *BackHalf) Provision(ctx context.Context, in Input) (Result, error) {
	if in.VMID <= 0 {
		return Result{}, fmt.Errorf("provision: needs a positive vmid")
	}
	if in.Endpoint == "" || in.Fingerprint == "" {
		return Result{}, fmt.Errorf("provision: needs the local-api endpoint and leaf fingerprint")
	}
	if in.Customer.ID == "" {
		return Result{}, fmt.Errorf("provision: needs a customer id (the hub config-pull target)")
	}
	if in.Hub.URL == "" || in.Hub.RetrievalPassword == "" {
		return Result{}, fmt.Errorf("provision: needs hub url + retrieval passphrase (so the controller can pull its config)")
	}
	guestPath := in.GuestPath
	if guestPath == "" {
		guestPath = DefaultGuestPath
	}
	idx := in.MountIndex
	if idx == 0 {
		idx = DefaultMountIndex
	}
	mountKey := "mp" + strconv.Itoa(idx)

	// 1. Mint the per-guest token (only its hash is persisted). The plaintext exists in `tok`
	//    until it is written into the mount below; it is never logged or returned.
	tok, err := b.tokens.Mint(in.VMID)
	if err != nil {
		return Result{}, fmt.Errorf("provision: mint token: %w", err)
	}

	// 2. Render the stable bootstrap.json contract (with the token injected).
	doc := Doc{
		Schema:   SchemaV2,
		Customer: in.Customer,
		Hub:      in.Hub,
		LocalAPI: DocLocalAPI{Endpoint: in.Endpoint, Fingerprint: in.Fingerprint, Token: tok},
	}
	rendered, err := doc.render()
	if err != nil {
		return Result{}, fmt.Errorf("provision: render bootstrap: %w", err)
	}

	// 3. Write it 0600 into the agent-owned per-guest config dir.
	hostDir := filepath.Join(b.stateDir, "guests", strconv.Itoa(in.VMID), "bootstrap")
	if err := os.MkdirAll(hostDir, 0o700); err != nil {
		return Result{}, fmt.Errorf("provision: config dir: %w", err)
	}
	bootPath := filepath.Join(hostDir, bootstrapFile)
	if err := os.WriteFile(bootPath, rendered, 0o600); err != nil {
		return Result{}, fmt.Errorf("provision: write bootstrap: %w", err)
	}

	// 4. chown to the unprivileged-LXC mapped root so the guest reads it as root:root 0600
	//    (spike gotcha 1). Host-root op via the Runner.
	if err := b.run(ctx, "chown", "-R", mappedRoot, hostDir); err != nil {
		return Result{}, fmt.Errorf("provision: chown config mount: %w", err)
	}

	// 5. Attach the read-only bind mount via `pct set` (host-root op; bind mounts are root@pam
	//    only, so this cannot be the API token). The golden's baked unit consumes it on boot.
	mpSpec := fmt.Sprintf("%s,mp=%s,ro=1", hostDir, guestPath)
	if err := b.run(ctx, "pct", "set", strconv.Itoa(in.VMID), "-"+mountKey, mpSpec); err != nil {
		return Result{}, fmt.Errorf("provision: attach config mount: %w", err)
	}

	b.logger.Info("provision: back-half complete",
		"vmid", in.VMID, "mount", mountKey, "guest_path", guestPath, "endpoint", in.Endpoint)
	// tok intentionally goes out of scope here — never logged, never returned.
	return Result{VMID: in.VMID, HostDir: hostDir, GuestPath: guestPath, MountKey: mountKey}, nil
}

// run executes a host-root command through the Runner, wrapping a nonzero exit with stderr.
func (b *BackHalf) run(ctx context.Context, name string, args ...string) error {
	_, stderr, err := b.runner.Run(ctx, name, args...)
	if err != nil {
		return fmt.Errorf("%s: %w: %s", name, err, string(stderr))
	}
	return nil
}
