package authz

import (
	"encoding/json"
	"fmt"
	"strings"
	"time"
)

// CanonicalBlob builds the canonical OpBlob bytes (phase4 §2 field order: keys sorted at
// every level, no insignificant whitespace, no trailing newline, UTF-8). This is the SINGLE
// production source of the signed bytes — the operator signing CLI (cmd/felhom-opsign) and the
// in-Go test minting both call it, so the signer can NEVER drift from what the verifier expects
// (the verifier authenticates over the RAW received bytes, so these bytes ARE the contract).
//
// params is canonicalized internally (parsed + re-marshaled → object keys sorted, whitespace
// stripped) so the same op+params always yields identical bytes; "" → "{}". Returns an error
// only when params is not valid JSON.
func CanonicalBlob(op, hostID, guestID, keyID, nonce, paramsJSON string, issued, expires time.Time) ([]byte, error) {
	params := strings.TrimSpace(paramsJSON)
	if params == "" {
		params = "{}"
	}
	var pv interface{}
	if err := json.Unmarshal([]byte(params), &pv); err != nil {
		return nil, fmt.Errorf("authz: params is not valid JSON: %w", err)
	}
	pc, err := json.Marshal(pv) // Go marshals object keys sorted, compact
	if err != nil {
		return nil, fmt.Errorf("authz: canonicalizing params: %w", err)
	}
	return []byte(fmt.Sprintf(
		`{"expires_at":%q,"issued_at":%q,"key_id":%q,"nonce":%q,"op":%q,"params":%s,"target":{"guest_id":%q,"host_id":%q}}`,
		expires.UTC().Format(time.RFC3339), issued.UTC().Format(time.RFC3339),
		keyID, nonce, op, pc, guestID, hostID)), nil
}

// Target binds an op to a specific box (and optionally a guest) — the anti-retarget
// field. The §7 reference omitted the json tags; production needs them so the
// signed canonical bytes decode correctly.
type Target struct {
	HostID  string `json:"host_id"`
	GuestID string `json:"guest_id"`
}

// OpBlob is the canonical signed object (phase4 §2). The signature covers the
// EXACT bytes of this object's canonical JSON (keys sorted at every level, no
// insignificant whitespace, no trailing newline, UTF-8) — produced by the
// operator CLI / hub, verified here over the raw received bytes.
type OpBlob struct {
	Op        string          `json:"op"`
	Target    Target          `json:"target"`
	Params    json.RawMessage `json:"params"`
	Nonce     string          `json:"nonce"`
	IssuedAt  time.Time       `json:"issued_at"`
	ExpiresAt time.Time       `json:"expires_at"`
	KeyID     string          `json:"key_id"`
}

// VerifiedOp is the authenticated, parsed op returned on success — everything the
// reconcile layer (slice 4) needs to route and execute, not just the op string.
type VerifiedOp struct {
	Op     string // the operation, e.g. "guest_destroy"
	HostID string // target host (== this agent's host)
	// GuestID is non-empty for a guest-scoped op; the caller routes by it. "" =
	// host-scoped op. The verifier does NOT need to know all guest ids.
	GuestID   string
	Params    json.RawMessage
	Nonce     string
	IssuedAt  time.Time
	ExpiresAt time.Time

	// KeyID is the blob's self-declared key id — ADVISORY / audit only, never an
	// authz input. Authz is the key-material allow-list match (Signer below).
	KeyID string
	// Signer is the allow-listed key whose material matched the signature.
	Signer AllowedSigner
	// KeyIDMatchesSigner is false when the blob's advisory KeyID disagrees with
	// the matched signer's id (a benign audit signal, not a rejection).
	KeyIDMatchesSigner bool
}
