-- allium: 1
-- bDS Media Editor View
-- Scope: UI content area — media editing surface
-- Distilled from: MediaEditor.tsx

-- Describes the layout and behaviour of the media editor rendered in
-- the main content area when a media tab is active.

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

-- ─── Media editor ─────────────────────────────────────────────

value MediaEditorView {
    media_id: String
    is_image: Boolean
    file_name: String                   -- originalName, read-only
    mime_type: String                   -- read-only
    file_size: String                   -- formatted, read-only
    dimensions: String?                 -- "W x H" if width/height exist, read-only
    title: String?                      -- editable text input
    alt_text: String?                   -- editable text input
    caption: String?                    -- editable textarea (3 rows)
    tags: String?                       -- comma-separated text input
    author: String?                     -- editable text input
    language: String?                   -- select from supported languages
    translations: List<MediaTranslationItem>
    linked_posts: List<LinkedPostItem>
}

value MediaTranslationItem {
    language: String
    flag_emoji: String
    title: String?
    alt_text: String?
    caption: String?
}

value LinkedPostItem {
    post_id: String
    title: String
}

value PostPickerOverlay {
    search_query: String
    results: List<PostPickerResult>
    overflow_count: Integer?             -- shown as "and N more" if > 0
}

surface PostPickerOverlaySurface {
    context overlay: PostPickerOverlay

    exposes:
        overlay.search_query
        for result in overlay.results:
            result.post_id
            result.title
        overlay.overflow_count when overlay.overflow_count != null

    provides:
        PostPickerResultClicked(media_id, post_id)
}

value PostPickerResult {
    post_id: String
    title: String
}

config {
    media_post_picker_max_results: Integer = 10
}

surface MediaEditorSurface {
    context editor: MediaEditorView

    exposes:
        editor.file_name
        editor.mime_type
        editor.file_size
        editor.dimensions when editor.dimensions != null
        editor.is_image
        editor.title
        editor.alt_text
        editor.caption
        editor.tags
        editor.author
        editor.language
        for t in editor.translations:
            t.language
            t.flag_emoji
            t.title
        for lp in editor.linked_posts:
            lp.post_id
            lp.title

    provides:
        MediaAIImageAnalysisRequested(editor.media_id)
            when editor.is_image
        MediaDetectLanguageRequested(editor.media_id)
        MediaTranslateMetadataRequested(editor.media_id, target_language)
        MediaReplaceFileRequested(editor.media_id)
        MediaSaveRequested(editor.media_id)
        MediaDeleteRequested(editor.media_id)
        MediaLinkToPostRequested(editor.media_id)
        MediaTranslationEditClicked(editor.media_id, language)
        MediaTranslationRefreshClicked(editor.media_id, language)
        MediaUnlinkPostRequested(editor.media_id, post_id)
        MediaDeleteConfirmed(editor.media_id)
        TranslationEditModalSaved(editor.media_id, language, title, alt, caption)

    @guarantee HeaderLayout
        -- Header bar with media display name.
        -- Actions (right side): Quick Actions dropdown, Replace File button,
        --   Save button, Delete button (danger style).

    @guarantee QuickActionsDropdown
        -- Dropdown menu in header with three entries:
        --   AI Image Analysis (robot icon) — only shown for image/* MIME types.
        --   Detect Language (magnifier icon).
        --   Translate Metadata (globe icon).

    @guarantee PreviewArea
        -- Images: rendered at full resolution via the /media-file/:id route
        --   (serves the original file; falls back to the large thumbnail for
        --   formats browsers cannot display inline) with cache-busting
        --   timestamp. Never upscaled beyond natural size. The sidebar keeps
        --   using small thumbnails.
        -- Non-images: SVG file icon placeholder + original filename text.

    @guarantee MetadataForm
        -- Form fields in order:
        --   File Name (disabled input), MIME Type (disabled input),
        --   Size + Dimensions row (disabled inputs),
        --   Title (text input), Alt Text (text input), Caption (textarea, 3 rows),
        --   Tags (comma-separated text input), Author (text input),
        --   Language (select from supported languages).

    @guarantee TranslationsSection
        -- Shown only when language is set.
        -- List of existing translations: flag emoji + language name + title.
        -- Per-translation actions: click to edit (opens modal), refresh button.
        -- No delete action; translations are only removed via the scripting
        --   API (script.allium bds.media.delete_translation) or when the
        --   media item itself is deleted.
        -- "No translations" message when list is empty.

    @guarantee TranslationEditModal
        -- Editing a translation opens a modal dialog ("Edit Translation"), not
        --   an inline form. Hidden language field plus Title, Alt Text, and
        --   Caption (textarea) inputs. Footer: Cancel and Save buttons.
        -- Save persists the translation; Cancel/close discards.

    @guarantee LinkedPostsSection
        -- "Link to Post" button opens inline post picker overlay.
        -- List of currently linked posts with document icon. Click navigates to post tab.
        -- Per-post unlink button (×).
        -- "Not linked to any posts" message when list is empty.

    @guarantee PostPickerOverlay
        -- Inline overlay positioned near "Link to Post" button (not a modal).
        -- Search input filtering unlinked posts by title.
        -- Up to config.media_post_picker_max_results results displayed.
        -- "and N more" text when total exceeds limit.
        -- Click result: links media to selected post, closes overlay.

    @guarantee NoAutoSave
        -- Unlike the post editor, media editor requires explicit Save button.
}

-- ─── Media editor actions ─────────────────────────────────────

rule MediaSave {
    when: MediaSaveRequested(media_id)
    let item = media/Media{id: media_id}
    ensures: UpdateMediaRequested(item, editor_field_changes(media_id))
        -- Persists all form fields; the engine update rewrites the
        -- sidecar and FTS entry (media.allium UpdateMedia,
        -- engine_side_effects.allium UpdateMediaSideEffects)
}

rule MediaAIImageAnalysis {
    when: MediaAIImageAnalysisRequested(media_id)
    -- Only available for image/* MIME types (editor.is_image guard
    -- on the provides entry; button hidden for non-images).
    ensures: AISuggestionRequested(entity_type: "media", entity_id: media_id)
        -- Shared flow: action_patterns.allium MediaAISuggestionFlow —
        -- airplane gating, image model over the 448x448 AI thumbnail,
        -- AISuggestionsModal with title/alt/caption, accepted fields
        -- trigger an explicit save.
}

rule MediaDetectLanguage {
    when: MediaDetectLanguageRequested(media_id)
    let item = media/Media{id: media_id}
    requires: active_endpoint_configured
        -- airplane-mode gating: ai.allium AirplaneModeGating
    let detected = ai_detect_language(concat(item.title, item.alt, item.caption))
        -- title model over the concatenated metadata text
    ensures: UpdateMediaRequested(item, language_change(detected))
        -- persisted immediately — no modal, no confirmation; the
        -- engine update rewrites the sidecar
}

rule MediaTranslateMetadata {
    when: MediaTranslateMetadataRequested(media_id, target_language)
    let item = media/Media{id: media_id}
    requires: active_endpoint_configured
    -- target_language comes from the language picker modal (same
    -- pattern as post translate).
    let source_language = item.language
        ?? ai_detect_language(concat(item.title, item.alt, item.caption))
        -- step 1: detect the source language first when unset
        -- (auto-persisted, as in MediaDetectLanguage)
    let translated = ai_translate_media_metadata(item, source_language, target_language)
        -- step 2: translate title, alt, caption via the title model
    ensures: UpsertMediaTranslationRequested(item, target_language,
        translated.title, translated.alt, translated.caption)
        -- creates/updates the translation record and writes the
        -- translated sidecar {path}.{lang}.meta (media.allium
        -- UpsertMediaTranslation)
}

rule MediaReplaceFile {
    when: MediaReplaceFileRequested(media_id)
    let item = media/Media{id: media_id}
    let source = native_file_dialog_selection()
        -- native file dialog, no MIME type filter; cancel aborts
    ensures: ReplaceMediaFileRequested(item, source)
        -- copies over the existing media file path; images regenerate
        -- thumbnails synchronously (media.allium ReplaceMediaFile)
    ensures: MediaPreviewRefreshed(media_id)
        -- preview reloads with a cache-busting timestamp query param
}

rule MediaDeleteAction {
    when: MediaDeleteRequested(media_id)
    let item = media/Media{id: media_id}
    ensures: ConfirmDeleteModalOpened(media_id, item.linked_posts)
        -- custom modal (not a native dialog): media display name,
        -- linked post count and list; Cancel + Delete (destructive
        -- red style) — action_patterns.allium
        -- confirmation_assignments: media_delete
}

rule MediaDeleteExecute {
    when: MediaDeleteConfirmed(media_id)
    let item = media/Media{id: media_id}
    ensures: DeleteMediaRequested(item)
        -- file, sidecar, thumbnails, translations, post-media links,
        -- FTS entry: media.allium DeleteMedia +
        -- engine_side_effects.allium DeleteMediaSideEffects
    ensures: closeTab(media_id)
        -- sidebar removes the item reactively
}

rule MediaLinkToPost {
    when: MediaLinkToPostRequested(media_id)
    ensures: PostPickerOverlayOpened(media_id)
        -- inline overlay near the button (not a modal): search input
        -- filters unlinked posts by title, at most
        -- config.media_post_picker_max_results results, "and N more"
        -- when the total exceeds the limit
}

rule MediaLinkToPostSelected {
    when: PostPickerResultClicked(media_id, post_id)
    let item = media/Media{id: media_id}
    ensures: MediaLinkedToPost(item, post/Post{id: post_id})
        -- creates the post-media link and updates sidecar
        -- linkedPostIds; the linked posts list refreshes reactively
    ensures: PostPickerOverlayClosed(media_id)
}

rule MediaUnlinkPost {
    when: MediaUnlinkPostRequested(media_id, post_id)
    let item = media/Media{id: media_id}
    -- No confirmation (action_patterns.allium
    -- confirmation_assignments: media_unlink)
    ensures: MediaUnlinkedFromPost(item, post_id)
        -- removes the post-media link row and updates sidecar
        -- linkedPostIds
}

rule MediaTranslationEdit {
    when: MediaTranslationEditClicked(media_id, language)
    ensures: TranslationEditModalOpened(media_id, language)
        -- "Edit Translation" modal pre-filled with the translation's
        -- title, alt, and caption; Cancel/close discards
}

rule MediaTranslationEditSave {
    when: TranslationEditModalSaved(media_id, language, title, alt, caption)
    let item = media/Media{id: media_id}
    ensures: UpsertMediaTranslationRequested(item, language, title, alt, caption)
        -- persists the DB record + translated sidecar
        -- {path}.{lang}.meta
}

rule MediaTranslationRefresh {
    when: MediaTranslationRefreshClicked(media_id, language)
    let item = media/Media{id: media_id}
    requires: active_endpoint_configured
    let translated = ai_translate_media_metadata(item, item.language, language)
        -- re-translates from the source language via the title model
    ensures: UpsertMediaTranslationRequested(item, language,
        translated.title, translated.alt, translated.caption)
        -- overwrites the existing translation fields and rewrites the
        -- translated sidecar
}
