package store

import "encoding/json"

// HostOOBRow is the latest operator-access (OOB) state per host (TASK H1), parsed from the newest
// host_report. Present is false when the agent sent no oob stanza (pre-H1 / feature off) → never
// alerted.
//
// ⚠ THIS STRUCT USED TO MIRROR FIVE OF THE AGENT'S EIGHT OOB FIELDS, and the three it dropped
// included the one that decides the question the OOB checker exists to answer (R-260, G-1). The
// agent has emitted `operator_key_configured` on every heartbeat since v0.72.0 — the same version
// that introduced the stanza itself — and having no field for it here meant encoding/json discarded
// it on arrival, so `oobDegraded` reported a box with felhom-sshd active, reachable, a valid config,
// a configured peer and NO OPERATOR KEY INSTALLED as `ok`. The agent knew and said so.
type HostOOBRow struct {
	HostID                 string
	CustomerID             string
	Present                bool
	FelhomSshdActive       bool
	FelhomSshdPort         int
	Reachable              bool
	ConfigInvalid          bool
	OperatorPeerConfigured bool
	// OperatorKeyConfigured — the operator's authorized_key is actually installed on the box.
	// `operator_peer_configured` above is NOT a substitute: that one only says the peer IP is in
	// desired-state, i.e. that OOB is MEANT to work. This says the credential that actually grants
	// entry is there.
	OperatorKeyConfigured bool
	// OperatorKeyReported distinguishes "the agent said false" from "the agent never said".
	// Absence must never read as "the key is installed" — that is this defect returning through the
	// version door, and this project has watched an absence read as a fact four times.
	//
	// It cannot be false while Present is true for any RELEASED agent: the field and the stanza
	// shipped together in v0.72.0 (2026-07-05), and the vouched floor is far above it. That is a
	// claim, so it is pinned by TestOOBDecode_StanzaWithoutOperatorKey_IsNotSilentlyOK rather than
	// left as a comment.
	OperatorKeyReported bool
	// WGHandshakeAgeS / HealedAt are carried for the ALERT MESSAGE, not for the predicate — the
	// operator reads this at 07:00 and needs the context, but a check that starts failing for
	// reasons nobody asked for is how a check stops being read. nil / "" when not reported.
	WGHandshakeAgeS *int64
	HealedAt        string
}

// decodeOOBInto parses the `oob` stanza of one raw host report into r.
//
// It is a named function rather than an inline literal SO THAT TESTS CAN DRIVE THE REAL DECODE
// BOUNDARY. A test that constructs HostOOBRow by hand cannot see a field that never decodes, which
// is the entire class of defect this exists because of (R-260): every hub test asked what the
// checker did with a row, none asked whether the row could carry the fact.
func decodeOOBInto(r *HostOOBRow, reportJSON string) error {
	var body struct {
		OOB *struct {
			FelhomSshdActive       bool   `json:"felhom_sshd_active"`
			FelhomSshdPort         int    `json:"felhom_sshd_port"`
			Reachable              bool   `json:"reachable"`
			ConfigInvalid          bool   `json:"config_invalid"`
			OperatorPeerConfigured bool   `json:"operator_peer_configured"`
			OperatorKeyConfigured  *bool  `json:"operator_key_configured"`
			WGHandshakeAgeS        *int64 `json:"wg_handshake_age_s"`
			HealedAt               string `json:"healed_at"`
		} `json:"oob"`
	}
	err := json.Unmarshal([]byte(reportJSON), &body)
	if body.OOB == nil {
		return err
	}
	r.Present = true
	r.FelhomSshdActive = body.OOB.FelhomSshdActive
	r.FelhomSshdPort = body.OOB.FelhomSshdPort
	r.Reachable = body.OOB.Reachable
	r.ConfigInvalid = body.OOB.ConfigInvalid
	r.OperatorPeerConfigured = body.OOB.OperatorPeerConfigured
	// A POINTER, deliberately: nil means the agent never said, which is not the same as saying no.
	if body.OOB.OperatorKeyConfigured != nil {
		r.OperatorKeyReported = true
		r.OperatorKeyConfigured = *body.OOB.OperatorKeyConfigured
	}
	r.WGHandshakeAgeS = body.OOB.WGHandshakeAgeS
	r.HealedAt = body.OOB.HealedAt
	return err
}

// GetHostOOBStates returns the latest oob stanza per host (mirrors GetHostMgmtPlaneStates). A report
// without the stanza yields Present=false; malformed JSON degrades to zero values, never an error.
func (s *Store) GetHostOOBStates() ([]HostOOBRow, error) {
	rows, err := s.db.Query(`
		SELECT hr.host_id, hr.customer_id, hr.report_json
		FROM host_reports hr
		JOIN (SELECT host_id, MAX(id) AS mx FROM host_reports GROUP BY host_id) latest
		  ON hr.id = latest.mx`)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var out []HostOOBRow
	for rows.Next() {
		var r HostOOBRow
		var reportJSON string
		if err := rows.Scan(&r.HostID, &r.CustomerID, &reportJSON); err != nil {
			return nil, err
		}
		if err := decodeOOBInto(&r, reportJSON); err != nil {
			// malformed JSON degrades to zero values, never an error (documented above)
			_ = err
		}
		out = append(out, r)
	}
	return out, rows.Err()
}
