package store

import (
	"database/sql"
	"encoding/json"
)

// DR recipe (SPIKE-dr-recipe-2026-06-16) — the secret-free reconstruction recipe, stored PLAINTEXT
// because it has NO secrets (it is the clean inverse of the retired infra-backup). The hub receives two
// halves via the existing report paths — the agent's storage/guest/PBS half (on the host-report) and
// the controller's customer/apps half (on the controller report) — and assembles them into one record
// keyed by customer. Both halves carry ONLY identifiers/intents/sizes/coordinates; the table is a
// DEDICATED `dr_recipe`, NOT host_escrow (opaque) and NOT the dropped infra_backup* tables.

// DRRecipe is the stored two-half record for one customer.
type DRRecipe struct {
	CustomerID    string
	RecipeVersion int
	HostID        string
	HostHalfJSON  string // the agent half (guests/pbs/drives/pve_storage); "" until a host-report lands
	AppHalfJSON   string // the controller half (customer/apps); "" until a controller report lands
	UpdatedAt     string
}

// SaveDRRecipeHostHalf upserts the agent (storage/guest/PBS) half for a customer, preserving any
// app half already stored. Last-write-wins on the host half + host_id + recipe_version.
func (s *Store) SaveDRRecipeHostHalf(customerID, hostID string, recipeVersion int, hostHalf []byte) error {
	_, err := s.db.Exec(`
		INSERT INTO dr_recipe (customer_id, recipe_version, host_id, host_half_json, app_half_json, updated_at)
		VALUES (?, ?, ?, ?, '', datetime('now'))
		ON CONFLICT(customer_id) DO UPDATE SET
			recipe_version = excluded.recipe_version,
			host_id = excluded.host_id,
			host_half_json = excluded.host_half_json,
			updated_at = datetime('now')`,
		customerID, recipeVersion, hostID, string(hostHalf),
	)
	return err
}

// SaveDRRecipeAppHalf upserts the controller (customer/apps) half for a customer, preserving any
// host half already stored. Last-write-wins on the app half + recipe_version.
func (s *Store) SaveDRRecipeAppHalf(customerID string, recipeVersion int, appHalf []byte) error {
	_, err := s.db.Exec(`
		INSERT INTO dr_recipe (customer_id, recipe_version, host_id, host_half_json, app_half_json, updated_at)
		VALUES (?, ?, '', '', ?, datetime('now'))
		ON CONFLICT(customer_id) DO UPDATE SET
			recipe_version = excluded.recipe_version,
			app_half_json = excluded.app_half_json,
			updated_at = datetime('now')`,
		customerID, recipeVersion, string(appHalf),
	)
	return err
}

// GetDRRecipe returns the stored two-half record for a customer (nil if none).
func (s *Store) GetDRRecipe(customerID string) (*DRRecipe, error) {
	var r DRRecipe
	err := s.db.QueryRow(`
		SELECT customer_id, recipe_version, host_id, host_half_json, app_half_json, updated_at
		FROM dr_recipe WHERE customer_id = ?`, customerID).
		Scan(&r.CustomerID, &r.RecipeVersion, &r.HostID, &r.HostHalfJSON, &r.AppHalfJSON, &r.UpdatedAt)
	if err == sql.ErrNoRows {
		return nil, nil
	}
	if err != nil {
		return nil, err
	}
	return &r, nil
}

// AssembledRecipe is the operator-facing single recipe: the two halves stitched together. The
// sub-sections are passed through as json.RawMessage so the assembly is robust to forward-compat
// additions inside either half (ignore-unknown at the top level, verbatim passthrough below).
type AssembledRecipe struct {
	RecipeVersion int             `json:"recipe_version"`
	Customer      json.RawMessage `json:"customer,omitempty"`
	Guests        json.RawMessage `json:"guests,omitempty"`
	PBS           json.RawMessage `json:"pbs,omitempty"`
	Drives        json.RawMessage `json:"drives,omitempty"`
	PVEStorage    json.RawMessage `json:"pve_storage,omitempty"`
	// BackupTarget (R-109) names WHICH storage holds the local whole-guest archives — the agent's
	// host-half emits it from v0.118.0.
	BackupTarget json.RawMessage `json:"backup_target,omitempty"`
	Apps         json.RawMessage `json:"apps,omitempty"`
	// OffsiteRestic (R-122) is the offsite restic repo's non-secret coordinates — WHERE to recover from.
	// The controller has emitted it since fork-4 and the hub dropped it for the whole time; see the
	// allow-list warning on appHalfShape.
	OffsiteRestic json.RawMessage `json:"offsite_restic,omitempty"`
}

// hostHalfShape / appHalfShape capture only the top-level keys the assembly stitches; encoding/json
// drops any unknown top-level key (forward-compat — a newer half with extra sections still parses).
//
// THAT FORWARD-COMPAT IS ALSO A TRAP, and it has already cost one shipped section. These two structs are
// ALLOW-LISTS: a section an emitter adds is silently discarded here until it is named in BOTH the shape
// struct and AssembledRecipe. `offsite_restic` proved it — the controller emitted it from fork-4, the hub
// stored it intact for every customer, and the delivered recipe never contained it because nothing here
// listed the key. Nothing failed; the section simply was not there (R-122).
//
// SO: adding a section to either half is a TWO-REPO change. TestAssembleDRRecipe_CarriesEveryEmittedSection
// pins the current set against captured real halves — extend it in the same commit as any new section.
type hostHalfShape struct {
	RecipeVersion int             `json:"recipe_version"`
	Guests        json.RawMessage `json:"guests"`
	PBS           json.RawMessage `json:"pbs"`
	Drives        json.RawMessage `json:"drives"`
	PVEStorage    json.RawMessage `json:"pve_storage"`
	BackupTarget  json.RawMessage `json:"backup_target"`
}
type appHalfShape struct {
	RecipeVersion int             `json:"recipe_version"`
	Customer      json.RawMessage `json:"customer"`
	Apps          json.RawMessage `json:"apps"`
	OffsiteRestic json.RawMessage `json:"offsite_restic"`
}

// AssembleDRRecipe stitches the two stored halves into one operator-facing recipe. Either half may be
// empty (not yet reported); the assembly fills what it has. recipe_version = the max of the two halves'
// versions (1 if both absent). Ignore-unknown: extra top-level keys in either half are dropped, nested
// shapes pass through verbatim — so the three repos can evolve the recipe without silent drift here.
func AssembleDRRecipe(rec *DRRecipe) (AssembledRecipe, error) {
	out := AssembledRecipe{RecipeVersion: 1}
	if rec == nil {
		return out, nil
	}
	if rec.HostHalfJSON != "" {
		var h hostHalfShape
		if err := json.Unmarshal([]byte(rec.HostHalfJSON), &h); err != nil {
			return out, err
		}
		out.Guests, out.PBS, out.Drives, out.PVEStorage = h.Guests, h.PBS, h.Drives, h.PVEStorage
		out.BackupTarget = h.BackupTarget
		if h.RecipeVersion > out.RecipeVersion {
			out.RecipeVersion = h.RecipeVersion
		}
	}
	if rec.AppHalfJSON != "" {
		var a appHalfShape
		if err := json.Unmarshal([]byte(rec.AppHalfJSON), &a); err != nil {
			return out, err
		}
		out.Customer, out.Apps = a.Customer, a.Apps
		out.OffsiteRestic = a.OffsiteRestic
		if a.RecipeVersion > out.RecipeVersion {
			out.RecipeVersion = a.RecipeVersion
		}
	}
	return out, nil
}
