-- allium: 1
-- bDS Post Editor View
-- Scope: UI content area — post editing surface
-- Distilled from: PostEditor.tsx

-- Describes the layout and behaviour of the post editor rendered in
-- the main content area when a post tab is active.
-- Tab routing is in tabs.allium. Sidebar navigation is in sidebar_views.allium.

use "./tabs.allium" as tabs
use "./post.allium" as post
use "./i18n.allium" as i18n

-- ─── Post editor ──────────────────────────────────────────────

value PostEditorView {
    post_id: String
    header: PostEditorHeader
    metadata: PostEditorMetadata
    metadata_expanded: Boolean          -- starts expanded when title is empty
    excerpt_expanded: Boolean
    editor_mode: String                 -- markdown | preview
    footer: PostEditorFooter
}

value PostEditorHeader {
    title: String                       -- post title or "Untitled"
    is_dirty: Boolean
    status: String                      -- draft | published | archived
    is_auto_saving: Boolean
}

value PostEditorMetadata {
    title: String                       -- editable text input
    tags: List<String>                  -- autocomplete chip input
    author: String?                     -- text input
    language: String?                   -- select from supported languages
    do_not_translate: Boolean           -- checkbox
    slug: String                        -- read-only text input
    categories: List<String>            -- chip input
    template_slug: String?              -- select (shown only when templates exist)
    post_links: PostLinksPanel
    linked_media: List<LinkedMediaItem>
}

value PostLinksPanel {
    backlinks: List<PostLinkReference>  -- posts linking to this post
    outlinks: List<PostLinkReference>   -- posts this post links to
}

value PostLinkReference {
    post_id: String
    title: String
}

value LinkedMediaItem {
    media_id: String
    has_thumbnail: Boolean
    name: String
    sort_order: Integer
}

value PostEditorFooter {
    created_at: String                  -- locale-formatted date
    updated_at: String                  -- locale-formatted date
    published_at: String?               -- locale-formatted date, only when post was published
}

value TranslationFlag {
    language: String
    flag_emoji: String
    status: String                      -- draft | published
    is_active: Boolean                  -- true when this language is currently being edited
}

surface TranslationFlagSurface {
    context flag: TranslationFlag

    exposes:
        flag.language
        flag.flag_emoji
        flag.status
        flag.is_active
}

surface PostEditorSurface {
    context editor: PostEditorView

    exposes:
        editor.header.title
        editor.header.is_dirty
        editor.header.status
        editor.header.is_auto_saving
        editor.metadata_expanded
        editor.excerpt_expanded
        editor.editor_mode
        editor.footer.created_at
        editor.footer.updated_at
        editor.footer.published_at when editor.footer.published_at != null

    provides:
        PostAIAnalysisRequested(editor.post_id)
        PostTranslateRequested(editor.post_id, target_language)
        PostSaved(editor.post_id)
        PostPublishRequested(editor.post_id)
            when editor.header.status = draft
        PostDiscardRequested(editor.post_id)
            when editor.header.status = draft
        PostDeleteRequested(editor.post_id)
        PostInsertLinkRequested(editor.post_id)
            when editor.editor_mode = markdown
        PostInsertMediaRequested(editor.post_id)
            when editor.editor_mode = markdown
        PostGalleryRequested(editor.post_id)
        ImageDroppedOnEditor(editor.post_id, file_path)
        PostLanguageDetectRequested(editor.post_id)
        PostDiscardConfirmed(editor.post_id)
        PostDeleteConfirmed(editor.post_id)

    @guarantee HeaderLayout
        -- Header bar with two areas.
        -- Left: title text with dirty indicator dot (●) when is_dirty is true.
        -- Right: status badge, auto-save indicator (when saving),
        --   Quick Actions dropdown, Publish button (only when draft, success style),
        --   Discard button (only when draft), Delete button.

    @guarantee QuickActionsDropdown
        -- Dropdown menu in header with two entries:
        --   AI Analysis (robot icon) — suggests title, excerpt, slug.
        --   Translate Post (globe icon) — opens translation modal.

    @guarantee MetadataSection
        -- Collapsible section. Starts expanded when title is empty.
        -- Two-column layout.
        -- Left column: Title, Tags, Author, Language + detect button,
        --   Do Not Translate checkbox, Slug (read-only), Categories,
        --   Template (select, only when templates exist), PostLinks.
        -- Right column: LinkedMediaPanel.

    @guarantee TagAutocomplete
        -- Tag input with autocomplete.
        -- While typing: substring match on existing tag names (case-insensitive),
        --   top 8 shown, plus a "create tag" row when the query is new.
        -- While the query is empty (on focus): semantic suggestions appear under a
        --   "Suggested tags" label — tags drawn from up to 10 similar posts,
        --   weighted by similarity, top 5, excluding tags already on the post.
        --   Requires semanticSimilarityEnabled and a built embedding index;
        --   it is an index read (no model inference) so it works offline and
        --   yields nothing when similarity is disabled or unindexed.

    @guarantee TranslationFlagsBar
        -- Row of flag emoji buttons inline with metadata toggle.
        -- One flag per language: canonical language + each translation.
        -- Each flag shows status (draft/published) via CSS class.
        -- Active flag highlighted. Click switches editor to that language's draft.

    @guarantee ExcerptSection
        -- Collapsible section with textarea (4 rows).

    @guarantee EditorBodyToolbar
        -- Toolbar: "Content" label, mode toggle (Markdown/Preview),
        --   action buttons (markdown mode only): Gallery (with media count),
        --   Insert Post Link, Insert Media.

    @guarantee EditorModes
        -- Markdown: code editor with markdown-with-macros language,
        --   highlighting [[macro ...]] syntax. Word wrap on, minimap off, 14px font.
        -- Preview: iframe served by the localhost preview server at the post's
        --   normal generated-site URL, using the shared site renderer. Draft
        --   content comes from DB; published content and assets come from files.
        -- Markdown and Preview are the only editor modes.

    @guarantee DragDropImages
        -- Drop image file onto editor area triggers import chain.

    @guarantee FooterLayout
        -- Three date stamps: Created, Updated, Published (only when published_at exists).
        -- Locale-formatted dates.

    @guarantee LinkedMediaPanel
        -- Right column of metadata showing media items linked to this post.
        -- Actions: Import & Link (native file dialog), Link Existing (media picker),
        --   Unlink (no confirmation), Reorder (drag handle), Click (opens media tab).
}

config {
    post_auto_save_delay: Integer = 3000
    post_content_sample_length: Integer = 2000
}

invariant PostAutoSave {
    -- Auto-saves after config.post_auto_save_delay ms of idle in the editor.
    -- Also auto-saves on unmount/tab switch.
    -- Ctrl/Cmd+S triggers immediate save.
    -- Saves: title, content, excerpt, tags, categories, templateSlug, language.
}

invariant PostDirtyTracking {
    -- Compares canonical draft + translation drafts against saved state.
    -- Dirty indicator shown in header (●) and tab bar.
}

invariant PostEditorModePersistence {
    -- Editor mode (markdown/preview) persists per session.
    -- Default mode comes from editor settings (markdown).
}

-- ─── Post editor actions ────────────────────────────────────

rule PostAIAnalysis {
    when: PostAIAnalysisRequested(post_id)
    ensures: AISuggestionRequested(entity_type: "post", entity_id: post_id)
        -- Shared flow: action_patterns.allium PostAISuggestionFlow —
        -- airplane gating, title model (not the default chat model)
        -- over title + excerpt + content sample, AISuggestionsModal
        -- with title/excerpt/slug (slug locked once published),
        -- accepted fields re-arm the auto-save timer.
}

rule PostTranslateAction {
    when: PostTranslateRequested(post_id, target_language)
    let target = post/Post{id: post_id}
    requires: active_endpoint_configured
        -- airplane-mode gating: ai.allium AirplaneModeGating
    requires: target_language in target.project.blog_languages
        -- chosen in the language picker modal; existing translations
        -- are listed with their status badge (draft/published)
    let translated_meta = ai_translate_post_metadata(target, target_language)
        -- AI call 1: title + excerpt via the title model
    let translated_content = ai_translate_post_content(target, target_language)
        -- AI call 2 (sequential): full markdown body via the title model
    ensures: PostTranslationUpserted(
        translation_record(target, target_language, translated_meta, translated_content),
        target)
        -- creates/updates the translation record; a published source
        -- post transitions back to draft with the file content copied
        -- to the DB so it can be edited (engine_side_effects.allium
        -- UpsertPostTranslationSideEffects)
}

-- Auto-translation on manual save is specified once in
-- action_patterns.allium AutoTranslationChain (triggered by PostSaved),
-- including the linked-media cascade. It is not duplicated here.

rule PostPublishAction {
    when: PostPublishRequested(post_id)
    let target = post/Post{id: post_id}
    ensures:
        if editor_is_dirty(post_id):
            PostSaved(post_id)
            -- implicit save first (awaited)
    ensures: PublishPostRequested(target)
        -- engine publish: file write, translation publishing, FTS,
        -- links (engine_side_effects.allium PublishPostSideEffects)
        -- UI updates reactively: status badge -> published, sidebar
        -- section move (ui_data_flow.allium PostStatusChanged)
}

rule PostDiscardChanges {
    when: PostDiscardRequested(post_id)
    let target = post/Post{id: post_id}
    requires: target.published_at != null
    requires: target.status = draft
        -- only published posts with pending draft changes
    ensures: DiscardConfirmShown(post_id)
        -- system confirm dialog: "Discard changes to this post?"
        -- (action_patterns.allium confirmation_assignments:
        -- post_discard)
}

rule PostDiscardExecute {
    when: PostDiscardConfirmed(post_id)
    let target = post/Post{id: post_id}
    ensures: DiscardPostChangesRequested(target)
        -- engine restores the published state from the .md file:
        -- content = null, status = published
    ensures: EditorReloaded(post_id)
        -- editor reloads with the restored content
}

rule PostDeleteAction {
    when: PostDeleteRequested(post_id)
    ensures: DeleteConfirmShown(post_id)
        -- system confirm dialog: "Delete this post?"
        -- (action_patterns.allium confirmation_assignments: post_delete)
}

rule PostDeleteExecute {
    when: PostDeleteConfirmed(post_id)
    let target = post/Post{id: post_id}
    ensures: DeletePostRequested(target)
        -- published posts also delete the .md file and all translation
        -- files; never-published drafts only the DB record
        -- (engine_side_effects.allium DeletePostSideEffects)
    ensures: closeTab(post_id)
        -- sidebar removes the item reactively (ui_data_flow.allium
        -- PostEditorDelete)
}

rule PostInsertLink {
    when: PostInsertLinkRequested(post_id)
    -- Keyboard shortcut: Ctrl/Cmd+K (markdown mode only, per the
    -- provides guard)
    ensures: InsertPostLinkModalOpened(post_id)
        -- two tabs: Internal (debounced title search, similarity
        -- ranking, Create Post row) and External (URL + display text);
        -- picked entries insert [title](/YYYY/MM/DD/slug) or
        -- [text](url) at the cursor — modals.allium InsertPostLinkModal
}

rule PostInsertMedia {
    when: PostInsertMediaRequested(post_id)
    ensures: InsertMediaModalOpened(post_id)
        -- media search grid with bds-thumb:// thumbnails; picked items
        -- insert ![alt](/media/YYYY/MM/file) for images or
        -- [originalName](/media/YYYY/MM/file) otherwise —
        -- modals.allium InsertMediaModal
}

rule PostGalleryAction {
    when: PostGalleryRequested(post_id)
    ensures: GalleryOverlayOpened(post_id)
        -- grid of all media linked to this post; clicking an image
        -- opens the lightbox (full-size preview, arrow navigation,
        -- ESC to close) — modals.allium GalleryOverlay / LightboxView
}

rule PostDragDropImage {
    when: ImageDroppedOnEditor(post_id, file_path)
    ensures: ImageFileDroppedOnPostEditor(post_id, file_path)
        -- full chain: action_patterns.allium PostDragDropImageChain
        -- (synchronous import + thumbnails + link + cursor insert,
        -- background AI auto-apply and translation cascade)
}

rule PostLanguageDetect {
    when: PostLanguageDetectRequested(post_id)
    let target = post/Post{id: post_id}
    requires: active_endpoint_configured
    let detected = ai_detect_language(post_suggestion_input(target))
        -- title model over the content sample; no modal
    ensures: target.language = detected
    ensures: PostAutoSaveTimerReset(post_id)
}
