#!/usr/bin/env python3
"""Synchronize known versions and policy-enabled translation branches."""

from __future__ import annotations

import argparse
import json
import re
import shutil
import subprocess
import sys
import tempfile
from dataclasses import dataclass, replace
from pathlib import Path

import yaml

REPO_ROOT = Path(__file__).resolve().parents[2]
SCRIPT_DIR = Path(__file__).resolve().parent
if str(SCRIPT_DIR) not in sys.path:
    sys.path.insert(0, str(SCRIPT_DIR))
SRC_ROOT = REPO_ROOT / "src"
if str(SRC_ROOT) not in sys.path:
    sys.path.insert(0, str(SRC_ROOT))

from cli_commands import (  # noqa: E402
    TRANSLATION_TREE_COMMAND,
    tool_command,
)

from dsw_document_template_tool.translation_repository import (  # noqa: E402
    TranslationRepositoryConfig,
    clean_artifact_version_paths,
    clean_artifact_versions,
    load_preview_runtimes,
    load_translation_repository_config,
    preview_runtime_for_version,
    sorted_versions,
    version_branch,
    version_paths,
    version_policy_allows_auto_refresh,
    version_policy_allows_manual_refresh,
    version_policy_decision,
)
from dsw_document_template_tool.yaml_config import (  # noqa: E402
    YamlConfigError,
    load_yaml_file,
)

VERSION_BRANCH_WORKFLOW_TEMPLATE = (
    REPO_ROOT / "examples" / "github-actions" / "document_template_translation_sync.yml"
)
DSW_COMPAT_PATH = REPO_ROOT / "config" / "dsw-compat.yml"
GITHUB_DIR = Path(".github")
VERSION_BRANCH_WORKFLOW_PATH = GITHUB_DIR / "workflows" / "document_template_translation_sync.yml"
TRANSLATION_TREE_MERGE_REPORT_PATH = Path(".translation-tree") / "merge-report.json"
VERSION_BRANCH_GITIGNORE = """# Generated by local/CI runs.
.cache/
outputs/

# Local editor and OS noise.
.DS_Store
*.swp
"""
VERSION_BRANCH_CLEANUP_PATHS = (
    Path(".cache"),
    GITHUB_DIR,
    Path("docs"),
    Path("fixtures"),
    Path("outputs"),
    Path("translation-config.yml"),
    Path("workspace") / "knowledge-models",
    Path("workspace") / "projects",
)

OBSOLETE_VERSION_BRANCH_PATHS = (
    GITHUB_DIR / "workflows" / "weblate_translation_promote.yml",
    Path("weblate") / "dsw-science-europe.zh_Hant.xlf",
)

BRANCH_LOCAL_DEMO_ASSET_DIRS = (
    Path("fixtures") / "knowledge-models",
    Path("fixtures") / "projects" / "demo",
    Path("workspace") / "knowledge-models",
    Path("workspace") / "projects",
)


@dataclass(frozen=True)
class SyncResult:
    """Result of one version-branch sync run."""

    previous_latest_version: str
    current_latest_version: str
    added_versions: tuple[str, ...]
    created_branches: tuple[str, ...]
    refreshed_branches: tuple[str, ...]
    updated_control_branches: tuple[str, ...]
    config_changed: bool


def build_argument_parser() -> argparse.ArgumentParser:
    """Build CLI arguments."""

    parser = argparse.ArgumentParser(
        description=(
            "Record clean scaffold versions and create or refresh policy-enabled "
            "translation version branches."
        ),
    )
    parser.add_argument(
        "--repo",
        default=".",
        help="Translation repository checkout containing translation-config.yml.",
    )
    parser.add_argument(
        "--tooling-root",
        required=True,
        help="Checked-out tooling repository.",
    )
    parser.add_argument(
        "--config",
        default="translation-config.yml",
        help="Path to translation-config.yml relative to --repo.",
    )
    parser.add_argument(
        "--clean-artifact-root",
        required=True,
        help="Downloaded clean upstream scaffold artifact root.",
    )
    parser.add_argument(
        "--tdk-executable",
        help="Path to dsw-tdk. Defaults to <tooling-root>/.venv/bin/dsw-tdk.",
    )
    parser.add_argument(
        "--push",
        action="store_true",
        help="Push updated control config and newly created translation branches.",
    )
    parser.add_argument(
        "--dry-run",
        action="store_true",
        help="Report changes without committing, pushing, or creating branches.",
    )
    parser.add_argument(
        "--refresh-existing",
        action="store_true",
        help=(
            "Refresh existing translation version branches from clean artifacts, "
            "preserving exact-match translations."
        ),
    )
    parser.add_argument(
        "--sync-workflows",
        action="store_true",
        help=(
            "Deprecated compatibility flag. Version-branch security workflows "
            "are always created or updated."
        ),
    )
    parser.add_argument(
        "--github-output",
        help="Optional GitHub Actions output file to write sync metadata to.",
    )
    parser.add_argument(
        "--policy-mode",
        choices=("auto", "manual"),
        default="auto",
        help=(
            "Version policy mode. Scheduled automation should use auto; "
            "operator-triggered syncs may use manual."
        ),
    )
    return parser


def main() -> None:
    """Run translation version branch synchronization."""

    args = build_argument_parser().parse_args()
    repo = Path(args.repo).resolve()
    tooling_root = Path(args.tooling_root).resolve()
    config_path = repo / args.config
    clean_artifact_root = Path(args.clean_artifact_root).resolve()
    tdk_executable = (
        Path(args.tdk_executable).resolve()
        if args.tdk_executable
        else tooling_root / ".venv" / "bin" / "dsw-tdk"
    )

    result = sync_translation_versions(
        repo=repo,
        tooling_root=tooling_root,
        config_path=config_path,
        clean_artifact_root=clean_artifact_root,
        tdk_executable=tdk_executable,
        push=args.push,
        dry_run=args.dry_run,
        refresh_existing=args.refresh_existing,
        sync_workflows=args.sync_workflows,
        policy_mode=args.policy_mode,
    )
    print_summary(result)
    if args.github_output:
        write_github_output(Path(args.github_output), result)


def sync_translation_versions(
    *,
    repo: Path,
    tooling_root: Path,
    config_path: Path,
    clean_artifact_root: Path,
    tdk_executable: Path,
    push: bool,
    dry_run: bool,
    refresh_existing: bool = False,
    sync_workflows: bool = False,
    policy_mode: str = "auto",
) -> SyncResult:
    """Update known versions and synchronize policy-enabled version branches."""

    # Version branches are the translator-facing trust boundary. Keep accepting the
    # legacy argument for API compatibility, but never allow callers to omit the
    # workflow that audits translator-controlled content before packaging it.
    sync_workflows = True

    config = load_translation_repository_config(config_path)
    existing_versions = tuple(config.template.supported_versions)
    previous_latest = sorted_versions(existing_versions)[-1]
    artifact_versions = tuple(
        clean_artifact_versions(config=config, artifact_root=clean_artifact_root)
    )
    if not artifact_versions:
        raise SystemExit(
            f"No clean artifact versions found under {clean_artifact_root}. "
            "Did the tooling workflow upload clean-upstream-version-artifacts?"
        )

    supported_versions = tuple(sorted_versions({*existing_versions, *artifact_versions}))
    added_versions = tuple(
        version for version in supported_versions if version not in existing_versions
    )
    current_latest = supported_versions[-1]
    config_changed = supported_versions != existing_versions

    if config_changed:
        if dry_run:
            print(
                "INFO: dry-run would update supported_versions to: " + ", ".join(supported_versions)
            )
            config = replace(
                config,
                template=replace(config.template, supported_versions=supported_versions),
            )
        else:
            write_supported_versions(config_path, supported_versions)
            ensure_git_identity(repo)
            _run(["git", "add", str(config_path.relative_to(repo))], cwd=repo)
            _run(
                [
                    "git",
                    "commit",
                    "-m",
                    "chore: sync supported template versions",
                ],
                cwd=repo,
            )
            if push:
                _run(
                    [
                        "git",
                        "push",
                        "origin",
                        f"HEAD:refs/heads/{config.branches.control_branch}",
                    ],
                    cwd=repo,
                )
            config = load_translation_repository_config(config_path)

    _run(["git", "fetch", "--prune", "origin"], cwd=repo)
    created_branches: list[str] = []
    refreshed_branches: list[str] = []
    with tempfile.TemporaryDirectory(prefix="dsw-version-branch-sync-") as temp_raw:
        temp_root = Path(temp_raw)
        updated_control_branches: list[str] = []
        for version in supported_versions:
            branch = version_branch(config, version)
            branch_exists = remote_branch_exists(repo, branch)
            if branch_exists and refresh_existing:
                if not version_refresh_allowed(
                    config=config,
                    version=version,
                    policy_mode=policy_mode,
                ):
                    if dry_run:
                        print(f"INFO: dry-run would update controls for {branch}")
                        updated_control_branches.append(branch)
                        continue
                    if update_version_branch_controls(
                        repo=repo,
                        config=config,
                        version=version,
                        branch=branch,
                        temp_root=temp_root,
                        push=push,
                        sync_workflows=sync_workflows,
                    ):
                        updated_control_branches.append(branch)
                    continue
                if dry_run:
                    print(f"INFO: dry-run would refresh {branch}")
                    refreshed_branches.append(branch)
                    continue
                if refresh_version_branch(
                    repo=repo,
                    tooling_root=tooling_root,
                    tdk_executable=tdk_executable,
                    config=config,
                    version=version,
                    branch=branch,
                    clean_artifact_root=clean_artifact_root,
                    temp_root=temp_root,
                    push=push,
                    sync_workflows=sync_workflows,
                ):
                    refreshed_branches.append(branch)
                continue
            if branch_exists:
                continue
            if not version_refresh_allowed(
                config=config,
                version=version,
                policy_mode=policy_mode,
            ):
                print(
                    f"INFO: [{branch}] skipped creation by version_policy "
                    f"refresh={version_policy_decision(config, version).refresh}"
                )
                continue
            created_branches.append(branch)
            if dry_run:
                print(f"INFO: dry-run would create {branch}")
                continue
            create_version_branch(
                repo=repo,
                tooling_root=tooling_root,
                tdk_executable=tdk_executable,
                config=config,
                version=version,
                branch=branch,
                clean_artifact_root=clean_artifact_root,
                temp_root=temp_root,
                push=push,
                sync_workflows=sync_workflows,
            )

    return SyncResult(
        previous_latest_version=previous_latest,
        current_latest_version=current_latest,
        added_versions=added_versions,
        created_branches=tuple(created_branches),
        refreshed_branches=tuple(refreshed_branches),
        updated_control_branches=tuple(updated_control_branches),
        config_changed=config_changed,
    )


def version_refresh_allowed(
    *,
    config: TranslationRepositoryConfig,
    version: str,
    policy_mode: str,
) -> bool:
    """Return whether this sync run may refresh/create a version workspace."""

    if policy_mode == "manual":
        return version_policy_allows_manual_refresh(config, version)
    return version_policy_allows_auto_refresh(config, version)


def update_version_branch_controls(
    *,
    repo: Path,
    config: TranslationRepositoryConfig,
    version: str,
    branch: str,
    temp_root: Path,
    push: bool,
    sync_workflows: bool,
) -> bool:
    """Update generated branch control files without touching translation content."""

    checkout = temp_root / f"{branch.replace('/', '-')}-controls"
    if checkout.exists():
        shutil.rmtree(checkout)
    add_existing_branch_worktree(
        repo=repo,
        checkout=checkout,
        branch=branch,
        push=push,
    )
    try:
        finalize_version_branch_workspace(
            checkout=checkout,
            config=config,
            version=version,
            sync_workflows=sync_workflows,
        )
        ensure_git_identity(checkout)
        _run(["git", "add", "-A"], cwd=checkout)
        staged_paths = tuple(staged_changed_paths(checkout))
        if not staged_paths:
            print(f"INFO: [{branch}] no control-file changes.")
            return False
        _run(
            ["git", "commit", "-m", f"chore: refresh {version} branch policy controls"],
            cwd=checkout,
        )
        if push:
            _run(["git", "push", "origin", f"HEAD:refs/heads/{branch}"], cwd=checkout)
        return True
    finally:
        _run(
            ["git", "worktree", "remove", "--force", str(checkout)],
            cwd=repo,
            check=False,
        )


def refresh_version_branch(
    *,
    repo: Path,
    tooling_root: Path,
    tdk_executable: Path,
    config: TranslationRepositoryConfig,
    version: str,
    branch: str,
    clean_artifact_root: Path,
    temp_root: Path,
    push: bool,
    sync_workflows: bool,
) -> bool:
    """Refresh one existing translation branch from the latest clean artifact."""

    checkout = temp_root / branch.replace("/", "-")
    preserved_tree = temp_root / f"{branch.replace('/', '-')}-preserved-tree"
    merged_tree = temp_root / f"{branch.replace('/', '-')}-merged-tree"
    if checkout.exists():
        shutil.rmtree(checkout)
    if preserved_tree.exists():
        shutil.rmtree(preserved_tree)
    if merged_tree.exists():
        shutil.rmtree(merged_tree)

    add_existing_branch_worktree(
        repo=repo,
        checkout=checkout,
        branch=branch,
        push=push,
    )
    try:
        paths = version_paths(config, version)
        existing_translation_tree = checkout / paths.translation_tree_dir
        had_existing_translation_tree = existing_translation_tree.is_dir()
        if had_existing_translation_tree:
            replace_tree(existing_translation_tree, preserved_tree)

        prune_version_branch_workspace(
            checkout=checkout,
            keep_existing_workflows=not sync_workflows,
        )
        restore_clean_workspace(
            checkout=checkout,
            config=config,
            version=version,
            clean_artifact_root=clean_artifact_root,
        )
        sync_public_readme_from_control_branch(
            checkout=checkout,
            repo=repo,
            config=config,
        )
        if had_existing_translation_tree:
            merge_preserved_translations(
                checkout=checkout,
                tooling_root=tooling_root,
                config=config,
                version=version,
                preserved_tree=preserved_tree,
                merged_tree=merged_tree,
            )
        sync_xliff_exchange(
            checkout=checkout,
            tooling_root=tooling_root,
            config=config,
            version=version,
        )
        sync_blank_translation_output(
            checkout=checkout,
            tooling_root=tooling_root,
            tdk_executable=tdk_executable,
            config=config,
            version=version,
        )
        finalize_version_branch_workspace(
            checkout=checkout,
            config=config,
            version=version,
            sync_workflows=sync_workflows,
        )
        ensure_git_identity(checkout)
        _run(["git", "add", "-A"], cwd=checkout)
        staged_paths = tuple(staged_changed_paths(checkout))
        if not staged_paths:
            print(f"INFO: [{branch}] no changes after refresh.")
            return False
        _run(
            ["git", "commit", "-m", f"chore: refresh {version} translation scaffold"],
            cwd=checkout,
        )
        if push:
            _run(["git", "push", "origin", f"HEAD:refs/heads/{branch}"], cwd=checkout)
        return True
    finally:
        _run(
            ["git", "worktree", "remove", "--force", str(checkout)],
            cwd=repo,
            check=False,
        )


def create_version_branch(
    *,
    repo: Path,
    tooling_root: Path,
    tdk_executable: Path,
    config: TranslationRepositoryConfig,
    version: str,
    branch: str,
    clean_artifact_root: Path,
    temp_root: Path,
    push: bool,
    sync_workflows: bool,
) -> None:
    """Create and initialize one missing translation version branch."""

    checkout = temp_root / branch.replace("/", "-")
    if checkout.exists():
        shutil.rmtree(checkout)
    add_new_branch_worktree(
        repo=repo,
        checkout=checkout,
        branch=branch,
        push=push,
    )
    try:
        prune_version_branch_workspace(
            checkout=checkout,
            keep_existing_workflows=False,
        )
        restore_clean_workspace(
            checkout=checkout,
            config=config,
            version=version,
            clean_artifact_root=clean_artifact_root,
        )
        sync_public_readme_from_control_branch(
            checkout=checkout,
            repo=repo,
            config=config,
        )
        sync_xliff_exchange(
            checkout=checkout,
            tooling_root=tooling_root,
            config=config,
            version=version,
        )
        sync_blank_translation_output(
            checkout=checkout,
            tooling_root=tooling_root,
            tdk_executable=tdk_executable,
            config=config,
            version=version,
        )
        finalize_version_branch_workspace(
            checkout=checkout,
            config=config,
            version=version,
            sync_workflows=sync_workflows,
        )
        ensure_git_identity(checkout)
        _run(["git", "add", "-A"], cwd=checkout)
        if not has_staged_changes(checkout):
            print(f"INFO: [{branch}] no changes after initialization.")
            return
        _run(
            [
                "git",
                "commit",
                "-m",
                f"chore: initialize {version} translation scaffold",
            ],
            cwd=checkout,
        )
        if push:
            _run(["git", "push", "origin", f"HEAD:refs/heads/{branch}"], cwd=checkout)
    finally:
        _run(
            ["git", "worktree", "remove", "--force", str(checkout)],
            cwd=repo,
            check=False,
        )


def restore_clean_workspace(
    *,
    checkout: Path,
    config: TranslationRepositoryConfig,
    version: str,
    clean_artifact_root: Path,
) -> None:
    """Copy clean compact/expanded/translation trees into a version branch."""

    target_paths = version_paths(config, version)
    artifact_paths = clean_artifact_version_paths(
        config=config,
        version=version,
        artifact_root=clean_artifact_root,
    )
    required_dirs = (
        artifact_paths.compact_template_dir,
        artifact_paths.expanded_template_dir,
        artifact_paths.translation_tree_dir,
    )
    missing = [path for path in required_dirs if not path.is_dir()]
    if missing:
        raise SystemExit(
            "Clean artifact root does not contain required paths for "
            f"{version}:\n" + "\n".join(f"- {path}" for path in missing)
        )

    replace_tree(
        artifact_paths.compact_template_dir,
        checkout / target_paths.compact_template_dir,
    )
    replace_tree(
        artifact_paths.expanded_template_dir,
        checkout / target_paths.expanded_template_dir,
    )
    replace_tree(
        artifact_paths.translation_tree_dir,
        checkout / target_paths.translation_tree_dir,
    )
    remove_branch_local_demo_assets(checkout)


def prune_version_branch_workspace(
    *,
    checkout: Path,
    keep_existing_workflows: bool,
) -> None:
    """Remove stale branch content before restoring clean scaffold artifacts.

    Refreshing a version branch must be artifact-first: the only content carried
    from the old branch is translated text that is merged back into the fresh
    translation tree. GitHub's default token cannot modify workflow files, so
    routine CI runs may keep an existing `.github/` directory untouched until an
    operator reruns sync with a workflow-scoped token and `--sync-workflows`.
    """

    for child in checkout.iterdir():
        if child.name == ".git":
            continue
        if keep_existing_workflows and child.name == GITHUB_DIR.name:
            continue
        remove_path(child)


def sync_public_readme_from_control_branch(
    *,
    checkout: Path,
    repo: Path,
    config: TranslationRepositoryConfig,
) -> None:
    """Copy the canonical public template README into an active version branch."""

    relative_path = config.public_readme.path
    source = repo / relative_path
    if _contains_symlink(repo, relative_path):
        raise ValueError(f"Refusing to copy public README through symlink: {source}")
    if not source.is_file():
        return

    target = checkout / relative_path
    if _contains_symlink(checkout, relative_path.parent):
        raise ValueError(f"Refusing to copy public README through symlink: {target.parent}")
    target.parent.mkdir(parents=True, exist_ok=True)
    if target.is_symlink():
        target.unlink()
    shutil.copy2(source, target)


def _contains_symlink(root: Path, relative_path: Path) -> bool:
    """Return whether an existing component below ``root`` is a symlink."""

    current = root
    for part in relative_path.parts:
        current /= part
        if current.is_symlink():
            return True
    return False


def finalize_version_branch_workspace(
    *,
    checkout: Path,
    config: TranslationRepositoryConfig,
    version: str,
    sync_workflows: bool,
) -> None:
    """Keep a translation version branch focused on translator-facing files."""

    for relative_path in VERSION_BRANCH_CLEANUP_PATHS:
        if relative_path == GITHUB_DIR and not sync_workflows:
            continue
        remove_path(checkout / relative_path)
    for relative_path in OBSOLETE_VERSION_BRANCH_PATHS:
        remove_path(checkout / relative_path)
    remove_translation_merge_report(checkout=checkout, config=config, version=version)
    write_version_branch_gitignore(checkout)
    write_version_branch_readme(checkout=checkout, config=config, version=version)
    if sync_workflows:
        write_version_branch_workflow(checkout=checkout, config=config, version=version)


def remove_translation_merge_report(
    *,
    checkout: Path,
    config: TranslationRepositoryConfig,
    version: str,
) -> None:
    """Drop merge diagnostics from canonical version branches."""

    paths = version_paths(config, version)
    remove_path(checkout / paths.translation_tree_dir / TRANSLATION_TREE_MERGE_REPORT_PATH)


def write_version_branch_gitignore(checkout: Path) -> None:
    """Write the ignore rules that are safe for translation version branches."""

    (checkout / ".gitignore").write_text(VERSION_BRANCH_GITIGNORE, encoding="utf-8")


def write_version_branch_readme(
    *,
    checkout: Path,
    config: TranslationRepositoryConfig,
    version: str,
) -> None:
    """Write a concise version-specific README for translators."""

    branch = version_branch(config, version)
    paths = version_paths(config, version)
    readme = f"""# {config.translation.translated_template_name} {version}

This branch contains the Traditional Chinese translation workspace for
`{config.template.organization_id}:{config.template.template_id}:{paths.version_number}`.

## Translate

Edit only translator-facing files under:

```text
{paths.translation_tree_dir.as_posix()}/tree/
```

Keep source placeholders intact and open translation PRs against `{branch}`.
The canonical translation state is the checked-in `translation.md` files.
Optional XLIFF exchange is a tooling feature, not the default branch workflow.

## Generated Outputs

CI generates the translated document-template package and preview PDF during
pull requests and branch pushes. Those files are uploaded as GitHub Actions
artifacts or release assets; they are not committed to this branch.

Repository operations, supported-version policy, and migration automation live
on `{config.branches.control_branch}`. Keep translation edits on this version
branch; do not use that operations branch for translator-facing work.
"""
    (checkout / "README.md").write_text(readme, encoding="utf-8")


def write_version_branch_workflow(
    *,
    checkout: Path,
    config: TranslationRepositoryConfig,
    version: str,
) -> None:
    """Render the version-specific translation sync workflow."""

    paths = version_paths(config, version)
    branch = version_branch(config, version)
    runtime = preview_runtime_for_version(
        version,
        runtimes=load_preview_runtimes(DSW_COMPAT_PATH),
    )
    policy = version_policy_decision(config, version)
    workflow = VERSION_BRANCH_WORKFLOW_TEMPLATE.read_text(encoding="utf-8")
    replacements = {
        "__COMPACT_TEMPLATE_DIR__": paths.compact_template_dir.as_posix(),
        "__DSW_TDK_VERSION__": runtime.tdk_version,
        "__DSW_VERSION__": runtime.dsw_version,
        "__EXPANDED_TEMPLATE_DIR__": paths.expanded_template_dir.as_posix(),
        "__OPERATIONS_BRANCH__": config.branches.control_branch,
        "__PROJECT_RENDER_OUTPUT__": paths.project_render_output.as_posix(),
        "__PUBLIC_README_PATH__": config.public_readme.path.as_posix(),
        "__PUBLISH_RELEASE_ASSETS__": str(policy.publish_release).lower(),
        "__REFRESH_TRANSLATION_INPUTS__": str(policy.refresh == "artifact").lower(),
        "__SOURCE_TEMPLATE_ID__": paths.source_template_id,
        "__TOOLING_REF__": config.tooling.ref,
        "__TOOLING_REPOSITORY__": config.tooling.repository,
        "__TRANSLATED_TEMPLATE_DESCRIPTION__": (
            config.translation.translated_template_description or ""
        ),
        "__TRANSLATED_TEMPLATE_DIR__": paths.translated_template_dir.as_posix(),
        "__TRANSLATED_TEMPLATE_ID__": config.translation.translated_template_id,
        "__TRANSLATED_TEMPLATE_NAME__": config.translation.translated_template_name,
        "__TRANSLATED_TEMPLATE_ORGANIZATION_ID__": (
            config.translation.translated_template_organization_id
        ),
        "__TRANSLATED_TEMPLATE_PACKAGE__": paths.translated_template_package.as_posix(),
        "__TRANSLATED_TEMPLATE_VERSION__": paths.version_number,
        "__TRANSLATION_SOURCE_LANG__": config.translation.source_language,
        "__TRANSLATION_TARGET_LABEL__": config.translation.target_language_label,
        "__TRANSLATION_TARGET_LANG__": config.translation.target_language,
        "__TRANSLATION_TREE_DIR__": paths.translation_tree_dir.as_posix(),
        "__UPSTREAM_TEMPLATE_PREVIEW_METAMODEL_VERSION__": runtime.metamodel_version,
        "__VERSION_BRANCH__": branch,
    }
    for token, value in replacements.items():
        if token not in workflow:
            raise SystemExit(f"Workflow template is missing expected token: {token}")
        rendered = value if token == "__VERSION_BRANCH__" else _yaml_scalar(value)
        workflow = workflow.replace(token, rendered)

    unresolved_tokens = sorted(set(re.findall(r"__[A-Z0-9_]+__", workflow)))
    if unresolved_tokens:
        raise SystemExit(
            "Workflow template contains unresolved tokens: " + ", ".join(unresolved_tokens)
        )

    workflow_path = checkout / VERSION_BRANCH_WORKFLOW_PATH
    workflow_path.parent.mkdir(parents=True, exist_ok=True)
    workflow_path.write_text(workflow, encoding="utf-8")


def _yaml_scalar(value: str) -> str:
    """Return a compact double-quoted YAML scalar."""

    return json.dumps(value, ensure_ascii=False)


def remove_branch_local_demo_assets(checkout: Path) -> None:
    """Remove stale demo fixtures from translation version branches.

    Version branches should use the demo project and Knowledge Model fixtures
    checked out from the tooling repository by their workflow. Keeping copies in
    downstream branches makes preview artifacts depend on whichever stale
    fixture happened to be committed there.
    """

    checkout_root = checkout.resolve()
    for relative_dir in BRANCH_LOCAL_DEMO_ASSET_DIRS:
        path = checkout / relative_dir
        current = checkout
        for part in relative_dir.parts:
            current /= part
            if current.is_symlink():
                raise ValueError(f"Refusing to remove demo assets through symlink: {current}")

        try:
            path.resolve().relative_to(checkout_root)
        except ValueError as exc:
            raise ValueError(f"Refusing to remove demo assets outside checkout: {path}") from exc
        remove_path(path)


def remove_path(path: Path) -> None:
    """Remove a file or directory if it exists."""

    if path.is_dir() and not path.is_symlink():
        shutil.rmtree(path)
    elif path.exists():
        path.unlink()


def remove_empty_parents(path: Path, *, stop_at: Path) -> None:
    """Remove empty parent directories up to, but not including, ``stop_at``."""

    current = path
    stop_at = stop_at.resolve()
    while current != stop_at and current.exists():
        try:
            current.rmdir()
        except OSError:
            return
        current = current.parent


def sync_blank_translation_output(
    *,
    checkout: Path,
    tooling_root: Path,
    tdk_executable: Path,
    config: TranslationRepositoryConfig,
    version: str,
) -> None:
    """Generate a valid fallback translated template package for one branch."""

    paths = version_paths(config, version)
    _run_tool(
        tooling_root,
        TRANSLATION_TREE_COMMAND,
        "audit",
        "--tree",
        checkout / paths.translation_tree_dir,
        "--source",
        checkout / paths.expanded_template_dir,
    )
    _run_tool(
        tooling_root,
        TRANSLATION_TREE_COMMAND,
        "sync",
        "--tree",
        checkout / paths.translation_tree_dir,
        "--source",
        checkout / paths.expanded_template_dir,
        "--output",
        checkout / paths.translated_template_dir,
        "--template-organization-id",
        config.translation.translated_template_organization_id,
        "--template-id",
        config.translation.translated_template_id,
        "--template-name",
        config.translation.translated_template_name,
        "--template-description",
        config.translation.translated_template_description or "",
        "--template-version",
        paths.version_number,
        "--public-readme",
        checkout / config.public_readme.path,
    )
    _run_tool(
        tooling_root,
        TRANSLATION_TREE_COMMAND,
        "audit-output",
        "--source",
        checkout / paths.expanded_template_dir,
        "--output",
        checkout / paths.translated_template_dir,
    )
    _run([str(tdk_executable), "verify", str(checkout / paths.translated_template_dir)])
    (checkout / paths.translated_template_package.parent).mkdir(parents=True, exist_ok=True)
    _run(
        [
            str(tdk_executable),
            "package",
            str(checkout / paths.translated_template_dir),
            "--output",
            str(checkout / paths.translated_template_package),
            "--force",
        ]
    )


def export_xliff(
    *,
    checkout: Path,
    tooling_root: Path,
    config: TranslationRepositoryConfig,
    version: str,
) -> None:
    """Export the version branch translation tree to XLIFF."""

    paths = version_paths(config, version)
    _run_tool(
        tooling_root,
        TRANSLATION_TREE_COMMAND,
        "export-xliff",
        "--tree",
        checkout / paths.translation_tree_dir,
        "--output",
        checkout / paths.xliff_exchange_path,
        "--source-lang",
        config.translation.source_language,
        "--target-lang",
        config.translation.target_language,
    )


def sync_xliff_exchange(
    *,
    checkout: Path,
    tooling_root: Path,
    config: TranslationRepositoryConfig,
    version: str,
) -> None:
    """Export or remove optional branch-local XLIFF exchange files."""

    paths = version_paths(config, version)
    if config.xliff_exchange.enabled:
        export_xliff(
            checkout=checkout,
            tooling_root=tooling_root,
            config=config,
            version=version,
        )
        return
    remove_path(checkout / paths.xliff_exchange_path)
    remove_empty_parents(checkout / paths.xliff_exchange_path.parent, stop_at=checkout)


def merge_preserved_translations(
    *,
    checkout: Path,
    tooling_root: Path,
    config: TranslationRepositoryConfig,
    version: str,
    preserved_tree: Path,
    merged_tree: Path,
) -> None:
    """Merge exact-match translations from a preserved tree into a fresh tree."""

    paths = version_paths(config, version)
    fresh_tree = checkout / paths.translation_tree_dir
    _run_tool(
        tooling_root,
        TRANSLATION_TREE_COMMAND,
        "merge",
        "--old-tree",
        preserved_tree,
        "--new-tree",
        fresh_tree,
        "--output",
        merged_tree,
        "--source-lang",
        config.translation.source_language,
        "--target-lang",
        config.translation.target_language,
    )
    replace_tree(merged_tree, fresh_tree)


def write_supported_versions(config_path: Path, versions: tuple[str, ...]) -> None:
    """Update the configured known upstream version ledger."""

    try:
        payload = load_yaml_file(config_path)
    except YamlConfigError as exc:
        raise SystemExit(str(exc)) from exc
    if not isinstance(payload, dict):
        raise SystemExit(f"Expected mapping root in {config_path}")
    payload["template"]["supported_versions"] = list(versions)
    config_path.write_text(
        yaml.safe_dump(payload, sort_keys=False, allow_unicode=True),
        encoding="utf-8",
    )


def write_github_output(path: Path, result: SyncResult) -> None:
    """Write GitHub Actions step outputs."""

    with path.open("a", encoding="utf-8") as handle:
        handle.write(f"previous_latest_version={result.previous_latest_version}\n")
        handle.write(f"current_latest_version={result.current_latest_version}\n")
        handle.write(f"added_versions={' '.join(result.added_versions)}\n")
        handle.write(f"created_branches={' '.join(result.created_branches)}\n")
        handle.write(f"refreshed_branches={' '.join(result.refreshed_branches)}\n")
        handle.write(f"updated_control_branches={' '.join(result.updated_control_branches)}\n")
        handle.write(f"config_changed={str(result.config_changed).lower()}\n")


def print_summary(result: SyncResult) -> None:
    """Print a concise human-readable sync summary."""

    print("INFO: translation version sync complete")
    print(f"INFO: previous latest version: {result.previous_latest_version}")
    print(f"INFO: current latest version: {result.current_latest_version}")
    print(f"INFO: added versions: {', '.join(result.added_versions) or '(none)'}")
    print(f"INFO: created branches: {', '.join(result.created_branches) or '(none)'}")
    print(f"INFO: refreshed branches: {', '.join(result.refreshed_branches) or '(none)'}")
    print(
        "INFO: updated operations/version branch files: "
        + (", ".join(result.updated_control_branches) or "(none)")
    )
    print(f"INFO: config changed: {result.config_changed}")


def remote_branch_exists(repo: Path, branch: str) -> bool:
    """Return whether ``origin/<branch>`` exists."""

    result = subprocess.run(
        ["git", "ls-remote", "--exit-code", "--heads", "origin", branch],
        cwd=repo,
        stdout=subprocess.DEVNULL,
        stderr=subprocess.DEVNULL,
        check=False,
    )
    return result.returncode == 0


def add_existing_branch_worktree(
    *,
    repo: Path,
    checkout: Path,
    branch: str,
    push: bool,
) -> bool:
    """Check out an existing remote branch for refresh work.

    A local branch can only be checked out by one worktree at a time. During
    day-to-day maintenance it is common to already have a sync/v...
    branch open elsewhere, so CI-safe refresh runs can use a detached worktree
    and push HEAD back to the remote branch.
    """

    if push:
        _run(
            ["git", "worktree", "add", "--detach", str(checkout), f"origin/{branch}"],
            cwd=repo,
        )
        return True
    if branch_checked_out_in_worktree(repo, branch):
        raise SystemExit(
            f"{branch} is checked out in another worktree; rerun with --push "
            "or remove that worktree before refreshing locally."
        )
    if local_branch_exists(repo, branch):
        ensure_local_branch_matches_remote(repo, branch)

    _run(
        ["git", "worktree", "add", "-B", branch, str(checkout), f"origin/{branch}"],
        cwd=repo,
    )
    return False


def add_new_branch_worktree(
    *,
    repo: Path,
    checkout: Path,
    branch: str,
    push: bool,
) -> None:
    """Check out the start point for a newly generated version branch.

    CI pushes the detached commit directly to ``refs/heads/<branch>``, so it
    does not need to create or reset a local branch. Local non-push runs keep a
    named branch because that is easier to inspect and continue manually.
    """

    if push:
        _run(["git", "worktree", "add", "--detach", str(checkout), "HEAD"], cwd=repo)
        return
    if branch_checked_out_in_worktree(repo, branch):
        raise SystemExit(
            f"{branch} is checked out in another worktree; rerun with --push "
            "or remove that worktree before creating it locally."
        )
    if local_branch_exists(repo, branch):
        raise SystemExit(
            f"{branch} already exists locally; rerun with --push or remove that "
            "local branch before creating it locally."
        )
    _run(["git", "worktree", "add", "-B", branch, str(checkout), "HEAD"], cwd=repo)


def local_branch_exists(repo: Path, branch: str) -> bool:
    """Return whether a local branch ref exists."""

    result = subprocess.run(
        ["git", "show-ref", "--verify", "--quiet", f"refs/heads/{branch}"],
        cwd=repo,
        check=False,
    )
    return result.returncode == 0


def ensure_local_branch_matches_remote(repo: Path, branch: str) -> None:
    """Fail if a local branch would hide unpushed refresh state."""

    local_sha = git_revision(repo, branch)
    remote_sha = git_revision(repo, f"origin/{branch}")
    if local_sha == remote_sha:
        return
    raise SystemExit(
        f"{branch} exists locally but differs from origin/{branch}; push, reset, "
        "or delete the local branch before refreshing locally."
    )


def git_revision(repo: Path, ref: str) -> str:
    """Return the object ID for one git ref."""

    result = subprocess.run(
        ["git", "rev-parse", "--verify", ref],
        cwd=repo,
        check=True,
        capture_output=True,
        text=True,
    )
    return result.stdout.strip()


def branch_checked_out_in_worktree(repo: Path, branch: str) -> bool:
    """Return whether a local branch is already attached to any worktree."""

    result = subprocess.run(
        ["git", "worktree", "list", "--porcelain"],
        cwd=repo,
        check=True,
        capture_output=True,
        text=True,
    )
    branch_marker = f"branch refs/heads/{branch}"
    return any(line.strip() == branch_marker for line in result.stdout.splitlines())


def replace_tree(source: Path, destination: Path) -> None:
    """Replace one directory tree."""

    symlink = (
        source
        if source.is_symlink()
        else next(
            (path for path in source.rglob("*") if path.is_symlink()),
            None,
        )
    )
    if symlink is not None:
        raise ValueError(f"Refusing to copy directory tree containing symlink: {symlink}")

    if destination.exists():
        shutil.rmtree(destination)
    destination.parent.mkdir(parents=True, exist_ok=True)
    shutil.copytree(source, destination)


def ensure_git_identity(repo: Path) -> None:
    """Configure the standard GitHub Actions bot identity."""

    _run(["git", "config", "user.name", "github-actions[bot]"], cwd=repo)
    _run(
        [
            "git",
            "config",
            "user.email",
            "41898282+github-actions[bot]@users.noreply.github.com",
        ],
        cwd=repo,
    )


def has_staged_changes(repo: Path) -> bool:
    """Return whether the index contains staged changes."""

    result = subprocess.run(
        ["git", "diff", "--cached", "--quiet"],
        cwd=repo,
        check=False,
    )
    return result.returncode != 0


def staged_changed_paths(repo: Path) -> list[str]:
    """Return staged paths relative to ``repo``."""

    result = subprocess.run(
        ["git", "diff", "--cached", "--name-only"],
        cwd=repo,
        check=True,
        capture_output=True,
        text=True,
    )
    return [line for line in result.stdout.splitlines() if line]


def _run_tool(tooling_root: Path, command: str, *args: object) -> None:
    _run(tool_command(tooling_root, command, *args))


def _run(
    args: list[str],
    *,
    cwd: Path | None = None,
    check: bool = True,
    env: dict[str, str] | None = None,
) -> subprocess.CompletedProcess[str]:
    print("+ " + " ".join(args))
    return subprocess.run(
        args,
        cwd=cwd,
        env=env,
        text=True,
        check=check,
    )


if __name__ == "__main__":
    main()
