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

Commands:
    airbyte-ops devin secret-request - Deliver a secret after Slack approval
    airbyte-ops devin secret-list    - List available secrets in the vault

## 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/devin.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 logging
import os
import subprocess
from typing import Annotated

import requests
from cyclopts import Parameter

from airbyte_ops_mcp.cli._base import App, app
from airbyte_ops_mcp.cli._shared import exit_with_error, print_json, print_success
from airbyte_ops_mcp.devin_api import (
    extract_session_id,
    send_session_message,
)
from airbyte_ops_mcp.onepassword import (
    DEFAULT_SHARE_EXPIRY,
    create_share_link,
    list_vault_items,
)
from airbyte_ops_mcp.slack_api import (
    SlackAPIError,
    SlackApprovalRecordError,
    SlackURLParseError,
    validate_slack_approval_record,
)

logger = logging.getLogger(__name__)

# ---------------------------------------------------------------------------
# Sub-app registration
# ---------------------------------------------------------------------------

devin_app = App(name="devin", help="Devin session operations.")
app.command(devin_app)


# ---------------------------------------------------------------------------
# CLI commands
# ---------------------------------------------------------------------------


@devin_app.command(name="secret-list")
def secret_list() -> None:
    """List available secrets in the 1Password vault.

    Lists all item titles in the 'devin-on-demand-secrets' vault and
    outputs them as compact single-line JSON. The MCP tool reads this
    output from the workflow job logs to return the list to the caller.

    NOTE: We intentionally use `print(json.dumps(...))` instead of
    `print_json()` (Rich) so the output stays on one log line, which
    the MCP log parser can reliably extract.
    """
    titles = list_vault_items()
    print(json.dumps({"available_secrets": titles, "available_count": len(titles)}))


@devin_app.command(name="secret-request")
def secret_request(
    session: Annotated[
        str,
        Parameter(
            help="Devin session ID or URL "
            "(e.g. 'abc123...' or 'https://app.devin.ai/sessions/abc123...').",
        ),
    ],
    secret_alias: Annotated[
        str,
        Parameter(
            help="Secret name matching an item title in the "
            "'devin-on-demand-secrets' 1Password vault.",
        ),
    ],
    approval_evidence_url: Annotated[
        str,
        Parameter(
            help="Slack approval record URL. Validates the approval "
            "and delivers the secret to the Devin session.",
        ),
    ],
    expected_request_id: Annotated[
        str | None,
        Parameter(
            help="Request ID from Phase 1. When provided, the "
            "approval record is validated against this ID for replay "
            "protection.",
        ),
    ] = None,
) -> None:
    """Deliver a Devin secret after Slack approval.

    Validates the approval record, creates a 1Password share link,
    and sends it to the Devin session.

    NOTE: The approval *request* (Phase 1) is handled entirely by the
    GitHub Actions workflow (bash + reusable HITL workflow), not by
    this CLI command.
    """
    try:
        session_id = extract_session_id(session)
    except ValueError as exc:
        exit_with_error(str(exc))
        return  # unreachable, but keeps the type checker happy

    _handle_secret_delivery(
        session_id=session_id,
        secret_alias=secret_alias,
        approval_evidence_url=approval_evidence_url,
        expected_request_id=expected_request_id,
    )


# ---------------------------------------------------------------------------
# Phase handlers
# ---------------------------------------------------------------------------


def _handle_secret_delivery(
    session_id: str,
    secret_alias: str,
    approval_evidence_url: str,
    *,
    expected_request_id: str | None = None,
) -> None:
    """Phase 2: Validate approval, create share link, deliver to session."""
    # Step 1: Validate the Slack approval record
    logger.info("Validating approval evidence: %s", approval_evidence_url)
    try:
        record = validate_slack_approval_record(
            approval_evidence_url,
            expected_secret_alias=secret_alias,
            expected_request_id=expected_request_id,
        )
    except (SlackURLParseError, SlackAPIError, SlackApprovalRecordError) as exc:
        exit_with_error(str(exc))
        return  # unreachable

    approver = record.user_name or record.user_id
    logger.info("Approval validated: approved by %s", approver)

    # Step 2: Create 1Password share link
    logger.info("Creating 1Password share link for '%s'...", secret_alias)
    try:
        share_link = create_share_link(secret_alias)
    except (RuntimeError, subprocess.TimeoutExpired) as exc:
        _handle_share_link_failure(
            secret_alias=secret_alias, session_id=session_id, error=exc
        )
        return  # unreachable

    # Mask the share link in GitHub Actions logs
    if os.environ.get("GITHUB_ACTIONS"):
        print(f"::add-mask::{share_link}")

    logger.info("Share link created (expires in %s)", DEFAULT_SHARE_EXPIRY)

    # Step 3: Send to Devin session
    logger.info("Sending share link to session %s...", session_id[:8])
    message = (
        f"Your requested secret `{secret_alias}` is ready.\n\n"
        f"Open this link in your browser to view and copy the secret values:\n"
        f"{share_link}\n\n"
        f"This link expires in {DEFAULT_SHARE_EXPIRY}. "
        f"Retrieve and store the value immediately in your secure local storage."
    )
    try:
        send_session_message(session_id, message)
    except (requests.RequestException, RuntimeError) as exc:
        exit_with_error(f"Failed to send message to session: {exc}")
        return  # unreachable

    print_success("Secret delivered successfully!")
    print_json(
        {
            "phase": "delivered",
            "secret_alias": secret_alias,
            "session_id": session_id,
            "approver": approver,
        }
    )


def _handle_share_link_failure(
    secret_alias: str,
    session_id: str,
    error: Exception,
) -> None:
    """Handle a failed share-link creation by listing available secrets.

    Attempts to list all available secret names in the vault so the agent
    (and approver) can see valid options. This helps recover from typos
    or incorrect secret names.
    """
    logger.warning("Share link creation failed for '%s': %s", secret_alias, error)

    available_hint = ""
    try:
        titles = list_vault_items()
        if titles:
            formatted = ", ".join(f"`{t}`" for t in titles)
            available_hint = f" Available secrets: {formatted}"
    except (RuntimeError, subprocess.TimeoutExpired, OSError):
        logger.warning("Could not list vault items to suggest alternatives.")

    error_message = (
        f"Failed to create share link for `{secret_alias}`: {error}{available_hint}"
    )
    # Best-effort: notify the session so the agent can self-correct
    try:
        send_session_message(session_id, error_message)
    except (requests.RequestException, RuntimeError):
        logger.warning(
            "Could not send failure notification to session %s", session_id[:8]
        )

    exit_with_error(f"Failed to create share link for '{secret_alias}': {error}")
