package hub

import "sort"

// DR recipe — the agent (storage/guest/PBS) HALF of the secret-free reconstruction recipe
// (SPIKE-dr-recipe-2026-06-16). The recipe complements escrow (keys) + PBS (bytes): it is
// the non-secret SCAFFOLDING an operator must rebuild before the PBS bytes can land — guest sizing,
// drive inventory (durable-id → mount → intent → size), PVE storage defs, and PBS coordinates.
//
// BOUNDARY (non-negotiable, the Phase-1 lesson): every field here is an identifier, intent, size, or
// coordinate — NEVER a key, password, token, hash, or ENC: value. Secrets live in the PBS whole-CT
// snapshot + escrow blobs, recovered with R, never regenerated, never here. TestDRRecipeHostHalf_NoSecrets
// asserts no field name matches the secret regex. The hub assembles this half with the controller's
// app half into one customer recipe.
//
// v1 host-half drive shape (v0.39.0) = identifiers/intent/size ONLY: {durable_id, mount_path, intent,
// fs_type?, total_bytes}. Two fields were deliberately DROPPED from v1:
//   - role — a drive's purpose (primary/bulk-data/…) is a hub/operator-owned manifest concept, not
//     cleanly derivable host-side (both demo externals are content=backup, yet one is the primary
//     data drive and the other holds no apps). Deferred until the hub/operator stamps it.
//   - restic_repo_coord — RESERVED for a future offsite bulk-volume backup tier. None exists today:
//     external-drive data has no offsite/second-failure-domain copy (cross-drive backup is rsync to
//     the SAME internal SSD), so the field named nothing real. Re-add when that tier ships.
//
// The pbs coord, by contrast, is resolved LIVE each collect (LiveSnapshotReporter) so the restore
// SOURCE is present whenever PBS is reachable — not gated on the 6 h verify cadence.
//
// recipe_version=1. The wire shape is byte-pinned in the cross-repo golden (host-report.golden.json
// here + the hub's copy) — see the manual checksum-diff discipline in CHANGELOG. Read is
// ignore-unknown (encoding/json default) for forward-compat, mirroring storage_manifest.
const DRRecipeVersion = 1

// DRRecipeHostHalf is the agent-emitted half (guest/drive/storage/PBS scaffolding). Derived entirely
// from facts the report already collects — no new privileged reads.
type DRRecipeHostHalf struct {
	RecipeVersion int            `json:"recipe_version"`
	Guests        []DRGuest      `json:"guests"`
	PBS           *DRPBSCoord    `json:"pbs,omitempty"`
	Drives        []DRDrive      `json:"drives"`
	PVEStorage    []DRPVEStorage `json:"pve_storage"`
}

// DRGuest is the sizing needed to recreate the LXC at the right size (GuestSpec, already on the wire).
type DRGuest struct {
	VMID        int   `json:"vmid"`
	Cores       int   `json:"cores"`
	MemoryBytes int64 `json:"memory_bytes"`
	DiskBytes   int64 `json:"disk_bytes"`
}

// DRPBSCoord is WHERE the whole-CT snapshot lives — COORDINATES ONLY. The encryption key is escrow-only;
// the access token is identity-escrow-only. Neither is here.
type DRPBSCoord struct {
	RepoID           string `json:"repo_id"`            // the PVE pbs storage id (e.g. "felhom-pbs") — not a token
	Namespace        string `json:"namespace"`          // PBS namespace the restore targets
	LatestSnapshotID string `json:"latest_snapshot_id"` // most-recent snapshot's backup_id (a coordinate)
}

// DRDrive is one user-data drive: identifiers + intent + size. v1 carries ONLY these fields (role +
// restic_repo_coord were dropped — see the file header for why). Every field is an identifier, intent,
// or size; none is a credential.
type DRDrive struct {
	DurableID  string `json:"durable_id"` // uuid:<fs-uuid> — a hardware identifier, not a credential
	MountPath  string `json:"mount_path"`
	Intent     string `json:"intent"` // enrolled | ejected | decommissioned
	FSType     string `json:"fs_type,omitempty"`
	TotalBytes int64  `json:"total_bytes"`
}

// DRPVEStorage is a PVE storage definition (to rebuild /etc/pve/storage.cfg scaffolding) — no auth.
type DRPVEStorage struct {
	Name    string `json:"name"`
	Type    string `json:"type"`
	Content string `json:"content"`
}

// driveIntentEnrolled is the v1 intent for an emitted user-data drive. The agent's authoritative
// per-drive intent (enrolled/ejected/decommissioned) lives in the GuestBindStore; v1 emits the
// reachable user-data drives it observes as enrolled, with the field present for forward refinement.
const driveIntentEnrolled = "enrolled"

// BuildDRRecipeHostHalf assembles the agent half from the already-collected report facts — pure, so
// it is unit-tested directly (no live reads). drives[] = the user-data external drives (usb/local-dir
// with a durable-id); pve_storage[] = every storage target (the storage.cfg scaffolding); pbs = the
// latest PBS snapshot's coordinates; guests[] = each guest's sizing (skip guests with no spec).
func BuildDRRecipeHostHalf(guests []Guest, targets []StorageTarget, pbs []PBSSnapshot) *DRRecipeHostHalf {
	h := &DRRecipeHostHalf{
		RecipeVersion: DRRecipeVersion,
		Guests:        []DRGuest{},
		Drives:        []DRDrive{},
		PVEStorage:    []DRPVEStorage{},
	}

	for _, g := range guests {
		if g.Spec == nil { // status unknown — no sizing to recreate from
			continue
		}
		h.Guests = append(h.Guests, DRGuest{
			VMID:        g.VMID,
			Cores:       g.Spec.Cores,
			MemoryBytes: g.Spec.MemoryBytes,
			DiskBytes:   g.Spec.DiskBytes,
		})
	}

	var pbsRepoID string
	for _, t := range targets {
		h.PVEStorage = append(h.PVEStorage, DRPVEStorage{Name: t.Name, Type: t.Type, Content: t.Content})
		if t.Type == StorageTypePBS && pbsRepoID == "" {
			pbsRepoID = t.Name // the pbs storage id is a coordinate, not the key
		}
		if isUserDataDrive(t) {
			h.Drives = append(h.Drives, DRDrive{
				DurableID:  t.DurableID,
				MountPath:  t.MountPath,
				Intent:     driveIntentEnrolled,
				TotalBytes: t.TotalBytes,
			})
		}
	}

	if c := latestPBSCoord(pbs, pbsRepoID); c != nil {
		h.PBS = c
	}
	return h
}

// isUserDataDrive selects the external user-data drives the recipe enumerates (felhom-usb / felhom-flash
// class): a usb or local-dir storage with a filesystem-UUID durable id and a host mount path. local /
// lvmthin / pbs / nfs / cifs are scaffolding (they land in pve_storage) but are not user-data drives.
func isUserDataDrive(t StorageTarget) bool {
	if t.Type != StorageTypeUSB && t.Type != StorageTypeLocalDir {
		return false
	}
	return t.DurableID != "" && t.MountPath != ""
}

// latestPBSCoord picks the most-recent snapshot (lexical max of the RFC3339 backup_time) and returns
// its coordinates. Returns nil when there is no snapshot to target.
func latestPBSCoord(snaps []PBSSnapshot, repoID string) *DRPBSCoord {
	if len(snaps) == 0 {
		return nil
	}
	sorted := append([]PBSSnapshot(nil), snaps...)
	sort.Slice(sorted, func(i, j int) bool { return sorted[i].BackupTime > sorted[j].BackupTime })
	latest := sorted[0]
	return &DRPBSCoord{
		RepoID:           repoID,
		Namespace:        latest.Namespace,
		LatestSnapshotID: latest.BackupID,
	}
}
