-- allium: 1
-- bDS Media Lifecycle
-- Scope: core (Wave 1)
-- Distilled from: src/main/engine/MediaEngine.ts, schema.ts

use "./project.allium" as project

surface MediaControlSurface {
    facing _: MediaOperator

    provides:
        ImportMediaRequested(project, source_file)
        UpdateMediaRequested(media, changes)
        DeleteMediaRequested(media)
        UpsertMediaTranslationRequested(media, language, title, alt, caption)
        RebuildMediaFromFilesRequested(project)
}

value ThumbnailSet {
    small: String       -- 150px width (binary path)
    medium: String      -- 400px width (binary path)
    large: String       -- 800px width (binary path)
    ai: String          -- 448x448 JPEG for vision models (binary path)
}

value SidecarFile {
    -- {media_file}.meta (YAML-like key-value format)
    -- Fields: title, alt, caption, author, tags, language, linkedPostIds
    -- Translations: {media_file}.{lang}.meta
    path: String
}

surface SidecarFileSurface {
    context sidecar: SidecarFile

    exposes:
        sidecar.path
}

entity Media {
    project: project/Project
    filename: String
    original_name: String
    mime_type: String
    size: Integer
    width: Integer?
    height: Integer?
    title: String?
    alt: String?
    caption: String?
    author: String?
    language: String?
    file_path: String
    sidecar_path: String
    checksum: String?
    tags: List<String>
    created_at: Timestamp
    updated_at: Timestamp

    -- Relationships
    translations: MediaTranslation with media = this
    linked_posts: PostMediaLink with media_id = this.id

    -- Derived
    available_languages: translations -> language
    thumbnails: ThumbnailSet
}

surface MediaSurface {
    context media: Media

    exposes:
        media.project
        media.filename
        media.original_name
        media.mime_type
        media.size
        media.width when media.width != null
        media.height when media.height != null
        media.title when media.title != null
        media.alt when media.alt != null
        media.caption when media.caption != null
        media.author when media.author != null
        media.language when media.language != null
        media.file_path
        media.sidecar_path
        media.checksum when media.checksum != null
        media.tags
        media.created_at
        media.updated_at
        media.translations.count
        media.linked_posts.count
        media.available_languages
        media.thumbnails.small
        media.thumbnails.medium
        media.thumbnails.large
        media.thumbnails.ai
}

entity MediaTranslation {
    media: Media
    language: String
    title: String?
    alt: String?
    caption: String?
}

invariant UniqueMediaTranslation {
    for a in MediaTranslations:
        for b in MediaTranslations:
            (a != b and a.media = b.media) implies a.language != b.language
}

invariant DateBasedMediaLayout {
    for m in Media:
        m.file_path = format("media/{yyyy}/{mm}/{uuid}.{ext}",
            yyyy: m.created_at.year,
            mm: m.created_at.month_padded,
            uuid: stem(m.filename),
            ext: extension(m.filename))
}

rule ImportMedia {
    when: ImportMediaRequested(project, source_file)
    let uuid_name = generate_uuid() + extension(source_file)
    let dest = format("media/{yyyy}/{mm}/{uuid_name}",
        yyyy: now.year, mm: now.month_padded)
    ensures: Media.created(
        project: project,
        filename: uuid_name,
        original_name: source_file.name,
        mime_type: detect_mime(source_file),
        size: source_file.size,
        width: detect_width(source_file),
        height: detect_height(source_file),
        file_path: dest,
        tags: {}
    )
    ensures: FileCopied(source_file, dest)
    ensures: SidecarWritten(media)
    ensures: ThumbnailsGenerated(media)
    ensures: SearchIndexUpdated(media)
}

rule UpdateMedia {
    when: UpdateMediaRequested(media, changes)
    ensures: MediaFieldsUpdated(media, changes)
    ensures: media.updated_at = now
    ensures: SidecarWritten(media)
        -- Metadata changes flush to .meta sidecar
    ensures: SearchIndexUpdated(media)
}

rule DeleteMedia {
    when: DeleteMediaRequested(media)
    ensures: not exists media
    ensures: MediaFileDeleted(media)
    ensures: SidecarDeleted(media)
    ensures: ThumbnailsDeleted(media)
    ensures:
        for t in media.translations:
            not exists t
    ensures: SearchIndexUpdated(media)
}

rule UpsertMediaTranslation {
    when: UpsertMediaTranslationRequested(media, language, title, alt, caption)
    ensures: MediaTranslation.created(
        media: media,
        language: language,
        title: title,
        alt: alt,
        caption: caption
    )
    ensures: TranslationSidecarWritten(media, language)
        -- Writes {file}.{lang}.meta
}

rule RebuildMediaFromFiles {
    when: RebuildMediaFromFilesRequested(project)
    -- Scans media directory for .meta sidecars, reimports to DB
    for sidecar in scan_directory(project.effective_data_dir + "/media", "*.meta"):
        let parsed = parse_sidecar(sidecar)
        ensures: Media.created(parsed)
            -- or updated if already exists
    @guidance
        -- This is the filesystem-to-DB reconciliation path
        -- Used after git pull or manual file changes
}

invariant SidecarRoundtrip {
    -- Sidecar files faithfully represent DB metadata
    for m in Media:
        parse_sidecar(m.sidecar_path).title = m.title
        parse_sidecar(m.sidecar_path).alt = m.alt
        parse_sidecar(m.sidecar_path).caption = m.caption
        parse_sidecar(m.sidecar_path).tags = m.tags
}
