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

Commands:
    airbyte-ops cloud connector get-version-info - Get connector version info
    airbyte-ops cloud connector set-version-override - Set connector version override
    airbyte-ops cloud connector clear-version-override - Clear connector version override
    airbyte-ops cloud connector live-test - Run live validation tests on a connector
    airbyte-ops cloud connector regression-test - Run regression tests comparing connector versions
"""

from __future__ import annotations

import json
from pathlib import Path
from typing import Annotated, Literal

from airbyte_protocol.models import ConfiguredAirbyteCatalog
from cyclopts import App, Parameter

from airbyte_ops_mcp.cli._base import app
from airbyte_ops_mcp.cli._shared import print_error, print_json, print_success
from airbyte_ops_mcp.live_tests.ci_output import (
    generate_regression_report,
    get_report_summary,
    write_github_output,
    write_github_outputs,
    write_github_summary,
    write_json_output,
    write_test_summary,
)
from airbyte_ops_mcp.live_tests.connection_fetcher import (
    fetch_connection_data,
    save_connection_data_to_files,
)
from airbyte_ops_mcp.live_tests.connector_runner import (
    ConnectorRunner,
    ensure_image_available,
)
from airbyte_ops_mcp.live_tests.http_metrics import (
    MitmproxyManager,
    parse_http_dump,
)
from airbyte_ops_mcp.live_tests.models import (
    Command,
    ConnectorUnderTest,
    ExecutionInputs,
    TargetOrControl,
)
from airbyte_ops_mcp.mcp.cloud_connector_versions import (
    get_cloud_connector_version,
    set_cloud_connector_version_override,
)

# Create the cloud sub-app
cloud_app = App(name="cloud", help="Airbyte Cloud operations.")
app.command(cloud_app)

# Create the connector sub-app under cloud
connector_app = App(
    name="connector", help="Deployed connector operations in Airbyte Cloud."
)
cloud_app.command(connector_app)


@connector_app.command(name="get-version-info")
def get_version_info(
    workspace_id: Annotated[
        str,
        Parameter(help="The Airbyte Cloud workspace ID."),
    ],
    connector_id: Annotated[
        str,
        Parameter(help="The ID of the deployed connector (source or destination)."),
    ],
    connector_type: Annotated[
        Literal["source", "destination"],
        Parameter(help="The type of connector."),
    ],
) -> None:
    """Get the current version information for a deployed connector."""
    result = get_cloud_connector_version(
        workspace_id=workspace_id,
        actor_id=connector_id,
        actor_type=connector_type,
    )
    print_json(result.model_dump())


@connector_app.command(name="set-version-override")
def set_version_override(
    workspace_id: Annotated[
        str,
        Parameter(help="The Airbyte Cloud workspace ID."),
    ],
    connector_id: Annotated[
        str,
        Parameter(help="The ID of the deployed connector (source or destination)."),
    ],
    connector_type: Annotated[
        Literal["source", "destination"],
        Parameter(help="The type of connector."),
    ],
    version: Annotated[
        str,
        Parameter(
            help="The semver version string to pin to (e.g., '2.1.5-dev.abc1234567')."
        ),
    ],
    reason: Annotated[
        str,
        Parameter(help="Explanation for the override (min 10 characters)."),
    ],
    reason_url: Annotated[
        str | None,
        Parameter(help="Optional URL with more context (e.g., issue link)."),
    ] = None,
) -> None:
    """Set a version override for a deployed connector.

    Requires admin authentication via AIRBYTE_INTERNAL_ADMIN_FLAG and
    AIRBYTE_INTERNAL_ADMIN_USER environment variables.
    """
    result = set_cloud_connector_version_override(
        workspace_id=workspace_id,
        actor_id=connector_id,
        actor_type=connector_type,
        version=version,
        unset=False,
        override_reason=reason,
        override_reason_reference_url=reason_url,
    )
    if result.success:
        print_success(result.message)
    else:
        print_error(result.message)
    print_json(result.model_dump())


@connector_app.command(name="clear-version-override")
def clear_version_override(
    workspace_id: Annotated[
        str,
        Parameter(help="The Airbyte Cloud workspace ID."),
    ],
    connector_id: Annotated[
        str,
        Parameter(help="The ID of the deployed connector (source or destination)."),
    ],
    connector_type: Annotated[
        Literal["source", "destination"],
        Parameter(help="The type of connector."),
    ],
) -> None:
    """Clear a version override from a deployed connector.

    Requires admin authentication via AIRBYTE_INTERNAL_ADMIN_FLAG and
    AIRBYTE_INTERNAL_ADMIN_USER environment variables.
    """
    result = set_cloud_connector_version_override(
        workspace_id=workspace_id,
        actor_id=connector_id,
        actor_type=connector_type,
        version=None,
        unset=True,
        override_reason=None,
        override_reason_reference_url=None,
    )
    if result.success:
        print_success(result.message)
    else:
        print_error(result.message)
    print_json(result.model_dump())


def _load_json_file(file_path: Path) -> dict | None:
    """Load a JSON file and return its contents.

    Returns None if the file doesn't exist or contains invalid JSON.
    """
    if not file_path.exists():
        return None
    try:
        return json.loads(file_path.read_text())
    except json.JSONDecodeError as e:
        print_error(f"Failed to parse JSON in file: {file_path}\nError: {e}")
        return None


def _run_connector_command(
    connector_image: str,
    command: Command,
    output_dir: Path,
    target_or_control: TargetOrControl,
    config_path: Path | None = None,
    catalog_path: Path | None = None,
    state_path: Path | None = None,
    proxy_url: str | None = None,
) -> dict:
    """Run a connector command and return results as a dict.

    Args:
        connector_image: Full connector image name with tag.
        command: The Airbyte command to run.
        output_dir: Directory to store output files.
        target_or_control: Whether this is target or control version.
        config_path: Path to connector config JSON file.
        catalog_path: Path to configured catalog JSON file.
        state_path: Path to state JSON file.
        proxy_url: Optional HTTP proxy URL for traffic capture.

    Returns:
        Dictionary with execution results.
    """
    connector = ConnectorUnderTest.from_image_name(connector_image, target_or_control)

    config = _load_json_file(config_path) if config_path else None
    state = _load_json_file(state_path) if state_path else None

    configured_catalog = None
    if catalog_path and catalog_path.exists():
        catalog_json = catalog_path.read_text()
        configured_catalog = ConfiguredAirbyteCatalog.parse_raw(catalog_json)

    execution_inputs = ExecutionInputs(
        connector_under_test=connector,
        command=command,
        output_dir=output_dir,
        config=config,
        configured_catalog=configured_catalog,
        state=state,
    )

    runner = ConnectorRunner(execution_inputs, proxy_url=proxy_url)
    result = runner.run()

    result.save_artifacts(output_dir)

    return {
        "connector": connector_image,
        "command": command.value,
        "success": result.success,
        "exit_code": result.exit_code,
        "stdout_file": str(result.stdout_file_path),
        "stderr_file": str(result.stderr_file_path),
        "message_counts": {
            k.value: v for k, v in result.get_message_count_per_type().items()
        },
        "record_counts_per_stream": result.get_record_count_per_stream(),
    }


@connector_app.command(name="live-test")
def live_test(
    connector_image: Annotated[
        str,
        Parameter(
            help="Full connector image name with tag (e.g., airbyte/source-github:1.0.0)."
        ),
    ],
    command: Annotated[
        Literal["spec", "check", "discover", "read"],
        Parameter(help="The Airbyte command to run."),
    ] = "check",
    connection_id: Annotated[
        str | None,
        Parameter(
            help="Airbyte Cloud connection ID to fetch config/catalog from. "
            "Mutually exclusive with config-path/catalog-path."
        ),
    ] = None,
    config_path: Annotated[
        str | None,
        Parameter(help="Path to the connector config JSON file."),
    ] = None,
    catalog_path: Annotated[
        str | None,
        Parameter(help="Path to the configured catalog JSON file (required for read)."),
    ] = None,
    state_path: Annotated[
        str | None,
        Parameter(help="Path to the state JSON file (optional for read)."),
    ] = None,
    output_dir: Annotated[
        str,
        Parameter(help="Directory to store test artifacts."),
    ] = "/tmp/live_test_artifacts",
) -> None:
    """Run live validation tests on a connector.

    This command runs the specified Airbyte protocol command against a connector
    and validates the output. Results are written to the output directory and
    to GitHub Actions outputs if running in CI.

    You can provide config/catalog either via file paths OR via a connection_id
    that fetches them from Airbyte Cloud.
    """
    output_path = Path(output_dir)
    output_path.mkdir(parents=True, exist_ok=True)

    cmd = Command(command)

    if not ensure_image_available(connector_image):
        print_error(f"Failed to pull connector image: {connector_image}")
        write_github_output("success", False)
        write_github_output("error", f"Failed to pull image: {connector_image}")
        return

    config_file: Path | None = None
    catalog_file: Path | None = None
    state_file = Path(state_path) if state_path else None

    if connection_id:
        if config_path or catalog_path:
            print_error(
                "Cannot specify both connection_id and config_path/catalog_path"
            )
            write_github_output("success", False)
            write_github_output(
                "error", "Cannot specify both connection_id and file paths"
            )
            return

        print_success(f"Fetching config/catalog from connection: {connection_id}")
        connection_data = fetch_connection_data(connection_id)
        config_file, catalog_file = save_connection_data_to_files(
            connection_data, output_path / "connection_data"
        )
        print_success(
            f"Fetched config for source: {connection_data.source_name} "
            f"with {len(connection_data.stream_names)} streams"
        )
    else:
        config_file = Path(config_path) if config_path else None
        catalog_file = Path(catalog_path) if catalog_path else None

    result = _run_connector_command(
        connector_image=connector_image,
        command=cmd,
        output_dir=output_path,
        target_or_control=TargetOrControl.TARGET,
        config_path=config_file,
        catalog_path=catalog_file,
        state_path=state_file,
    )

    print_json(result)

    write_github_outputs(
        {
            "success": result["success"],
            "connector": connector_image,
            "command": command,
            "exit_code": result["exit_code"],
        }
    )

    write_test_summary(
        connector_image=connector_image,
        test_type="live-test",
        success=result["success"],
        results={
            "command": command,
            "exit_code": result["exit_code"],
            "output_dir": output_dir,
        },
    )

    if result["success"]:
        print_success(f"Live test passed for {connector_image}")
    else:
        print_error(f"Live test failed for {connector_image}")


def _run_with_optional_http_metrics(
    connector_image: str,
    command: Command,
    output_dir: Path,
    target_or_control: TargetOrControl,
    enable_http_metrics: bool,
    config_path: Path | None,
    catalog_path: Path | None,
    state_path: Path | None,
) -> dict:
    """Run a connector command with optional HTTP metrics capture.

    When enable_http_metrics is True, starts mitmproxy to capture HTTP traffic.
    If mitmproxy fails to start, falls back to running without metrics.

    Args:
        connector_image: Full connector image name with tag.
        command: The Airbyte command to run.
        output_dir: Directory to store output files.
        target_or_control: Whether this is target or control version.
        enable_http_metrics: Whether to capture HTTP metrics via mitmproxy.
        config_path: Path to connector config JSON file.
        catalog_path: Path to configured catalog JSON file.
        state_path: Path to state JSON file.

    Returns:
        Dictionary with execution results, optionally including http_metrics.
    """
    if not enable_http_metrics:
        return _run_connector_command(
            connector_image=connector_image,
            command=command,
            output_dir=output_dir,
            target_or_control=target_or_control,
            config_path=config_path,
            catalog_path=catalog_path,
            state_path=state_path,
        )

    with MitmproxyManager.start(output_dir) as session:
        if session is None:
            print_error("Mitmproxy unavailable, running without HTTP metrics")
            return _run_connector_command(
                connector_image=connector_image,
                command=command,
                output_dir=output_dir,
                target_or_control=target_or_control,
                config_path=config_path,
                catalog_path=catalog_path,
                state_path=state_path,
            )

        print_success(f"Started mitmproxy on {session.proxy_url}")
        result = _run_connector_command(
            connector_image=connector_image,
            command=command,
            output_dir=output_dir,
            target_or_control=target_or_control,
            config_path=config_path,
            catalog_path=catalog_path,
            state_path=state_path,
            proxy_url=session.proxy_url,
        )

        http_metrics = parse_http_dump(session.dump_file_path)
        result["http_metrics"] = {
            "flow_count": http_metrics.flow_count,
            "duplicate_flow_count": http_metrics.duplicate_flow_count,
        }
        print_success(
            f"Captured {http_metrics.flow_count} HTTP flows "
            f"({http_metrics.duplicate_flow_count} duplicates)"
        )
        return result


@connector_app.command(name="regression-test")
def regression_test(
    target_image: Annotated[
        str,
        Parameter(
            help="Target connector image (new version) with tag (e.g., airbyte/source-github:2.0.0)."
        ),
    ],
    control_image: Annotated[
        str,
        Parameter(
            help="Control connector image (baseline version) with tag (e.g., airbyte/source-github:1.0.0)."
        ),
    ],
    command: Annotated[
        Literal["spec", "check", "discover", "read"],
        Parameter(help="The Airbyte command to run."),
    ] = "check",
    connection_id: Annotated[
        str | None,
        Parameter(
            help="Airbyte Cloud connection ID to fetch config/catalog from. "
            "Mutually exclusive with config-path/catalog-path."
        ),
    ] = None,
    config_path: Annotated[
        str | None,
        Parameter(help="Path to the connector config JSON file."),
    ] = None,
    catalog_path: Annotated[
        str | None,
        Parameter(help="Path to the configured catalog JSON file (required for read)."),
    ] = None,
    state_path: Annotated[
        str | None,
        Parameter(help="Path to the state JSON file (optional for read)."),
    ] = None,
    output_dir: Annotated[
        str,
        Parameter(help="Directory to store test artifacts."),
    ] = "/tmp/regression_test_artifacts",
    enable_http_metrics: Annotated[
        bool,
        Parameter(
            help="Capture HTTP traffic metrics via mitmproxy (experimental). "
            "Requires mitmdump to be installed."
        ),
    ] = False,
) -> None:
    """Run regression tests comparing two connector versions.

    This command runs the specified Airbyte protocol command against both the
    target (new) and control (baseline) connector versions, then compares the
    results. This helps identify regressions between versions.

    Results are written to the output directory and to GitHub Actions outputs
    if running in CI.

    You can provide config/catalog either via file paths OR via a connection_id
    that fetches them from Airbyte Cloud.
    """
    output_path = Path(output_dir)
    output_path.mkdir(parents=True, exist_ok=True)

    cmd = Command(command)

    for image in [target_image, control_image]:
        if not ensure_image_available(image):
            print_error(f"Failed to pull connector image: {image}")
            write_github_output("success", False)
            write_github_output("error", f"Failed to pull image: {image}")
            return

    config_file: Path | None = None
    catalog_file: Path | None = None
    state_file = Path(state_path) if state_path else None

    if connection_id:
        if config_path or catalog_path:
            print_error(
                "Cannot specify both connection_id and config_path/catalog_path"
            )
            write_github_output("success", False)
            write_github_output(
                "error", "Cannot specify both connection_id and file paths"
            )
            return

        print_success(f"Fetching config/catalog from connection: {connection_id}")
        connection_data = fetch_connection_data(connection_id)
        config_file, catalog_file = save_connection_data_to_files(
            connection_data, output_path / "connection_data"
        )
        print_success(
            f"Fetched config for source: {connection_data.source_name} "
            f"with {len(connection_data.stream_names)} streams"
        )
    else:
        config_file = Path(config_path) if config_path else None
        catalog_file = Path(catalog_path) if catalog_path else None

    target_output = output_path / "target"
    control_output = output_path / "control"

    target_result = _run_with_optional_http_metrics(
        connector_image=target_image,
        command=cmd,
        output_dir=target_output,
        target_or_control=TargetOrControl.TARGET,
        enable_http_metrics=enable_http_metrics,
        config_path=config_file,
        catalog_path=catalog_file,
        state_path=state_file,
    )

    control_result = _run_with_optional_http_metrics(
        connector_image=control_image,
        command=cmd,
        output_dir=control_output,
        target_or_control=TargetOrControl.CONTROL,
        enable_http_metrics=enable_http_metrics,
        config_path=config_file,
        catalog_path=catalog_file,
        state_path=state_file,
    )

    both_succeeded = target_result["success"] and control_result["success"]
    regression_detected = target_result["success"] != control_result["success"]

    combined_result = {
        "target": target_result,
        "control": control_result,
        "both_succeeded": both_succeeded,
        "regression_detected": regression_detected,
    }

    print_json(combined_result)

    write_github_outputs(
        {
            "success": both_succeeded and not regression_detected,
            "target_image": target_image,
            "control_image": control_image,
            "command": command,
            "target_exit_code": target_result["exit_code"],
            "control_exit_code": control_result["exit_code"],
            "regression_detected": regression_detected,
        }
    )

    write_json_output("regression_report", combined_result)

    report_path = generate_regression_report(
        target_image=target_image,
        control_image=control_image,
        command=command,
        target_result=target_result,
        control_result=control_result,
        output_dir=output_path,
    )
    print_success(f"Generated regression report: {report_path}")

    summary = get_report_summary(report_path)
    write_github_summary(summary)

    if regression_detected:
        print_error(f"Regression detected between {target_image} and {control_image}")
    elif both_succeeded:
        print_success(f"Regression test passed for {target_image} vs {control_image}")
    else:
        print_error(f"Both versions failed for {target_image} vs {control_image}")
