// Package snapshot implements the v0.8.17 runtime-state capture
// envelope. Capture() reads every section the
// pause-resume-snapshot RFC defines (agent_defs, agent_def_active,
// memory, channels, evaluations, paused_runs, interaction_history)
// into a single JSON envelope; HTTP handlers + the CLI consume the
// resulting *store.SnapshotRow to persist + export.
//
// The Memory section's `embedding` field is locked at v1.0 with the
// optional shape defined in doc-internal/rfcs/semantic-memory.md
// § "Snapshot integration". In Phase 1 (this package) every memory
// entry's embedding is null because the Phase 2 vector ops haven't
// landed yet; the shape ships now so the v1.0 → v1.1 migration
// is avoidable when vector lands. See memory_embedding.go.
package snapshot

import (
	"encoding/json"
	"time"

	"github.com/denn-gubsky/loomcycle/internal/store"
)

// SchemaVersion is the outer envelope version. Bumped only when a
// structurally breaking change lands; per-section schemas evolve
// independently via the additive-fields rule (see
// doc-internal/rfcs/pause-resume-snapshot.md § "Forward-compat
// additive-fields rule").
const SchemaVersion = 1

// SectionVersion is the wire-stable per-section version string.
// Today every section is at "1.0"; PR 3 (export/restore) builds a
// per-section migration registry keyed on these strings.
const SectionVersion = "1.0"

// Envelope is the full snapshot JSON shape. Marshalled by Capture()
// and stored in snapshots.json_content. The outer shape pins:
//
//	{
//	  "schema_version": 1,
//	  "created_at": "...",
//	  "sections": {
//	    "agent_defs":         { "version": "1.0", "entries": [...] },
//	    "agent_def_active":   { "version": "1.0", "entries": [...] },
//	    "memory":             { "version": "1.0", "entries": [...] },
//	    "channels":           { "version": "1.0", "config":  [...], "messages": [...], "cursors": [...] },
//	    "evaluations":        { "version": "1.0", "entries": [...] },
//	    "paused_runs":        { "version": "1.0", "entries": [...] },
//	    "interaction_history":{ "version": "1.0", "since_ts": "...", "events": [...] }  // optional
//	  }
//	}
type Envelope struct {
	SchemaVersion int       `json:"schema_version"`
	CreatedAt     time.Time `json:"created_at"`
	Sections      Sections  `json:"sections"`
	// Checksum is an OPTIONAL "sha256:<hex>" digest over the canonical JSON
	// bytes of the Sections object (exp7 I4). Additive + backward-compatible:
	// Export stamps it, Restore verifies it ONLY when present — snapshots
	// captured before this field (no checksum) still restore unchanged.
	// omitempty keeps pre-checksum readers byte-identical and lets the
	// human-only ExportPretty path omit it.
	Checksum string `json:"checksum,omitempty"`
}

// Sections is the named section map. Each field is the section's
// declared shape; the per-section version field is the migration
// anchor (PR 3 reads it to dispatch into the registry).
type Sections struct {
	AgentDefs          AgentDefsSection           `json:"agent_defs"`
	AgentDefActive     AgentDefActiveSection      `json:"agent_def_active"`
	SkillDefs          SkillDefsSection           `json:"skill_defs"`
	SkillDefActive     SkillDefActiveSection      `json:"skill_def_active"`
	TeamDefs           TeamDefsSection            `json:"team_defs"`
	TeamDefActive      TeamDefActiveSection       `json:"team_def_active"`
	MCPServerDefs      MCPServerDefsSection       `json:"mcp_server_defs"`
	MCPServerDefActive MCPServerDefActiveSection  `json:"mcp_server_def_active"`
	Memory             MemorySection              `json:"memory"`
	Channels           ChannelsSection            `json:"channels"`
	Evaluations        EvaluationsSection         `json:"evaluations"`
	PausedRuns         PausedRunsSection          `json:"paused_runs"`
	InteractionHistory *InteractionHistorySection `json:"interaction_history,omitempty"`
	// SqlMem is the optional RFC AA Phase-3e SQL Memory facet — a logical dump
	// of every durable per-scope SQL database. Present only when the runtime has
	// SQL Memory enabled at capture time (the pointer is omitempty); a snapshot
	// taken without it, or restored into a reader that predates it, sees the
	// section absent (back-compat, like interaction_history).
	SqlMem *SqlMemSection `json:"sqlmem,omitempty"`
}

// AgentDefsSection wraps the list of every agent_defs row.
type AgentDefsSection struct {
	Version string          `json:"version"`
	Entries []AgentDefEntry `json:"entries"`
}

// AgentDefEntry mirrors store.AgentDefRow with stable JSON field
// names. Kept distinct from the store type so the snapshot envelope
// can evolve independently — adding a field here doesn't require
// adding it to the store struct, and vice versa.
type AgentDefEntry struct {
	DefID string `json:"def_id"`
	// TenantID is the RFC N tenant-isolation axis. Carried through the snapshot
	// so a multi-tenant capture→restore preserves each def's owning tenant;
	// without it every def collapsed to the shared "" tenant on restore
	// (cross-tenant disclosure + same-name active-pointer collisions). Additive
	// (omitempty) — older snapshots restore with an empty tenant as before.
	TenantID               string          `json:"tenant_id,omitempty"`
	Name                   string          `json:"name"`
	Version                int             `json:"version"`
	ParentDefID            string          `json:"parent_def_id,omitempty"`
	Definition             json.RawMessage `json:"definition"`
	Description            string          `json:"description,omitempty"`
	CreatedAt              time.Time       `json:"created_at"`
	CreatedByAgentID       string          `json:"created_by_agent_id,omitempty"`
	CreatedByRunID         string          `json:"created_by_run_id,omitempty"`
	Retired                bool            `json:"retired"`
	BootstrappedFromStatic bool            `json:"bootstrapped_from_static"`
	// ContentSHA256 — v0.9.x deterministic hash of the agent's content-
	// bearing fields. Empty on snapshots captured before v0.9.x; the
	// boot-time backfill in cmd/loomcycle/main.go computes the hash for
	// rows restored without one on the next startup. Additive field
	// (omitempty); restoring older snapshots into newer readers works
	// without a section-version bump.
	ContentSHA256 string `json:"content_sha256,omitempty"`
}

// AgentDefActiveSection wraps the active-pointer entries.
type AgentDefActiveSection struct {
	Version string                `json:"version"`
	Entries []AgentDefActiveEntry `json:"entries"`
}

type AgentDefActiveEntry struct {
	Name              string    `json:"name"`
	TenantID          string    `json:"tenant_id,omitempty"` // RFC N — see AgentDefEntry.TenantID
	DefID             string    `json:"def_id"`
	PromotedAt        time.Time `json:"promoted_at"`
	PromotedByAgentID string    `json:"promoted_by_agent_id,omitempty"`
}

// SkillDefsSection mirrors AgentDefsSection. v0.8.22.
type SkillDefsSection struct {
	Version string          `json:"version"`
	Entries []SkillDefEntry `json:"entries"`
}

// SkillDefEntry mirrors AgentDefEntry. Same field set; the
// definition payload's JSON schema (body / description /
// tools) is owned by the SkillDef tool.
type SkillDefEntry struct {
	DefID                  string          `json:"def_id"`
	TenantID               string          `json:"tenant_id,omitempty"` // RFC N — see AgentDefEntry.TenantID
	Name                   string          `json:"name"`
	Version                int             `json:"version"`
	ParentDefID            string          `json:"parent_def_id,omitempty"`
	Definition             json.RawMessage `json:"definition"`
	Description            string          `json:"description,omitempty"`
	CreatedAt              time.Time       `json:"created_at"`
	CreatedByAgentID       string          `json:"created_by_agent_id,omitempty"`
	CreatedByRunID         string          `json:"created_by_run_id,omitempty"`
	Retired                bool            `json:"retired"`
	BootstrappedFromStatic bool            `json:"bootstrapped_from_static"`
	// ContentSHA256 — see AgentDefEntry.ContentSHA256.
	ContentSHA256 string `json:"content_sha256,omitempty"`
}

// SkillDefActiveSection mirrors AgentDefActiveSection.
type SkillDefActiveSection struct {
	Version string                `json:"version"`
	Entries []SkillDefActiveEntry `json:"entries"`
}

type SkillDefActiveEntry struct {
	Name              string    `json:"name"`
	TenantID          string    `json:"tenant_id,omitempty"` // RFC N — see AgentDefEntry.TenantID
	DefID             string    `json:"def_id"`
	PromotedAt        time.Time `json:"promoted_at"`
	PromotedByAgentID string    `json:"promoted_by_agent_id,omitempty"`
}

// TeamDefsSection mirrors SkillDefsSection.
type TeamDefsSection struct {
	Version string         `json:"version"`
	Entries []TeamDefEntry `json:"entries"`
}

// TeamDefEntry mirrors SkillDefEntry. Same field set; the
// definition payload (an opaque workflow graph) is owned by the
// TeamDef tool.
type TeamDefEntry struct {
	DefID                  string          `json:"def_id"`
	TenantID               string          `json:"tenant_id,omitempty"` // RFC N — see AgentDefEntry.TenantID
	Name                   string          `json:"name"`
	Version                int             `json:"version"`
	ParentDefID            string          `json:"parent_def_id,omitempty"`
	Definition             json.RawMessage `json:"definition"`
	Description            string          `json:"description,omitempty"`
	CreatedAt              time.Time       `json:"created_at"`
	CreatedByAgentID       string          `json:"created_by_agent_id,omitempty"`
	CreatedByRunID         string          `json:"created_by_run_id,omitempty"`
	Retired                bool            `json:"retired"`
	BootstrappedFromStatic bool            `json:"bootstrapped_from_static"`
	// ContentSHA256 — see AgentDefEntry.ContentSHA256.
	ContentSHA256 string `json:"content_sha256,omitempty"`
}

// TeamDefActiveSection mirrors SkillDefActiveSection.
type TeamDefActiveSection struct {
	Version string               `json:"version"`
	Entries []TeamDefActiveEntry `json:"entries"`
}

type TeamDefActiveEntry struct {
	Name              string    `json:"name"`
	TenantID          string    `json:"tenant_id,omitempty"` // RFC N — see AgentDefEntry.TenantID
	DefID             string    `json:"def_id"`
	PromotedAt        time.Time `json:"promoted_at"`
	PromotedByAgentID string    `json:"promoted_by_agent_id,omitempty"`
}

// MCPServerDefsSection mirrors SkillDefsSection. v0.9.x dynamic MCP
// server registration substrate. Additive snapshot section — pre-
// v0.9.x readers don't know about it; pre-v0.9.x snapshots restored
// to v0.9.x readers find this section empty.
type MCPServerDefsSection struct {
	Version string              `json:"version"`
	Entries []MCPServerDefEntry `json:"entries"`
}

// MCPServerDefEntry mirrors AgentDefEntry / SkillDefEntry. The
// definition payload schema (transport / url / headers / description
// / discovered_tools) is owned by the MCPServerDef tool.
type MCPServerDefEntry struct {
	DefID                  string          `json:"def_id"`
	TenantID               string          `json:"tenant_id,omitempty"` // RFC N — see AgentDefEntry.TenantID
	Name                   string          `json:"name"`
	Version                int             `json:"version"`
	ParentDefID            string          `json:"parent_def_id,omitempty"`
	Definition             json.RawMessage `json:"definition"`
	Description            string          `json:"description,omitempty"`
	CreatedAt              time.Time       `json:"created_at"`
	CreatedByAgentID       string          `json:"created_by_agent_id,omitempty"`
	CreatedByRunID         string          `json:"created_by_run_id,omitempty"`
	Retired                bool            `json:"retired"`
	BootstrappedFromStatic bool            `json:"bootstrapped_from_static"`
	// ContentSHA256 — see AgentDefEntry.ContentSHA256.
	ContentSHA256 string `json:"content_sha256,omitempty"`
}

// MCPServerDefActiveSection mirrors SkillDefActiveSection.
type MCPServerDefActiveSection struct {
	Version string                    `json:"version"`
	Entries []MCPServerDefActiveEntry `json:"entries"`
}

type MCPServerDefActiveEntry struct {
	Name              string    `json:"name"`
	TenantID          string    `json:"tenant_id,omitempty"` // RFC N — see AgentDefEntry.TenantID
	DefID             string    `json:"def_id"`
	PromotedAt        time.Time `json:"promoted_at"`
	PromotedByAgentID string    `json:"promoted_by_agent_id,omitempty"`
}

// MemorySection wraps memory rows. Each entry carries an optional
// `embedding` field — null in Phase 1 (vector ops not yet shipped),
// populated in Phase 2. The field's presence-and-nil shape is the
// load-bearing forward-compat contract: snapshots written by Phase 1
// readers carry the field as null; Phase 2 readers populate it; old
// readers that don't know about embedding ignore the field
// gracefully (Go's encoding/json drops unknown fields).
type MemorySection struct {
	Version string        `json:"version"`
	Entries []MemoryEntry `json:"entries"`
}

// MemoryEntry mirrors store.MemorySnapshotEntry with the additional
// `embedding` field. CreatedAt / UpdatedAt come from MemorySnapshotEntry's
// embedded MemoryEntry. ExpiresAt is omitted when zero (no TTL).
type MemoryEntry struct {
	// TenantID is the row's isolation axis (RFC BL). omitempty so a
	// single-tenant snapshot stays byte-identical to a pre-RFC-BL one, and a
	// snapshot written before the column existed decodes to "" (the legacy
	// tenant) on restore — the graceful cross-version path.
	TenantID  string                   `json:"tenant_id,omitempty"`
	Scope     string                   `json:"scope"`
	ScopeID   string                   `json:"scope_id"`
	Key       string                   `json:"key"`
	Value     json.RawMessage          `json:"value"`
	ExpiresAt *time.Time               `json:"expires_at,omitempty"`
	CreatedAt time.Time                `json:"created_at"`
	UpdatedAt time.Time                `json:"updated_at"`
	Embedding *MemoryEmbeddingSnapshot `json:"embedding"` // explicit null when nil; see memory_embedding.go
}

// ChannelsSection wraps channels config + messages + cursors. Channel
// config (declared channels list from operator yaml) lives in the
// snapshot so a restore on a different machine knows what channels
// existed; the operator on the new machine reconciles against their
// own yaml.
type ChannelsSection struct {
	Version  string                `json:"version"`
	Config   []ChannelConfigEntry  `json:"config"`
	Messages []ChannelMessageEntry `json:"messages"`
	Cursors  []ChannelCursorEntry  `json:"cursors"`
}

// ChannelConfigEntry mirrors the operator-yaml shape that loomcycle
// validates per-channel. Captured at snapshot time so restore on a
// different host has the operator-declared shape to reconcile
// against.
type ChannelConfigEntry struct {
	Name              string   `json:"name"`
	Description       string   `json:"description,omitempty"`
	Scope             string   `json:"scope"`
	TTLSeconds        int      `json:"ttl_seconds,omitempty"`
	MaxMessages       int      `json:"max_messages,omitempty"`
	AllowedPublishers []string `json:"allowed_publishers,omitempty"`
}

type ChannelMessageEntry struct {
	ID                string          `json:"id"`
	Channel           string          `json:"channel"`
	Scope             string          `json:"scope"`
	ScopeID           string          `json:"scope_id"`
	Payload           json.RawMessage `json:"payload"`
	PublishedAt       time.Time       `json:"published_at"`
	ExpiresAt         *time.Time      `json:"expires_at,omitempty"`
	VisibleAt         *time.Time      `json:"visible_at,omitempty"`
	PublishedByUserID string          `json:"published_by_user_id,omitempty"`
}

type ChannelCursorEntry struct {
	Channel   string    `json:"channel"`
	Scope     string    `json:"scope"`
	ScopeID   string    `json:"scope_id"`
	Cursor    string    `json:"cursor"`
	UpdatedAt time.Time `json:"updated_at"`
}

// EvaluationsSection wraps evaluation rows.
type EvaluationsSection struct {
	Version string            `json:"version"`
	Entries []EvaluationEntry `json:"entries"`
}

type EvaluationEntry struct {
	EvalID         string             `json:"eval_id"`
	RunID          string             `json:"run_id"`
	DefID          string             `json:"def_id,omitempty"`
	Score          float64            `json:"score"`
	Dimensions     map[string]float64 `json:"dimensions,omitempty"`
	Judgement      json.RawMessage    `json:"judgement,omitempty"`
	Rationale      string             `json:"rationale,omitempty"`
	EmitterRole    string             `json:"emitter_role"`
	EmitterAgentID string             `json:"emitter_agent_id,omitempty"`
	EmitterRunID   string             `json:"emitter_run_id,omitempty"`
	CreatedAt      time.Time          `json:"created_at"`
}

// PausedRunsSection wraps paused runs + their transcript events. The
// transcript events are required for resume — the model picks up
// from the last completed iteration boundary, and needs the prior
// turns in its context.
type PausedRunsSection struct {
	Version string           `json:"version"`
	Entries []PausedRunEntry `json:"entries"`
}

type PausedRunEntry struct {
	RunID         string    `json:"run_id"`
	AgentID       string    `json:"agent_id,omitempty"`
	ParentAgentID string    `json:"parent_agent_id,omitempty"`
	UserID        string    `json:"user_id,omitempty"`
	UserTier      string    `json:"user_tier,omitempty"`
	Agent         string    `json:"agent"`
	AgentDefID    string    `json:"agent_def_id,omitempty"`
	SessionID     string    `json:"session_id"`
	StartedAt     time.Time `json:"started_at"`
	Model         string    `json:"model,omitempty"`
	PauseState    string    `json:"pause_state"`
	// Interactive marks a persistent interactive run (F42 / RFC X Phase 2),
	// captured so a restored paused run re-dispatches with the correct
	// park-at-end_turn (vs run-to-completion) semantics. Omitted for batch
	// runs + snapshots taken before this field existed (decode to false).
	Interactive bool `json:"interactive,omitempty"`
	// ParentContext is the run's opaque caller-tracking lineage (v0.12.x),
	// carried through the snapshot so a paused run's parent_context
	// survives pause→snapshot→restore. Omitted when the run had none.
	ParentContext    *store.ParentContext `json:"parent_context,omitempty"`
	TranscriptEvents []TranscriptEvent    `json:"transcript_events"`
	// TranscriptError records a per-run transcript-read failure. Set
	// when GetTranscript returned an error during capture; the entry
	// is otherwise included in the snapshot (RunID, AgentID etc.
	// survive). Operators inspect this field to know which runs lost
	// their replay context and may need manual recovery.
	TranscriptError string `json:"transcript_error,omitempty"`
}

// TranscriptEvent is one event from the run's transcript — replayed
// to the model on resume to reconstruct context. Payload is the raw
// providers.Event JSON; the snapshot package doesn't decode it.
type TranscriptEvent struct {
	Seq     int64           `json:"seq"`
	TsNs    int64           `json:"ts_ns"`
	Type    string          `json:"type"`
	Payload json.RawMessage `json:"payload"`
}

// InteractionHistorySection is optional — captured only when
// CaptureOptions.IncludeHistory is true. Carries every event whose
// ts >= since_ts across the captured run set (or all runs if no
// scope restriction was applied at snapshot time).
type InteractionHistorySection struct {
	Version string            `json:"version"`
	SinceTs time.Time         `json:"since_ts"`
	Events  []TranscriptEvent `json:"events"`
}

// SqlMemSection is the RFC AA Phase-3e SQL Memory facet: a tier-tagged logical
// dump of every durable per-scope SQL database. Tier ("sqlite" | "postgres") is
// the load-bearing field — Restore SKIPS a cross-tier archive (a postgres dump
// carries per-column cast types a sqlite scope can't replay, and vice versa),
// mirroring how the embedding snapshot skips on a vector-support mismatch.
type SqlMemSection struct {
	Version string        `json:"version"`
	Tier    string        `json:"tier"`
	Scopes  []SqlMemScope `json:"scopes"`
	// SkippedScopes records durable scopes EXCLUDED from this snapshot because
	// their logical dump exceeded the per-scope cap (CaptureOptions
	// SqlMemMaxScopeBytes, Phase 3f.2). Recording them here — rather than failing
	// the whole snapshot or silently dropping — keeps one runaway scope from
	// sinking an otherwise-good capture, stays visible on inspection, and lets
	// Restore warn. Omitted (nil) when no scope was skipped.
	SkippedScopes []SqlMemSkippedScope `json:"skipped_scopes,omitempty"`
}

// SqlMemSkippedScope identifies a scope left out of the snapshot for exceeding
// the per-scope size cap, with the dump size that tripped it.
type SqlMemSkippedScope struct {
	Tenant  string `json:"tenant"`
	Scope   string `json:"scope"`
	ScopeID string `json:"scope_id"`
	Bytes   int64  `json:"bytes"`
}

// SqlMemScope is one durable scope's identity + logical dump. DDL is the
// pre-data schema (sequences/tables/non-FK constraints/indexes); PostDDL holds
// FK constraints applied after data load; Tables is the per-table data.
type SqlMemScope struct {
	Tenant  string        `json:"tenant"`
	Scope   string        `json:"scope"`
	ScopeID string        `json:"scope_id"`
	DDL     []string      `json:"ddl"`
	PostDDL []string      `json:"post_ddl,omitempty"`
	Tables  []SqlMemTable `json:"tables"`
}

// SqlMemTable is one table's data. ColumnTypes is populated for the postgres
// tier (used to cast text values back to their column type on restore) and
// empty for sqlite. Rows hold JSON-safe values (strings/numbers/null, or a
// {"$b64": "..."} object for a sqlite binary cell).
type SqlMemTable struct {
	Name        string   `json:"name"`
	Columns     []string `json:"columns"`
	ColumnTypes []string `json:"column_types,omitempty"`
	Rows        [][]any  `json:"rows"`
}
