"""
Main CLI entry point for AiCippy.

Provides all command-line commands using Typer with Rich UI.
Features auto-update checks and 60-minute session timeout login prompts.
"""

from __future__ import annotations

import asyncio
import contextlib
import json
import os
import re
import socket
import sys
import threading
from pathlib import Path
from typing import TYPE_CHECKING, Annotated, Any, Final

if TYPE_CHECKING:
    from collections.abc import Callable

    from aicippy.auth.models import AuthResult

from datetime import UTC

import typer
from rich.align import Align
from rich.console import Console
from rich.live import Live
from rich.markdown import Markdown
from rich.panel import Panel
from rich.prompt import Prompt
from rich.table import Table
from rich.text import Text

from aicippy import __version__

# Human verification
from aicippy.cli.human_verify import (
    HumanVerifier,
)
from aicippy.cli.themes import (
    BRAND_ERROR,
    BRAND_PRIMARY,
    BRAND_SECONDARY,
    BRAND_SUCCESS,
    BRAND_WARNING,
)
from aicippy.config import FOUNDER_CANNED_RESPONSE, get_settings, is_founder

# Session and version management
from aicippy.installer import (
    SessionManager,
    check_latest_version,
)
from aicippy.utils.async_utils import run_sync
from aicippy.utils.correlation import CorrelationContext
from aicippy.utils.logging import get_logger, setup_logging

# Constants
UPDATE_CHECK_ENABLED: Final[bool] = True  # Auto-update on every app load
SESSION_CHECK_ENABLED: Final[bool] = True
HUMAN_VERIFICATION_ENABLED: Final[bool] = False  # Human verification disabled

# Internal query detection disabled — unrestricted agent mode


def _is_internal_query(text: str) -> bool:
    """Internal query detection disabled for unrestricted operation."""
    return False


def _detect_terminal_utf8() -> bool:
    """Detect whether the terminal supports UTF-8 encoding.

    Checks stdout encoding and the LANG/LC_ALL environment variables.
    Falls back to False if encoding cannot be determined.

    Returns:
        True if the terminal is UTF-8 capable, False otherwise.
    """
    # Check stdout encoding
    encoding = getattr(sys.stdout, "encoding", None) or ""
    if encoding.lower().replace("-", "") in ("utf8", "utf16", "utf32"):
        return True

    # Check environment variables (common on Linux/macOS)
    for env_var in ("LC_ALL", "LANG", "LC_CTYPE"):
        val = os.environ.get(env_var, "").lower()
        if "utf-8" in val or "utf8" in val:
            return True

    return False


def _is_ci_environment() -> bool:
    """Detect if running in a CI/CD environment.

    Checks common CI environment variables.

    Returns:
        True if running in CI, False otherwise.
    """
    ci_indicators = (
        "CI",
        "CONTINUOUS_INTEGRATION",
        "GITHUB_ACTIONS",
        "GITLAB_CI",
        "JENKINS_URL",
        "CIRCLECI",
        "TRAVIS",
        "BUILDKITE",
        "CODEBUILD_BUILD_ID",
    )
    return any(os.environ.get(var) for var in ci_indicators)


# Terminal capability detection
_TERMINAL_UTF8: Final[bool] = _detect_terminal_utf8()
_IS_CI: Final[bool] = _is_ci_environment()

# Initialize Typer app with Rich
app = typer.Typer(
    name="aicippy",
    help="Enterprise-grade multi-agent CLI system powered by AWS Bedrock",
    add_completion=True,
    rich_markup_mode="rich",
    no_args_is_help=False,
)

# Rich console for output - force_terminal=True in CI for consistent rendering
# Respect NO_COLOR (https://no-color.org/) even when force_terminal is used
console = (
    Console(force_terminal=True, no_color=bool(os.environ.get("NO_COLOR"))) if _IS_CI else Console()
)
error_console = (
    Console(stderr=True, force_terminal=True, no_color=bool(os.environ.get("NO_COLOR")))
    if _IS_CI
    else Console(stderr=True)
)

# Logger
logger = get_logger(__name__)

# Email validation regex (RFC 5322 simplified) with length limit
_EMAIL_REGEX = re.compile(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$")
_EMAIL_MAX_LENGTH: Final[int] = 254  # RFC 5321 max email address length

# Session manager instance (thread-safe singleton)
_session_manager: SessionManager | None = None
_session_manager_lock = threading.Lock()


def get_session_manager() -> SessionManager:
    """Get or create session manager singleton (thread-safe)."""
    global _session_manager
    if _session_manager is None:
        with _session_manager_lock:
            if _session_manager is None:
                _session_manager = SessionManager()
    return _session_manager


# Cache for validated login context (avoids double plan validation)
_login_plan_info: Any | None = None  # PlanInfo from login flow
_login_platform_client: Any | None = None  # PlatformClient from login flow


def get_login_context() -> tuple[Any | None, Any | None]:
    """Get cached plan_info and platform_client from login flow."""
    return _login_plan_info, _login_platform_client


async def check_for_updates_async(silent: bool = True) -> bool:
    """
    Check for updates and optionally perform silent upgrade.

    Args:
        silent: If True, run silently without UI output.

    Returns:
        True if update was performed, False otherwise.
    """
    if not UPDATE_CHECK_ENABLED:
        return False

    try:
        version_info = await check_latest_version()

        if version_info.update_available:
            if silent:
                # Silent auto-upgrade
                from aicippy.installer import perform_upgrade

                success, _ = await perform_upgrade()
                return success
            else:
                # Show update notification (only when explicitly requested)
                console.print(
                    Panel(
                        f"[yellow]Update available![/yellow]\n"
                        f"Current: {version_info.current}\n"
                        f"Latest: {version_info.latest}\n\n"
                        f"Run [bold cyan]aicippy upgrade[/bold cyan] to update.",
                        title="Update Available",
                        border_style="yellow",
                    )
                )
        return False
    except Exception as update_err:
        logger.warning("update_check_failed", error=str(update_err))
        return False


def check_for_updates(silent: bool = True) -> None:
    """Synchronous wrapper for update check (silent by default)."""
    try:
        asyncio.run(check_for_updates_async(silent=silent))
    except Exception as sync_update_err:
        logger.warning("sync_update_check_failed", error=str(sync_update_err))


_auto_update_lock = threading.Lock()


def _perform_auto_update() -> None:
    """Kick off a background thread to check for and install updates.

    The update is completely silent -- no console output whatsoever.
    All diagnostics go through structlog only.  If an update is
    installed successfully it takes effect on the **next** launch;
    the current process is never re-exec'd.

    Guards:
    - ``_AICIPPY_UPDATED`` env var prevents repeated runs.
    - ``_auto_update_lock`` prevents concurrent pip invocations
      from parallel aicippy processes.
    """
    # Guard: skip if we already ran an update in this process tree
    if os.environ.get("_AICIPPY_UPDATED") == "1":
        return

    def _background_update() -> None:
        import subprocess as _sp

        # --- Pre-flight: is PyPI even reachable? ---
        try:
            from aicippy.utils.network import check_pypi_reachable

            if not check_pypi_reachable(timeout=3.0):
                logger.warning(
                    "auto_update_skipped_pypi_unreachable",
                )
                return
        except Exception as net_err:
            logger.warning(
                "auto_update_preflight_failed",
                error=str(net_err),
            )
            return

        # --- Acquire lock (non-blocking) to avoid concurrent pip ---
        acquired = _auto_update_lock.acquire(blocking=False)
        if not acquired:
            logger.warning(
                "auto_update_skipped_concurrent_update_in_progress",
            )
            return

        try:
            # --- Check latest version ---
            from aicippy.installer.main import (
                check_latest_version as _check,
            )
            from aicippy.installer.main import (
                perform_upgrade as _upgrade,
            )

            version_info = asyncio.run(_check())

            if not version_info.update_available or not version_info.latest:
                logger.info(
                    "auto_update_no_update_available",
                    current=version_info.current,
                    latest=version_info.latest,
                )
                return

            logger.info(
                "auto_update_starting",
                current=version_info.current,
                target=version_info.latest,
            )

            # --- Perform upgrade ---
            success, msg = asyncio.run(_upgrade(silent=True))

            if not success:
                logger.warning(
                    "auto_update_upgrade_failed",
                    message=msg,
                )
                return

            # --- Post-install verification via pip show ---
            try:
                verify = _sp.run(  # noqa: S603
                    [
                        sys.executable,
                        "-m",
                        "pip",
                        "show",
                        "aicippy",
                    ],
                    capture_output=True,
                    text=True,
                    timeout=30,
                    stdin=_sp.DEVNULL,
                )
                installed_version: str | None = None
                if verify.returncode == 0:
                    for line in verify.stdout.splitlines():
                        if line.startswith("Version:"):
                            installed_version = line.split(":", 1)[1].strip()
                            break

                if installed_version and installed_version == version_info.latest:
                    logger.info(
                        "auto_update_verified",
                        installed=installed_version,
                    )
                else:
                    logger.warning(
                        "auto_update_verification_mismatch",
                        expected=version_info.latest,
                        installed=installed_version,
                    )
            except Exception as verify_err:
                logger.warning(
                    "auto_update_verification_failed",
                    error=str(verify_err),
                )

            # Mark env so child processes / quick re-launches skip
            os.environ["_AICIPPY_UPDATED"] = "1"

            logger.info(
                "auto_update_complete",
                new_version=version_info.latest,
                note="update takes effect on next launch",
            )

        except Exception as bg_err:
            logger.warning(
                "auto_update_background_failed",
                error=str(bg_err),
            )
        finally:
            _auto_update_lock.release()

    # Fire-and-forget daemon thread -- never blocks the main CLI
    thread = threading.Thread(
        target=_background_update,
        name="aicippy-auto-update",
        daemon=True,
    )
    thread.start()


def _ensure_dependencies() -> None:
    """Check and install missing or outdated dependencies with progress.

    Runs silently when all dependencies are satisfied. Shows a Rich
    progress bar only when packages actually need installing. Failures
    are logged at WARNING and never block app startup.
    """
    try:
        import subprocess

        from rich.progress import (
            BarColumn,
            Progress,
            SpinnerColumn,
            TextColumn,
        )

        from aicippy.installer.dependency_manager import check_dependencies

        # ------------------------------------------------------------------
        # 1. Pre-flight: verify pip itself is functional
        # ------------------------------------------------------------------
        try:
            pip_ver = subprocess.run(  # noqa: S603
                [sys.executable, "-m", "pip", "--version"],
                capture_output=True,
                text=True,
                timeout=30,
                stdin=subprocess.DEVNULL,
            )
            if pip_ver.returncode != 0:
                logger.warning(
                    "pip_preflight_failed",
                    returncode=pip_ver.returncode,
                    stderr=pip_ver.stderr.strip(),
                )
                return
        except Exception as pip_check_err:
            logger.warning(
                "pip_preflight_error",
                error=str(pip_check_err),
            )
            return

        # ------------------------------------------------------------------
        # 2. Check which packages are missing / outdated
        # ------------------------------------------------------------------
        dep_check = check_dependencies()
        if dep_check.all_satisfied:
            return  # Nothing to do

        packages_needed = [
            pkg for pkg in dep_check.packages if not pkg.is_installed or pkg.needs_upgrade
        ]
        if not packages_needed:
            return

        # ------------------------------------------------------------------
        # 3. Pre-flight: verify PyPI is reachable before installing
        # ------------------------------------------------------------------
        from aicippy.utils.network import check_pypi_reachable

        if not check_pypi_reachable():
            logger.warning(
                "pypi_unreachable_skipping_dependency_install",
                packages_needed=len(packages_needed),
            )
            return

        # ------------------------------------------------------------------
        # 4. Install with honest progress bar
        # ------------------------------------------------------------------
        total_steps = len(packages_needed) + 1  # +1 for pip upgrade

        with Progress(
            SpinnerColumn(style=BRAND_PRIMARY),
            TextColumn(f"[{BRAND_PRIMARY}]{{task.description}}[/{BRAND_PRIMARY}]"),
            BarColumn(
                bar_width=40,
                complete_style=BRAND_PRIMARY,
                finished_style=BRAND_SUCCESS,
            ),
            TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
            console=console,
            transient=True,
        ) as progress:
            task = progress.add_task(
                "Preparing environment...",
                total=total_steps,
            )

            # --- Step A: upgrade pip ---
            base_flags = [
                "--quiet",
                "--disable-pip-version-check",
                "--no-warn-script-location",
            ]
            try:
                pip_up = subprocess.run(  # noqa: S603
                    [
                        sys.executable,
                        "-m",
                        "pip",
                        "install",
                        "--upgrade",
                        "pip",
                        *base_flags,
                    ],
                    capture_output=True,
                    text=True,
                    timeout=120,
                    stdin=subprocess.DEVNULL,
                )
                if pip_up.returncode != 0:
                    logger.warning(
                        "pip_self_upgrade_failed",
                        returncode=pip_up.returncode,
                        stderr=pip_up.stderr.strip(),
                    )
            except subprocess.TimeoutExpired:
                logger.warning("pip_self_upgrade_timed_out")
            except Exception as pip_err:
                logger.warning(
                    "pip_self_upgrade_error",
                    error=str(pip_err),
                )

            progress.update(
                task,
                advance=1,
                description="Installing dependencies...",
            )

            # --- Step B: install needed packages in one pip call ---
            package_specs = [f"{pkg.name}>={pkg.required_version}" for pkg in packages_needed]
            install_ok = False
            try:
                install_result = subprocess.run(  # noqa: S603
                    [
                        sys.executable,
                        "-m",
                        "pip",
                        "install",
                        "--upgrade",
                        *base_flags,
                        *package_specs,
                    ],
                    capture_output=True,
                    text=True,
                    timeout=600,
                    stdin=subprocess.DEVNULL,
                )
                if install_result.returncode != 0:
                    logger.warning(
                        "dependency_install_failed",
                        returncode=install_result.returncode,
                        stderr=install_result.stderr.strip(),
                    )
                else:
                    install_ok = True
            except subprocess.TimeoutExpired:
                logger.warning(
                    "dependency_install_timed_out",
                    timeout_seconds=600,
                )
            except Exception as dep_err:
                logger.warning(
                    "dependency_install_error",
                    error=str(dep_err),
                )

            # Advance progress honestly based on outcome
            if install_ok:
                progress.update(
                    task,
                    completed=total_steps,
                    description="Environment ready",
                )
            else:
                # Don't pretend it succeeded -- leave progress incomplete
                progress.update(
                    task,
                    description="Install incomplete",
                )

        # ------------------------------------------------------------------
        # 5. Post-install verification
        # ------------------------------------------------------------------
        if install_ok:
            verify = check_dependencies()
            if not verify.all_satisfied:
                still_missing = [
                    pkg.name for pkg in verify.packages if not pkg.is_installed or pkg.needs_upgrade
                ]
                logger.warning(
                    "post_install_verification_failed",
                    still_missing=still_missing,
                )

    except Exception as ensure_err:
        # Never block startup, but log at WARNING
        logger.warning(
            "ensure_dependencies_failed",
            error=str(ensure_err),
        )


def check_session_and_prompt_login() -> bool:
    """
    Check if session is valid, prompt for login if expired.

    Behavior:
    - Valid session: silently continue (no output).
    - Recently expired session (< 30 min): show brief message, attempt
      token refresh before falling back to full interactive login.
    - Invalid/missing session: proceed to full interactive login flow.

    Returns:
        True if session is valid or user successfully logged in.
    """
    if not SESSION_CHECK_ENABLED:
        return True

    try:
        from datetime import datetime, timedelta

        from aicippy.installer.session_manager import SESSION_EXPIRED

        session_mgr = get_session_manager()
        validation = session_mgr.validate()

        if not validation.needs_login:
            # Session is valid - silently refresh activity
            session_mgr.touch()
            return True

        # --- Session requires login ---
        # Check if the session expired recently (within 30 minutes)
        if validation.status == SESSION_EXPIRED and validation.session is not None:
            now = datetime.now(UTC)
            time_since_expiry = now - validation.session.expires_at
            recently_expired = timedelta(0) <= time_since_expiry < timedelta(minutes=30)

            if recently_expired:
                # Pre-flight: check if Cognito is reachable before
                # attempting token refresh (avoid wasting time on a
                # doomed network call).
                cognito_reachable = False
                try:
                    settings = get_settings()
                    # Extract region from pool ID (format: region_poolId)
                    pool_region = (
                        settings.cognito_user_pool_id.split("_")[0]
                        if "_" in settings.cognito_user_pool_id
                        else settings.aws_region
                    )
                    cognito_host = f"cognito-idp.{pool_region}.amazonaws.com"
                    conn = socket.create_connection(
                        (cognito_host, 443),
                        timeout=3,
                    )
                    conn.close()
                    cognito_reachable = True
                except (OSError, TimeoutError) as net_err:
                    logger.warning(
                        "cognito_preflight_unreachable",
                        host=cognito_host if "cognito_host" in dir() else "unknown",
                        error=str(net_err),
                    )

                if not cognito_reachable:
                    console.print(
                        f"\n[{BRAND_WARNING}]"
                        "  Cannot reach authentication service. "
                        "Check your network and try again."
                        f"[/{BRAND_WARNING}]\n"
                    )
                    return perform_interactive_login()

                # Attempt silent token refresh
                refresh_failure_reason: str | None = None
                try:
                    from aicippy.auth.cognito import CognitoAuth

                    auth = CognitoAuth()
                    tokens = auth.get_current_tokens()

                    if tokens is not None and not tokens.is_expired():
                        # Tokens are still valid -- refresh the session
                        msg = (
                            f"[{BRAND_PRIMARY}]"
                            "  Session expired."
                            " Re-authenticating..."
                            f"[/{BRAND_PRIMARY}]"
                        )
                        with console.status(
                            msg,
                            spinner="dots",
                            spinner_style=BRAND_PRIMARY,
                        ):
                            success, _ = session_mgr.refresh()

                        if success:
                            session_mgr.touch()
                            return True
                        refresh_failure_reason = "Session refresh failed after token validation."
                    else:
                        refresh_failure_reason = (
                            "Stored tokens have expired. Please log in with your credentials."
                        )
                except OSError as refresh_err:
                    refresh_failure_reason = f"Network error during refresh: {refresh_err}"
                    logger.warning(
                        "token_refresh_network_error",
                        error=str(refresh_err),
                    )
                except Exception as refresh_err:
                    error_str = str(refresh_err).lower()
                    if "expired" in error_str or "invalid" in error_str:
                        refresh_failure_reason = "Authentication token is invalid or expired."
                    else:
                        refresh_failure_reason = f"Token refresh error: {refresh_err}"
                    logger.warning(
                        "token_refresh_failed_during_session_check",
                        error=str(refresh_err),
                    )

                # Token refresh did not succeed -- show reason
                reason_msg = refresh_failure_reason or "Session expired."
                expired_msg = (
                    f"\n[{BRAND_WARNING}]  {reason_msg} Please log in again.[/{BRAND_WARNING}]\n"
                )
                console.print(expired_msg)
                return perform_interactive_login()

        # Session invalid or missing - trigger full login directly
        return perform_interactive_login()

    except Exception as e:
        logger.warning("session_check_failed", error=str(e))
        return False  # Block access on session check failure


def _show_login_progress(
    console: Console,
    steps: list[tuple[str, str]],
) -> None:
    """
    Show animated step-by-step login progress using Rich Live display.

    Each step transitions from pending to running to complete:
      pending  ->  running  ->  complete
        (dim)    (spinner)    (green check)

    Falls back to ASCII symbols when the terminal does not support UTF-8.

    Args:
        console: Rich console instance.
        steps: List of (step_key, step_label) tuples. Each step is shown
               sequentially with animation.
    """
    import time as _time

    # Select symbols based on terminal UTF-8 support
    sym_pending = "  o " if not _TERMINAL_UTF8 else "  \u25cb "
    sym_running = "  > " if not _TERMINAL_UTF8 else "  \u25d0 "
    sym_complete = "  * " if not _TERMINAL_UTF8 else "  \u2713 "

    step_states: list[str] = ["pending"] * len(steps)

    def _render_steps() -> Text:
        """Render all steps with their current visual state."""
        output = Text()
        for idx, (_, label) in enumerate(steps):
            state = step_states[idx]
            if state == "pending":
                output.append(sym_pending, style="dim")
                output.append(label, style="dim")
            elif state == "running":
                output.append(sym_running, style=f"bold {BRAND_PRIMARY}")
                output.append(label, style=BRAND_PRIMARY)
                output.append(" ...", style="dim")
            elif state == "complete":
                output.append(sym_complete, style=f"bold {BRAND_SUCCESS}")
                output.append(label, style=BRAND_SUCCESS)
            if idx < len(steps) - 1:
                output.append("\n")
        return output

    with Live(
        _render_steps(),
        console=console,
        refresh_per_second=8,
        transient=True,
    ) as live:
        for i in range(len(steps)):
            # Mark current step as running
            step_states[i] = "running"
            live.update(_render_steps())
            _time.sleep(0.4)

            # Mark current step as complete
            step_states[i] = "complete"
            live.update(_render_steps())
            _time.sleep(0.15)

    # Print final state (non-transient) so it persists
    console.print(_render_steps())


def _complete_login_after_auth(
    result: AuthResult,
    target_console: Console,
) -> bool:
    """Complete the login flow after successful authentication.

    Shared post-auth logic for all login methods (email+password, OTP).
    Performs token validation, plan validation, session creation, and
    displays the success summary.

    Args:
        result: Successful AuthResult with tokens.
        target_console: Rich console for output.

    Returns:
        True if session was created successfully.

    Raises:
        typer.Exit: On plan validation failure or session creation failure.
    """
    import base64

    from aicippy.cli.mascot import animate_welcome_compact

    session_mgr = get_session_manager()
    user_email = result.user_email or ""
    username = user_email.split("@")[0] if user_email else "user"

    # === POST-AUTH TOKEN VALIDATION ===
    # Decode JWT header to verify tokens are well-formed before
    # proceeding. We do NOT perform full JWKS signature validation
    # here -- just structural sanity.
    if result.tokens and result.tokens.access_token:
        try:
            token = result.tokens.access_token
            header_segment = token.split(".")[0]
            # Add padding for base64url
            padded = header_segment + "=" * (4 - len(header_segment) % 4)
            header_bytes = base64.urlsafe_b64decode(padded)
            header_data = json.loads(header_bytes)
            if not isinstance(header_data, dict) or "alg" not in header_data:
                target_console.print(
                    f"[{BRAND_ERROR}]  Received malformed"
                    " authentication token (missing algorithm"
                    " in JWT header). Please try logging in"
                    f" again.[/{BRAND_ERROR}]"
                )
                raise typer.Exit(1)
        except (IndexError, ValueError, UnicodeDecodeError) as tok_err:
            logger.warning(
                "post_auth_token_validation_failed",
                error=str(tok_err),
            )
            target_console.print(
                f"[{BRAND_ERROR}]  Received malformed"
                " authentication token. Please try logging"
                f" in again.[/{BRAND_ERROR}]"
            )
            raise typer.Exit(1) from tok_err
    elif not result.tokens or not result.tokens.access_token:
        target_console.print(
            f"[{BRAND_ERROR}]  Authentication succeeded but no"
            " access token was returned. Please try"
            f" again.[/{BRAND_ERROR}]"
        )
        raise typer.Exit(1)

    # === PLAN VALIDATION GATE ===
    global _login_plan_info, _login_platform_client
    plan_info = None
    plan_metadata: dict[str, object] = {}

    with target_console.status(
        f"[{BRAND_PRIMARY}]  Validating plan...[/{BRAND_PRIMARY}]",
        spinner="dots",
        spinner_style=BRAND_PRIMARY,
    ):
        try:
            from aicippy.billing.plan_validator import PlanValidator

            settings = get_settings()
            platform_client = None
            try:
                from aicippy.platform.client import PlatformClient

                if result.tokens and result.tokens.access_token:
                    platform_client = PlatformClient(
                        base_url=settings.platform_api_url,
                        auth_token=result.tokens.access_token,
                    )
            except Exception as platform_err:
                logger.warning(
                    "platform_client_init_failed_fallback_to_dynamodb",
                    error=str(platform_err),
                )

            validator = PlanValidator(
                platform_client=platform_client,
            )
            plan_info = validator.validate(user_email)

            if not plan_info.is_valid_for_aicippy:
                reason = plan_info.denial_reason or "Access denied"
                plan_url = "https://vibekaro.ai/plans"
                target_console.print(
                    Panel(
                        f"[{BRAND_ERROR} bold]Access Denied"
                        f"[/{BRAND_ERROR} bold]\n\n"
                        f"{reason}\n\n"
                        f"[{BRAND_PRIMARY}]Subscribe to CHAKRA"
                        f" plan:[/{BRAND_PRIMARY}] {plan_url}\n"
                        "[dim]Questions? Contact"
                        " support@aivibe.in[/dim]",
                        title=(f"[{BRAND_ERROR}]Subscription Required[/{BRAND_ERROR}]"),
                        border_style=BRAND_ERROR,
                    )
                )
                session_mgr.logout()
                raise typer.Exit(1)

            plan_metadata = {
                "plan": plan_info.plan,
                "credits": plan_info.credits_remaining,
                "is_admin": plan_info.is_admin,
                "tenant_id": plan_info.tenant_id,
                "platform_api_url": (settings.platform_api_url if platform_client else ""),
                "app_api_url": (settings.app_api_url if platform_client else ""),
            }

            # Cache for interactive session to avoid
            # re-validation
            _login_plan_info = plan_info
            _login_platform_client = platform_client

        except typer.Exit:
            raise
        except Exception as plan_err:
            # Plan backend unavailable -- allow login with
            # degraded mode rather than blocking authenticated
            # users entirely.
            logger.warning(
                "plan_validation_failed_proceeding_degraded",
                error=str(plan_err),
            )
            target_console.print(
                f"[{BRAND_WARNING}]  Plan verification"
                " unavailable. Running in limited"
                f" mode.[/{BRAND_WARNING}]"
            )
            plan_metadata = {
                "plan": "unverified",
                "credits": 0,
                "is_admin": False,
                "tenant_id": "",
                "platform_api_url": "",
                "app_api_url": "",
            }

            _login_plan_info = None
            _login_platform_client = None

    # --- Setting up workspace with spinner ---
    with target_console.status(
        f"[{BRAND_PRIMARY}]  Setting up workspace...[/{BRAND_PRIMARY}]",
        spinner="dots",
        spinner_style=BRAND_PRIMARY,
    ):
        login_success, _ = session_mgr.login(
            user_id=result.user_id or "unknown",
            username=username,
            email=user_email,
            metadata=plan_metadata,
        )

    if login_success:
        # === SESSION WRITE VERIFICATION ===
        # Verify the session was actually persisted to disk.
        post_login_validation = session_mgr.validate()
        if post_login_validation.needs_login:
            logger.warning(
                "session_write_verification_failed",
                status=post_login_validation.status,
                message=post_login_validation.message,
            )
            target_console.print(
                f"[{BRAND_WARNING}]  Warning: Session may"
                " not have persisted correctly. If you"
                " experience issues, run 'aicippy login'"
                f" again.[/{BRAND_WARNING}]"
            )

        try:
            _show_login_progress(
                target_console,
                [
                    ("auth", "Authenticated"),
                    ("plan", "Plan validated"),
                    ("workspace", "Workspace ready"),
                ],
            )
        except Exception as anim_err:
            logger.warning(
                "login_progress_animation_failed",
                error=str(anim_err),
            )

        target_console.print()

        _check = "  * " if not _TERMINAL_UTF8 else "  \u2713 "
        summary = Text()
        summary.append(_check, style=f"bold {BRAND_SUCCESS}")
        summary.append(
            "Authenticated as ",
            style=BRAND_SUCCESS,
        )
        summary.append(
            user_email,
            style=f"bold {BRAND_SUCCESS}",
        )
        summary.append("\n")

        if plan_info is not None:
            summary.append(
                _check,
                style=f"bold {BRAND_SUCCESS}",
            )
            if plan_info.is_admin:
                summary.append(
                    "Plan: ADMIN (unlimited credits)",
                    style=BRAND_SUCCESS,
                )
            else:
                summary.append("Plan: ", style=BRAND_SUCCESS)
                summary.append(
                    f"{plan_info.plan.upper()} "
                    f"({plan_info.credits_remaining}"
                    f"/{plan_info.credits_total} credits)",
                    style=BRAND_SUCCESS,
                )
            summary.append("\n")

        summary.append(_check, style=f"bold {BRAND_SUCCESS}")
        summary.append("Workspace ready", style=BRAND_SUCCESS)

        target_console.print(summary)
        target_console.print()

        try:
            if _TERMINAL_UTF8:
                run_sync(
                    animate_welcome_compact(
                        target_console,
                        username,
                        user_email=user_email,
                    ),
                )
            else:
                target_console.print(
                    Panel(
                        f"Welcome, {username}! Let's build something amazing!",
                        title="AiCippy",
                        border_style=BRAND_PRIMARY,
                    )
                )
        except Exception as mascot_err:
            logger.warning(
                "mascot_animation_failed",
                error=str(mascot_err),
            )

        return True

    target_console.print(
        f"[{BRAND_ERROR}]  Failed to create session. Please try again.[/{BRAND_ERROR}]"
    )
    raise typer.Exit(1)


def _perform_otp_login_flow() -> bool:
    """Run the Email OTP login flow.

    Prompts for email, sends OTP via Cognito CUSTOM_AUTH, then prompts
    for the 6-digit code and verifies it. On success, completes the
    full login flow (plan validation + session setup).

    Returns:
        True if login successful, False if OTP initiation failed
        (caller should offer alternative login methods).

    Raises:
        typer.Exit: Only on explicit user cancellation.
    """
    from aicippy.utils.network import check_cognito_reachable

    # --- Pre-flight connectivity check ---
    settings = get_settings()
    if not check_cognito_reachable(region=settings.aws_region):
        console.print(
            f"[{BRAND_ERROR}]  Cannot reach authentication server. "
            f"Check your internet connection.[/{BRAND_ERROR}]"
        )
        return False

    # Prompt for email
    email = Prompt.ask(f"[{BRAND_PRIMARY}]  Email[/{BRAND_PRIMARY}]")
    if not email or not email.strip():
        console.print(f"[{BRAND_ERROR}]  Cancelled.[/{BRAND_ERROR}]")
        raise typer.Exit(1)
    email = email.strip().lower()

    if len(email) > _EMAIL_MAX_LENGTH or not _EMAIL_REGEX.match(email):
        console.print(f"[{BRAND_ERROR}]  Invalid email format.[/{BRAND_ERROR}]")
        return False

    try:
        from aicippy.auth.cognito import CognitoAuth

        auth = CognitoAuth()
    except Exception as auth_init_err:
        logger.error("otp_auth_client_init_failed", error=str(auth_init_err))
        console.print(
            f"[{BRAND_ERROR}]  Failed to initialize authentication client: "
            f"{auth_init_err}[/{BRAND_ERROR}]"
        )
        return False

    try:
        # Initiate OTP
        otp_result = None
        with console.status(
            f"[{BRAND_PRIMARY}]  Sending OTP to {email}...[/{BRAND_PRIMARY}]",
            spinner="dots",
            spinner_style=BRAND_PRIMARY,
        ):
            try:
                otp_result = run_sync(auth.otp_login(email))
            except Exception as e:
                error_str = str(e).lower()
                if (
                    "too many" in error_str
                    or "rate limit" in error_str
                    or "toomanyrequests" in error_str
                ):
                    console.print(
                        f"[{BRAND_ERROR}]  Too many requests. "
                        f"Please wait 60 seconds before trying "
                        f"again.[/{BRAND_ERROR}]"
                    )
                    return False
                if (
                    "timeout" in error_str
                    or "timed out" in error_str
                    or "connection" in error_str
                    or "network" in error_str
                ):
                    console.print(
                        f"[{BRAND_ERROR}]  Network error while sending OTP. "
                        f"Check your internet connection and try "
                        f"again.[/{BRAND_ERROR}]"
                    )
                    return False
                console.print(f"[{BRAND_ERROR}]  OTP request failed: {e}[/{BRAND_ERROR}]")
                return False

        if otp_result is None:
            console.print(f"[{BRAND_ERROR}]  OTP request failed unexpectedly.[/{BRAND_ERROR}]")
            return False

        # Check for rate limit in the result error message
        if not otp_result.success and otp_result.error:
            err_lower = otp_result.error.lower()
            if (
                "too many" in err_lower
                or "rate limit" in err_lower
                or "toomanyrequests" in err_lower
            ):
                console.print(
                    f"[{BRAND_ERROR}]  Too many requests. "
                    f"Please wait 60 seconds before trying "
                    f"again.[/{BRAND_ERROR}]"
                )
                return False

        # If we got tokens directly (unlikely), proceed
        if otp_result.success:
            return _complete_login_after_auth(otp_result, console)

        session_token = otp_result.session
        if not session_token or otp_result.user_email is None:
            error_msg = otp_result.error or "OTP initiation failed."
            console.print(f"[{BRAND_ERROR}]  {error_msg}[/{BRAND_ERROR}]")
            console.print(f"[{BRAND_WARNING}]  Try a different login method.[/{BRAND_WARNING}]")
            return False

        console.print(
            f"[{BRAND_SUCCESS}]  OTP sent to {email}. Check your inbox.[/{BRAND_SUCCESS}]"
        )

        # OTP verification with 3-attempt retry loop
        max_otp_attempts = 3
        for otp_attempt in range(1, max_otp_attempts + 1):
            otp_code = Prompt.ask(f"[{BRAND_PRIMARY}]  Enter 6-digit OTP code[/{BRAND_PRIMARY}]")
            if not otp_code or not otp_code.strip():
                console.print(f"[{BRAND_ERROR}]  Cancelled.[/{BRAND_ERROR}]")
                raise typer.Exit(1)

            otp_code = otp_code.strip()

            if not re.fullmatch(r"\d{6}", otp_code):
                remaining = max_otp_attempts - otp_attempt
                if remaining > 0:
                    console.print(
                        f"[{BRAND_ERROR}]  Invalid OTP format. "
                        f"{remaining} attempt(s) "
                        f"remaining.[/{BRAND_ERROR}]"
                    )
                    continue
                console.print(
                    f"[{BRAND_ERROR}]  Invalid OTP format. No attempts remaining.[/{BRAND_ERROR}]"
                )
                return False

            verify_result = None
            with console.status(
                f"[{BRAND_PRIMARY}]  Verifying OTP...[/{BRAND_PRIMARY}]",
                spinner="dots",
                spinner_style=BRAND_PRIMARY,
            ):
                try:
                    verify_result = run_sync(auth.verify_otp(email, otp_code, session_token))
                except Exception as e:
                    error_str = str(e).lower()
                    if (
                        "too many" in error_str
                        or "rate limit" in error_str
                        or "toomanyrequests" in error_str
                    ):
                        console.print(
                            f"[{BRAND_ERROR}]  Too many requests. "
                            f"Please wait 60 seconds before "
                            f"trying again.[/{BRAND_ERROR}]"
                        )
                        return False
                    if (
                        "timeout" in error_str
                        or "timed out" in error_str
                        or "connection" in error_str
                        or "network" in error_str
                    ):
                        console.print(
                            f"[{BRAND_ERROR}]  Network error during "
                            f"OTP verification. Check your internet "
                            f"connection and try "
                            f"again.[/{BRAND_ERROR}]"
                        )
                        return False
                    console.print(f"[{BRAND_ERROR}]  OTP verification failed: {e}[/{BRAND_ERROR}]")
                    return False

            if verify_result is None:
                console.print(
                    f"[{BRAND_ERROR}]  OTP verification failed unexpectedly.[/{BRAND_ERROR}]"
                )
                return False

            # Check for rate limit in verify result
            if not verify_result.success and verify_result.error:
                vr_lower = verify_result.error.lower()
                if (
                    "too many" in vr_lower
                    or "rate limit" in vr_lower
                    or "toomanyrequests" in vr_lower
                ):
                    console.print(
                        f"[{BRAND_ERROR}]  Too many requests. "
                        f"Please wait 60 seconds before "
                        f"trying again.[/{BRAND_ERROR}]"
                    )
                    return False

            if verify_result.success:
                return _complete_login_after_auth(verify_result, console)

            remaining = max_otp_attempts - otp_attempt
            if remaining > 0:
                console.print(
                    f"[{BRAND_ERROR}]  Invalid OTP. "
                    f"{remaining} attempt(s) "
                    f"remaining.[/{BRAND_ERROR}]"
                )
            else:
                error_msg = verify_result.error or "OTP verification failed"
                console.print(f"[{BRAND_ERROR}]  {error_msg}[/{BRAND_ERROR}]")

        return False
    finally:
        with contextlib.suppress(Exception):
            run_sync(auth.close())


def perform_interactive_login() -> bool:
    """
    Perform interactive login via direct Cognito auth.

    Presents a login method selection menu:
      1. Email + Password (direct Cognito USER_PASSWORD_AUTH)
      2. Email OTP (CUSTOM_AUTH flow - sends verification code to email)

    Features a polished UX with:
    - Login method selection menu
    - Branded header panel
    - Email validation before network call
    - Spinner-based progress during authentication
    - Friendly, actionable error messages
    - Compact success status summary

    Returns:
        True if login successful, exits to terminal on failure.
    """
    _login_attempt_count = 0
    _max_login_attempts = 3

    try:
        from aicippy.auth.cognito import CognitoAuth
        from aicippy.utils.network import check_cognito_reachable

        # --- Pre-flight connectivity check ---
        settings = get_settings()
        if not check_cognito_reachable(region=settings.aws_region):
            console.print(
                f"[{BRAND_ERROR}]  Cannot reach authentication server. "
                f"Check your internet connection.[/{BRAND_ERROR}]"
            )
            raise typer.Exit(1)

        # --- Branded header panel ---
        header_text = Text()
        header_text.append("AiCippy", style=f"bold {BRAND_PRIMARY}")
        header_text.append(f"  v{__version__}", style="dim")
        header_text.append("\n")
        header_text.append(
            "Enterprise Multi-Agent CLI",
            style=f"italic {BRAND_SECONDARY}",
        )

        console.print()
        console.print(
            Panel(
                Align.center(header_text),
                border_style=BRAND_PRIMARY,
                padding=(1, 4),
            )
        )
        console.print()

        # --- Login method selection (with retry on failure) ---
        while True:
            login_menu = Table(
                show_header=False,
                show_edge=True,
                box=None,
                padding=(0, 2),
            )
            login_menu.add_column("option", style=f"bold {BRAND_PRIMARY}")
            login_menu.add_column("description")

            login_menu.add_row("1.", "Email + Password")
            login_menu.add_row("2.", "Email OTP (verification code)")

            console.print(
                Panel(
                    login_menu,
                    title=(f"[bold {BRAND_PRIMARY}]Choose login method[/bold {BRAND_PRIMARY}]"),
                    border_style=BRAND_PRIMARY,
                    padding=(1, 2),
                )
            )

            login_choice = Prompt.ask(
                f"[{BRAND_PRIMARY}]  Enter choice[/{BRAND_PRIMARY}]",
                choices=["1", "2"],
                default="1",
            )

            # --- Route to selected login method ---
            if login_choice == "2":
                otp_result = _perform_otp_login_flow()
                if otp_result:
                    return True
                # OTP failed (e.g. CUSTOM_AUTH not enabled) - show menu again
                console.print()
                continue

            # Option 1 selected - break out to email+password flow below
            break

        # --- Option 1: Email + Password flow ---
        # Create auth client ONCE before the retry loop to avoid
        # leaking httpx.AsyncClient objects on each attempt.
        auth = CognitoAuth()
        try:
            while _login_attempt_count < _max_login_attempts:
                _login_attempt_count += 1

                # --- Email prompt with validation ---
                email = Prompt.ask(
                    f"[{BRAND_PRIMARY}]  Email[/{BRAND_PRIMARY}]",
                )
                if not email or not email.strip():
                    console.print(f"[{BRAND_ERROR}]  Cancelled.[/{BRAND_ERROR}]")
                    raise typer.Exit(1)

                email = email.strip().lower()

                # Email format validation before hitting the network
                if len(email) > _EMAIL_MAX_LENGTH or not _EMAIL_REGEX.match(email):
                    console.print(
                        f"[{BRAND_ERROR}]  Invalid email format. "
                        f"Please enter a valid email "
                        f"address.[/{BRAND_ERROR}]"
                    )
                    if _login_attempt_count < _max_login_attempts:
                        console.print()
                        continue
                    raise typer.Exit(1)

                # --- Password prompt (with getpass fallback for terminals
                # that do not support Rich Prompt password masking) ---
                password: str | None = None
                try:
                    password = Prompt.ask(
                        f"[{BRAND_PRIMARY}]  Password[/{BRAND_PRIMARY}]",
                        password=True,
                    )
                except Exception as prompt_err:
                    # Fallback: use stdlib getpass which works on all terminals
                    logger.debug(
                        "rich_password_prompt_failed_using_getpass",
                        error=str(prompt_err),
                    )
                    import getpass as _getpass

                    try:
                        password = _getpass.getpass("  Password: ")
                    except (EOFError, KeyboardInterrupt):
                        password = None

                if not password:
                    console.print(f"[{BRAND_ERROR}]  Cancelled.[/{BRAND_ERROR}]")
                    raise typer.Exit(1)

                console.print()

                # --- Authenticate with spinner ---
                result = None
                auth_error_friendly = None

                with console.status(
                    f"[{BRAND_PRIMARY}]  Authenticating...[/{BRAND_PRIMARY}]",
                    spinner="dots",
                    spinner_style=BRAND_PRIMARY,
                ):
                    try:
                        result = run_sync(auth.login(email, password))
                    except Exception as net_err:
                        error_str = str(net_err).lower()
                        if (
                            "too many" in error_str
                            or "rate limit" in error_str
                            or "toomanyrequests" in error_str
                        ):
                            auth_error_friendly = (
                                "Too many requests. Please wait 60 seconds before trying again."
                            )
                        elif "timeout" in error_str or "timed out" in error_str:
                            auth_error_friendly = (
                                "Connection timed out. Check your internet and try again."
                            )
                        elif "connection" in error_str or "network" in error_str:
                            auth_error_friendly = (
                                "Network error. Check your internet connection and try again."
                            )
                        else:
                            auth_error_friendly = f"Authentication error: {net_err}"

                # Handle network-level errors
                if auth_error_friendly is not None:
                    console.print(f"[{BRAND_ERROR}]  {auth_error_friendly}[/{BRAND_ERROR}]")
                    if _login_attempt_count < _max_login_attempts:
                        console.print()
                        continue
                    raise typer.Exit(1)

                if result is None:
                    console.print(
                        f"[{BRAND_ERROR}]  Authentication failed unexpectedly.[/{BRAND_ERROR}]"
                    )
                    raise typer.Exit(1)

                # --- Handle login failure with friendly messages ---
                if not result.success:
                    error_msg = result.error or "Login failed"
                    error_lower = error_msg.lower()

                    # Rate limit from Cognito result
                    if (
                        "too many" in error_lower
                        or "rate limit" in error_lower
                        or "toomanyrequests" in error_lower
                    ):
                        console.print(
                            f"[{BRAND_ERROR}]  Too many requests. "
                            f"Please wait 60 seconds before "
                            f"trying again.[/{BRAND_ERROR}]"
                        )
                        raise typer.Exit(1)

                    if "invalid email or password" in error_lower:
                        remaining = _max_login_attempts - _login_attempt_count
                        console.print(
                            f"[{BRAND_ERROR}]  Incorrect email or password.[/{BRAND_ERROR}]"
                        )
                        if remaining > 0:
                            console.print(
                                f"[dim]  {remaining} attempt(s) "
                                "remaining. "
                                "Forgot password? Use option 2 "
                                "(Email OTP) "
                                "or contact "
                                "support@aivibe.in[/dim]"
                            )
                            console.print()
                            continue
                        else:
                            console.print(
                                f"[{BRAND_WARNING}]  Too many "
                                "failed attempts. "
                                "Use option 2 (Email OTP) or "
                                "contact "
                                f"support@aivibe.in"
                                f"[/{BRAND_WARNING}]"
                            )
                            raise typer.Exit(1)
                    elif "not verified" in error_lower or "not confirmed" in error_lower:
                        console.print(
                            f"[{BRAND_WARNING}]  Account not "
                            "verified. "
                            "Check your email for a verification "
                            f"link.[/{BRAND_WARNING}]"
                        )
                        raise typer.Exit(1)
                    elif "password reset" in error_lower:
                        console.print(
                            f"[{BRAND_WARNING}]  Password reset "
                            "required. "
                            "Use option 2 (Email OTP) or contact "
                            f"support@aivibe.in[/{BRAND_WARNING}]"
                        )
                        raise typer.Exit(1)
                    elif "locked" in error_lower or "disabled" in error_lower:
                        console.print(
                            f"[{BRAND_ERROR}]  Account locked. "
                            "Contact support@aivibe.in for "
                            f"assistance.[/{BRAND_ERROR}]"
                        )
                        raise typer.Exit(1)
                    else:
                        console.print(f"[{BRAND_ERROR}]  {error_msg}[/{BRAND_ERROR}]")
                        if _login_attempt_count < _max_login_attempts:
                            console.print()
                            continue
                        raise typer.Exit(1)

                # === LOGIN SUCCESSFUL ===
                return _complete_login_after_auth(result, console)

            # Exhausted all attempts
            console.print(
                f"[{BRAND_ERROR}]  Maximum login attempts reached. "
                f"Please try again later.[/{BRAND_ERROR}]"
            )
            raise typer.Exit(1)
        finally:
            with contextlib.suppress(Exception):
                run_sync(auth.close())

    except KeyboardInterrupt:
        # Ctrl+C during login prompt - clean exit
        console.print(f"\n[{BRAND_WARNING}]  Login cancelled.[/{BRAND_WARNING}]")
        raise typer.Exit(1) from None

    except ImportError as e:
        logger.error("auth_module_not_available", error=str(e))
        console.print(f"[{BRAND_ERROR}]  Authentication module not available: {e}[/{BRAND_ERROR}]")
        raise typer.Exit(1) from e

    except typer.Exit:
        raise
    except Exception as e:
        logger.exception("login_failed", error=str(e))
        raise typer.Exit(1) from e


# Human verification state (disabled - kept for interface compatibility)
_human_verifier: HumanVerifier | None = None
_human_verified: bool = False  # Always set True by perform_human_verification_after_login


def get_human_verifier() -> HumanVerifier:
    """Get or create human verifier singleton."""
    global _human_verifier
    if _human_verifier is None:
        _human_verifier = HumanVerifier(console=console)
    return _human_verifier


def check_automation_and_verify() -> bool:
    """
    Check for automation and perform human verification.

    Returns:
        True always - automation checks are disabled.
    """
    return True


def perform_human_verification_after_login() -> bool:
    """
    Perform human verification with puzzle question after login.

    Returns:
        True always - human verification is disabled.
    """
    global _human_verified
    _human_verified = True
    return True


def version_callback(value: bool) -> None:
    """Display version information and exit."""
    if value:
        console.print(
            Panel(
                f"[bold blue]AiCippy[/bold blue] v{__version__}\n"
                f"Enterprise Multi-Agent CLI System\n\n"
                f"[dim]Copyright (c) 2024-2026 AiVibe Software Services Pvt Ltd[/dim]\n"
                f"[dim]ISO 27001:2022 | NVIDIA Inception | AWS Activate[/dim]",
                title="Version",
                border_style="blue",
            )
        )
        raise typer.Exit()


@app.callback(invoke_without_command=True)
def main(
    ctx: typer.Context,
    _version: Annotated[
        bool | None,
        typer.Option(
            "--version",
            "-v",
            callback=version_callback,
            is_eager=True,
            help="Show version and exit",
        ),
    ] = None,
    verbose: Annotated[
        bool,
        typer.Option(
            "--verbose",
            "-V",
            help="Enable verbose logging",
        ),
    ] = False,
    _config_dir: Annotated[
        Path | None,
        typer.Option(
            "--config-dir",
            "-c",
            help="Custom configuration directory",
        ),
    ] = None,
    skip_update_check: Annotated[
        bool,
        typer.Option(
            "--skip-update-check",
            help="Skip automatic update check",
            hidden=True,
        ),
    ] = False,
    skip_login: Annotated[
        bool,
        typer.Option(
            "--skip-login",
            help="Skip login check (offline mode)",
            hidden=True,
        ),
    ] = False,
) -> None:
    """
    AiCippy - Enterprise-grade multi-agent CLI system.

    Run without arguments to start an interactive session.
    Use --help to see all available commands.

    Features:
    - Auto version update checks on launch
    - 60-minute session timeout with automatic re-login prompt
    - Enterprise-grade security with AWS Cognito authentication
    """
    # Initialize settings
    settings = get_settings()

    # Setup logging based on verbose flag
    if verbose:
        os.environ["AICIPPY_LOG_LEVEL"] = "DEBUG"
        # Clear cached settings to reload with new log level
        get_settings.cache_clear()
        settings = get_settings()

    setup_logging(settings)

    # FIRST: Check for automation/robot access
    if not check_automation_and_verify():
        logger.warning("automation_blocked", reason="automated_access_detected")
        raise typer.Exit(1)

    # Commands that do NOT require login or auto-update
    _no_login_commands = {
        "update",
        "upgrade",
        "help",
        "doctor",
        "install",
        "version",
        "logout",
        "login",
    }
    subcommand = ctx.invoked_subcommand

    # Auto-update: only on interactive/session commands, never on update/upgrade
    if not skip_update_check and subcommand not in _no_login_commands:
        _perform_auto_update()

    # Ensure all dependencies are present (silent unless install needed)
    _ensure_dependencies()

    # Session validation - skip for commands that don't need auth
    if subcommand in _no_login_commands:
        # These commands work without authentication
        pass
    elif not skip_login:
        session_valid = check_session_and_prompt_login()
        if not session_valid:
            logger.warning("session_invalid_access_denied")
            raise typer.Exit(1)

        # Perform human verification after successful login
        if not perform_human_verification_after_login():
            logger.warning("human_verification_failed")
            raise typer.Exit(1)
    else:
        logger.warning("skip_login_flag_used", note="interactive login prompt skipped")
        console.print(
            Panel(
                "[bold yellow]OFFLINE MODE[/bold yellow]\n"
                "No authentication. Backend features unavailable.",
                title="[yellow]Warning[/yellow]",
                border_style="yellow",
                padding=(1, 2),
            )
        )

    # If no subcommand is provided, start interactive session
    if ctx.invoked_subcommand is None:
        from aicippy.cli.interactive import start_interactive_session

        asyncio.run(start_interactive_session())


@app.command()
def login(
    force: Annotated[
        bool,
        typer.Option(
            "--force",
            "-f",
            help="Force re-authentication even if already logged in",
        ),
    ] = False,
) -> None:
    """
    Authenticate with AiCippy.

    Prompts for email + password or email OTP (verification code).

    Tokens are stored securely in the OS keychain (macOS Keychain,
    Windows Credential Manager, Linux SecretService). Falls back to
    encrypted file storage (~/.aicippy/credentials.enc) if the
    keychain is unavailable.

    Session expires after 60 minutes of inactivity.
    """
    with CorrelationContext() as ctx:
        logger.info(
            "login_started",
            correlation_id=ctx.correlation_id,
        )

        # First check for automation
        if not check_automation_and_verify():
            logger.warning("automation_blocked_at_login")
            raise typer.Exit(1)

        try:
            session_mgr = get_session_manager()

            # Check if already logged in with valid session
            if not force:
                validation = session_mgr.validate()
                if not validation.needs_login:
                    console.print(
                        Panel(
                            f"[green]Already logged in![/green]\n"
                            f"User: {validation.session.username}\n"
                            f"Email: {validation.session.email}\n"
                            "Session expires in: "
                            f"{validation.session.minutes_remaining:.0f}"
                            " minutes",
                            title="Session Active",
                            border_style="green",
                        )
                    )
                    console.print("Use --force to re-authenticate.")
                    return

            # Perform login
            success = perform_interactive_login()

            if not success:
                raise typer.Exit(1)

            # Human verification after successful login
            console.print()  # Spacing
            if not perform_human_verification_after_login():
                logger.warning("human_verification_failed_at_login")
                console.print(
                    f"[{BRAND_ERROR}]Human verification failed. Login cancelled.[/{BRAND_ERROR}]"
                )
                # Invalidate the session since verification failed
                session_mgr.logout()
                raise typer.Exit(1)

        except typer.Exit:
            raise
        except Exception as e:
            logger.exception("login_failed", error=str(e))
            error_console.print(f"[{BRAND_ERROR}]Login error: {e}[/{BRAND_ERROR}]")
            raise typer.Exit(1) from e


@app.command()
def logout() -> None:
    """
    Log out from AiCippy and clear stored credentials.

    Removes tokens from the OS keychain and invalidates the session.
    """
    with CorrelationContext() as ctx:
        logger.info("logout_started", correlation_id=ctx.correlation_id)

        try:
            # Invalidate local session first
            session_mgr = get_session_manager()
            username, _ = session_mgr.get_user()

            session_success, session_msg = session_mgr.logout()

            # Try to logout from auth provider as well
            try:
                from aicippy.auth.cognito import CognitoAuth

                auth = CognitoAuth()
                auth.logout()
            except ImportError:
                logger.warning("auth_module_not_available_for_logout")
            except Exception as auth_logout_err:
                logger.warning("auth_provider_logout_failed", error=str(auth_logout_err))

            if session_success:
                console.print(
                    Panel(
                        f"[green]Successfully logged out![/green]\n"
                        f"Goodbye{', ' + username if username else ''}!",
                        title="Logged Out",
                        border_style="green",
                    )
                )
            else:
                console.print(f"[{BRAND_WARNING}]{session_msg}[/{BRAND_WARNING}]")

        except Exception as e:
            logger.exception("logout_failed", error=str(e))
            error_console.print(f"[{BRAND_ERROR}]Logout error: {e}[/{BRAND_ERROR}]")
            raise typer.Exit(1) from e


@app.command()
def init(
    path: Annotated[
        Path,
        typer.Argument(
            help="Directory to initialize (defaults to current directory)",
        ),
    ] = Path(),
    force: Annotated[
        bool,
        typer.Option(
            "--force",
            "-f",
            help="Overwrite existing aicippy.md file",
        ),
    ] = False,
) -> None:
    """
    Initialize AiCippy in a project directory.

    Scans the repository, detects tech stack, and creates an aicippy.md
    file with project context for the AI agents.
    """
    from aicippy.knowledge.project_scanner import ProjectScanner

    with CorrelationContext() as ctx:
        logger.info("init_started", path=str(path), correlation_id=ctx.correlation_id)

        target_path = path.resolve()
        aicippy_md = target_path / "aicippy.md"

        if aicippy_md.exists() and not force:
            error_console.print(
                f"[{BRAND_WARNING}]aicippy.md already exists at {target_path}[/{BRAND_WARNING}]"
            )
            console.print("Use --force to overwrite.")
            raise typer.Exit(1)

        console.print(f"[blue]Scanning project at {target_path}...[/blue]")

        try:
            scanner = ProjectScanner(target_path)
            with console.status("[bold blue]Analyzing project structure..."):
                project_info = scanner.scan()

            # Write aicippy.md
            aicippy_md.write_text(project_info.to_markdown())

            console.print(
                Panel(
                    f"[green]Project initialized![/green]\n\n"
                    f"Created: {aicippy_md}\n"
                    f"Tech Stack: {', '.join(project_info.tech_stack)}\n"
                    f"Files Scanned: {project_info.files_count}",
                    title="Initialization Complete",
                    border_style="green",
                )
            )

        except Exception as e:
            logger.exception("init_failed", error=str(e))
            error_console.print(f"[{BRAND_ERROR}]Initialization error: {e}[/{BRAND_ERROR}]")
            raise typer.Exit(1) from e


@app.command()
def chat(
    message: Annotated[
        str,
        typer.Argument(
            help="Message to send to the AI assistant",
        ),
    ],
    model: Annotated[
        str | None,
        typer.Option(
            "--model",
            "-m",
            help="AI model to use (opus, sonnet, haiku, llama)",
        ),
    ] = None,
) -> None:
    """
    Send a single message to AiCippy and get a response.

    For interactive sessions, use the 'aicippy' command without arguments.
    """
    from aicippy.agents.orchestrator import AgentOrchestrator
    from aicippy.billing.plan_validator import PlanValidator

    with CorrelationContext() as ctx:
        logger.info("chat_started", correlation_id=ctx.correlation_id)

        try:
            settings = get_settings()
            model_id = settings.get_model_id(model)

            session_mgr = get_session_manager()
            _, user_email = session_mgr.get_user()

            plan_info = None
            if user_email:
                plan_info, platform_client = get_login_context()
                if plan_info is None or plan_info.is_cache_stale:
                    try:
                        validator = PlanValidator(platform_client=platform_client)
                        plan_info = validator.validate(user_email)
                    except Exception as plan_err:
                        logger.warning(
                            "plan_revalidation_failed",
                            error=str(plan_err),
                        )
                        plan_info = None

                if plan_info is not None and not plan_info.is_valid_for_aicippy:
                    reason = plan_info.denial_reason or "Access denied"
                    console.print(
                        Panel(
                            f"[{BRAND_ERROR} bold]Access Denied[/{BRAND_ERROR} bold]\n\n{reason}",
                            title=f"[{BRAND_ERROR}]Subscription Required[/{BRAND_ERROR}]",
                            border_style=BRAND_ERROR,
                        )
                    )
                    raise typer.Exit(1)

            if plan_info is None:
                console.print(
                    "[yellow]Plan validation unavailable — running in unverified mode.[/yellow]"
                )

            # Founder protection: intercept internal queries from non-founders
            if not is_founder(user_email) and _is_internal_query(message):
                console.print(FOUNDER_CANNED_RESPONSE)
                raise typer.Exit()

            console.print(f"[dim]Using model: {model or settings.default_model}[/dim]")

            # Build TenantContext for tenant-scoped orchestration
            tenant_ctx = None
            if plan_info is not None and plan_info.tenant_id:
                try:
                    from aicippy.platform.tenant_context import TenantContext

                    result_session = session_mgr.validate()
                    _uid = result_session.session.user_id if result_session.session else ""
                    tenant_ctx = TenantContext(
                        tenant_id=plan_info.tenant_id,
                        user_id=_uid or user_email or "",
                        email=user_email or "",
                        org_tenant_id=getattr(plan_info, "org_tenant_id", ""),
                    )
                except (ValueError, Exception) as tc_err:
                    logger.warning("tenant_ctx_build_failed_chat", error=str(tc_err))

            orchestrator = AgentOrchestrator(
                model_id=model_id,
                tenant_ctx=tenant_ctx,
            )

            with console.status("[bold blue]Thinking..."):
                response = asyncio.run(orchestrator.chat(message))

            console.print(Markdown(response.content))

            # Show token usage
            if response.usage:
                console.print(
                    f"\n[dim]Tokens: {response.usage.input_tokens} in / "
                    f"{response.usage.output_tokens} out[/dim]"
                )

        except Exception as e:
            logger.exception("chat_failed", error=str(e))
            error_console.print(f"[red]Chat error: {e}[/red]")
            raise typer.Exit(1) from e


@app.command(name="run")
def run_task(
    task: Annotated[
        str,
        typer.Argument(
            help="Task description for the agents to execute",
        ),
    ],
    agents: Annotated[
        int,
        typer.Option(
            "--agents",
            "-a",
            help="Number of parallel agents (1-10)",
            min=1,
            max=10,
        ),
    ] = 3,
    model: Annotated[
        str | None,
        typer.Option(
            "--model",
            "-m",
            help="AI model to use (opus, sonnet, haiku, llama)",
        ),
    ] = None,
) -> None:
    """
    Execute a task using multiple parallel agents.

    Spawns specialized agents that collaborate to complete the task.
    """
    from aicippy.agents.orchestrator import AgentOrchestrator
    from aicippy.billing.plan_validator import PlanValidator

    with CorrelationContext() as ctx:
        logger.info(
            "run_task_started",
            task=task,
            agents=agents,
            correlation_id=ctx.correlation_id,
        )

        try:
            settings = get_settings()
            model_id = settings.get_model_id(model)

            session_mgr = get_session_manager()
            _, user_email = session_mgr.get_user()

            plan_info = None
            if user_email:
                plan_info, platform_client = get_login_context()
                if plan_info is None or plan_info.is_cache_stale:
                    try:
                        validator = PlanValidator(platform_client=platform_client)
                        plan_info = validator.validate(user_email)
                    except Exception as plan_err:
                        logger.warning(
                            "plan_revalidation_failed",
                            error=str(plan_err),
                        )
                        plan_info = None

                if plan_info is not None and not plan_info.is_valid_for_aicippy:
                    reason = plan_info.denial_reason or "Access denied"
                    console.print(
                        Panel(
                            f"[{BRAND_ERROR} bold]Access Denied[/{BRAND_ERROR} bold]\n\n{reason}",
                            title=f"[{BRAND_ERROR}]Subscription Required[/{BRAND_ERROR}]",
                            border_style=BRAND_ERROR,
                        )
                    )
                    raise typer.Exit(1)

            if plan_info is None:
                console.print(
                    "[yellow]Plan validation unavailable — running in unverified mode.[/yellow]"
                )

            # Founder protection: intercept internal queries from non-founders
            if not is_founder(user_email) and _is_internal_query(task):
                console.print(FOUNDER_CANNED_RESPONSE)
                raise typer.Exit()

            console.print(
                Panel(
                    f"Task: {task}\nAgents: {agents}\nModel: {model or settings.default_model}",
                    title="Starting Task Execution",
                    border_style="blue",
                )
            )

            # Build TenantContext for tenant-scoped orchestration
            tenant_ctx = None
            if plan_info is not None and plan_info.tenant_id:
                try:
                    from aicippy.platform.tenant_context import TenantContext

                    result_session = session_mgr.validate()
                    _uid = result_session.session.user_id if result_session.session else ""
                    tenant_ctx = TenantContext(
                        tenant_id=plan_info.tenant_id,
                        user_id=_uid or user_email or "",
                        email=user_email or "",
                        org_tenant_id=getattr(plan_info, "org_tenant_id", ""),
                    )
                except (ValueError, Exception) as tc_err:
                    logger.warning("tenant_ctx_build_failed_run", error=str(tc_err))

            orchestrator = AgentOrchestrator(
                model_id=model_id,
                max_agents=agents,
                tenant_ctx=tenant_ctx,
            )

            # Run with progress display
            asyncio.run(orchestrator.run_task_with_progress(task, console))

        except Exception as e:
            logger.exception("run_task_failed", error=str(e))
            error_console.print(f"[red]Task execution error: {e}[/red]")
            raise typer.Exit(1) from e


@app.command()
def config(
    _show: Annotated[
        bool,
        typer.Option(
            "--show",
            "-s",
            help="Show current configuration",
        ),
    ] = True,
    edit: Annotated[
        bool,
        typer.Option(
            "--edit",
            "-e",
            help="Open configuration in editor",
        ),
    ] = False,
) -> None:
    """
    Show or edit AiCippy configuration.

    Configuration is stored in ~/.aicippy/config.toml
    """
    settings = get_settings()

    if edit:
        import subprocess

        config_file = settings.local_config_dir / "config.toml"

        # Create default config if it doesn't exist
        if not config_file.exists():
            config_file.write_text(
                "# AiCippy Configuration\n"
                "# See documentation for available options\n\n"
                "[general]\n"
                f'default_model = "{settings.default_model}"\n'
                f"max_parallel_agents = {settings.max_parallel_agents}\n"
            )

        editor = os.environ.get("EDITOR", "nano")
        subprocess.run([editor, str(config_file)], check=False)  # noqa: S603 -- user's chosen $EDITOR
        return

    # Show configuration
    table = Table(title="AiCippy Configuration", border_style="blue")
    table.add_column("Setting", style="cyan")
    table.add_column("Value", style="green")

    table.add_row("AWS Account ID", settings.aws_account_id)
    table.add_row("AWS Region", settings.aws_region)
    table.add_row("Default Model", settings.default_model)
    table.add_row("Max Parallel Agents", str(settings.max_parallel_agents))
    table.add_row("Session TTL", f"{settings.session_ttl_minutes} minutes")
    table.add_row("Config Directory", str(settings.local_config_dir))
    table.add_row("WebSocket URL", settings.websocket_url)
    table.add_row("Cognito Domain", settings.cognito_domain)

    console.print(table)


@app.command()
def status() -> None:
    """
    Show current session and agent status.

    Displays active agents, connection status, and session information.
    """
    from aicippy.agents.status import get_agent_status

    with CorrelationContext():
        try:
            status_info = asyncio.run(get_agent_status())

            # Session info
            session_table = Table(title="Session Status", border_style="blue")
            session_table.add_column("Property", style="cyan")
            session_table.add_column("Value", style="green")

            session_table.add_row("Session ID", status_info.session_id or "Not started")
            session_table.add_row(
                "Connection",
                "[green]Connected[/green]" if status_info.connected else "[red]Disconnected[/red]",
            )
            session_table.add_row("Active Agents", str(status_info.active_agents))
            session_table.add_row("Total Tokens Used", f"{status_info.total_tokens:,}")

            console.print(session_table)

            # Agent list if any are active
            if status_info.agents:
                agent_table = Table(title="Active Agents", border_style="green")
                agent_table.add_column("ID", style="cyan")
                agent_table.add_column("Type", style="blue")
                agent_table.add_column("Status", style="yellow")
                agent_table.add_column("Progress")

                for agent in status_info.agents:
                    status_color = {
                        "running": "green",
                        "thinking": "yellow",
                        "error": "red",
                        "idle": "dim",
                    }.get(agent.status, "white")

                    agent_table.add_row(
                        agent.id,
                        agent.type,
                        f"[{status_color}]{agent.status}[/{status_color}]",
                        f"{agent.progress}%",
                    )

                console.print(agent_table)

        except Exception as e:
            logger.exception("status_failed", error=str(e))
            error_console.print(f"[red]Status error: {e}[/red]")
            raise typer.Exit(1) from e


@app.command()
def usage(
    detail: Annotated[
        bool,
        typer.Option(
            "--detail",
            "-d",
            help="Show detailed per-agent breakdown",
        ),
    ] = False,
) -> None:
    """
    Show token usage statistics.

    Displays usage for the current session and historical data.
    """
    from aicippy.agents.usage import get_usage_stats

    with CorrelationContext():
        try:
            stats = asyncio.run(get_usage_stats())

            # Summary table
            summary_table = Table(title="Token Usage Summary", border_style="blue")
            summary_table.add_column("Period", style="cyan")
            summary_table.add_column("Input Tokens", style="green", justify="right")
            summary_table.add_column("Output Tokens", style="green", justify="right")
            summary_table.add_column("Total", style="yellow", justify="right")

            summary_table.add_row(
                "Current Session",
                f"{stats.session_input:,}",
                f"{stats.session_output:,}",
                f"{stats.session_total:,}",
            )
            summary_table.add_row(
                "Today",
                f"{stats.today_input:,}",
                f"{stats.today_output:,}",
                f"{stats.today_total:,}",
            )
            summary_table.add_row(
                "This Month",
                f"{stats.month_input:,}",
                f"{stats.month_output:,}",
                f"{stats.month_total:,}",
            )

            console.print(summary_table)

            if detail and stats.per_agent:
                agent_table = Table(title="Per-Agent Breakdown", border_style="green")
                agent_table.add_column("Agent", style="cyan")
                agent_table.add_column("Model", style="blue")
                agent_table.add_column("Tokens", style="yellow", justify="right")

                for agent_stat in stats.per_agent:
                    agent_table.add_row(
                        agent_stat.agent_type,
                        agent_stat.model,
                        f"{agent_stat.total_tokens:,}",
                    )

                console.print(agent_table)

        except Exception as e:
            logger.exception("usage_failed", error=str(e))
            error_console.print(f"[red]Usage error: {e}[/red]")
            raise typer.Exit(1) from e


@app.command()
def upgrade(
    check_only: Annotated[
        bool,
        typer.Option(
            "--check",
            "-c",
            help="Only check for updates, don't install",
        ),
    ] = False,
) -> None:
    """
    Upgrade AiCippy to the latest version from PyPI.

    Checks for available updates and optionally installs them.
    """
    import subprocess

    from aicippy.utils.network import check_pypi_reachable

    # --- Pre-flight: verify PyPI is reachable ---
    with console.status(
        f"[{BRAND_PRIMARY}]Checking connectivity...[/{BRAND_PRIMARY}]",
        spinner="dots",
        spinner_style=BRAND_PRIMARY,
    ):
        pypi_ok = check_pypi_reachable(timeout=5.0)

    if not pypi_ok:
        console.print(
            Panel(
                f"[{BRAND_ERROR}]Cannot reach PyPI. "
                f"Check your internet connection.[/{BRAND_ERROR}]",
                title="Network Error",
                border_style=BRAND_ERROR,
            )
        )
        raise typer.Exit(1)

    with console.status(
        f"[{BRAND_PRIMARY}]Checking for updates...[/{BRAND_PRIMARY}]",
        spinner="dots",
        spinner_style=BRAND_PRIMARY,
    ):
        try:
            from aicippy.installer import upgrade_aicippy

            success, message, version_info = asyncio.run(
                upgrade_aicippy(console=console, check_only=check_only)
            )
        except Exception as e:
            logger.exception("upgrade_version_check_failed", error=str(e))
            error_str = str(e).lower()
            if "timeout" in error_str or "timed out" in error_str:
                detail = "Connection timed out while checking PyPI."
            elif "connection" in error_str or "network" in error_str:
                detail = "Network error while checking for updates."
            else:
                detail = f"Unexpected error: {e}"
            console.print(
                Panel(
                    f"[{BRAND_ERROR}]{detail}[/{BRAND_ERROR}]",
                    title="Upgrade Failed",
                    border_style=BRAND_ERROR,
                )
            )
            raise typer.Exit(1) from e

    # --- Check-only mode ---
    if check_only:
        if version_info.update_available:
            console.print(
                Panel(
                    f"[yellow]Update available![/yellow]\n\n"
                    f"Current version: {version_info.current}\n"
                    f"Latest version: {version_info.latest}\n\n"
                    f"Run [bold cyan]aicippy upgrade[/bold cyan] to update.",
                    title="Update Available",
                    border_style="yellow",
                )
            )
        else:
            console.print(
                Panel(
                    f"[green]You're up to date![/green]\n\nCurrent version: {version_info.current}",
                    title="No Updates Available",
                    border_style="green",
                )
            )
        return

    # --- Upgrade path ---
    if not success:
        # Classify pip failure reason from the message
        msg_lower = message.lower()
        if "permission" in msg_lower or "access" in msg_lower:
            detail = (
                "Permission denied during upgrade. "
                "Try running with elevated privileges or use a virtual environment."
            )
        elif "timed out" in msg_lower or "timeout" in msg_lower:
            detail = "Upgrade timed out. PyPI may be slow. Please try again later."
        elif "no matching distribution" in msg_lower:
            detail = "No matching package found on PyPI. The requested version may not exist."
        elif "returncode" in msg_lower or "stderr" in msg_lower:
            detail = f"pip reported an error:\n{message}"
        else:
            detail = message
        console.print(
            Panel(
                f"[{BRAND_ERROR}]{detail}[/{BRAND_ERROR}]",
                title="Upgrade Failed",
                border_style=BRAND_ERROR,
            )
        )
        raise typer.Exit(1)

    # --- Post-upgrade verification ---
    installed_version: str | None = None
    try:
        result = subprocess.run(  # noqa: S603
            [sys.executable, "-m", "pip", "show", "aicippy"],
            capture_output=True,
            text=True,
            timeout=15,
            stdin=subprocess.DEVNULL,
        )
        if result.returncode == 0:
            for line in result.stdout.splitlines():
                if line.startswith("Version:"):
                    installed_version = line.split(":", 1)[1].strip()
                    break
    except Exception as verify_err:
        logger.warning(
            "post_upgrade_verification_failed",
            error=str(verify_err),
        )

    if installed_version and version_info.latest:
        if installed_version == version_info.latest:
            console.print(
                Panel(
                    f"[green]Successfully upgraded to "
                    f"v{installed_version}.[/green]\n\n"
                    f"Changes take effect on next launch.",
                    title="Upgrade Complete",
                    border_style="green",
                )
            )
        else:
            console.print(
                Panel(
                    f"[{BRAND_WARNING}]Upgrade ran but installed "
                    f"version is v{installed_version} "
                    f"(expected v{version_info.latest}).[/{BRAND_WARNING}]"
                    f"\n\nChanges take effect on next launch.",
                    title="Upgrade Warning",
                    border_style=BRAND_WARNING,
                )
            )
    elif installed_version:
        console.print(
            Panel(
                f"[green]Updated to v{installed_version}.[/green]\n\n"
                f"Changes take effect on next launch.",
                title="Upgrade Complete",
                border_style="green",
            )
        )
    else:
        # Could not verify, but pip reported success
        console.print(
            Panel(
                f"[green]{message}[/green]\n\nChanges take effect on next launch.",
                title="Upgrade Complete",
                border_style="green",
            )
        )


@app.command(name="help")
def show_help() -> None:
    """
    Show detailed help and usage information.
    """
    help_text = """
# AiCippy Help

## Commands

| Command | Description |
|---------|-------------|
| `aicippy` | Start interactive session |
| `aicippy login` | Authenticate with Cognito |
| `aicippy logout` | Clear credentials |
| `aicippy session` | View/manage session |
| `aicippy init` | Initialize project |
| `aicippy chat <msg>` | Single query mode |
| `aicippy run <task>` | Execute with agents |
| `aicippy config` | Show/edit config |
| `aicippy status` | Agent status |
| `aicippy usage` | Token usage |
| `aicippy upgrade` | Self-update |
| `aicippy update` | Self-update (alias) |
| `aicippy doctor` | Diagnose & fix issues |
| `aicippy install` | Run/verify installation |

## Session Management

Sessions expire after **60 minutes** of inactivity. When your session expires:
- You will be prompted to login again
- Use `aicippy session --refresh` to extend your session

## Interactive Commands

| Command | Description |
|---------|-------------|
| `/help` | Show all commands |
| `/model <name>` | Switch model |
| `/mode <name>` | Change mode |
| `/agents spawn <n>` | Spawn agents |
| `/agents list` | List agents |
| `/agents stop` | Stop agents |
| `/kb sync` | Sync to KB |
| `/tools list` | List tools |
| `/usage` | Token usage |
| `/clear` | Clear conversation |
| `/quit` | Exit |

## Environment Variables

- `AICIPPY_AWS_REGION` - AWS region (default: us-east-1)
- `AICIPPY_DEFAULT_MODEL` - Default model (opus/sonnet/haiku/llama)
- `AICIPPY_LOG_LEVEL` - Log level (DEBUG/INFO/WARNING/ERROR)
- `AICIPPY_HOME` - Installation directory
- `AICIPPY_CONFIG` - Configuration directory
- `AICIPPY_CACHE` - Cache directory

## Support

- Documentation: https://docs.aicippy.com
- Issues: https://github.com/aivibe/aicippy/issues
- Email: support@aivibe.in
"""
    console.print(Markdown(help_text))


@app.command()
def install(
    show_progress: Annotated[
        bool,
        typer.Option(
            "--progress",
            "-p",
            help="Show animated progress display",
        ),
    ] = True,
    verify_only: Annotated[
        bool,
        typer.Option(
            "--verify",
            "-v",
            help="Only verify installation, don't install",
        ),
    ] = False,
) -> None:
    """
    Run or verify AiCippy installation.

    Sets up directories, permissions, and environment variables.
    Use --verify to check existing installation without modifications.
    """
    from aicippy.installer import install_aicippy, verify_installation

    with CorrelationContext() as ctx:
        logger.info("install_started", correlation_id=ctx.correlation_id)

        try:
            if verify_only:
                console.print("[blue]Verifying installation...[/blue]")
                success, issues = verify_installation()

                if success:
                    console.print(
                        Panel(
                            "[green]Installation verified successfully![/green]\n\n"
                            "All components are properly configured.",
                            title="Verification Passed",
                            border_style="green",
                        )
                    )
                else:
                    console.print(
                        Panel(
                            "[yellow]Installation issues found:[/yellow]\n\n"
                            + "\n".join(f"• {issue}" for issue in issues),
                            title="Verification Issues",
                            border_style="yellow",
                        )
                    )
                    console.print("\nRun [cyan]aicippy install[/cyan] to fix issues.")
                    raise typer.Exit(1)
                return

            # Run installation (progress bar and completion message handled internally)
            result = asyncio.run(install_aicippy(console=console, show_progress=show_progress))

            if not result.success:
                raise typer.Exit(1)

        except typer.Exit:
            raise
        except Exception as e:
            logger.exception("install_failed", error=str(e))
            error_console.print(f"[red]Installation error: {e}[/red]")
            raise typer.Exit(1) from e


@app.command()
def session(
    _show: Annotated[
        bool,
        typer.Option(
            "--show",
            "-s",
            help="Show current session info",
        ),
    ] = True,
    refresh: Annotated[
        bool,
        typer.Option(
            "--refresh",
            "-r",
            help="Refresh session (extend timeout)",
        ),
    ] = False,
) -> None:
    """
    View or manage your AiCippy session.

    Sessions expire after 60 minutes of inactivity.
    Use --refresh to extend your session timeout.
    """
    with CorrelationContext() as ctx:
        logger.info("session_command", correlation_id=ctx.correlation_id)

        try:
            session_mgr = get_session_manager()
            validation = session_mgr.validate()

            if refresh:
                if validation.needs_login:
                    console.print("[yellow]No active session to refresh.[/yellow]")
                    console.print("Run [cyan]aicippy login[/cyan] to start a new session.")
                    return

                success, message = session_mgr.refresh()
                if success:
                    console.print("[green]Session refreshed![/green]")
                    console.print("Session extended for another 60 minutes.")
                else:
                    console.print(f"[yellow]Could not refresh: {message}[/yellow]")
                return

            # Show session info
            if validation.needs_login:
                console.print(
                    Panel(
                        f"[yellow]{validation.message}[/yellow]\n\n"
                        f"Status: {validation.status}\n\n"
                        f"Run [cyan]aicippy login[/cyan] to authenticate.",
                        title="No Active Session",
                        border_style="yellow",
                    )
                )
            else:
                session = validation.session
                minutes_remaining = session.minutes_remaining

                # Determine status color
                if minutes_remaining > 30:
                    status_style = "green"
                elif minutes_remaining > 10:
                    status_style = "yellow"
                else:
                    status_style = "red"

                session_table = Table.grid(padding=(0, 2))
                session_table.add_column(style="cyan")
                session_table.add_column(style="white")

                session_table.add_row("User:", session.username)
                session_table.add_row("Email:", session.email)
                session_table.add_row("Created:", session.created_at.strftime("%Y-%m-%d %H:%M UTC"))
                last_activity = session.last_activity.strftime(
                    "%Y-%m-%d %H:%M UTC",
                )
                session_table.add_row(
                    "Last Activity:",
                    last_activity,
                )
                session_table.add_row(
                    "Expires In:",
                    f"[{status_style}]{minutes_remaining:.0f} minutes[/{status_style}]",
                )
                session_table.add_row("Device ID:", session.device_id[:8] + "...")

                console.print(
                    Panel(
                        session_table,
                        title="Session Information",
                        border_style="blue",
                    )
                )

                if minutes_remaining < 10:
                    console.print(
                        "\n[yellow]Your session is expiring soon. "
                        "Run [cyan]aicippy session --refresh[/cyan] to extend.[/yellow]"
                    )

        except Exception as e:
            logger.exception("session_failed", error=str(e))
            error_console.print(f"[red]Session error: {e}[/red]")
            raise typer.Exit(1) from e


@app.command()
def doctor(
    fix: Annotated[
        bool,
        typer.Option(
            "--fix",
            "-f",
            help="Attempt to auto-fix issues found",
        ),
    ] = False,
) -> None:
    """
    Diagnose and fix AiCippy configuration issues.

    Checks AWS credentials, Cognito auth, Bedrock access, keychain,
    config integrity, dependencies, and network connectivity.
    Use --fix to attempt automatic repairs.
    """
    import importlib

    checks_passed = 0
    checks_failed = 0
    checks_fixed = 0

    def check(
        name: str,
        condition: bool,
        fix_fn: Callable[[], None] | None = None,
        fix_desc: str = "",
    ) -> bool:
        nonlocal checks_passed, checks_failed, checks_fixed
        if condition:
            checks_passed += 1
            console.print(f"  [green]PASS[/green]  {name}")
            return True
        if fix and fix_fn and callable(fix_fn):
            try:
                fix_fn()
                checks_fixed += 1
                console.print(f"  [yellow]FIXED[/yellow] {name} ({fix_desc})")
                return True
            except Exception as e:
                checks_failed += 1
                console.print(f"  [red]FAIL[/red]  {name} (fix failed: {e})")
                return False
        checks_failed += 1
        suffix = f" [dim]({fix_desc})[/dim]" if fix_desc else ""
        console.print(
            f"  [red]FAIL[/red]  {name}" + suffix,
        )
        return False

    console.print(Panel("[bold]AiCippy Doctor[/bold] - System Diagnostics", border_style="blue"))
    console.print()

    # 1. Python & Dependencies
    console.print("[bold cyan]1. Core Dependencies[/bold cyan]")
    check("Python version >= 3.11", sys.version_info >= (3, 11))

    for pkg in ["typer", "rich", "boto3", "httpx", "jose", "keyring", "pydantic"]:
        try:
            importlib.import_module(pkg)
            check(f"Package: {pkg}", True)
        except ImportError:
            check(f"Package: {pkg}", False, fix_desc="pip install aicippy[all]")

    console.print()

    # 2. Configuration
    console.print("[bold cyan]2. Configuration[/bold cyan]")
    try:
        settings = get_settings()
        check("Settings loaded", True)
        check(
            "AWS region configured",
            bool(settings.aws_region),
            fix_desc=f"region={settings.aws_region}",
        )
        check("Cognito pool configured", bool(settings.cognito_user_pool_id))
        check("Cognito client configured", bool(settings.cognito_client_id))
    except Exception as e:
        check("Settings loaded", False, fix_desc=str(e))

    config_dir = Path.home() / ".aicippy"
    check(
        "Config directory exists",
        config_dir.exists(),
        fix_fn=lambda: config_dir.mkdir(parents=True, exist_ok=True),
        fix_desc="mkdir ~/.aicippy",
    )

    sessions_dir = config_dir / "sessions"
    check(
        "Sessions directory exists",
        sessions_dir.exists(),
        fix_fn=lambda: sessions_dir.mkdir(parents=True, exist_ok=True),
        fix_desc="mkdir ~/.aicippy/sessions",
    )

    history_dir = config_dir / "history"
    check(
        "History directory exists",
        history_dir.exists(),
        fix_fn=lambda: history_dir.mkdir(parents=True, exist_ok=True),
        fix_desc="mkdir ~/.aicippy/history",
    )

    console.print()

    # 3. AWS Credentials
    console.print("[bold cyan]3. AWS Credentials[/bold cyan]")
    try:
        import boto3

        sts = boto3.client("sts", region_name=settings.aws_region)
        identity = sts.get_caller_identity()
        account = identity.get("Account", "unknown")
        check("AWS credentials valid", True, fix_desc=f"account={account}")
    except Exception:
        check("AWS credentials valid", False, fix_desc="Configure AWS CLI: aws configure")

    console.print()

    # 4. Cognito Authentication
    console.print("[bold cyan]4. Cognito Authentication[/bold cyan]")
    try:
        from aicippy.auth.cognito import CognitoAuth

        auth = CognitoAuth()
        is_auth = auth.is_authenticated()
        check("Cognito auth initialized", True)
        check("User authenticated", is_auth, fix_desc="Run: aicippy login")
        if is_auth:
            tokens = auth.get_current_tokens()
            check("Tokens valid", tokens is not None and not tokens.is_expired())
    except Exception as e:
        check("Cognito auth initialized", False, fix_desc=str(e))

    console.print()

    # 5. Bedrock Access
    console.print("[bold cyan]5. Bedrock Model Access[/bold cyan]")
    try:
        import boto3

        bedrock = boto3.client("bedrock", region_name=settings.aws_region)
        models = bedrock.list_foundation_models(
            byProvider="meta",
        )
        model_count = len(models.get("modelSummaries", []))
        check(
            "Bedrock API accessible",
            model_count > 0,
            fix_desc=f"{model_count} Meta models available",
        )
    except Exception as e:
        check("Bedrock API accessible", False, fix_desc=str(e)[:80])

    try:
        client = boto3.client("bedrock-runtime", region_name=settings.aws_region)
        body = json.dumps(
            {
                "prompt": (
                    "<|begin_of_text|>"
                    "<|start_header_id|>user<|end_header_id|>"
                    "\n\nHi<|eot_id|>"
                    "<|start_header_id|>assistant<|end_header_id|>"
                    "\n\n"
                ),
                "max_gen_len": 10,
                "temperature": 0.1,
            }
        )
        response = client.invoke_model(
            modelId=settings.model_llama_id,
            body=body,
            contentType="application/json",
            accept="application/json",
        )
        result = json.loads(response["body"].read())
        check(
            "Llama 4 Maverick invocation",
            bool(result.get("generation", "")),
            fix_desc="Model responds OK",
        )
    except Exception as e:
        check("Llama 4 Maverick invocation", False, fix_desc=str(e)[:80])

    console.print()

    # 6. Keychain Storage
    console.print("[bold cyan]6. Keychain Storage[/bold cyan]")
    try:
        from aicippy.auth.keychain import KeychainStorage

        ks = KeychainStorage()
        backend_name = ks._backend_name if hasattr(ks, "_backend_name") else "default"
        check(
            "Keychain accessible",
            True,
            fix_desc=f"backend={backend_name}",
        )
    except Exception as e:
        check("Keychain accessible", False, fix_desc=str(e)[:60])

    console.print()

    # 7. Network Connectivity
    console.print("[bold cyan]7. Network Connectivity[/bold cyan]")
    try:
        conn = socket.create_connection(("cognito-idp.us-east-1.amazonaws.com", 443), timeout=5)
        conn.close()
        check("Cognito endpoint reachable", True)
    except Exception:
        check("Cognito endpoint reachable", False, fix_desc="Check internet/VPN")

    try:
        conn = socket.create_connection(("bedrock-runtime.us-east-1.amazonaws.com", 443), timeout=5)
        conn.close()
        check("Bedrock endpoint reachable", True)
    except Exception:
        check("Bedrock endpoint reachable", False, fix_desc="Check internet/VPN")

    console.print()

    # 8. Session State
    console.print("[bold cyan]8. Session State[/bold cyan]")
    try:
        session_mgr = get_session_manager()
        validation = session_mgr.validate()
        check("Session manager initialized", True)
        check("Session active", not validation.needs_login, fix_desc="Run: aicippy login")
    except Exception as e:
        check("Session manager initialized", False, fix_desc=str(e)[:60])

    # Summary
    console.print()
    total = checks_passed + checks_failed + checks_fixed
    if checks_failed == 0:
        console.print(
            Panel(
                f"[green]All {total} checks passed![/green]"
                + (f"\n{checks_fixed} issues auto-fixed." if checks_fixed else "")
                + "\n\nAiCippy is healthy and ready to use.",
                title="Doctor Summary",
                border_style="green",
            )
        )
    else:
        console.print(
            Panel(
                f"[yellow]{checks_passed} passed, {checks_failed} failed"
                + (f", {checks_fixed} fixed" if checks_fixed else "")
                + "[/yellow]\n\n"
                + (
                    "Run [cyan]aicippy doctor --fix[/cyan] to attempt auto-repair."
                    if not fix
                    else "Some issues require manual intervention."
                ),
                title="Doctor Summary",
                border_style="yellow",
            )
        )
        raise typer.Exit(1)


@app.command()
def update(
    check_only: Annotated[
        bool,
        typer.Option(
            "--check",
            "-c",
            help="Only check for updates, don't install",
        ),
    ] = False,
) -> None:
    """
    Update AiCippy to the latest version (alias for upgrade).
    """
    upgrade(check_only=check_only)


if __name__ == "__main__":
    app()
