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

Commands:
    airbyte-ops devin secret-request - Request or deliver a secret on demand
    airbyte-ops devin secret-list    - List available secrets in the vault
"""

from __future__ import annotations

import json
import logging
import os
import subprocess
from typing import Annotated

import requests
from cyclopts import Parameter
from uuid_extensions import uuid7str

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,
    resolve_session_url,
    send_session_message,
)
from airbyte_ops_mcp.human_in_the_loop import dispatch_escalation
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__)

# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------

SECRET_REQUEST_CHANNEL = "human-in-the-loop"

# ---------------------------------------------------------------------------
# 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 | None,
        Parameter(
            help="Slack approval record URL. If provided, validates the "
            "approval and delivers the secret. If omitted, sends an "
            "approval request to Slack.",
        ),
    ] = None,
    target_approver: Annotated[
        str | None,
        Parameter(
            help="Person to notify for approval (GitHub handle, email, or "
            "Slack user ID). Required when requesting approval.",
        ),
    ] = None,
    expected_request_id: Annotated[
        str | None,
        Parameter(
            help="Request ID (UUIDv7) from Phase 1. When provided, the "
            "approval record is validated against this ID for replay "
            "protection.",
        ),
    ] = None,
) -> None:
    """Request or deliver a Devin secret.

    Without --approval-evidence-url: sends an approval request to Slack.
    With --approval-evidence-url: validates the approval, creates a
    1Password share link, and sends it to the Devin session.
    """
    try:
        session_id = extract_session_id(session)
    except ValueError as exc:
        exit_with_error(str(exc))
        return  # unreachable, but keeps the type checker happy

    if not approval_evidence_url:
        if not target_approver:
            exit_with_error(
                "--target-approver is required when requesting approval "
                "(no --approval-evidence-url provided)."
            )
            return  # unreachable
        _handle_approval_request(
            session_id=session_id,
            session_url=resolve_session_url(session),
            secret_alias=secret_alias,
            target_approver=target_approver,
        )
        return

    _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_approval_request(
    session_id: str,
    session_url: str,
    secret_alias: str,
    target_approver: str,
) -> None:
    """Phase 1: Send an approval request to Slack via the HITL workflow."""
    approver = target_approver
    short_session = session_id[:8]
    request_id = uuid7str()

    result = dispatch_escalation(
        target_person=approver,
        message=(
            f"*Secret access requested:* `{secret_alias}`\n"
            f"*Requesting session:* `{short_session}...`\n\n"
            f"A Devin session is requesting access to the "
            f"`{secret_alias}` secret. If approved, a time-limited "
            f"1Password share link will be sent to the requesting "
            f"session.\n\n"
            f"Please review and approve or reject this request."
        ),
        agent_session_url=session_url,
        approval_requested=True,
        approval_request_summary=(
            f"Grant access to secret `{secret_alias}` for Devin session {short_session}"
        ),
        approval_metadata={
            "secret_alias": secret_alias,
            "request_id": request_id,
        },
        channel_override=SECRET_REQUEST_CHANNEL,
        header_emoji="🔑",
        header_label="Secret Access Request",
    )

    print_success(f"Approval request sent to #{SECRET_REQUEST_CHANNEL}")
    print_json(
        {
            "phase": "approval_requested",
            "secret_alias": secret_alias,
            "session_id": session_id,
            "request_id": request_id,
            "workflow_url": result.workflow_url,
            "run_id": result.run_id,
            "run_url": result.run_url,
        }
    )


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}")
