package store

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

// HostRecoveryCredential is the break-glass PVE console credential for a host (TASK G1). Secret is
// the root@pam password — a hub-held secret, operator-retrievable (NOT zero-knowledge like escrow).
type HostRecoveryCredential struct {
	HostID   string
	Username string
	Secret   string
	SetAt    time.Time
}

// SaveHostRecoveryCredential upserts a host's break-glass credential (last-write-wins: day-0 sets it,
// --rotate re-sets). The secret is stored as-is at rest; the hub NEVER logs it and only ever returns
// it over the operator-authenticated retrieval path.
func (s *Store) SaveHostRecoveryCredential(hostID, username, secret string) error {
	_, err := s.db.Exec(`
		INSERT INTO host_recovery (host_id, username, secret, set_at, updated_at)
		VALUES (?, ?, ?, datetime('now'), datetime('now'))
		ON CONFLICT(host_id) DO UPDATE SET
			username   = excluded.username,
			secret     = excluded.secret,
			updated_at = datetime('now')`,
		hostID, username, secret)
	return err
}

// GetHostRecoveryCredential returns a host's break-glass credential, or (nil, nil) if none is vaulted.
func (s *Store) GetHostRecoveryCredential(hostID string) (*HostRecoveryCredential, error) {
	var c HostRecoveryCredential
	var setAt string
	err := s.db.QueryRow(
		`SELECT host_id, username, secret, set_at FROM host_recovery WHERE host_id = ?`, hostID).
		Scan(&c.HostID, &c.Username, &c.Secret, &setAt)
	if err == sql.ErrNoRows {
		return nil, nil
	}
	if err != nil {
		return nil, err
	}
	c.SetAt = parseSQLiteTime(setAt)
	return &c, nil
}

// HostRecoveryMeta is the NON-SECRET shape of a vaulted break-glass credential: what the operator's
// host page shows without the plaintext ever entering the rendered document. The secret column is
// deliberately absent from both the struct and the query — the render path must be unable to carry it.
type HostRecoveryMeta struct {
	HostID   string
	Username string
	SetAt    time.Time
}

// GetHostRecoveryMeta returns a host's credential metadata, or (nil, nil) if none is vaulted.
// Use this — NOT GetHostRecoveryCredential — on any path that renders a page: the secret can only
// leave the hub through the explicit, CSRF-gated, audited reveal endpoint.
func (s *Store) GetHostRecoveryMeta(hostID string) (*HostRecoveryMeta, error) {
	var m HostRecoveryMeta
	var setAt string
	err := s.db.QueryRow(
		`SELECT host_id, username, set_at FROM host_recovery WHERE host_id = ?`, hostID).
		Scan(&m.HostID, &m.Username, &setAt)
	if err == sql.ErrNoRows {
		return nil, nil
	}
	if err != nil {
		return nil, err
	}
	m.SetAt = parseSQLiteTime(setAt)
	return &m, nil
}

// HasHostRecoveryCredential reports whether a host already has a vaulted credential (day-0 idempotency:
// don't regenerate/re-set on a re-run unless --rotate).
func (s *Store) HasHostRecoveryCredential(hostID string) (bool, error) {
	var one int
	err := s.db.QueryRow(`SELECT 1 FROM host_recovery WHERE host_id = ?`, hostID).Scan(&one)
	if err == sql.ErrNoRows {
		return false, nil
	}
	if err != nil {
		return false, err
	}
	return true, nil
}

// HostMgmtPlaneRow is the latest management-plane state per host (TASK G1), parsed from the newest
// host_report. PrivsepHealedAt is the watchdog heal-marker timestamp ("" when never healed / old agent).
type HostMgmtPlaneRow struct {
	HostID          string
	CustomerID      string
	PrivsepDirOK    bool
	SshdReachable   bool
	PrivsepHealedAt string
}

// GetHostMgmtPlaneStates returns the latest mgmt_plane stanza per host (mirrors
// GetHostLeafFingerprints). A report without the stanza (old agent, feature off) yields zero values →
// no alert. Malformed JSON degrades to zero values, never an error for that host.
func (s *Store) GetHostMgmtPlaneStates() ([]HostMgmtPlaneRow, 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 []HostMgmtPlaneRow
	for rows.Next() {
		var r HostMgmtPlaneRow
		var reportJSON string
		if err := rows.Scan(&r.HostID, &r.CustomerID, &reportJSON); err != nil {
			return nil, err
		}
		var body struct {
			MgmtPlane *struct {
				PrivsepDirOK    bool   `json:"privsep_dir_ok"`
				SshdReachable   bool   `json:"sshd_reachable"`
				PrivsepHealedAt string `json:"privsep_healed_at"`
			} `json:"mgmt_plane"`
		}
		_ = json.Unmarshal([]byte(reportJSON), &body) // malformed/old → nil mgmt_plane → zero values
		if body.MgmtPlane != nil {
			r.PrivsepDirOK = body.MgmtPlane.PrivsepDirOK
			r.SshdReachable = body.MgmtPlane.SshdReachable
			r.PrivsepHealedAt = body.MgmtPlane.PrivsepHealedAt
		}
		out = append(out, r)
	}
	return out, rows.Err()
}
