-- allium: 1
-- bDS Action Patterns and Chains
-- Scope: cross-cutting (all waves)
-- Distilled from: PostEditor.tsx, MediaEditor.tsx, appStore.ts

-- Cross-cutting patterns for AI operations, auto-translation,
-- drag-and-drop chains, and confirmation dialogs.

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

-- ─── External surfaces ──────────────────────────────────────

surface UserAction {
    provides: AISuggestionRequested(entity_type, entity_id)
    provides: PostSaved(post_id)
    provides: PostAutoTranslateCompleted(post_id, language)
    provides: ImageFileDroppedOnPostEditor(post_id, file)
    provides: AISuggestionsModalConfirmed(entity_type, entity_id, fields)
    provides: AISuggestionsModalCancelled(entity_type, entity_id)
}

surface AiEngine {
    facing _: AiRuntime

    provides: AiFieldSuggestionsResponded(entity_type, entity_id, suggestions)
        -- The active endpoint's response, parsed into per-field suggestions
}

surface AutoTranslationRuntime {
    facing _: AiRuntime

    provides: PostAutoTranslateAttemptFailed(post_id, language, reason)
    provides: PostAutoTranslateRetryFailed(post_id, language, reason)
}

-- ─── AI operation gating ────────────────────────────────────
--
-- All AI operations route through the active endpoint for the current
-- mode; endpoint selection and the active_endpoint_configured predicate
-- are specified in ai.allium (AirplaneModeGating, TwoEndpointModel).
-- Rules below gate on active_endpoint_configured and refuse with a
-- toast when it is false. Two model categories are used:
--   title model: text analysis (suggestions, translation, language detect)
--   image model: vision-capable (image analysis only)
-- Model selection: per-operation default from settings, overrideable
-- per-conversation in the chat panel only.

rule AiOperationRefusedWithoutEndpoint {
    when: AISuggestionRequested(entity_type, entity_id)
    requires: not active_endpoint_configured
    ensures: ToastShown(message_key: "ai.unavailable_configure_endpoint")
        -- "AI unavailable — configure {online|airplane} endpoint in Settings"
    ensures: AiOperationAborted(entity_type, entity_id)
}

-- ─── AI suggestions pattern ─────────────────────────────────
--
-- Shared flow used by PostAIAnalysis (editor_post.allium) and
-- MediaAIImageAnalysis (editor_media.allium). Modal layout:
-- see modals.allium AISuggestionsModal / AISuggestionField.

config {
    post_suggestion_sample_length: Integer = 2000
}

rule PostAISuggestionFlow {
    when: AISuggestionRequested(entity_type, entity_id)
    requires: entity_type = "post"
    requires: active_endpoint_configured
    let target = post/Post{id: entity_id}
    ensures: AiFieldSuggestionsRequested(entity_id,
        model: "title",
        input: post_suggestion_input(target))
        -- black box: title + excerpt + first
        -- config.post_suggestion_sample_length chars of content
}

rule MediaAISuggestionFlow {
    when: AISuggestionRequested(entity_type, entity_id)
    requires: entity_type = "media"
    requires: active_endpoint_configured
    let target = media/Media{id: entity_id}
    ensures: AiFieldSuggestionsRequested(entity_id,
        model: "image",
        input: ai_thumbnail(target))
        -- black box: the 448x448 AI thumbnail JPEG
}

rule PresentAiSuggestions {
    when: AiFieldSuggestionsResponded(entity_type, entity_id, suggestions)
    -- Response parsed into per-field suggestions; each field row shows
    -- label, current value, suggested value, and an accept checkbox
    -- defaulting to true. The post slug field is locked (no checkbox)
    -- if the post was ever published.
    ensures: AISuggestionsModalOpened(entity_type, entity_id, suggestions)
}

rule ApplyAcceptedSuggestions {
    when: AISuggestionsModalConfirmed(entity_type, entity_id, fields)
    let accepted = filter(fields, f => f.accepted and not f.locked)
    for field in accepted:
        ensures: SuggestionFieldApplied(entity_type, entity_id, field)
    ensures:
        if entity_type = "post":
            PostAutoSaveTimerReset(entity_id)
            -- Posts: applied fields go through the 3s auto-save timer
        else:
            MediaSaveRequested(entity_id)
            -- Media: applied fields trigger an explicit save
}

rule CancelAiSuggestions {
    when: AISuggestionsModalCancelled(entity_type, entity_id)
    ensures: AiSuggestionsDiscarded(entity_type, entity_id)
        -- All suggestions discarded, no entity changes
}

-- ─── Auto-translation chain ─────────────────────────────────

rule AutoTranslationChain {
    when: PostSaved(post_id)
    -- Triggered only by explicit manual save, never by publish, auto-save,
    -- unmount/tab-switch persistence, post creation, import, or scripting.
    let saved_post = post/Post{id: post_id}
    requires: active_endpoint_configured
    requires: not saved_post.do_not_translate
    let missing = saved_post.project.blog_languages - saved_post.available_languages
    for language in missing:
        ensures: PostTranslationTaskEnqueued(post_id, language)
        -- One background task per missing language. Each task:
        --   1. translate metadata (title, excerpt) via title model
        --   2. translate content (full markdown) via title model
        --   3. create/update the translation record in the DB
        -- Sequential within a language, parallel across languages;
        -- progress visible in the Tasks panel. A save with no missing
        -- languages enqueues nothing (empty set).
}

rule RetryFailedAutoTranslationOnce {
    when: PostAutoTranslateAttemptFailed(post_id, language, reason)
    ensures: ToastShown(message_key: "engine.autoTranslationRetrying")
    ensures: PostAutoTranslateRetried(post_id, language, maximum_attempts: 2)
}

rule ReportFailedAutoTranslationRetry {
    when: PostAutoTranslateRetryFailed(post_id, language, reason)
    ensures: BackgroundTaskFailed(post_id, language, reason)
    ensures: ToastShown(level: "error", reason: reason)
}

rule MediaMetadataTranslationCascade {
    when: PostAutoTranslateCompleted(post_id, language)
    let saved_post = post/Post{id: post_id}
    for link in saved_post.linked_media:
        let item = media/Media{id: link.media_id}
        if item.language != null
            and not exists media/MediaTranslation{media: item, language: language}:
            ensures: MediaMetadataTranslationTaskEnqueued(link.media_id, language)
            -- Translates title, alt, caption via the title model and
            -- writes the translated sidecar {path}.{lang}.meta
}

-- ─── Drag-and-drop image chain ──────────────────────────────
--
-- Full chain when an image file is dropped on the post editor body.
-- See editor_post.allium PostDragDropImage for the trigger rule.

rule PostDragDropImageChain {
    when: ImageFileDroppedOnPostEditor(post_id, file)
    let target = post/Post{id: post_id}
    -- Synchronous steps (user waits):
    let imported = import_media(target.project, file)
        -- black box: new media record + file copy + base sidecar
        -- (media.allium ImportMedia)
    ensures: ThumbnailGenerationStarted(imported)
        -- async start: small/medium/large/ai variants
    ensures: MediaLinkedToPost(imported, target)
        -- updates sidecar linkedPostIds
    ensures: MarkdownImageInserted(target, imported)
        -- inserts ![](/media/YYYY/MM/file) at the cursor position
    ensures: DragDropImportCompleted(target, imported)
}

rule DragDropAiAutoApply {
    when: DragDropImportCompleted(target, imported)
    requires: active_endpoint_configured
    -- Background, non-blocking: image model on the 448x448 AI thumbnail.
    -- Results are auto-applied WITHOUT the AISuggestionsModal (unlike
    -- the manual Quick Action).
    ensures: MediaMetadataUpdated(imported, ai_image_analysis(imported))
    ensures: MediaSidecarRewritten(imported)
}

rule DragDropTranslationCascade {
    when: DragDropImportCompleted(target, imported)
    requires: active_endpoint_configured
    requires: not target.do_not_translate
    for language in target.project.blog_languages:
        ensures: MediaMetadataTranslationTaskEnqueued(imported.id, language)
        -- creates translated sidecar files
}

-- ─── Confirmation dialog patterns ───────────────────────────
--
-- Four distinct patterns used across the application. The assignment
-- table below is the authoritative mapping from destructive or
-- irreversible actions to their confirmation UI.

enum ConfirmationPattern {
    system_confirm
        -- Simple yes/no system dialog, no custom UI
    confirm_delete_modal
        -- Custom modal with entity name, reference counts, linked
        -- entity list; buttons Cancel + Delete (destructive red style)
    confirm_dialog
        -- Custom modal describing the action and its consequences;
        -- buttons Cancel + Confirm
    none
        -- Immediate execution on click, no dialog
}

value ConfirmationAssignment {
    action: String
    pattern: ConfirmationPattern
}

config {
    confirmation_assignments: Set<ConfirmationAssignment> = {
        ConfirmationAssignment(action: "post_delete", pattern: system_confirm),
        ConfirmationAssignment(action: "post_discard", pattern: system_confirm),
        ConfirmationAssignment(action: "template_delete", pattern: confirm_delete_modal),
            -- shows template title, no reference list
        ConfirmationAssignment(action: "template_delete_with_references", pattern: system_confirm),
        ConfirmationAssignment(action: "media_delete", pattern: confirm_delete_modal),
            -- shows linked posts
        ConfirmationAssignment(action: "tag_delete", pattern: confirm_delete_modal),
            -- shows post count
        ConfirmationAssignment(action: "tag_merge", pattern: confirm_dialog),
            -- "Merge N tags into {target}? Cannot be undone."
        ConfirmationAssignment(action: "rebuild_operations", pattern: none),
        ConfirmationAssignment(action: "script_delete", pattern: confirm_delete_modal),
            -- shows script title, no reference list
        ConfirmationAssignment(action: "menu_item_delete", pattern: none),
        ConfirmationAssignment(action: "metadata_diff_field_sync", pattern: none),
        ConfirmationAssignment(action: "import_execute", pattern: none),
        ConfirmationAssignment(action: "media_unlink", pattern: none)
    }
}
