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

Commands:
    airbyte-ops gh workflow status - Check GitHub Actions workflow status
    airbyte-ops gh workflow trigger - Trigger a GitHub Actions CI workflow
    airbyte-ops gh connector info - Get connector metadata from GitHub
    airbyte-ops gh connector get-version - Get connector version from GitHub
    airbyte-ops gh connector list - List connectors via GitHub API

## CLI reference

The commands below are regenerated by `poe docs-generate` via cyclopts's
programmatic docs API; see `docs/generate_cli.py`.

.. include:: ../../../docs/generated/cli/gh.md
   :start-line: 2
"""

from __future__ import annotations

# Hide Python-level members from the pdoc page for this module; the rendered
# docs for this CLI group come entirely from the grafted `.. include::` in
# the module docstring above.
__all__: list[str] = []

import json
import sys
import time
from typing import Annotated, Literal

from cyclopts import Parameter

from airbyte_ops_mcp.airbyte_repo.list_connectors import (
    format_github_actions_connector_matrix,
    get_modified_connectors_from_github,
)
from airbyte_ops_mcp.airbyte_repo.utils import parse_pr_info
from airbyte_ops_mcp.cli._base import App, app
from airbyte_ops_mcp.cli._shared import exit_with_error, print_json
from airbyte_ops_mcp.connector_metadata import (
    ConnectorMetadataDpathError,
    ConnectorMetadataDpathNotFoundError,
    format_metadata_dpath_value,
    get_connector_version_from_metadata,
    load_raw_connector_metadata_from_github,
)
from airbyte_ops_mcp.github_api import GitHubAPIError, resolve_default_github_token
from airbyte_ops_mcp.mcp.github_actions import (
    check_ci_workflow_status,
    trigger_ci_workflow,
)

AIRBYTE_REPO_OWNER = "airbytehq"
AIRBYTE_REPO_NAME = "airbyte"

# Create the gh sub-app
gh_app = App(name="gh", help="GitHub operations.")
app.command(gh_app)

# Create the workflow sub-app under gh
workflow_app = App(name="workflow", help="GitHub Actions workflow operations.")
gh_app.command(workflow_app)

# Create the connector sub-app under gh
connector_app = App(name="connector", help="Connector operations via GitHub API.")
gh_app.command(connector_app)


ConnectorListOutputFormat = Literal["lines", "csv", "json", "json-gh-matrix"]


def _parse_pr_details(pr: str) -> tuple[int, str | None, str | None]:
    """Parse pull request details from a number or GitHub PR URL."""
    pr_number, pr_owner, pr_repo = parse_pr_info(pr)
    if pr_number is None:
        exit_with_error(
            "PR must be a pull request number or GitHub URL like "
            "https://github.com/airbytehq/airbyte/pull/123."
        )
    return pr_number, pr_owner, pr_repo


def _print_connector_list(
    connectors: list[str],
    output_format: ConnectorListOutputFormat,
) -> None:
    """Print connector names in the requested format."""
    if output_format == "lines":
        for connector in connectors:
            sys.stdout.write(connector + "\n")
    elif output_format == "csv":
        sys.stdout.write(",".join(connectors) + "\n")
    elif output_format == "json":
        sys.stdout.write(json.dumps(connectors, separators=(",", ":")) + "\n")
    elif output_format == "json-gh-matrix":
        sys.stdout.write(
            json.dumps(
                format_github_actions_connector_matrix(connectors),
                separators=(",", ":"),
            )
            + "\n"
        )


@connector_app.command(name="info")
def connector_info(
    name: Annotated[
        str,
        Parameter(help="Connector technical name (e.g., source-github)."),
    ],
    ref: Annotated[
        str,
        Parameter(help="Git ref to read metadata.yaml from."),
    ],
    owner: Annotated[
        str,
        Parameter(help="Repository owner (e.g., airbytehq)."),
    ] = AIRBYTE_REPO_OWNER,
    repo: Annotated[
        str,
        Parameter(help="Repository name (e.g., airbyte or airbyte-enterprise)."),
    ] = AIRBYTE_REPO_NAME,
    gh_token: Annotated[
        str | None,
        Parameter(
            help=(
                "GitHub API token. If omitted, uses `resolve_default_github_token`; "
                "public repos can fall back to unauthenticated requests."
            )
        ),
    ] = None,
    dpath_expression: Annotated[
        str | None,
        Parameter(
            name="--dpath",
            help=(
                "Evaluate this dpath expression against the parsed metadata.yaml "
                "object and print only that value (e.g., data/dockerImageTag)."
            ),
        ),
    ] = None,
) -> None:
    """Get connector metadata from GitHub."""
    try:
        value = load_raw_connector_metadata_from_github(
            name,
            owner=owner,
            repo=repo,
            ref=ref,
            gh_token=gh_token or resolve_default_github_token(allow_none=True),
            dpath_expression=dpath_expression,
        )
    except (
        ConnectorMetadataDpathError,
        ConnectorMetadataDpathNotFoundError,
        FileNotFoundError,
        ValueError,
    ) as e:
        exit_with_error(str(e))

    if dpath_expression is None:
        print_json(value)
        return

    sys.stdout.write(format_metadata_dpath_value(value) + "\n")


@connector_app.command(name="get-version")
def connector_get_version(
    name: Annotated[
        str,
        Parameter(help="Connector technical name (e.g., source-github)."),
    ],
    ref: Annotated[
        str,
        Parameter(help="Git ref to read metadata.yaml from."),
    ],
    owner: Annotated[
        str,
        Parameter(help="Repository owner (e.g., airbytehq)."),
    ] = AIRBYTE_REPO_OWNER,
    repo: Annotated[
        str,
        Parameter(help="Repository name (e.g., airbyte or airbyte-enterprise)."),
    ] = AIRBYTE_REPO_NAME,
    gh_token: Annotated[
        str | None,
        Parameter(
            help=(
                "GitHub API token. If omitted, uses `resolve_default_github_token`; "
                "public repos can fall back to unauthenticated requests."
            )
        ),
    ] = None,
) -> None:
    """Get connector version from GitHub.

    This is analogous to `gh connector info --dpath data/dockerImageTag` and uses
    the same dpath evaluation internally.
    """
    try:
        metadata = load_raw_connector_metadata_from_github(
            name,
            owner=owner,
            repo=repo,
            ref=ref,
            gh_token=gh_token or resolve_default_github_token(allow_none=True),
        )
        version = get_connector_version_from_metadata(metadata)
    except (
        ConnectorMetadataDpathError,
        ConnectorMetadataDpathNotFoundError,
        FileNotFoundError,
        ValueError,
    ) as e:
        exit_with_error(str(e))
    sys.stdout.write(version + "\n")


@connector_app.command(name="list")
def connector_list(
    modified_only: Annotated[
        bool,
        Parameter(help="Include only modified connectors from the provided PR."),
    ] = False,
    pr: Annotated[
        str | None,
        Parameter(
            help="Pull request number or GitHub URL to inspect for changed connector files."
        ),
    ] = None,
    owner: Annotated[
        str,
        Parameter(help="Repository owner (e.g., airbytehq)."),
    ] = AIRBYTE_REPO_OWNER,
    repo: Annotated[
        str,
        Parameter(help="Repository name (e.g., airbyte or airbyte-enterprise)."),
    ] = AIRBYTE_REPO_NAME,
    gh_token: Annotated[
        str | None,
        Parameter(
            help=("GitHub API token. If omitted, uses `resolve_default_github_token`.")
        ),
    ] = None,
    output_format: Annotated[
        ConnectorListOutputFormat,
        Parameter(
            help=(
                'Output format: "lines", "csv", "json", or "json-gh-matrix". '
                'The GitHub Actions matrix format is {"connector":["source-x"]} '
                'and returns {"connector":[""]} when no connectors changed.'
            )
        ),
    ] = "lines",
) -> None:
    """List connector names from GitHub without a local checkout."""
    if not modified_only:
        exit_with_error("`--modified-only` is required for GitHub connector listing.")
    if pr is None:
        exit_with_error("`--pr` is required when `--modified-only` is set.")

    pr_number, pr_owner, pr_repo = _parse_pr_details(pr)
    try:
        connectors = get_modified_connectors_from_github(
            pr_number=pr_number,
            pr_owner=pr_owner or owner,
            pr_repo=pr_repo or repo,
            gh_token=gh_token or resolve_default_github_token(),
        )
    except (GitHubAPIError, ValueError) as e:
        exit_with_error(str(e))
    _print_connector_list(connectors, output_format)


@workflow_app.command(name="status")
def workflow_status(
    url: Annotated[
        str | None,
        Parameter(
            help="Full GitHub Actions workflow run URL "
            "(e.g., 'https://github.com/owner/repo/actions/runs/12345')."
        ),
    ] = None,
    owner: Annotated[
        str | None,
        Parameter(help="Repository owner (e.g., 'airbytehq')."),
    ] = None,
    repo: Annotated[
        str | None,
        Parameter(help="Repository name (e.g., 'airbyte')."),
    ] = None,
    run_id: Annotated[
        int | None,
        Parameter(help="Workflow run ID."),
    ] = None,
) -> None:
    """Check the status of a GitHub Actions workflow run.

    Provide either --url OR all of (--owner, --repo, --run-id).
    """
    # Validate input parameters
    if url:
        if owner or repo or run_id:
            exit_with_error(
                "Cannot specify --url together with --owner/--repo/--run-id. "
                "Use either --url OR the component parts."
            )
    elif not (owner and repo and run_id):
        exit_with_error(
            "Must provide either --url OR all of (--owner, --repo, --run-id)."
        )

    result = check_ci_workflow_status(
        workflow_url=url,
        owner=owner,
        repo=repo,
        run_id=run_id,
    )
    print_json(result.model_dump())


@workflow_app.command(name="trigger")
def workflow_trigger(
    owner: Annotated[
        str,
        Parameter(help="Repository owner (e.g., 'airbytehq')."),
    ],
    repo: Annotated[
        str,
        Parameter(help="Repository name (e.g., 'airbyte')."),
    ],
    workflow_file: Annotated[
        str,
        Parameter(help="Workflow file name (e.g., 'connector-regression-test.yml')."),
    ],
    workflow_definition_ref: Annotated[
        str | None,
        Parameter(
            help="Branch name or PR number for the workflow definition to use. "
            "If a PR number is provided, it resolves to the PR's head branch name. "
            "Defaults to 'main' if not specified."
        ),
    ] = None,
    inputs: Annotated[
        str | None,
        Parameter(
            help='Workflow inputs as a JSON string (e.g., \'{"key": "value"}\').'
        ),
    ] = None,
    wait: Annotated[
        bool,
        Parameter(help="Wait for the workflow to complete before returning."),
    ] = False,
    wait_seconds: Annotated[
        int,
        Parameter(
            help="Maximum seconds to wait for workflow completion (default: 600)."
        ),
    ] = 600,
) -> None:
    """Trigger a GitHub Actions CI workflow via workflow_dispatch.

    This command triggers a workflow in any GitHub repository that has workflow_dispatch
    enabled. It resolves PR numbers to branch names automatically.
    """
    # Parse inputs JSON if provided
    parsed_inputs: dict[str, str] | None = None
    if inputs:
        try:
            parsed_inputs = json.loads(inputs)
        except json.JSONDecodeError as e:
            exit_with_error(f"Invalid JSON for --inputs: {e}")

    # Trigger the workflow
    result = trigger_ci_workflow(
        owner=owner,
        repo=repo,
        workflow_file=workflow_file,
        workflow_definition_ref=workflow_definition_ref,
        inputs=parsed_inputs,
    )

    print_json(result.model_dump())

    # If wait is enabled and we have a run_id, poll for completion
    if wait and result.run_id:
        print(f"\nWaiting for workflow to complete (timeout: {wait_seconds}s)...")
        start_time = time.time()
        poll_interval = 10  # seconds

        while time.time() - start_time < wait_seconds:
            status_result = check_ci_workflow_status(
                owner=owner,
                repo=repo,
                run_id=result.run_id,
            )

            if status_result.status == "completed":
                print(
                    f"\nWorkflow completed with conclusion: {status_result.conclusion}"
                )
                print_json(status_result.model_dump())
                return

            elapsed = int(time.time() - start_time)
            print(f"  Status: {status_result.status} (elapsed: {elapsed}s)")
            time.sleep(poll_interval)

        print(f"\nTimeout reached after {wait_seconds}s. Workflow still running.")
        # Print final status
        final_status = check_ci_workflow_status(
            owner=owner,
            repo=repo,
            run_id=result.run_id,
        )
        print_json(final_status.model_dump())
