# Copyright (c) 2025 Airbyte, Inc., all rights reserved.
"""CLI commands for connector registry operations.

This module provides CLI wrappers for registry operations. The core logic
lives in the `airbyte_ops_mcp.registry` capability module.

Command groups:
    airbyte-ops registry rc promote - Promote RC to stable
    airbyte-ops registry rc rollback - Rollback RC version override
    airbyte-ops registry connector list - List all connectors in registry
    airbyte-ops registry connector-version list - List versions for a connector
    airbyte-ops registry connector-version next - Compute next version tag (prerelease/RC)
    airbyte-ops registry connector-version yank - Mark a connector version as yanked
    airbyte-ops registry connector-version unyank - Remove yank marker from a connector version
    airbyte-ops registry connector-version metadata get - Read connector metadata from GCS
    airbyte-ops registry connector-version artifacts generate - Generate version artifacts locally via docker
    airbyte-ops registry connector-version artifacts publish - Publish version artifacts to GCS
    airbyte-ops registry store mirror --local|--gcs-bucket|--s3-bucket - Mirror entire registry for testing
    airbyte-ops registry store compile --store coral:dev|coral:prod - Compile registry indexes and sync latest/ dirs
    airbyte-ops registry marketing-stubs sync --store coral:prod - Sync connector_stubs.json to GCS
    airbyte-ops registry marketing-stubs check --store coral:prod - Compare local file with GCS
"""

from __future__ import annotations

import contextlib
import sys
from pathlib import Path
from typing import Annotated, Literal

import yaml
from cyclopts import App, Parameter

from airbyte_ops_mcp.cli._base import app
from airbyte_ops_mcp.cli._shared import (
    error_console,
    exit_with_error,
    print_error,
    print_json,
    print_success,
)
from airbyte_ops_mcp.github_api import (
    get_file_contents_at_ref,
    resolve_github_token,
)
from airbyte_ops_mcp.mcp.prerelease import (
    compute_prerelease_docker_image_tag,
)
from airbyte_ops_mcp.registry import (
    resolve_store_target,
)
from airbyte_ops_mcp.registry.compare import compare_stores
from airbyte_ops_mcp.registry.connector_stubs import (
    CONNECTOR_STUBS_FILE,
)
from airbyte_ops_mcp.registry.generate import generate_version_artifacts
from airbyte_ops_mcp.registry.registry_store_base import (
    RegistryStoreBase,
    get_registry_store,
)

# Create the registry sub-app
registry_app = App(
    name="registry", help="Connector registry operations (GCS metadata service)."
)
app.command(registry_app)

# Create the RC sub-app under registry
rc_app = App(name="rc", help="Release candidate lifecycle operations.")
registry_app.command(rc_app)

# Create the connector sub-app under registry
connector_app = App(name="connector", help="Connector listing operations.")
registry_app.command(connector_app)

# Create the connector-version sub-app under registry
connector_version_app = App(
    name="connector-version",
    help="Connector version operations (list, yank, unyank, artifacts).",
)
registry_app.command(connector_version_app)

# Create the metadata sub-app under connector-version
metadata_app = App(
    name="metadata",
    help="Connector metadata inspection.",
)
connector_version_app.command(metadata_app)

# Create the artifacts sub-app under connector-version
artifacts_app = App(
    name="artifacts",
    help="Version artifact generation and publishing.",
)
connector_version_app.command(artifacts_app)

# Create the store sub-app under registry (whole-registry operations)
store_app = App(
    name="store",
    help="Whole-registry store operations (mirror, compile).",
)
registry_app.command(store_app)

# Create the marketing-stubs sub-app under registry (for whole-file GCS operations)
marketing_stubs_app = App(
    name="marketing-stubs",
    help="Marketing connector stubs GCS operations (whole-file sync).",
)
registry_app.command(marketing_stubs_app)


AIRBYTE_REPO_OWNER = "airbytehq"
AIRBYTE_ENTERPRISE_REPO_NAME = "airbyte-enterprise"
AIRBYTE_REPO_NAME = "airbyte"
CONNECTOR_PATH_PREFIX = "airbyte-integrations/connectors"


def _resolve_store(
    store: str | None,
    connector_name: str | None = None,
) -> RegistryStoreBase:
    """Resolve `--store` to a `RegistryStoreBase` instance.

    Wraps `resolve_store_target` and `get_registry_store`, converting
    `ValueError` into a user-friendly CLI error via `exit_with_error`.
    """
    try:
        target = resolve_store_target(store=store, connector_name=connector_name)
    except ValueError as e:
        exit_with_error(str(e))
        raise  # unreachable; satisfies type checker
    return get_registry_store(target)


def _get_connector_version_from_github(
    connector_name: str,
    ref: str,
    token: str | None = None,
) -> str | None:
    """Fetch connector version from metadata.yaml via GitHub API.

    Args:
        connector_name: Connector name (e.g., "source-github")
        ref: Git ref (commit SHA, branch name, or tag)
        token: GitHub API token (optional for public repos)

    Returns:
        Version string from metadata.yaml, or None if not found.
    """
    path = f"{CONNECTOR_PATH_PREFIX}/{connector_name}/metadata.yaml"
    contents = get_file_contents_at_ref(
        owner=AIRBYTE_REPO_OWNER,
        repo=AIRBYTE_REPO_NAME,
        path=path,
        ref=ref,
        token=token,
    )
    if contents is None:
        return None

    metadata = yaml.safe_load(contents)
    return metadata.get("data", {}).get("dockerImageTag")


@connector_version_app.command(name="next")
def compute_next_version(
    name: Annotated[
        str,
        Parameter(help="Connector name (e.g., 'source-github')."),
    ],
    sha: Annotated[
        str,
        Parameter(help="Git commit SHA (full or at least 7 characters)."),
    ],
    base_version: Annotated[
        str | None,
        Parameter(
            help="Base version override. If not provided, fetched from metadata.yaml at the given SHA."
        ),
    ] = None,
) -> None:
    """Compute the next version tag for a connector.

    Outputs the version tag to stdout for easy capture in shell scripts.
    This is the single source of truth for pre-release version format.

    The command fetches the connector's metadata.yaml from GitHub at the given SHA
    to determine the base version. It also compares against the master branch and
    prints a warning to stderr if no version bump is detected.

    If --base-version is provided, it is used directly instead of fetching from GitHub.

    Examples:
        airbyte-ops registry connector-version next --name source-github --sha abcdef1234567
        # Output: 1.2.3-preview.abcdef1

        airbyte-ops registry connector-version next --name source-github --sha abcdef1234567 --base-version 1.2.3
        # Output: 1.2.3-preview.abcdef1 (uses provided version, skips GitHub API)
    """
    # Try to get a GitHub token (optional, but helps avoid rate limiting)
    # Token resolution may fail if no token is configured, which is fine for public repos
    token: str | None = None
    with contextlib.suppress(ValueError):
        token = resolve_github_token()

    # Determine base version
    version: str
    if base_version:
        version = base_version
    else:
        # Fetch version from metadata.yaml at the given SHA
        fetched_version = _get_connector_version_from_github(name, sha, token)
        if fetched_version is None:
            print(
                f"Error: Could not fetch metadata.yaml for {name} at ref {sha}",
                file=sys.stderr,
            )
            sys.exit(1)
        version = fetched_version

    # Compare with master branch version and warn if no bump detected
    master_version = _get_connector_version_from_github(name, "master", token)
    if master_version and master_version == version:
        print(
            f"Warning: No version bump detected for {name}. "
            f"Version {version} matches master branch.",
            file=sys.stderr,
        )

    # Compute and output the prerelease tag
    tag = compute_prerelease_docker_image_tag(version, sha)
    print(tag)


# =============================================================================
# RC LIFECYCLE COMMANDS
# =============================================================================


@rc_app.command(name="promote")
def rc_promote(
    name: Annotated[
        str,
        Parameter(help="Connector technical name (e.g., source-github)."),
    ],
    repo_path: Annotated[
        Path,
        Parameter(help="Path to the Airbyte monorepo. Defaults to current directory."),
    ] = Path.cwd(),
    dry_run: Annotated[
        bool,
        Parameter(help="Show what would be published without making changes."),
    ] = False,
    store: Annotated[
        str | None,
        Parameter(
            help="Store target (e.g. 'coral:dev', 'coral:prod'). "
            "Auto-detected from connector name when omitted.",
        ),
    ] = None,
) -> None:
    """Promote a release candidate to stable.

    Copies the release candidate metadata to the 'latest' path in GCS,
    then deletes the release candidate metadata.

    Uses `--store` to select the registry store and environment.
    When omitted, the store is auto-detected from the connector name.

    Requires GCS_CREDENTIALS environment variable to be set.
    """
    if not repo_path.exists():
        exit_with_error(f"Repository path not found: {repo_path}")

    registry = _resolve_store(store, connector_name=name)

    result = registry.rc_promote(
        repo_path=repo_path,
        connector_name=name,
        dry_run=dry_run,
    )

    print_json(result.model_dump())

    if result.status == "failure":
        exit_with_error(result.message or "Operation failed", code=1)


@rc_app.command(name="rollback")
def rc_rollback(
    name: Annotated[
        str,
        Parameter(help="Connector technical name (e.g., source-github)."),
    ],
    repo_path: Annotated[
        Path,
        Parameter(help="Path to the Airbyte monorepo. Defaults to current directory."),
    ] = Path.cwd(),
    dry_run: Annotated[
        bool,
        Parameter(help="Show what would be published without making changes."),
    ] = False,
    store: Annotated[
        str | None,
        Parameter(
            help="Store target (e.g. 'coral:dev', 'coral:prod'). "
            "Auto-detected from connector name when omitted.",
        ),
    ] = None,
) -> None:
    """Rollback a release candidate version override.

    Deletes both the release candidate metadata and the versioned metadata
    from GCS after verifying their hashes match.

    Uses `--store` to select the registry store and environment.
    When omitted, the store is auto-detected from the connector name.

    Requires GCS_CREDENTIALS environment variable to be set.
    """
    if not repo_path.exists():
        exit_with_error(f"Repository path not found: {repo_path}")

    registry = _resolve_store(store, connector_name=name)

    result = registry.rc_rollback(
        repo_path=repo_path,
        connector_name=name,
        dry_run=dry_run,
    )

    print_json(result.model_dump())

    if result.status == "failure":
        exit_with_error(result.message or "Operation failed", code=1)


# =============================================================================
# REGISTRY I/O - READ COMMANDS
# =============================================================================


@metadata_app.command(name="get")
def get_connector_version_metadata_cmd(
    name: Annotated[
        str,
        Parameter(
            help="Connector name (e.g., 'source-faker', 'destination-postgres')."
        ),
    ],
    version: Annotated[
        str,
        Parameter(help="Version to read (e.g., 'latest', '1.2.3')."),
    ] = "latest",
    format: Annotated[
        Literal["json", "raw"],
        Parameter(help="Output format: 'json' for JSON, 'raw' for YAML."),
    ] = "json",
    store: Annotated[
        str | None,
        Parameter(
            help="Store target (e.g. 'coral:dev', 'coral:prod'). "
            "Auto-detected from connector name when omitted.",
        ),
    ] = None,
) -> None:
    """Read a connector version's metadata from the registry.

    Returns the full metadata.yaml content for a connector at the specified version.

    Requires GCS_CREDENTIALS environment variable to be set.

    Examples:
        airbyte-ops registry connector-version metadata get --name source-faker
        airbyte-ops registry connector-version metadata get --name source-faker --version 6.2.38
        airbyte-ops registry connector-version metadata get --name source-faker --store coral:prod
    """
    registry = _resolve_store(store, connector_name=name)

    try:
        metadata = registry.get_connector_metadata(
            connector_name=name,
            version=version,
        )
    except FileNotFoundError as e:
        exit_with_error(str(e), code=1)
    except Exception as e:
        exit_with_error(f"Error reading metadata: {e}", code=1)

    if format == "json":
        print_json(metadata)
    else:
        print(yaml.dump(metadata, default_flow_style=False))


@connector_app.command(name="list")
def list_connectors_cmd(
    format: Annotated[
        Literal["json", "text"],
        Parameter(
            help="Output format: 'json' for JSON array, 'text' for newline-separated."
        ),
    ] = "json",
    store: Annotated[
        str | None,
        Parameter(
            help="Store target (e.g. 'coral:dev', 'coral:prod'). "
            "Defaults to 'coral:dev'.",
        ),
    ] = "coral:dev",
) -> None:
    """List all connectors in the registry.

    Scans the registry bucket to find all connectors that have metadata files.

    Requires GCS_CREDENTIALS environment variable to be set.
    """
    registry = _resolve_store(store)

    try:
        connectors = registry.list_connectors()
    except Exception as e:
        exit_with_error(f"Error listing connectors: {e}", code=1)

    if format == "json":
        print_json({"connectors": connectors, "count": len(connectors)})
    else:
        for connector in connectors:
            print(connector)


@connector_version_app.command(name="list")
def list_connector_versions_cmd(
    name: Annotated[
        str,
        Parameter(help="Connector name (e.g., 'source-faker')."),
    ],
    format: Annotated[
        Literal["json", "text"],
        Parameter(
            help="Output format: 'json' for JSON array, 'text' for newline-separated."
        ),
    ] = "json",
    store: Annotated[
        str | None,
        Parameter(
            help="Store target (e.g. 'coral:dev', 'coral:prod'). "
            "Auto-detected from connector name when omitted.",
        ),
    ] = None,
) -> None:
    """List all versions of a connector in the registry.

    Scans the registry bucket to find all versions of a specific connector.

    Requires GCS_CREDENTIALS environment variable to be set.
    """
    registry = _resolve_store(store, connector_name=name)

    try:
        versions = registry.list_connector_versions(
            connector_name=name,
        )
    except Exception as e:
        exit_with_error(f"Error listing versions: {e}", code=1)

    if format == "json":
        print_json({"connector": name, "versions": versions, "count": len(versions)})
    else:
        for v in versions:
            print(v)


@marketing_stubs_app.command(name="check")
def marketing_stubs_check(
    store: Annotated[
        str | None,
        Parameter(
            help="Store target (e.g. 'coral:dev', 'coral:prod'). "
            "Defaults to 'coral:dev'.",
        ),
    ] = "coral:dev",
    repo_root: Annotated[
        Path,
        Parameter(
            help="Path to the airbyte-enterprise repository root. Defaults to current directory."
        ),
    ] = Path.cwd(),
) -> None:
    """Compare local connector_stubs.json with the version in GCS.

    This command reads the entire local connector_stubs.json file and compares it
    with the version currently published in GCS.

    Exit codes:
        0: Local file matches GCS (check passed)
        1: Differences found (check failed)

    Output:
        STDOUT: JSON representation of the comparison result
        STDERR: Informational messages and comparison details

    Example:
        airbyte-ops registry marketing-stubs check --store coral:prod --repo-root /path/to/airbyte-enterprise
        airbyte-ops registry marketing-stubs check --store coral:dev
    """
    registry = _resolve_store(store)

    try:
        result = registry.marketing_stubs_check(repo_root=repo_root)
    except FileNotFoundError as e:
        exit_with_error(str(e))
    except ValueError as e:
        exit_with_error(str(e))

    error_console.print(
        f"Comparing local {CONNECTOR_STUBS_FILE} with {result.get('bucket', '')}/{result.get('path', '')}"
    )

    differences = result.get("differences", [])
    if differences:
        error_console.print(
            f"[yellow]Warning:[/yellow] {len(differences)} difference(s) found:"
        )
        for diff in differences:
            error_console.print(f"  {diff['id']}: {diff['status']}")
        print_json(result)
        sys.exit(1)

    error_console.print(
        f"[green]Local file is in sync with GCS ({result.get('local_count', 0)} stubs)[/green]"
    )
    print_json(result)


@marketing_stubs_app.command(name="sync")
def marketing_stubs_sync(
    store: Annotated[
        str | None,
        Parameter(
            help="Store target (e.g. 'coral:dev', 'coral:prod'). "
            "Defaults to 'coral:dev'.",
        ),
    ] = "coral:dev",
    repo_root: Annotated[
        Path,
        Parameter(
            help="Path to the airbyte-enterprise repository root. Defaults to current directory."
        ),
    ] = Path.cwd(),
    dry_run: Annotated[
        bool,
        Parameter(help="Show what would be uploaded without making changes."),
    ] = False,
) -> None:
    """Sync local connector_stubs.json to GCS.

    This command uploads the entire local connector_stubs.json file to GCS,
    replacing the existing file. Use this after merging changes to master
    in the airbyte-enterprise repository.

    Exit codes:
        0: Sync successful (or dry-run completed)
        1: Error (file not found, validation failed, etc.)

    Output:
        STDOUT: JSON representation of the sync result
        STDERR: Informational messages and status updates

    Example:
        airbyte-ops registry marketing-stubs sync --store coral:prod --repo-root /path/to/airbyte-enterprise
        airbyte-ops registry marketing-stubs sync --store coral:dev
        airbyte-ops registry marketing-stubs sync --store coral:dev --dry-run
    """
    registry = _resolve_store(store)

    try:
        result = registry.marketing_stubs_sync(
            repo_root=repo_root,
            dry_run=dry_run,
        )
    except FileNotFoundError as e:
        exit_with_error(str(e))
    except ValueError as e:
        exit_with_error(str(e))

    bucket_name = result.get("bucket", "")
    path = result.get("path", "")
    stub_count = result.get("stub_count", 0)

    if dry_run:
        error_console.print(
            f"[DRY RUN] Would upload {stub_count} stubs to {bucket_name}/{path}"
        )
    else:
        error_console.print(
            f"[green]Synced {stub_count} stubs to {bucket_name}/{path}[/green]"
        )
    print_json(result)


# =============================================================================
# REGISTRY REBUILD COMMANDS
# =============================================================================


@store_app.command(name="mirror")
def mirror_cmd(
    local: Annotated[
        bool,
        Parameter(
            help="Write output to a local directory. Mutually exclusive with --gcs-bucket and --s3-bucket.",
            negative="",
        ),
    ] = False,
    gcs_bucket: Annotated[
        str | None,
        Parameter(
            help="Write output to a GCS bucket. Must not be the prod bucket. "
            "Mutually exclusive with --local and --s3-bucket.",
        ),
    ] = None,
    s3_bucket: Annotated[
        str | None,
        Parameter(
            help="Write output to an S3 bucket. "
            "Mutually exclusive with --local and --gcs-bucket.",
        ),
    ] = None,
    output_path_root: Annotated[
        str | None,
        Parameter(
            help="Root path/prefix for the output. For --local, this is a directory path "
            "(defaults to a new temp dir if omitted). For --gcs-bucket/--s3-bucket, "
            "this prefix is prepended to all blob paths.",
        ),
    ] = None,
    dry_run: Annotated[
        bool,
        Parameter(help="Show what would be rebuilt without writing any files."),
    ] = False,
    source_store: Annotated[
        str | None,
        Parameter(
            help="Source store to read from (e.g. 'coral:prod', 'coral:dev'). "
            "Defaults to 'coral:prod'.",
        ),
    ] = "coral:prod",
    connector_name: Annotated[
        tuple[str, ...] | None,
        Parameter(
            help="Only rebuild these connectors (by name). "
            "Can be specified multiple times, e.g. "
            "--connector-name source-faker --connector-name destination-bigquery.",
        ),
    ] = None,
) -> None:
    """Create a mirror of the connector registry from the source store.

    Reads all connector metadata from the source store and copies it
    to the specified output target. Supports local filesystem, GCS, and S3
    as output targets via fsspec.

    Output targets are mutually exclusive: specify exactly one of
    --local, --gcs-bucket, or --s3-bucket.

    The production bucket is categorically disallowed as an output target.

    Examples:
        airbyte-ops registry store mirror --local
        airbyte-ops registry store mirror --local --source-store coral:dev
        airbyte-ops registry store mirror --gcs-bucket dev-airbyte-cloud-connector-metadata-service-2 \
            --output-path-root test-run-123
        airbyte-ops registry store mirror --s3-bucket my-test-bucket --dry-run
    """
    # Validate mutually exclusive output targets
    targets = [local, gcs_bucket is not None, s3_bucket is not None]
    if sum(targets) != 1:
        exit_with_error(
            "Specify exactly one output target: --local, --gcs-bucket, or --s3-bucket."
        )

    if local:
        output_mode = "local"
    elif gcs_bucket is not None:
        output_mode = "gcs"
    else:
        output_mode = "s3"

    registry = _resolve_store(source_store)

    result = registry.mirror(
        output_mode=output_mode,
        output_path_root=output_path_root,
        gcs_bucket=gcs_bucket,
        s3_bucket=s3_bucket,
        dry_run=dry_run,
        connector_name=list(connector_name) if connector_name else None,
    )

    print_json(
        {
            "status": result.status,
            "source_bucket": result.source_bucket,
            "output_mode": result.output_mode,
            "output_root": result.output_root,
            "connectors_processed": result.connectors_processed,
            "blobs_copied": result.blobs_copied,
            "blobs_skipped": result.blobs_skipped,
            "error_count": len(result.errors),
            "dry_run": result.dry_run,
        }
    )

    if result.errors:
        for err in result.errors[:10]:
            print_error(err)
        if len(result.errors) > 10:
            print_error(f"... and {len(result.errors) - 10} more errors")

    if result.status == "success":
        print_success(result.summary())
    elif result.status == "dry-run":
        print_success(f"[DRY RUN] {result.summary()}")
    else:
        error_console.print(f"[yellow]{result.summary()}[/yellow]")


# =============================================================================
# VERSION YANK COMMANDS
# =============================================================================


@connector_version_app.command(name="yank")
def yank_cmd(
    name: Annotated[
        str,
        Parameter(help="Connector name (e.g., 'source-faker')."),
    ],
    version: Annotated[
        str,
        Parameter(help="Version to yank (e.g., '1.2.3')."),
    ],
    reason: Annotated[
        str,
        Parameter(help="Reason for yanking this version."),
    ] = "",
    dry_run: Annotated[
        bool,
        Parameter(help="Show what would be done without making changes."),
    ] = False,
    store: Annotated[
        str | None,
        Parameter(
            help="Store target (e.g. 'coral:dev', 'coral:prod'). "
            "Auto-detected from connector name when omitted.",
        ),
    ] = None,
) -> None:
    """Mark a connector version as yanked.

    Writes a version-yank.yml marker file to the version's directory in GCS.
    Yanked versions are excluded when determining the latest version of a
    connector.

    Requires GCS_CREDENTIALS environment variable to be set.

    Examples:
        airbyte-ops registry connector-version yank --name source-faker --version 1.2.3
        airbyte-ops registry connector-version yank --name source-faker --version 1.2.3 --reason "Critical bug"
        airbyte-ops registry connector-version yank --name source-faker --version 1.2.3 --store coral:prod
    """
    registry = _resolve_store(store, connector_name=name)

    result = registry.yank(
        connector_name=name,
        version=version,
        reason=reason,
        dry_run=dry_run,
    )

    print_json(result.to_dict())

    if result.success:
        print_success(result.message)
    else:
        exit_with_error(result.message, code=1)


@connector_version_app.command(name="unyank")
def unyank_cmd(
    name: Annotated[
        str,
        Parameter(help="Connector name (e.g., 'source-faker')."),
    ],
    version: Annotated[
        str,
        Parameter(help="Version to unyank (e.g., '1.2.3')."),
    ],
    dry_run: Annotated[
        bool,
        Parameter(help="Show what would be done without making changes."),
    ] = False,
    store: Annotated[
        str | None,
        Parameter(
            help="Store target (e.g. 'coral:dev', 'coral:prod'). "
            "Auto-detected from connector name when omitted.",
        ),
    ] = None,
) -> None:
    """Remove the yank marker from a connector version.

    Deletes the version-yank.yml marker file from the version's directory in GCS,
    making the version eligible again when determining the latest version.

    Requires GCS_CREDENTIALS environment variable to be set.

    Examples:
        airbyte-ops registry connector-version unyank --name source-faker --version 1.2.3
        airbyte-ops registry connector-version unyank --name source-faker --version 1.2.3 --store coral:prod
    """
    registry = _resolve_store(store, connector_name=name)

    result = registry.unyank(
        connector_name=name,
        version=version,
        dry_run=dry_run,
    )

    print_json(result.to_dict())

    if result.success:
        print_success(result.message)
    else:
        exit_with_error(result.message, code=1)


# =============================================================================
# ARTIFACT GENERATION COMMANDS
# =============================================================================


@artifacts_app.command(name="generate")
def generate_version_artifacts_cmd(
    metadata_file: Annotated[
        Path,
        Parameter(help="Path to the connector's metadata.yaml file."),
    ],
    docker_image: Annotated[
        str,
        Parameter(
            help="Docker image to run spec against (e.g., 'airbyte/source-faker:6.2.38')."
        ),
    ],
    output_dir: Annotated[
        Path | None,
        Parameter(
            help="Directory to write artifacts to. If not specified, a temp directory is created."
        ),
    ] = None,
    repo_root: Annotated[
        Path | None,
        Parameter(
            help=(
                "Root of the Airbyte repo checkout (for resolving doc.md). "
                "If not specified, inferred by walking up from metadata-file."
            ),
        ),
    ] = None,
    dry_run: Annotated[
        bool,
        Parameter(
            help="Show what would be generated without running docker or writing files."
        ),
    ] = False,
    with_validate: Annotated[
        bool,
        Parameter(
            ("--with-validate/--no-validate",),
            help=(
                "Run metadata validators after generation (default: enabled). "
                "Use --no-validate to skip."
            ),
        ),
    ] = True,
) -> None:
    """Generate version artifacts for a connector locally.

    Runs the connector's docker image with DEPLOYMENT_MODE=cloud and
    DEPLOYMENT_MODE=oss to obtain both spec variants, then generates
    the registry entries (cloud.json, oss.json) by applying
    registryOverrides from the metadata.

    The generated metadata.yaml is enriched with git commit info, SBOM URL,
    and (when applicable) components SHA before writing.  Validation is run
    after generation by default; pass `--no-validate` to skip.

    This is a local-only operation -- no files are uploaded to GCS.
    Use `artifacts publish` to upload generated artifacts to GCS.

    Examples:
        airbyte-ops registry connector-version artifacts generate \\
            --metadata-file path/to/metadata.yaml \\
            --docker-image airbyte/source-faker:6.2.38

        airbyte-ops registry connector-version artifacts generate \\
            --metadata-file path/to/metadata.yaml \\
            --docker-image airbyte/source-faker:6.2.38 \\
            --output-dir ./artifacts --with-validate
    """
    result = generate_version_artifacts(
        metadata_file=metadata_file,
        docker_image=docker_image,
        output_dir=output_dir,
        repo_root=repo_root,
        dry_run=dry_run,
        with_validate=with_validate,
    )

    print_json(result.to_dict())

    if result.success:
        print_success(
            f"Generated {len(result.artifacts_written)} artifacts to {result.output_dir}"
        )
    else:
        all_errors = result.errors + result.validation_errors
        exit_with_error(
            f"Generation completed with {len(all_errors)} error(s): "
            + "; ".join(all_errors),
            code=1,
        )


@artifacts_app.command(name="publish")
def publish_version_artifacts_cmd(
    *,
    name: Annotated[
        str,
        Parameter(help="Connector name (e.g., 'source-faker')."),
    ],
    version: Annotated[
        str,
        Parameter(help="Version to publish artifacts for (e.g., '1.2.3')."),
    ],
    artifacts_dir: Annotated[
        Path,
        Parameter(
            help="Directory containing generated artifacts to publish (from 'artifacts generate')."
        ),
    ],
    store: Annotated[
        str | None,
        Parameter(
            help="Store target (e.g. 'coral:dev', 'coral:prod', 'coral:dev/prefix'). "
            "Auto-detected from connector name when omitted.",
        ),
    ] = None,
    dry_run: Annotated[
        bool,
        Parameter(help="Show what would be published without writing to GCS."),
    ] = False,
    with_validate: Annotated[
        bool,
        Parameter(
            ("--with-validate/--no-validate",),
            help=(
                "Validate metadata before uploading (default: enabled). "
                "Use --no-validate to skip."
            ),
        ),
    ] = True,
) -> None:
    """Publish version artifacts to GCS using fsspec rsync.

    Uploads locally generated artifacts (from `artifacts generate`) to the
    versioned path in GCS.  By default, metadata is validated before upload;
    pass `--no-validate` to skip.

    Uses `--store` to select the destination store and environment:

    * `coral:dev`              → coral dev bucket at root
    * `coral:prod`             → coral prod bucket at root
    * `coral:dev/aj-test100`   → coral dev bucket under `aj-test100/` prefix

    Requires GCS_CREDENTIALS environment variable to be set.

    Examples:
        airbyte-ops registry connector-version artifacts publish \\
            --name source-faker --version 6.2.38 \\
            --artifacts-dir ./artifacts --with-validate

        airbyte-ops registry connector-version artifacts publish \\
            --name source-faker --version 6.2.38 \\
            --artifacts-dir ./artifacts --store coral:prod

        airbyte-ops registry connector-version artifacts publish \\
            --name source-faker --version 6.2.38 \\
            --artifacts-dir ./artifacts --store coral:dev/aj-test100
    """
    registry = _resolve_store(store, connector_name=name)

    result = registry.publish_version_artifacts(
        connector_name=name,
        version=version,
        artifacts_dir=artifacts_dir,
        dry_run=dry_run,
        with_validate=with_validate,
    )

    print_json(
        {
            "status": result.status,
            "connector_name": result.connector_name,
            "version": result.version,
            "target": result.target,
            "gcs_destination": result.gcs_destination,
            "files_uploaded": result.files_uploaded,
            "errors": result.errors,
            "validation_errors": result.validation_errors,
            "dry_run": result.dry_run,
        }
    )

    if result.success:
        print_success(
            f"Published {len(result.files_uploaded)} artifacts for "
            f"{result.connector_name}@{result.version} → {result.gcs_destination}"
        )
    else:
        all_errors = result.errors + result.validation_errors
        exit_with_error(
            f"Publish completed with {len(all_errors)} error(s): "
            + "; ".join(all_errors),
            code=1,
        )


# =============================================================================
# COMPILE COMMAND
# =============================================================================


@store_app.command(name="compile")
def compile_cmd(
    store: Annotated[
        str | None,
        Parameter(
            help="Store target (e.g. 'coral:dev', 'coral:prod', 'coral:dev/prefix'). "
            "Defaults to 'coral:dev'.",
        ),
    ] = "coral:dev",
    connector_name: Annotated[
        tuple[str, ...] | None,
        Parameter(
            help="Only compile these connectors (can be repeated).",
        ),
    ] = None,
    dry_run: Annotated[
        bool,
        Parameter(help="Show what would be done without writing."),
    ] = False,
    with_secrets_mask: Annotated[
        bool,
        Parameter(
            help=(
                "Also regenerate specs_secrets_mask.yaml by scanning all "
                "connector specs for airbyte_secret properties."
            ),
        ),
    ] = False,
) -> None:
    """Compile the registry: sync latest/ dirs, write global and per-connector indexes.

    Scans all version directories in the target store, determines the latest GA
    semver per connector (excluding yanked and pre-release versions), ensures
    each `latest/` directory matches the computed latest, and writes:

    * `registries/v0/cloud_registry.json` -- global cloud registry index
    * `registries/v0/oss_registry.json`   -- global OSS registry index
    * `metadata/airbyte/<connector>/versions.json` -- per-connector version index

    With `--with-secrets-mask`, also regenerates:

    * `registries/v0/specs_secrets_mask.yaml` -- properties marked as secrets

    Uses efficient glob patterns for scanning (no file downloads during discovery).

    Requires GCS_CREDENTIALS environment variable to be set.

    Examples:
        airbyte-ops registry store compile --store coral:dev --dry-run

        airbyte-ops registry store compile --store coral:dev/aj-test100 \\
            --connector-name source-faker --connector-name destination-bigquery

        airbyte-ops registry store compile --store coral:prod --with-secrets-mask
    """
    registry = _resolve_store(store)

    result = registry.compile(
        connector_name=list(connector_name) if connector_name else None,
        dry_run=dry_run,
        with_secrets_mask=with_secrets_mask,
    )

    print_json(
        {
            "status": result.status,
            "target": result.target,
            "connectors_scanned": result.connectors_scanned,
            "versions_found": result.versions_found,
            "yanked_versions": result.yanked_versions,
            "latest_updated": result.latest_updated,
            "latest_already_current": result.latest_already_current,
            "cloud_registry_entries": result.cloud_registry_entries,
            "oss_registry_entries": result.oss_registry_entries,
            "version_indexes_written": result.version_indexes_written,
            "specs_secrets_mask_properties": result.specs_secrets_mask_properties,
            "errors": result.errors,
            "dry_run": result.dry_run,
        }
    )

    if result.status == "success" or result.status == "dry-run":
        print_success(result.summary())
    else:
        exit_with_error(
            f"Compile completed with {len(result.errors)} error(s): "
            + "; ".join(result.errors),
            code=1,
        )


# =============================================================================
# STORE COMPARE COMMAND
# =============================================================================


@store_app.command(name="compare")
def compare_cmd(
    store: Annotated[
        str,
        Parameter(
            help="Store target being evaluated (e.g. 'coral:dev/20260306-mirror-compile').",
        ),
    ],
    reference_store: Annotated[
        str,
        Parameter(
            help="Known-good reference store to compare against. Defaults to 'coral:prod'.",
        ),
    ] = "coral:prod",
    connector_name: Annotated[
        tuple[str, ...] | None,
        Parameter(
            help="Only compare these connectors (can be repeated).",
        ),
    ] = None,
    with_artifacts: Annotated[
        bool,
        Parameter(
            help="Compare per-connector artifact files "
            "(metadata.yaml, cloud.json, oss.json, spec.json).",
            negative="--no-artifacts",
        ),
    ] = True,
    with_indexes: Annotated[
        bool,
        Parameter(
            help="Compare global registry index files "
            "(cloud_registry.json, oss_registry.json).",
            negative="--no-indexes",
        ),
    ] = True,
) -> None:
    """Compare a store against a reference store and report differences.

    Evaluates the `--store` target against `--reference-store` (default:
    `coral:prod`) and reports per-connector artifact diffs and global index
    diffs.

    Requires GCS_CREDENTIALS environment variable to be set.

    Examples:
        airbyte-ops registry store compare --store coral:dev/20260306-mirror \\
            --reference-store coral:prod

        airbyte-ops registry store compare --store coral:dev/my-test \\
            --connector-name source-faker --no-indexes

        airbyte-ops registry store compare --store coral:dev/my-test \\
            --no-artifacts
    """
    store_target = _resolve_store(store)
    ref_target = _resolve_store(reference_store)

    store_prefix = f"{store_target.prefix}/" if store_target.prefix else ""
    ref_prefix = f"{ref_target.prefix}/" if ref_target.prefix else ""

    result = compare_stores(
        store_bucket=store_target.bucket_name,
        store_prefix=store_prefix,
        reference_bucket=ref_target.bucket_name,
        reference_prefix=ref_prefix,
        connector_name=list(connector_name) if connector_name else None,
        with_artifacts=with_artifacts,
        with_indexes=with_indexes,
    )

    print_json(result.to_dict())

    if result.status == "match":
        print_success(result.summary())
    elif result.status == "differences-found":
        error_console.print(f"[yellow]{result.summary()}[/yellow]")

        # Print a concise per-connector diff summary
        for diff in result.connector_diffs:
            if diff.status in ("only_in_store", "only_in_reference"):
                error_console.print(f"  {diff.connector}: {diff.status}")
            else:
                for ad in diff.artifact_diffs:
                    error_console.print(
                        f"  {diff.connector}/{ad.file}: {ad.status}"
                        + (f" ({ad.details})" if ad.details else "")
                    )

        for idx_diff in result.index_diffs:
            if idx_diff.status != "match":
                error_console.print(
                    f"  [index] {idx_diff.file}: {idx_diff.status}"
                    + (
                        f" (store={idx_diff.entry_count_store},"
                        f" ref={idx_diff.entry_count_reference})"
                        if idx_diff.entry_count_store or idx_diff.entry_count_reference
                        else ""
                    )
                )

        sys.exit(1)
    else:
        exit_with_error(
            f"Compare completed with {len(result.errors)} error(s): "
            + "; ".join(result.errors[:5]),
            code=1,
        )
