-- allium: 1
-- bDS Git Integration
-- Scope: extension (Bucket A — Git + Validation)
-- Distilled from: src/main/engine/GitEngine.ts

use "./post.allium" as post
use "./script.allium" as script
use "./template.allium" as template

value GitProvider {
    kind: github | gitlab | gitea_forgejo
    -- Detected from remote URL patterns
}

value GitSyncStatus {
    -- Per-commit: local_only | remote_only | both
    kind: local_only | remote_only | both
}

value GitFileStatus {
    path: String
    old_path: String?
    kind: added | modified | deleted | renamed | untracked
}

value GitCommit {
    hash: String
    subject: String?
    author: String?
    date: String?
    sync_status: GitSyncStatus
}

value GitRemoteState {
    local_branch: String?
    upstream_branch: String?
    has_upstream: Boolean
    ahead: Integer
    behind: Integer
}

config {
    local_timeout: Duration = 15.seconds
    network_timeout: Duration = 120.seconds
    file_history_limit: Integer = 50
}

surface GitSyncStatusSurface {
    context status: GitSyncStatus

    exposes:
        status.kind
}

entity GitRepository {
    is_initialized: Boolean
    remote_url: String?
    provider: GitProvider?
    current_branch: String?
    has_lfs: Boolean
}

surface GitRepositorySurface {
    context repo: GitRepository

    exposes:
        repo.is_initialized
        repo.remote_url when repo.remote_url != null
        repo.provider when repo.provider != null
        repo.current_branch when repo.current_branch != null
        repo.has_lfs
}

surface GitControlSurface {
    facing _: GitOperator

    exposes:
        GitFileStatus
        GitCommit
        GitRemoteState

    provides:
        InitializeRepoRequested(project)
        GitStatusRequested(project)
        GitDiffRequested(project)
        GitFileDiffRequested(project, file_path)
        GitHistoryRequested(project, branch)
        GitFileHistoryRequested(project, file_path)
        GitRemoteStateRequested(project)
        GitFetchRequested(project)
        GitPullRequested(project)
        GitPushRequested(project)
        GitNetworkRunCancelled(project)
        GitCommitAllRequested(project, message)
        GitReconcileRequested(project, old_commit, new_commit)
        PruneLfsCacheRequested(project, retain_recent)
}

rule InitializeRepo {
    when: InitializeRepoRequested(project)
    ensures: GitRepository.created(
        is_initialized: true,
        remote_url: null,
        provider: null,
        current_branch: "master",
        has_lfs: true
    )
    ensures: GitignoreCreated(project)
        -- .gitignore manages generated artifacts, cached assets, dependency directories, etc.
    ensures: LfsTrackingConfigured(project)
        -- Git LFS auto-tracks image patterns (*.jpg, *.png, *.gif, etc.)
}

rule GetStatus {
    when: GitStatusRequested(project)
    -- Returns file-level status: added, modified, deleted, renamed, untracked
    ensures: GitStatusReport(files)
}

rule GetDiff {
    when: GitDiffRequested(project)
    ensures: GitDiffReport(staged_diff, unstaged_diff)
}

rule GetFileDiff {
    when: GitFileDiffRequested(project, file_path)
    -- Returns the file at HEAD and in the working tree. A missing side is
    -- represented by empty content so added and deleted files remain viewable.
    ensures: GitFileDiffReport(file_path, original_content, modified_content)
}

rule GetHistory {
    when: GitHistoryRequested(project, branch)
    -- Returns commit history with sync status per commit
    ensures: GitHistoryReport(commits)
    @guidance
        -- Each commit annotated with: local_only, remote_only, or both
        -- This drives the "push needed" / "pull needed" indicators
}


rule GetFileHistory {
    when: GitFileHistoryRequested(project, file_path)
    -- Follows renames and returns the newest config.file_history_limit commits.
    -- A file with no history produces an empty report rather than an error.
    ensures: GitFileHistoryReport(file_path, commits)
}

rule GetRemoteState {
    when: GitRemoteStateRequested(project)
    -- A repository without an upstream reports zero ahead/behind counts.
    ensures: GitRemoteStateReport(project, local_branch, upstream_branch, ahead, behind)
}

rule Fetch {
    when: GitFetchRequested(project)
    ensures: RemoteRefsUpdated(project)

    @guidance
        -- Network output is streamed live and the run can be cancelled.
}

rule Pull {
    when: GitPullRequested(project)
    ensures: LocalBranchUpdated(project)
    ensures: GitReconcileRequested(project, previous_head(project), current_head(project))
        -- After pull, detect changed files and reconcile DB

    @guidance
        -- Pull is fast-forward-only. Network output is streamed live and the
        -- run can be cancelled.
}

rule Push {
    when: GitPushRequested(project)
    ensures: RemoteBranchUpdated(project)

    @guidance
        -- Network output is streamed live and the run can be cancelled.
}

rule CancelNetworkRun {
    when: GitNetworkRunCancelled(project)
    ensures: RunningGitProcessTerminated(project)
    ensures: GitNetworkRunFinished(project, cancelled)
}

rule CommitAll {
    when: GitCommitAllRequested(project, message)
    requires: message != ""
    ensures: AllChangesStaged(project)
    ensures: CommitCreated(project, message)
}

-- Git reconciliation: sync DB from filesystem changes

rule ReconcileFromGit {
    when: GitReconcileRequested(project, old_commit, new_commit)
    -- Detect changed files between commits for posts, scripts, templates
    let post_changes = changed_post_files(old_commit, new_commit)
    let script_changes = changed_script_files(old_commit, new_commit)
    let template_changes = changed_template_files(old_commit, new_commit)

    for added in post_changes.added:
        ensures: post/Post.created(parse_post_file(added))
    for modified in post_changes.modified:
        ensures: PostUpdatedFromFile(modified)
    for deleted in post_changes.deleted:
        ensures: PostDeletedByPath(deleted)
    for renamed in post_changes.renamed:
        ensures: PostFileRenamed(renamed.old, renamed.new)

    -- Same pattern for scripts and templates
    for added in script_changes.added:
        ensures: script/Script.created(parse_script_file(added))
    for added in template_changes.added:
        ensures: template/Template.created(parse_template_file(added))

    ensures: EntityChangedEventsEmitted(project)
}

invariant NonInteractiveGit {
    -- All git operations run non-interactively:
    --   GIT_TERMINAL_PROMPT=0
    --   GCM_INTERACTIVE=never
    --   ssh -oBatchMode=yes
    -- No password prompts ever surface to the user
}

invariant StructuredAuthErrors {
    -- Auth failures produce structured guidance:
    --   per platform (macOS/Windows/Linux)
    --   per provider (GitHub/GitLab/Gitea)
    -- Instead of raw git error messages
}

invariant BoundedGitExecution {
    -- Local operations stop after config.local_timeout and network operations
    -- stop after config.network_timeout. Timeout errors identify the operation
    -- and preserve captured output; the spawned process is terminated.
}

invariant OfflineNetworkGate {
    -- Fetch, pull, and push are refused by their UI/automation caller while
    -- airplane mode is active. Repository inspection and local commits remain
    -- available.
}

rule PruneLfsCache {
    when: PruneLfsCacheRequested(project, retain_recent)
    -- Prunes LFS cache with configurable recent commit retention
    ensures: LfsCachePruned(project)
}
