-- allium: 1
-- bDS Local Preview Server
-- Scope: core (Wave 4)
-- Distilled from: src/main/engine/PreviewServer.ts, PageRenderer.ts

use "./template.allium" as template
use "./generation.allium" as generation

entity PreviewServer {
    host: String                -- 127.0.0.1
    port: Integer               -- 4123
    is_running: Boolean
}

config {
    preview_host: String = "127.0.0.1"
    preview_port: Integer = 4123
}

surface PreviewControlSurface {
    facing _: PreviewOperator

    provides:
        StartPreviewRequested(project)
        StopPreviewRequested(server)
        OpenPreviewInBrowserRequested(project)
}

surface PreviewHttpSurface {
    facing _: PreviewClient

    provides:
        PreviewRequest(path)
        PreviewDraftRequest(path, post_id)
}

rule StartPreview {
    when: StartPreviewRequested(project)
    ensures: PreviewServer.created(
        host: config.preview_host,
        port: config.preview_port,
        is_running: true
    )
}

rule StopPreview {
    when: StopPreviewRequested(server)
    -- Graceful shutdown with inflight request tracking
    ensures: server.is_running = false
}

rule OpenPreviewInBrowser {
    when: OpenPreviewInBrowserRequested(project)
    -- Starts or reuses the project's localhost preview server, then opens its
    -- base URL in the external system browser when no post editor is active.
    -- With an active post editor it opens that post's normal generated-site URL.
    -- This is separate from the post editor's internal preview pane.
    ensures: ExternalBrowserOpened(
        url: active_post_generated_url or preview_base_url(project)
    )
}

-- Route resolution
-- Preview renders all posts (published + draft) on-demand via Liquid templates.
-- Draft content comes from the DB; published content comes from its .md file.
-- See invariant PreviewDraftOverlay below.

rule ServePostPreview {
    when: PreviewRequest(path)
    requires: is_post_path(path)
        -- path matches "/{yyyy}/{mm}/{dd}/{slug}"
    -- Finds post by slug+date regardless of status (published or draft).
    -- Draft bodies resolve from DB; published bodies resolve from .md files.
    -- Renders via Liquid template with full PageRenderer context.
    ensures: PreviewResponse(rendered_html)
}

rule ServeDraftPreview {
    when: PreviewDraftRequest(path, post_id)
    -- Editor preview uses the post's canonical generated route with draft and
    -- post_id query markers, so links continue through the normal route tree.
    -- Renders draft content from DB and published content from the filesystem.
    ensures: PreviewResponse(rendered_html)
}

rule ServeArchivePreview {
    when: PreviewRequest(path)
    requires: is_archive_path(path)
        -- Category, tag, date archives with pagination
    -- Includes both published and draft posts in listings.
    ensures: PreviewResponse(rendered_html)
}

rule ServeMediaFile {
    when: PreviewRequest(path)
    requires: is_media_path(path)
        -- Path-traversal protection: validates path stays within media directory
    ensures: PreviewResponse(media_file)
}

rule ServeAssets {
    when: PreviewRequest(path)
    requires: is_asset_path(path)
    ensures: PreviewResponse(asset_file)
}

rule ServeGeneratedRuntimeData {
    when: PreviewRequest(path)
    requires: path = "/calendar.json" or is_feed_path(path)
        or is_pagefind_path(path) or is_image_path(path)
    -- Calendar and feed data use the preview post universe and normal list
    -- visibility rules. Pagefind and image files are read from the same project
    -- filesystem locations as the generated site.
    ensures: PreviewResponse(runtime_file)
}

rule ServeLanguagePrefixedRoute {
    when: PreviewRequest(path)
    requires: is_language_prefixed(path)
        -- Detects language prefix from supported languages
        -- Renders with translation overlay for that language
    ensures: PreviewResponse(translated_html)
}

invariant PreviewDraftOverlay {
    -- Preview is the draft workspace: it shows what the blog *will* look like,
    -- not what it currently looks like on the published site.
    --
    -- Post universe: all posts with status in {published, draft}.
    --   Archived posts are excluded.
    --
-- Content source (per post or translation):
--   1. status=draft      → DB content, falling back to its last-published file
--   2. status=published  → published .md file
--   3. No DB body or file → empty string
    --
    -- This means:
    --   - A purely draft post (never published) renders from DB content.
    --   - A published-then-edited post renders from DB content (the draft edits).
    --   - A published post renders from its .md file.
    --
    -- Contrast with generation (see generation.allium GenerationPublishedOnly):
    --   Generation uses *only* published .md file content, never DB draft content,
    --   and excludes posts that have never been published.
}

invariant ThemeSwitching {
    -- Preview supports live theme/mode switching via query params
    -- ?theme=amber&mode=dark etc.
    -- Uses Pico CSS with configurable themes
}

invariant PreviewServerBinding {
    for server in PreviewServers where server.is_running:
        server.host = config.preview_host and server.port = config.preview_port
}

invariant LocalhostOnly {
    -- Preview server binds to 127.0.0.1 only, never 0.0.0.0
}
