// Package stage defines the presentation panel as a content sink rather than a // page renderer. // // The contract (docs/phase-2-static-site.md §2.4): // // Stage ← driver(route) // Mode A: HTMX fragment swap, pre-rendered files // Stage ← driver(SSE) // Mode B: streamed fragments (Phase 5) // // Layout code renders a Binding. It never asks which driver produced it, so // re-pointing the panel at a stream is a one-line change at the call site in // Mode B's page and no change at all to Layout or Stage. package stage import ( "context" "encoding/json" "fmt" "sort" "github.com/a-h/templ" ) // Fragment kinds the panel can display. These are the whole vocabulary: adding // one is a deliberate change to §2.4. const ( KindText = "text" KindCode = "code" KindImage = "image" KindVideo = "video" KindDiagram = "diagram" KindLinks = "links" ) // PanelID is the element both drivers swap into. The templates render this id // and the bindings target it, so there is one name for it in the codebase. const PanelID = "stage-content" // PanelTarget is the CSS selector for PanelID. func PanelTarget() string { return "#" + PanelID } // DocFragmentID is the id of the fragment that carries a document's own body. // The shape lives here, with the rest of the panel contract, so that a row in an // evidence table, a pre-rendered file name and a streamed frame all agree on it. func DocFragmentID(docID string) string { return "doc-" + docID } // ArtefactFragmentID is the id of the i-th artefact in a document. It is // derived from the document and the artefact's position, so it stays stable // across builds and a saved link keeps working when the content is re-rendered. func ArtefactFragmentID(docID string, i int) string { return fmt.Sprintf("art-%s-%02d", docID, i+1) } // Mode is how a panel receives content. type Mode string const ( // ModeRoute pulls one pre-rendered fragment per interaction. Mode A. ModeRoute Mode = "route" // ModeStream receives fragments pushed over SSE. Mode B. ModeStream Mode = "stream" ) // Code is a highlighted code exhibit. type Code struct { Language string Label string HTML templ.Component // chroma-highlighted, or a
 fallback
}

// Link is one row of a link-list fragment.
type Link struct {
	Label string
	Href  string
	Note  string
}

// Fragment is one thing the panel can show. The same struct is rendered to a
// static file for the route driver and pushed over SSE for the stream driver.
type Fragment struct {
	ID         string
	Kind       string
	Title      string
	Subtitle   string
	Body       templ.Component // pre-rendered HTML for text fragments
	Code       *Code
	Src        string // image, video or diagram source
	Alt        string
	Links      []Link
	DocURL     string
	DocTitle   string
	Confidence string
	// Rendered marks Body as a complete fragment: the build already rendered
	// the article and everything inside it, so a transport writes these bytes
	// as-is instead of wrapping them again. A source that hands over structured
	// content leaves it false and the panel renders the fragment normally.
	Rendered bool
}

// Binding is everything the Stage component needs to know about its driver.
// Route mode contributes nothing but a marker; stream mode contributes the SSE
// wiring. Both are rendered by the same Stage template.
type Binding struct {
	Mode     Mode
	Endpoint string // stream: the SSE endpoint
	Event    string // stream: the event name to swap on
	Target   string // CSS selector the driver swaps into
	Swap     string // htmx swap style
	Label    string // human description of the driver, shown in the empty state
	Hint     string // one line telling the reader what to do
}

// Attributes returns the htmx/SSE attributes for the panel. Only the stream
// driver needs any: in route mode the trigger elements carry hx-get and
// hx-target, because a fragment is chosen by clicking evidence.
func (b Binding) Attributes() templ.Attributes {
	attrs := templ.Attributes{"data-stage-driver": string(b.Mode)}
	switch b.Mode {
	case ModeStream:
		attrs["hx-ext"] = "sse"
		attrs["sse-connect"] = b.Endpoint
		attrs["sse-swap"] = b.Event
		attrs["hx-swap"] = b.Swap
		attrs["aria-live"] = "polite"
	}
	return attrs
}

// TargetSelector is the panel a link or a stream should swap into.
func (b Binding) TargetSelector() string {
	if b.Target == "" {
		return PanelTarget()
	}
	return b.Target
}

// Driver is the whole contract between the panel and whatever feeds it.
type Driver interface {
	Mode() Mode
	// Bind returns the binding to render on the Stage panel.
	Bind() Binding
}

// RouteDriver feeds the panel from pre-rendered fragment files. Mode A.
type RouteDriver struct {
	// BaseURL is where fragment files are served from, e.g. "/stage".
	BaseURL string
	Label   string
	Hint    string
}

// Mode is part of the Driver contract.
func (RouteDriver) Mode() Mode { return ModeRoute }

// Bind is part of the Driver contract.
func (d RouteDriver) Bind() Binding {
	return Binding{
		Mode:   ModeRoute,
		Target: PanelTarget(),
		Swap:   "innerHTML",
		Label:  defaultString(d.Label, "Evidence panel · driven by route"),
		Hint:   defaultString(d.Hint, "Choose a claim from the evidence table; the artefact appears here."),
	}
}

// FragmentURL is the pre-rendered URL for a fragment id.
func (d RouteDriver) FragmentURL(id string) string {
	base := d.BaseURL
	if base == "" {
		base = "/stage"
	}
	return base + "/" + id
}

// SSEDriver feeds the panel from an event stream. Same panel, same fragments,
// different transport. Used by the Phase 5 console; demonstrated in Phase 2 by
// a stream of the pre-built fragments so the plumbing is provable without AI.
type SSEDriver struct {
	Endpoint string
	Event    string
	Label    string
	Hint     string
}

// Mode is part of the Driver contract.
func (SSEDriver) Mode() Mode { return ModeStream }

// Bind is part of the Driver contract.
func (d SSEDriver) Bind() Binding {
	event := d.Event
	if event == "" {
		event = "stage"
	}
	return Binding{
		Mode:     ModeStream,
		Endpoint: d.Endpoint,
		Event:    event,
		Target:   PanelTarget(),
		Swap:     "innerHTML",
		Label:    defaultString(d.Label, "Evidence panel · driven by stream"),
		Hint:     defaultString(d.Hint, "Frames arrive as they are produced and replace the panel each time."),
	}
}

func defaultString(v, fallback string) string {
	if v == "" {
		return fallback
	}
	return v
}

// Source produces the fragments a stream driver pushes. Phase 2 provides
// ManifestSource; Phase 5 provides the retrieval-driven source. Neither
// touches the panel.
type Source interface {
	// Stream sends fragments for keys, in order, until done or ctx is done.
	// Returning an error ends the stream; the panel keeps the last fragment.
	Stream(ctx context.Context, keys []string, send func(Fragment) error) error
}

// Entry is one pre-rendered fragment, as recorded at build time.
type Entry struct {
	ID    string `json:"id"`
	DocID string `json:"doc"`
	Title string `json:"title"`
	Kind  string `json:"kind"`
	// File is relative to the published root, e.g. "stage/edge-fleet/1.html".
	File string `json:"file"`
}

// Manifest is the build-time index of fragments. The server uses it to stream
// fragments that were rendered once, and the route driver serves the same
// files directly.
type Manifest struct {
	Entries []Entry `json:"entries"`
}

// ByID looks an entry up.
func (m *Manifest) ByID(id string) (Entry, bool) {
	for _, e := range m.Entries {
		if e.ID == id {
			return e, true
		}
	}
	return Entry{}, false
}

// ForDoc returns the entries belonging to a document, oldest first.
func (m *Manifest) ForDoc(docID string) []Entry {
	var out []Entry
	for _, e := range m.Entries {
		if e.DocID == docID {
			out = append(out, e)
		}
	}
	return out
}

// Marshal encodes the manifest for the build to write.
func (m *Manifest) Marshal() ([]byte, error) { return json.MarshalIndent(m, "", "  ") }

// LoadManifest parses a manifest written by the build.
func LoadManifest(b []byte) (*Manifest, error) {
	var m Manifest
	if err := json.Unmarshal(b, &m); err != nil {
		return nil, fmt.Errorf("stage manifest: %w", err)
	}
	return &m, nil
}

// ManifestSource streams fragments that were pre-rendered at build time. It
// exists to prove the stream path end to end without a model in the loop; the
// Phase 5 source implements the same interface with retrieval behind it.
type ManifestSource struct {
	manifest *Manifest
	// HTML returns the pre-rendered markup for an entry.
	HTML func(Entry) (string, error)
	// Frame is an optional hook called as each fragment is sent, for pacing.
	Frame func(index int)
}

// NewManifestSource wires a manifest and a reader together.
func NewManifestSource(m *Manifest, read func(Entry) (string, error), frame func(int)) *ManifestSource {
	return &ManifestSource{manifest: m, HTML: read, Frame: frame}
}

// Stream is part of the Source contract.
func (s *ManifestSource) Stream(ctx context.Context, keys []string, send func(Fragment) error) error {
	entries := make([]Entry, 0, len(keys))
	if len(keys) == 0 {
		entries = append(entries, s.manifest.Entries...)
	}
	for _, k := range keys {
		if e, ok := s.manifest.ByID(k); ok {
			entries = append(entries, e)
			continue
		}
		// A document id expands to that document's fragments.
		entries = append(entries, s.manifest.ForDoc(k)...)
	}
	sort.SliceStable(entries, func(i, j int) bool { return entries[i].ID < entries[j].ID })

	for i, e := range entries {
		if err := ctx.Err(); err != nil {
			return err
		}
		html, err := s.HTML(e)
		if err != nil {
			return err
		}
		if s.Frame != nil {
			s.Frame(i)
		}
		frag := Fragment{ID: e.ID, Kind: e.Kind, Title: e.Title, Body: templ.Raw(html), Rendered: true}
		if err := send(frag); err != nil {
			return err
		}
	}
	return nil
}