// Package dr consumes the host-loss restore_directive (slice 10D / S5) into an inspectable restore
// PLAN. It is DERIVE-AND-SURFACE only: the plan is logged (and exposed for the report), never
// executed — the destructive restore is a separate, operator-present, STOP-gated step. The Consumer
// has NO restore/destroy API by construction, so "execute nothing" is a structural guarantee.
package dr

import (
	"context"
	"log/slog"
	"sync"

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

// RestorePlan is the derived-but-not-executed host-loss plan: per guest → restore coords + sizing;
// per drive → durable_id → expected mount. No secrets (coordinates/identifiers/sizes only).
type RestorePlan struct {
	Mode   string          `json:"mode"`
	Guests []PlannedGuest  `json:"guests"`
	Drives []PlannedDrive  `json:"drives"`
	PBS    *hub.DRPBSCoord `json:"pbs,omitempty"` // WHERE the offsite backups live (repo/ns/latest snapshot)
}

// PlannedGuest is one guest to restore in place, from the offsite datastore, at its original sizing.
type PlannedGuest struct {
	VMID          int    `json:"vmid"`
	Archive       string `json:"archive,omitempty"` // explicit archive from the directive; "" → resolve latest at restore time
	TargetStorage string `json:"target_storage"`    // where the restored volumes land (e.g. local-lvm)
	Cores         int    `json:"cores"`
	MemoryBytes   int64  `json:"memory_bytes"`
	DiskBytes     int64  `json:"disk_bytes"`
}

// PlannedDrive is one data drive to re-attach BY DURABLE_ID (the wrong-disk guard: a match attaches,
// a non-match is refused — the matcher, exercised in the Part-4 spike, never resolves to a near disk).
type PlannedDrive struct {
	DurableID     string `json:"durable_id"`
	ExpectedMount string `json:"expected_mount"`
	Intent        string `json:"intent"`
}

// BuildRestorePlan derives the plan from a host_loss directive + the live DR recipe. Returns
// (nil,false) for a guest_loss/absent directive or a nil recipe (nothing to plan). PURE: reads
// nothing, executes nothing — the whole point of this slice's safe half.
func BuildRestorePlan(directive *hub.WireRestoreDirective, recipe *hub.DRRecipeHostHalf, restoreStorage string) (*RestorePlan, bool) {
	if directive == nil || directive.Mode != "host_loss" || recipe == nil {
		return nil, false
	}
	plan := &RestorePlan{Mode: directive.Mode, PBS: recipe.PBS}
	for _, g := range recipe.Guests {
		pg := PlannedGuest{
			VMID:          g.VMID,
			TargetStorage: restoreStorage,
			Cores:         g.Cores,
			MemoryBytes:   g.MemoryBytes,
			DiskBytes:     g.DiskBytes,
		}
		// The directive may name an explicit archive for a specific guest (else the restore step
		// resolves the latest snapshot from the PBS coord at execution time).
		if directive.Archive != "" && (directive.VMID == 0 || directive.VMID == g.VMID) {
			pg.Archive = directive.Archive
		}
		plan.Guests = append(plan.Guests, pg)
	}
	for _, d := range recipe.Drives {
		plan.Drives = append(plan.Drives, PlannedDrive{
			DurableID:     d.DurableID,
			ExpectedMount: d.MountPath,
			Intent:        d.Intent,
		})
	}
	return plan, true
}

// RecipeFunc yields the current DR recipe (the agent-derived scaffolding). It is called ONLY when a
// host_loss directive is present (a rare DR event), so an on-demand Collect is acceptable.
type RecipeFunc func(ctx context.Context) *hub.DRRecipeHostHalf

// Consumer implements desired.RawConsumer: on a host_loss restore_directive it builds + SURFACES the
// plan (structured log + LastPlan for the report/inspection) and executes NOTHING. A guest_loss or
// absent directive clears the plan. It holds no restore/destroy dependency — surfacing is all it can do.
type Consumer struct {
	recipe         RecipeFunc
	restoreStorage string
	logger         *slog.Logger

	mu       sync.Mutex
	lastPlan *RestorePlan
}

// NewConsumer builds the DR plan consumer. recipe may be nil (then no plan can be built — logged).
func NewConsumer(recipe RecipeFunc, restoreStorage string, logger *slog.Logger) *Consumer {
	if logger == nil {
		logger = slog.Default()
	}
	return &Consumer{recipe: recipe, restoreStorage: restoreStorage, logger: logger}
}

// OnDesiredState implements desired.RawConsumer. Non-host_loss → clear + no-op.
func (c *Consumer) OnDesiredState(ctx context.Context, resp *hub.DesiredStateResponse) {
	if resp == nil {
		return
	}
	dir := resp.DesiredState.RestoreDirective
	if dir == nil || dir.Mode != "host_loss" {
		c.mu.Lock()
		c.lastPlan = nil
		c.mu.Unlock()
		return
	}
	var recipe *hub.DRRecipeHostHalf
	if c.recipe != nil {
		recipe = c.recipe(ctx)
	}
	plan, ok := BuildRestorePlan(dir, recipe, c.restoreStorage)
	if !ok {
		c.logger.Warn("dr: host_loss restore_directive present but no DR recipe available yet — cannot build a plan",
			"directive_vmid", dir.VMID)
		return
	}
	c.mu.Lock()
	c.lastPlan = plan
	c.mu.Unlock()
	// SURFACE only — the destructive restore is a separate, operator-present step.
	c.logger.Warn("dr: host_loss RESTORE PLAN derived (NOT executed — supervised in-place restore is a separate, gated step)",
		"mode", plan.Mode, "guests", len(plan.Guests), "drives", len(plan.Drives), "plan", plan)
}

// LastPlan returns the most recently derived plan (nil if none / cleared). For the report + tests.
func (c *Consumer) LastPlan() *RestorePlan {
	c.mu.Lock()
	defer c.mu.Unlock()
	return c.lastPlan
}
