"""Adapters for connector version pinning workflows."""

from __future__ import annotations

import json
from collections.abc import Mapping
from dataclasses import asdict
from datetime import datetime

from airbyte import constants
from airbyte.exceptions import PyAirbyteInputError
from airbyte_ops_mcp.cloud_admin import api_client
from airbyte_ops_mcp.cloud_admin.payment_config import (
    PaymentConfigAPIError,
    get_organization_info,
)
from airbyte_ops_mcp.cloud_admin.registry_lookup import (
    _fetch_cloud_registry,
    resolve_canonical_name_to_definition_id,
    resolve_definition_id_to_registry_info,
)
from airbyte_ops_mcp.cloud_admin.version_overrides import (
    ResolvedCloudAuth,
    VersionOverrideTarget,
    set_version_override,
)
from airbyte_ops_mcp.prod_db_access.queries import (
    query_connector_rollouts,
    query_connector_rollouts_for_connector,
    query_connector_versions,
    query_destination_connection_stats,
    query_new_connector_releases,
    query_raw_pins_for_version,
    query_source_connection_stats,
    query_versions_with_pins_or_rollouts,
)
from airbyte_ops_mcp.tier_cache import resolve_workspace

from airbyte_ops_webapp.models import (
    ConnectorOption,
    ConnectorRelease,
    ConnectorRollout,
    ConnectorType,
    ConnectorVersion,
    ContextResolution,
    CurrentVersionState,
    OperationPreview,
    OperationResult,
    OverridePlan,
    ScopedConfiguration,
    ScopeType,
    VersionPinRow,
    build_version_override_payload,
    version_override_tool_name,
)

__all__ = [
    "OpsMcpAdapter",
    "configuration_rows",
    "operation_result_to_json",
    "preview_to_json",
]

SCOPE_PRIORITY: dict[ScopeType, int] = {
    "organization": 0,
    "workspace": 1,
    "actor": 2,
}
REQUIRED_APPROVAL_FIELDS: tuple[str, ...] = (
    "override_reason",
    "override_reason_reference_url",
    "customer_tier_filter",
)
SAFE_PREVIEW_WARNINGS: tuple[str, ...] = (
    "Preview only: no connector version override has been executed.",
    "TIER_0 and TIER_1 customers require human escalation before action.",
)
CONTEXT_RESOLUTION_MISS_STATUS_CODES: frozenset[int] = frozenset({400, 404, 422})

CLOUD_UI_BASE_URL = "https://cloud.airbyte.com"


def _fmt_date(value: str) -> str:
    """Format an ISO datetime string to `yyyy-mm-dd (ddd)`."""
    if not value:
        return ""
    try:
        parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
    except ValueError:
        return value
    return parsed.strftime("%Y-%m-%d (%a)")


def _cloud_scope_url(
    *,
    scope_type: str,
    scope_id: str,
    workspace_id: str = "",
    actor_type: str = "",
) -> str:
    """Build an Airbyte Cloud URL for viewing the target scope."""
    if scope_type == "workspace":
        return f"{CLOUD_UI_BASE_URL}/workspaces/{scope_id}"
    if scope_type == "organization":
        return f"{CLOUD_UI_BASE_URL}/organizations/{scope_id}/settings"
    if scope_type == "actor" and workspace_id:
        if actor_type in ("source", "destination"):
            return (
                f"{CLOUD_UI_BASE_URL}/workspaces/{workspace_id}/{actor_type}/{scope_id}"
            )
        return f"{CLOUD_UI_BASE_URL}/workspaces/{workspace_id}"
    return f"{CLOUD_UI_BASE_URL}/workspaces"


class OpsMcpAdapter:
    """Adapter backed by `airbyte-internal-ops` APIs."""

    mode = "real"

    def __init__(
        self,
        *,
        bearer_token: str | None = None,
        client_id: str | None = None,
        client_secret: str | None = None,
        config_api_root: str = constants.CLOUD_CONFIG_API_ROOT,
    ) -> None:
        self.bearer_token = bearer_token
        self.client_id = client_id
        self.client_secret = client_secret
        self.config_api_root = config_api_root

    def search_connectors(self, query: str) -> tuple[ConnectorOption, ...]:
        """Resolve a connector name or definition ID into connector options."""
        normalized_query = query.strip()
        if not normalized_query:
            return self.list_connectors()

        connectors: list[ConnectorOption] = []
        connector_by_id = self._connector_from_definition_id(normalized_query)
        if connector_by_id:
            connectors.append(connector_by_id)

        connector_by_name = self._connector_from_name(normalized_query)
        if connector_by_name and connector_by_name not in connectors:
            connectors.append(connector_by_name)

        if connectors:
            return tuple(connectors)

        return self.list_connectors(normalized_query)

    def list_connectors(self, query: str = "") -> tuple[ConnectorOption, ...]:
        """List Cloud registry connectors, optionally filtered by `query`."""
        if self._is_local_config_api:
            return self._list_local_connectors(query)
        data = _fetch_cloud_registry()
        connectors = [
            *(
                self._connector_from_registry_entry(source, "source")
                for source in data.get("sources", [])
                if isinstance(source, Mapping)
            ),
            *(
                self._connector_from_registry_entry(destination, "destination")
                for destination in data.get("destinations", [])
                if isinstance(destination, Mapping)
            ),
        ]
        normalized_query = query.strip().lower()
        if not normalized_query:
            return tuple(sorted(connectors, key=lambda connector: connector.name))
        return tuple(
            connector
            for connector in sorted(connectors, key=lambda connector: connector.name)
            if normalized_query in connector.name.lower()
            or normalized_query in connector.id.lower()
            or normalized_query in connector.docker_repository.lower()
        )

    def get_connector(self, connector_id: str) -> ConnectorOption:
        """Return a connector by definition ID."""
        if self._is_local_config_api:
            for connector in self.list_connectors():
                if connector.id == connector_id:
                    return connector
        connector = self._connector_from_definition_id(connector_id)
        if connector:
            return connector
        raise ValueError(f"Unknown connector ID: {connector_id}")

    def list_versions(self, connector_id: str) -> tuple[ConnectorVersion, ...]:
        """List published versions for a connector definition."""
        if self._is_local_config_api:
            connector = self.get_connector(connector_id)
            return (
                ConnectorVersion(
                    version_id=api_client.resolve_connector_version_id(
                        actor_definition_id=connector.id,
                        connector_type=connector.connector_type,
                        version=connector.latest_version,
                        config_api_root=self.config_api_root,
                        client_id=self.client_id,
                        client_secret=self.client_secret,
                        bearer_token=self.bearer_token,
                    ),
                    docker_image_tag=connector.latest_version,
                    docker_repository=connector.docker_repository,
                    release_stage="",
                    support_level="",
                    cdk_version="",
                    language="",
                    last_published="",
                ),
            )
        return tuple(
            self._version_from_row(row)
            for row in query_connector_versions(connector_definition_id=connector_id)
        )

    def list_recent_releases(
        self,
        *,
        days: int = 30,
        limit: int | None = None,
    ) -> tuple[ConnectorRelease, ...]:
        """List recent published releases across connectors."""
        release_rows = query_new_connector_releases(days=days, limit=limit)
        return tuple(self._release_from_row(row) for row in release_rows)

    def list_active_rollouts(
        self,
        connector_id: str,
    ) -> tuple[ConnectorRollout, ...]:
        """List active progressive rollouts for a connector definition."""
        rollout_rows = query_connector_rollouts_for_connector(
            actor_definition_id=connector_id,
            active_only=True,
        )
        return tuple(self._rollout_from_row(row) for row in rollout_rows)

    def list_progressive_rollouts(
        self,
        *,
        limit: int | None = None,
    ) -> tuple[ConnectorRollout, ...]:
        """List active progressive rollouts across connector definitions."""
        rollout_rows = query_connector_rollouts(active_only=True, limit=limit)
        return tuple(self._rollout_from_row(row) for row in rollout_rows)

    def list_version_pins(
        self,
        version_id: str,
        *,
        limit: int = 25,
        offset: int = 0,
    ) -> tuple[list[VersionPinRow], int]:
        """Return pins for a specific connector version from prod DB.

        Returns a tuple of (pin_rows, total_count). Uses raw
        `scoped_configuration` entries so the total matches the Pinned
        Versions tab's pin_count aggregation.
        """
        raw_rows = query_raw_pins_for_version(version_id)
        total = len(raw_rows)
        page = raw_rows[offset : offset + limit]
        pins = [self._pin_row_from_db(row) for row in page]
        return pins, total

    def get_connection_health_summary(
        self,
        connector_id: str,
        connector_type: str,
        version_tag: str = "",
    ) -> str:
        """Build a one-line connection health summary for a connector.

        Queries prod DB for connection stats grouped by pinned version,
        then formats a human-readable summary string.
        """
        if connector_type == "source":
            rows = query_source_connection_stats(connector_id)
        else:
            rows = query_destination_connection_stats(connector_id)

        if not rows:
            return ""

        total = 0
        active = 0
        succeeded = 0
        failed = 0
        running = 0
        pinned = 0
        version_active = 0
        version_succeeded = 0
        version_failed = 0

        for row in rows:
            row_total = int(row.get("total_connections", 0))
            row_active = int(row.get("active_connections", 0))
            row_succeeded = int(row.get("latest_succeeded", 0))
            row_failed = int(row.get("latest_failed", 0))
            row_running = int(row.get("latest_running", 0))
            row_pinned_version = row.get("docker_image_tag", "")

            total += row_total
            active += row_active
            succeeded += row_succeeded
            failed += row_failed
            running += row_running
            if row.get("pinned_version_id"):
                pinned += row_total

            if version_tag and row_pinned_version == version_tag:
                version_active = row_active
                version_succeeded = row_succeeded
                version_failed = row_failed

        parts = [f"{active} active ({total} total) connections"]
        if succeeded or failed or running:
            status_parts = []
            if succeeded:
                status_parts.append(f"{succeeded} succeeded")
            if failed:
                status_parts.append(f"{failed} failed")
            if running:
                status_parts.append(f"{running} running")
            parts[0] += f" ({', '.join(status_parts)})"

        if pinned:
            parts.append(f"{pinned} pinned")

        summary = " | ".join(parts)

        if version_tag and (version_active or version_succeeded or version_failed):
            v_parts = [f"{version_active} active"]
            if version_succeeded:
                v_parts.append(f"{version_succeeded} ok")
            if version_failed:
                v_parts.append(f"{version_failed} failing")
            summary += f" | v{version_tag}: {', '.join(v_parts)}"

        return summary

    def list_versions_with_pins_or_rollouts(
        self,
        connector_id: str | None = None,
    ) -> list[dict[str, object]]:
        """Return connector versions that have at least one pin or an active rollout.

        Each row includes `version_id`, `connector_definition_id`, `connector_name`,
        `docker_repository`, `docker_image_tag`, `last_published`, `pin_count`,
        `rollout_state`, and `rollout_id`.
        """
        return query_versions_with_pins_or_rollouts(
            actor_definition_id=connector_id,
        )

    @staticmethod
    def _pin_row_from_db(row: dict[str, object]) -> VersionPinRow:
        """Map a raw `scoped_configuration` row to a `VersionPinRow`."""
        scope_type = str(row.get("pin_scope_type", "actor"))
        scope_id = str(row.get("scope_id", ""))
        origin_email = row.get("pinned_by_user_email") or row.get("pinned_by_user_name")
        origin_name = (
            str(origin_email) if origin_email else str(row.get("pinned_by_user_id", ""))
        )
        return VersionPinRow(
            scope_type=scope_type,
            scope_id=scope_id,
            scope_url=_cloud_scope_url(
                scope_type=scope_type,
                scope_id=scope_id,
                workspace_id="",
                actor_type="",
            ),
            origin_type=str(row.get("origin_type", "")),
            origin_name=origin_name,
            description=str(row.get("description", "")),
            created_at=str(row.get("created_at", "")),
            created_at_display=_fmt_date(str(row.get("created_at", ""))),
            expires_at=str(row.get("expires_at", "") or ""),
            expires_at_display=_fmt_date(str(row.get("expires_at", "") or "")),
            reference_url=str(row.get("reference_url", "") or ""),
        )

    def get_current_context(
        self,
        *,
        connector_id: str,
        scope_type: ScopeType,
        scope_id: str,
        workspace_id: str | None = None,
    ) -> CurrentVersionState:
        """Return current version context for a selected scope."""
        connector = self.get_connector(connector_id)
        latest_version = connector.latest_version
        if scope_type in ("workspace", "organization"):
            scoped_configs = self._scope_context(connector, scope_type, scope_id)
            active_config = self._active_config(scoped_configs, scope_type, scope_id)
            return CurrentVersionState(
                connector_id=connector.id,
                connector_name=connector.name,
                connector_type=connector.connector_type,
                latest_version=latest_version,
                active_version=active_config.value_name
                if active_config
                else latest_version,
                is_version_pinned=active_config is not None,
                active_scope=active_config.scope_type if active_config else None,
                active_scope_id=active_config.scope_id if active_config else None,
                ancestor_configurations=tuple(
                    config
                    for config in scoped_configs
                    if SCOPE_PRIORITY[config.scope_type] < SCOPE_PRIORITY[scope_type]
                ),
                descendant_configurations=tuple(
                    config
                    for config in scoped_configs
                    if SCOPE_PRIORITY[config.scope_type] > SCOPE_PRIORITY[scope_type]
                ),
            )
        if scope_type != "actor":
            return CurrentVersionState(
                connector_id=connector.id,
                connector_name=connector.name,
                connector_type=connector.connector_type,
                latest_version=latest_version,
                active_version=latest_version,
                is_version_pinned=False,
                active_scope=None,
                active_scope_id=None,
                ancestor_configurations=(),
                descendant_configurations=(),
            )

        version_data = api_client.get_connector_version(
            connector_id=scope_id,
            connector_type=connector.connector_type,
            config_api_root=self.config_api_root,
            client_id=self.client_id,
            client_secret=self.client_secret,
            bearer_token=self.bearer_token,
            workspace_id=workspace_id,
        )
        active_version = (
            self._string_field(version_data, "dockerImageTag") or latest_version
        )
        scoped_configs = self._scoped_configurations(
            connector=connector,
            scoped_configs=version_data.get("scopedConfigs"),
        )
        active_config = self._active_config(scoped_configs, scope_type, scope_id)
        return CurrentVersionState(
            connector_id=connector.id,
            connector_name=connector.name,
            connector_type=connector.connector_type,
            latest_version=latest_version,
            active_version=active_version,
            is_version_pinned=active_config is not None
            or bool(version_data.get("isVersionOverrideApplied")),
            active_scope=active_config.scope_type if active_config else None,
            active_scope_id=active_config.scope_id if active_config else None,
            ancestor_configurations=tuple(
                config
                for config in scoped_configs
                if SCOPE_PRIORITY[config.scope_type] < SCOPE_PRIORITY[scope_type]
            ),
            descendant_configurations=tuple(
                config
                for config in scoped_configs
                if SCOPE_PRIORITY[config.scope_type] > SCOPE_PRIORITY[scope_type]
            ),
        )

    def summary_by_connector(self) -> tuple[dict[str, str | int], ...]:
        """Return override summary rows when available."""
        return ()

    def configuration_rows(self) -> tuple[dict[str, str], ...]:
        """Return scoped override rows when available."""
        return ()

    def list_instance_admin_users(self) -> tuple[dict[str, str], ...]:
        """Return Config API instance-admin users for interactive apply identity."""
        return api_client.list_instance_admin_users(
            config_api_root=self.config_api_root,
            client_id=self.client_id,
            client_secret=self.client_secret,
            bearer_token=self.bearer_token,
        )

    def stage_override(self, plan: OverridePlan) -> OperationPreview:
        """Build a non-mutating preview for the matching Ops MCP tool."""
        return OperationPreview(
            tool_name=version_override_tool_name(plan.scope_type),
            mutating=False,
            mode=self.mode,
            payload=build_version_override_payload(plan),
            required_approval_fields=REQUIRED_APPROVAL_FIELDS,
            warnings=SAFE_PREVIEW_WARNINGS,
        )

    def apply_override(self, plan: OverridePlan) -> OperationResult:
        """Apply the matching version override operation."""
        auth = ResolvedCloudAuth(
            bearer_token=self.bearer_token,
            client_id=self.client_id,
            client_secret=self.client_secret,
        )
        payload = build_version_override_payload(plan)
        target = self._target_from_plan(plan)
        result = set_version_override(
            auth=auth,
            target=target,
            approval_comment_url=payload.approval_comment_url,
            version=payload.version,
            unset=payload.unset,
            override_reason=payload.override_reason,
            override_reason_reference_url=payload.override_reason_reference_url,
            issue_url=payload.override_reason_reference_url,
            ai_agent_session_url=payload.ai_agent_session_url,
            customer_tier_filter=payload.customer_tier_filter,
            force=payload.force,
            config_api_root=self.config_api_root,
            user_email=plan.user_email,
        )

        return OperationResult(
            tool_name=version_override_tool_name(plan.scope_type),
            success=result.success,
            mutating=True,
            mode=self.mode,
            message=result.message,
            payload=payload,
        )

    @property
    def _is_local_config_api(self) -> bool:
        return self.config_api_root.startswith("http://localhost:")

    def _list_local_connectors(self, query: str = "") -> tuple[ConnectorOption, ...]:
        connectors = [
            *self._local_definition_options("source"),
            *self._local_definition_options("destination"),
        ]
        normalized_query = query.strip().lower()
        if not normalized_query:
            return tuple(sorted(connectors, key=lambda connector: connector.name))
        return tuple(
            connector
            for connector in sorted(connectors, key=lambda connector: connector.name)
            if normalized_query in connector.name.lower()
            or normalized_query in connector.id.lower()
            or normalized_query in connector.docker_repository.lower()
        )

    def _local_definition_options(
        self,
        connector_type: ConnectorType,
    ) -> tuple[ConnectorOption, ...]:
        endpoint_name = (
            "source_definitions/list_latest"
            if connector_type == "source"
            else "destination_definitions/list_latest"
        )
        access_token = api_client._get_access_token(
            client_id=self.client_id,
            client_secret=self.client_secret,
            bearer_token=self.bearer_token,
            config_api_root=self.config_api_root,
        )
        response = api_client.requests.post(
            f"{self.config_api_root}/{endpoint_name}",
            json={},
            headers={
                "Authorization": f"Bearer {access_token}",
                "User-Agent": api_client.ops_constants.USER_AGENT,
                "Content-Type": "application/json",
            },
            timeout=30,
        )
        if response.status_code != 200:
            raise PyAirbyteInputError(
                message=(
                    f"Failed to list local {connector_type} definitions: "
                    f"{response.status_code} {response.text}"
                ),
                context={"endpoint": f"{self.config_api_root}/{endpoint_name}"},
            )
        key = (
            "sourceDefinitions"
            if connector_type == "source"
            else "destinationDefinitions"
        )
        return tuple(
            self._connector_from_registry_entry(definition, connector_type)
            for definition in response.json().get(key, [])
            if isinstance(definition, Mapping)
        )

    def _scope_context(
        self,
        connector: ConnectorOption,
        scope_type: ScopeType,
        scope_id: str,
    ) -> tuple[ScopedConfiguration, ...]:
        access_token = api_client._get_access_token(
            client_id=self.client_id,
            client_secret=self.client_secret,
            bearer_token=self.bearer_token,
            config_api_root=self.config_api_root,
        )
        active_config = api_client._get_scoped_configuration_context(
            actor_definition_id=connector.id,
            scope_type=api_client._ScopeType(scope_type),
            scope_id=scope_id,
            config_api_root=self.config_api_root,
            access_token=access_token,
        )
        if not active_config:
            return ()
        return self._scoped_configurations(
            connector=connector,
            scoped_configs={scope_type: active_config},
        )

    def _target_from_plan(self, plan: OverridePlan) -> VersionOverrideTarget:
        if plan.scope_type == "actor":
            return VersionOverrideTarget(
                scope="actor",
                organization_id=plan.organization_id,
                connector_type=plan.connector_type,
                workspace_id=plan.workspace_id,
                actor_id=plan.actor_id,
            )
        if plan.scope_type == "workspace":
            return VersionOverrideTarget(
                scope="workspace",
                organization_id=plan.organization_id,
                connector_type=plan.connector_type,
                workspace_id=plan.workspace_id,
                connector_name=plan.connector_name,
            )
        return VersionOverrideTarget(
            scope="organization",
            organization_id=plan.organization_id,
            connector_type=plan.connector_type,
            connector_name=plan.connector_name,
        )

    def resolve_organization_id(self, scope_type: ScopeType, scope_id: str) -> str:
        """Return the organization ID for the selected target scope."""
        if scope_type == "organization":
            return scope_id
        if scope_type == "actor":
            return ""
        return resolve_workspace(scope_id).organization_id or ""

    def resolve_context_guid(
        self,
        *,
        connector: ConnectorOption,
        context_guid: str,
    ) -> ContextResolution:
        """Resolve a context GUID into organization, workspace, or actor scope."""
        normalized = context_guid.strip()
        if not normalized:
            raise PyAirbyteInputError(message="Context GUID is required.")

        actor_resolution = self._resolve_actor_context(connector, normalized)
        if actor_resolution:
            return actor_resolution

        workspace_resolution = self._resolve_workspace_context(normalized)
        if workspace_resolution:
            return workspace_resolution

        organization_resolution = self._resolve_organization_context(normalized)
        if organization_resolution:
            return organization_resolution

        raise PyAirbyteInputError(message="Context GUID could not be resolved.")

    @staticmethod
    def _version_from_row(row: Mapping[str, object]) -> ConnectorVersion:
        return ConnectorVersion(
            version_id=OpsMcpAdapter._string_field(row, "version_id"),
            docker_image_tag=OpsMcpAdapter._string_field(row, "docker_image_tag"),
            docker_repository=OpsMcpAdapter._string_field(row, "docker_repository"),
            release_stage=OpsMcpAdapter._string_field(row, "release_stage"),
            support_level=OpsMcpAdapter._string_field(row, "support_level"),
            cdk_version=OpsMcpAdapter._string_field(row, "cdk_version"),
            language=OpsMcpAdapter._string_field(row, "language"),
            last_published=OpsMcpAdapter._string_field(row, "last_published"),
        )

    @staticmethod
    def _connector_name_from_repository(docker_repository: str) -> str:
        return docker_repository.rsplit("/", maxsplit=1)[-1]

    @staticmethod
    def _connector_type_from_repository(docker_repository: str) -> ConnectorType:
        connector_name = OpsMcpAdapter._connector_name_from_repository(
            docker_repository,
        )
        return "destination" if connector_name.startswith("destination-") else "source"

    @staticmethod
    def _release_from_row(row: Mapping[str, object]) -> ConnectorRelease:
        docker_repository = OpsMcpAdapter._string_field(row, "docker_repository")
        return ConnectorRelease(
            version_id=OpsMcpAdapter._string_field(row, "version_id"),
            connector_id=OpsMcpAdapter._string_field(row, "actor_definition_id"),
            connector_name=OpsMcpAdapter._connector_name_from_repository(
                docker_repository,
            ),
            connector_type=OpsMcpAdapter._connector_type_from_repository(
                docker_repository,
            ),
            docker_image_tag=OpsMcpAdapter._string_field(row, "docker_image_tag"),
            docker_repository=docker_repository,
            release_stage=OpsMcpAdapter._string_field(row, "release_stage"),
            last_published=OpsMcpAdapter._string_field(row, "last_published"),
        )

    @staticmethod
    def _rollout_from_row(row: Mapping[str, object]) -> ConnectorRollout:
        docker_repository = OpsMcpAdapter._string_field(row, "rc_docker_repository")
        rc_pin_count_raw = row.get("rc_pin_count", 0)
        return ConnectorRollout(
            rollout_id=OpsMcpAdapter._string_field(row, "rollout_id"),
            connector_id=OpsMcpAdapter._string_field(row, "actor_definition_id"),
            connector_name=OpsMcpAdapter._connector_name_from_repository(
                docker_repository,
            ),
            connector_type=OpsMcpAdapter._connector_type_from_repository(
                docker_repository,
            ),
            docker_repository=docker_repository,
            state=OpsMcpAdapter._string_field(row, "state"),
            rc_docker_image_tag=OpsMcpAdapter._string_field(
                row,
                "rc_docker_image_tag",
            ),
            initial_docker_image_tag=OpsMcpAdapter._string_field(
                row,
                "initial_docker_image_tag",
            ),
            current_target_rollout_pct=OpsMcpAdapter._string_field(
                row,
                "current_target_rollout_pct",
            ),
            final_target_rollout_pct=OpsMcpAdapter._string_field(
                row,
                "final_target_rollout_pct",
            ),
            created_at=OpsMcpAdapter._string_field(row, "created_at"),
            updated_at=OpsMcpAdapter._string_field(row, "updated_at"),
            rollout_strategy=OpsMcpAdapter._string_field(row, "rollout_strategy"),
            rc_pin_count=int(rc_pin_count_raw) if rc_pin_count_raw else 0,
        )

    @staticmethod
    def _scoped_configurations(
        *,
        connector: ConnectorOption,
        scoped_configs: object,
    ) -> tuple[ScopedConfiguration, ...]:
        if not isinstance(scoped_configs, Mapping):
            return ()

        configs: list[ScopedConfiguration] = []
        for scope_type, config in scoped_configs.items():
            if scope_type in SCOPE_PRIORITY and isinstance(config, Mapping):
                configs.append(
                    ScopedConfiguration(
                        id=OpsMcpAdapter._string_field(config, "id"),
                        connector_id=connector.id,
                        connector_name=connector.name,
                        connector_type=connector.connector_type,
                        scope_type=scope_type,
                        scope_id=OpsMcpAdapter._string_field(config, "scope_id"),
                        scope_name=OpsMcpAdapter._string_field(config, "scope_name"),
                        value_name=OpsMcpAdapter._string_field(config, "value_name"),
                        description=OpsMcpAdapter._string_field(config, "description"),
                        origin_type=OpsMcpAdapter._string_field(config, "origin_type"),
                        origin_name=OpsMcpAdapter._string_field(config, "origin_name"),
                        expires_at=OpsMcpAdapter._string_field(config, "expires_at"),
                        reference_url=OpsMcpAdapter._string_field(
                            config, "reference_url"
                        ),
                    )
                )
        return tuple(configs)

    @staticmethod
    def _active_config(
        configurations: tuple[ScopedConfiguration, ...],
        scope_type: ScopeType,
        scope_id: str,
    ) -> ScopedConfiguration | None:
        scoped = tuple(
            config
            for config in configurations
            if config.scope_type == scope_type and config.scope_id == scope_id
        )
        if scoped:
            return scoped[0]
        inherited = tuple(
            config
            for config in configurations
            if SCOPE_PRIORITY[config.scope_type] < SCOPE_PRIORITY[scope_type]
        )
        if not inherited:
            return None
        return max(inherited, key=lambda config: SCOPE_PRIORITY[config.scope_type])

    @staticmethod
    def _string_field(row: Mapping[str, object], field_name: str) -> str:
        value = row.get(field_name)
        if value is None:
            return ""
        return str(value)

    @staticmethod
    def _connector_from_registry_entry(
        entry: Mapping[str, object],
        connector_type: ConnectorType,
    ) -> ConnectorOption:
        definition_id_field = (
            "sourceDefinitionId"
            if connector_type == "source"
            else "destinationDefinitionId"
        )
        raw_name = OpsMcpAdapter._string_field(entry, "name")
        docker_repository = OpsMcpAdapter._string_field(entry, "dockerRepository")
        canonical_name = docker_repository.rsplit("/", maxsplit=1)[-1] or raw_name
        return ConnectorOption(
            id=OpsMcpAdapter._string_field(entry, definition_id_field),
            name=canonical_name,
            connector_type=connector_type,
            latest_version=OpsMcpAdapter._string_field(entry, "dockerImageTag"),
            docker_repository=docker_repository,
        )

    def _resolve_actor_context(
        self,
        connector: ConnectorOption,
        context_guid: str,
    ) -> ContextResolution | None:
        access_token = api_client._get_access_token(
            client_id=self.client_id,
            client_secret=self.client_secret,
            bearer_token=self.bearer_token,
            config_api_root=self.config_api_root,
        )
        response = api_client.requests.post(
            f"{self.config_api_root}/{connector.connector_type}s/get",
            json={f"{connector.connector_type}Id": context_guid},
            headers={
                "Authorization": f"Bearer {access_token}",
                "User-Agent": api_client.ops_constants.USER_AGENT,
                "Content-Type": "application/json",
            },
            timeout=30,
        )
        if response.status_code in CONTEXT_RESOLUTION_MISS_STATUS_CODES:
            return None
        if response.status_code != 200:
            raise PyAirbyteInputError(
                message=f"Failed to resolve actor context: {response.status_code} {response.text}",
            )
        actor_info = response.json()
        definition_id = self._string_field(
            actor_info,
            f"{connector.connector_type}DefinitionId",
        )
        if definition_id and definition_id != connector.id:
            return None
        actor_name = self._string_field(actor_info, "name")
        workspace_id = self._string_field(actor_info, "workspaceId")
        workspace_resolution = self._resolve_workspace_context(workspace_id)
        if not workspace_resolution:
            raise PyAirbyteInputError(message="Actor workspace could not be resolved.")
        return ContextResolution(
            scope_type="actor",
            scope_id=context_guid,
            organization_id=workspace_resolution.organization_id,
            scope_name=actor_name,
            workspace_id=workspace_id,
            actor_id=context_guid,
        )

    def _resolve_workspace_context(
        self,
        context_guid: str,
    ) -> ContextResolution | None:
        if not context_guid:
            return None
        access_token = api_client._get_access_token(
            client_id=self.client_id,
            client_secret=self.client_secret,
            bearer_token=self.bearer_token,
            config_api_root=self.config_api_root,
        )
        response = api_client.requests.post(
            f"{self.config_api_root}/workspaces/get",
            json={"workspaceId": context_guid},
            headers={
                "Authorization": f"Bearer {access_token}",
                "User-Agent": api_client.ops_constants.USER_AGENT,
                "Content-Type": "application/json",
            },
            timeout=30,
        )
        if response.status_code in CONTEXT_RESOLUTION_MISS_STATUS_CODES:
            return None
        if response.status_code != 200:
            raise PyAirbyteInputError(
                message=f"Failed to resolve workspace context: {response.status_code} {response.text}",
            )
        ws_data = response.json()
        organization_id = self._string_field(ws_data, "organizationId")
        workspace_name = self._string_field(ws_data, "name")
        if not organization_id:
            raise PyAirbyteInputError(
                message="Workspace organization could not be resolved."
            )
        return ContextResolution(
            scope_type="workspace",
            scope_id=context_guid,
            organization_id=organization_id,
            scope_name=workspace_name,
            workspace_id=context_guid,
        )

    def _resolve_organization_context(
        self,
        context_guid: str,
    ) -> ContextResolution | None:
        try:
            organization = get_organization_info(
                organization_id=context_guid,
                config_api_root=self.config_api_root,
                client_id=self.client_id,
                client_secret=self.client_secret,
                bearer_token=self.bearer_token,
            )
        except PaymentConfigAPIError as error:
            raise PyAirbyteInputError(
                message=f"Failed to resolve organization context: {error}",
            ) from error
        if not organization:
            return None
        return ContextResolution(
            scope_type="organization",
            scope_id=context_guid,
            organization_id=context_guid,
            scope_name=organization.organization_name,
        )

    def _connector_from_definition_id(
        self,
        actor_definition_id: str,
    ) -> ConnectorOption | None:
        try:
            (
                connector_name,
                connector_type,
                latest_version,
                docker_repository,
            ) = resolve_definition_id_to_registry_info(actor_definition_id)
        except PyAirbyteInputError:
            return None
        typed_connector_type: ConnectorType = (
            "destination" if connector_type == "destination" else "source"
        )
        return ConnectorOption(
            id=actor_definition_id,
            name=connector_name,
            connector_type=typed_connector_type,
            latest_version=latest_version,
            docker_repository=docker_repository,
        )

    def _connector_from_name(self, connector_name: str) -> ConnectorOption | None:
        try:
            actor_definition_id = resolve_canonical_name_to_definition_id(
                connector_name
            )
        except PyAirbyteInputError:
            return None
        return self._connector_from_definition_id(actor_definition_id)


def preview_to_json(preview: OperationPreview) -> str:
    """Serialize an operation preview for display."""
    return json.dumps(
        {
            "tool_name": preview.tool_name,
            "mutating": preview.mutating,
            "payload": preview.payload.model_dump(mode="json"),
            "required_approval_fields": preview.required_approval_fields,
            "warnings": preview.warnings,
        },
        indent=2,
        sort_keys=True,
    )


def operation_result_to_json(result: OperationResult) -> str:
    """Serialize an operation result for display."""
    return json.dumps(
        {
            "tool_name": result.tool_name,
            "success": result.success,
            "mutating": result.mutating,
            "mode": result.mode,
            "message": result.message,
            "payload": result.payload.model_dump(mode="json"),
        },
        indent=2,
        sort_keys=True,
    )


def configuration_rows(
    configurations: tuple[ScopedConfiguration, ...],
) -> tuple[dict[str, str], ...]:
    """Return override rows for display tables."""
    return tuple(asdict(config) for config in configurations)
