-- allium: 1
-- bDS SSH Publishing
-- Scope: core (Wave 5)
-- Distilled from: src/main/engine/PublishEngine.ts

use "./metadata.allium" as meta

enum PublishJobStatus {
    pending
    running
    completed
    failed
}

entity PublishJob {
    ssh_host: String
    ssh_user: String
    ssh_remote_path: String
    ssh_mode: scp | rsync
    status: PublishJobStatus

    transitions status {
        pending -> running
        running -> completed
        running -> failed
    }
}

value UploadTarget {
    kind: html | thumbnails | media
    local_dir: String
    remote_dir: String
}

surface PublishJobSurface {
    context job: PublishJob

    exposes:
        job.ssh_host
        job.ssh_user
        job.ssh_remote_path
        job.ssh_mode
        job.status
}

surface UploadTargetSurface {
    context target: UploadTarget

    exposes:
        target.kind
        target.local_dir
        target.remote_dir
}

surface PublishingControlSurface {
    facing _: PublishOperator

    provides:
        UploadSiteRequested(project, credentials)
}

surface PublishingRuntimeSurface {
    facing _: PublishRuntime

    provides:
        PublishJobStarted(project, job, credentials)
        PublishTargetFailed(job, target, error)
}

rule UploadSite {
    when: UploadSiteRequested(project, credentials)
    ensures:
        let job = PublishJob.created(
            ssh_host: credentials.ssh_host,
            ssh_user: credentials.ssh_user,
            ssh_remote_path: credentials.ssh_remote_path,
            ssh_mode: credentials.ssh_mode,
            status: pending
        )
        job.status = pending
        PublishJobStarted(project, job, credentials)
}

rule StartPublishJob {
    when: PublishJobStarted(project, job, credentials)
    requires: job.status = pending
    ensures: job.status = running
    ensures: UploadTargetStarted(job, html, "html/", credentials.ssh_remote_path, credentials)
    ensures: UploadTargetStarted(job, thumbnails, "thumbnails/", credentials.ssh_remote_path + "/thumbnails", credentials)
    ensures: UploadTargetStarted(job, media, "media/", credentials.ssh_remote_path + "/media", credentials)
}

rule UploadViaScp {
    when: UploadTargetStarted(job, target, local_dir, remote_dir, credentials)
    requires: credentials.ssh_mode = scp
    -- mtime-based upload detection: skip unchanged files
    -- Uses SSH agent (SSH_AUTH_SOCK) for authentication
    ensures: ScpUploadCompleted(job, target)
    ensures: UploadTargetCompleted(job, target)
}

rule UploadViaRsync {
    when: UploadTargetStarted(job, target, local_dir, remote_dir, credentials)
    requires: credentials.ssh_mode = rsync
    -- rsync --update --compress --verbose
    -- Media uploads exclude .meta sidecar files
    ensures: RsyncUploadCompleted(job, target)
    ensures: UploadTargetCompleted(job, target)

    @guidance
        -- rsync exclude filters for .meta files on media target
}

rule CompletePublishJob {
    when: PublishTargetsCompleted(job)
    requires: job.status = running
    ensures: job.status = completed
}

rule FailPublishJob {
    when: PublishTargetFailed(job, target, error)
    requires: job.status = running
    ensures: job.status = failed
}

rule TrackUploadCompletion {
    when: UploadTargetCompleted(job, target)
    requires: all_upload_targets_completed(job)
    ensures: PublishTargetsCompleted(job)
}

invariant MediaSidecarsExcludedFromUpload {
    -- .meta sidecar files are never uploaded to the remote server
    -- They are project metadata, not public content
}

invariant PublishJobLifecycle {
    -- UploadSiteRequested creates one PublishJob in pending state.
    -- PublishJobStarted moves the job to running before any target starts.
    -- A job reaches completed only after PublishTargetsCompleted(job).
    -- Any PublishTargetFailed(job, target, error) transitions the job to failed.
}

invariant SshAgentAuth {
    -- Publishing uses SSH_AUTH_SOCK for key-based authentication
    -- No password prompts, no interactive auth
}
