-- allium: 1
-- bDS Translation System
-- Scope: core (Wave 1)
-- Distilled from: src/main/engine/PostEngine.ts (translation methods),
--   postTranslationFileUtils.ts, MediaEngine.ts

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

enum PostTranslationStatus {
    draft
    published
}

surface TranslationControlSurface {
    facing _: TranslationOperator

    provides:
        UpsertPostTranslationRequested(post, language, title, content, excerpt)
        DeletePostTranslationRequested(translation)
        ValidateTranslationsRequested(project)
}

surface TranslationRuntimeSurface {
    facing _: TranslationRuntime

    provides:
        PostPublished(post)
        PublishTranslationRequested(translation)
        TranslationEdited(translation, changes)
}

value SupportedLanguage {
    -- en, de, fr, it, es
    code: String
}

surface SupportedLanguageSurface {
    context language: SupportedLanguage

    exposes:
        language.code
}

entity PostTranslation {
    canonical_post: post/Post
    language: SupportedLanguage
    title: String
    excerpt: String?
    content: String?
    status: PostTranslationStatus
    file_path: String
    checksum: String?
    created_at: Timestamp
    updated_at: Timestamp
    published_at: Timestamp?

    -- Derived
    content_location: if status = published: file_path else: content

    transitions status {
        draft -> published
        published -> draft
    }
}

invariant TranslationFilesStoreOnlyLanguageSpecificMetadata {
    -- Translation markdown files persist only fields that differ by language.
    -- Shared metadata such as publication status and timestamps belongs to the
    -- canonical post file and is inherited from the canonical post when
    -- rebuilding or diffing translation files.
    for t in PostTranslations where file_path != "":
        translation_file(t).omits_shared_metadata = true
}

surface PostTranslationSurface {
    context translation: PostTranslation

    exposes:
        translation.canonical_post
        translation.language.code
        translation.title
        translation.excerpt when translation.excerpt != null
        translation.content when translation.content != null
        translation.status
        translation.file_path
        translation.checksum when translation.checksum != null
        translation.created_at
        translation.updated_at
        translation.published_at when translation.published_at != null
        translation.content_location
}

invariant UniqueTranslationPerLanguage {
    for a in PostTranslations:
        for b in PostTranslations:
            (a != b and a.canonical_post = b.canonical_post)
                implies a.language != b.language
}

invariant TranslationFilePath {
    -- posts/YYYY/MM/{slug}.{language}.md
    for t in PostTranslations where file_path != "":
        t.file_path = format("posts/{yyyy}/{mm}/{slug}.{lang}.md",
            yyyy: t.canonical_post.created_at.year,
            mm: t.canonical_post.created_at.month_padded,
            slug: t.canonical_post.slug,
            lang: t.language.code)
}

rule UpsertPostTranslation {
    when: UpsertPostTranslationRequested(post, language, title, content, excerpt)
    requires: not post.do_not_translate
    ensures:
        let translation = PostTranslation.created(
            canonical_post: post,
            language: language,
            title: title,
            content: content,
            excerpt: excerpt,
            status: draft,
            file_path: ""
        )
        translation.status = draft
    -- If translation already exists, update it instead
}

rule PublishPostTranslation {
    when: PostPublished(post)
    -- All translations are also published when the canonical post is published
    for t in post.translations:
        ensures: PublishTranslationRequested(t)
}

rule PublishTranslation {
    when: PublishTranslationRequested(translation)
    requires: translation.status = draft
    ensures: translation.status = published
    ensures: translation.published_at = translation.published_at ?? now
    ensures: TranslationFileWritten(translation)
    ensures: translation.content = null
        -- Content moves to filesystem
}

rule ReopenPublishedTranslation {
    when: TranslationEdited(translation, changes)
    requires: translation.status = published
    requires: translation_edit_affects_published_content(changes)
    ensures: translation.status = draft
    ensures: translation.updated_at = now
}

rule DeletePostTranslation {
    when: DeletePostTranslationRequested(translation)
    ensures: not exists translation
    ensures: TranslationFileDeleted(translation)
    ensures: SearchIndexUpdated(translation.canonical_post)
        -- FTS index includes all translations of a post
}

rule ValidateTranslations {
    when: ValidateTranslationsRequested(project)
    -- Checks all posts against configured blog languages
    -- Reports: missing translations, orphan translation files,
    --   posts marked do_not_translate
    for post in project.posts where status = published:
        for lang in project.blog_languages:
            if lang != project.main_language:
                if not post.do_not_translate:
                    if not (lang in post.available_languages):
                        ensures: ValidationIssueReported(post, lang, "missing")

    @guidance
        -- This produces a validation report, not automatic fixes
        -- The report drives targeted re-rendering
}

invariant FtsIncludesTranslations {
    -- Full-text search index for a post includes stemmed content
    -- from all its translations, not just the canonical language
    for post in Posts:
        includes_text(search_index(post), post.title)
        for t in post.translations:
            includes_text(search_index(post), t.title)
}
