#!/usr/bin/env python3
"""
Smart Table CLI

Agent-friendly command-line helper for Smart Table API automation.

Features:
- Capabilities discovery
- Table and column management
- Row creation and cell patching
- Column run + estimate
- Sequence management + enrollment helpers
- Table webhook token + ingest helpers
- Bulk-job polling
- Optional Mongo-backed status/watch helpers

Environment variables:
- SMARTTABLE_API_URL   (default: https://app.autotouch.ai)
- SMARTTABLE_TOKEN     (developer key or JWT bearer, or pass --token)
- MONGODB_URI          (for status when using direct DB aggregation)
- MONGODB_DB_NAME      (default: autotouch)
"""

from __future__ import annotations

import argparse
import getpass
import importlib.resources as importlib_resources
import json
import os
import sys
import time
from pathlib import Path
from typing import Any, Callable, Dict, List, Optional, Tuple

import requests
from autotouch_cli.cli_contracts import (
    build_cli_manifest as build_cli_manifest_payload,
    build_cli_reference_markdown,
)
from autotouch_cli.commands.columns import (
    ColumnCommandRuntime as ColumnCommandHandlerRuntime,
    cmd_columns_create as cmd_columns_create_impl,
    cmd_columns_delete as cmd_columns_delete_impl,
    cmd_columns_estimate as cmd_columns_estimate_impl,
    cmd_columns_list as cmd_columns_list_impl,
    cmd_columns_move as cmd_columns_move_impl,
    cmd_columns_projections as cmd_columns_projections_impl,
    cmd_columns_run as cmd_columns_run_impl,
    cmd_columns_run_next as cmd_columns_run_next_impl,
    cmd_columns_stop as cmd_columns_stop_impl,
    cmd_columns_test_http_request as cmd_columns_test_http_request_impl,
    cmd_columns_update as cmd_columns_update_impl,
)
from autotouch_cli.commands.cells import (
    CellCommandRuntime as CellCommandHandlerRuntime,
    cmd_cells_get as cmd_cells_get_impl,
    cmd_cells_patch as cmd_cells_patch_impl,
    cmd_cells_schema as cmd_cells_schema_impl,
)
from autotouch_cli.commands.auth import (
    AuthCommandRuntime as AuthCommandHandlerRuntime,
    cmd_auth_bootstrap as cmd_auth_bootstrap_impl,
    cmd_auth_check as cmd_auth_check_impl,
    cmd_auth_clear as cmd_auth_clear_impl,
    cmd_auth_login as cmd_auth_login_impl,
    cmd_auth_status as cmd_auth_status_impl,
    cmd_auth_set_key as cmd_auth_set_key_impl,
    cmd_auth_show as cmd_auth_show_impl,
    cmd_auth_whoami as cmd_auth_whoami_impl,
)
from autotouch_cli.commands.jobs import (
    JobCommandRuntime as JobCommandHandlerRuntime,
    cmd_jobs_get as cmd_jobs_get_impl,
    cmd_jobs_list as cmd_jobs_list_impl,
    cmd_jobs_watch as cmd_jobs_watch_impl,
)
from autotouch_cli.commands.leads import (
    LeadCommandRuntime as LeadCommandHandlerRuntime,
    cmd_leads_count as cmd_leads_count_impl,
    cmd_leads_create as cmd_leads_create_impl,
    cmd_leads_filters_metadata as cmd_leads_filters_metadata_impl,
    cmd_leads_get as cmd_leads_get_impl,
    cmd_leads_query as cmd_leads_query_impl,
    cmd_leads_reassign_owner as cmd_leads_reassign_owner_impl,
    cmd_leads_research as cmd_leads_research_impl,
    cmd_leads_research_tables as cmd_leads_research_tables_impl,
    cmd_leads_stats as cmd_leads_stats_impl,
    cmd_leads_update as cmd_leads_update_impl,
    cmd_leads_update_status as cmd_leads_update_status_impl,
    cmd_leads_upsert_contact_channels as cmd_leads_upsert_contact_channels_impl,
)
from autotouch_cli.commands.linkedin import (
    LinkedInCommandRuntime as LinkedInCommandHandlerRuntime,
    cmd_linkedin_comments as cmd_linkedin_comments_impl,
    cmd_linkedin_limits as cmd_linkedin_limits_impl,
    cmd_linkedin_post as cmd_linkedin_post_impl,
    cmd_linkedin_posts as cmd_linkedin_posts_impl,
    cmd_linkedin_reactions as cmd_linkedin_reactions_impl,
    cmd_linkedin_recipe as cmd_linkedin_recipe_impl,
    cmd_linkedin_search as cmd_linkedin_search_impl,
    cmd_linkedin_search_params as cmd_linkedin_search_params_impl,
    cmd_linkedin_status as cmd_linkedin_status_impl,
)
from autotouch_cli.commands.rows import (
    RowCommandRuntime as RowCommandHandlerRuntime,
    cmd_rows_add as cmd_rows_add_impl,
    cmd_rows_dedupe as cmd_rows_dedupe_impl,
    cmd_rows_delete as cmd_rows_delete_impl,
    cmd_rows_get as cmd_rows_get_impl,
    cmd_rows_import_csv as cmd_rows_import_csv_impl,
    cmd_rows_import_rollback as cmd_rows_import_rollback_impl,
    cmd_rows_import_status as cmd_rows_import_status_impl,
    cmd_rows_import_verify as cmd_rows_import_verify_impl,
    cmd_rows_list as cmd_rows_list_impl,
)
from autotouch_cli.commands.search import (
    SearchCommandRuntime as SearchCommandHandlerRuntime,
    cmd_search_companies as cmd_search_companies_impl,
    cmd_search_filings as cmd_search_filings_impl,
    cmd_search_local_businesses as cmd_search_local_businesses_impl,
    cmd_search_news as cmd_search_news_impl,
    cmd_search_people as cmd_search_people_impl,
    cmd_search_recipe as cmd_search_recipe_impl,
    cmd_search_reviews as cmd_search_reviews_impl,
    cmd_search_similar_companies as cmd_search_similar_companies_impl,
)
from autotouch_cli.commands.sequences import (
    SequenceCommandRuntime as SequenceCommandHandlerRuntime,
    cmd_sequences_clone as cmd_sequences_clone_impl,
    cmd_sequences_create as cmd_sequences_create_impl,
    cmd_sequences_delete as cmd_sequences_delete_impl,
    cmd_sequences_delivery_status as cmd_sequences_delivery_status_impl,
    cmd_sequences_enroll as cmd_sequences_enroll_impl,
    cmd_sequences_get as cmd_sequences_get_impl,
    cmd_sequences_list as cmd_sequences_list_impl,
    cmd_sequences_pause_enrollment as cmd_sequences_pause_enrollment_impl,
    cmd_sequences_stats as cmd_sequences_stats_impl,
    cmd_sequences_status as cmd_sequences_status_impl,
    cmd_sequences_update as cmd_sequences_update_impl,
)
from autotouch_cli.commands.tables import (
    TableCommandRuntime as TableCommandHandlerRuntime,
    cmd_tables_create as cmd_tables_create_impl,
    cmd_tables_list as cmd_tables_list_impl,
    cmd_tables_update as cmd_tables_update_impl,
    cmd_tables_identity_get as cmd_tables_identity_get_impl,
    cmd_tables_identity_set as cmd_tables_identity_set_impl,
    cmd_tables_identity_clear as cmd_tables_identity_clear_impl,
)
from autotouch_cli.commands.tasks import (
    TaskCommandRuntime as TaskCommandHandlerRuntime,
    cmd_tasks_assignee_counts as cmd_tasks_assignee_counts_impl,
    cmd_tasks_bulk_delete as cmd_tasks_bulk_delete_impl,
    cmd_tasks_bulk_update as cmd_tasks_bulk_update_impl,
    cmd_tasks_count as cmd_tasks_count_impl,
    cmd_tasks_create as cmd_tasks_create_impl,
    cmd_tasks_delete as cmd_tasks_delete_impl,
    cmd_tasks_draft as cmd_tasks_draft_impl,
    cmd_tasks_get as cmd_tasks_get_impl,
    cmd_tasks_query as cmd_tasks_query_impl,
    cmd_tasks_recipe as cmd_tasks_recipe_impl,
    cmd_tasks_schedule_email as cmd_tasks_schedule_email_impl,
    cmd_tasks_stats as cmd_tasks_stats_impl,
    cmd_tasks_update as cmd_tasks_update_impl,
)
from autotouch_cli.commands.webhooks import (
    WebhookCommandRuntime as WebhookCommandHandlerRuntime,
    cmd_webhooks_configure_ingest as cmd_webhooks_configure_ingest_impl,
    cmd_webhooks_deliveries_attempts as cmd_webhooks_deliveries_attempts_impl,
    cmd_webhooks_deliveries_list as cmd_webhooks_deliveries_list_impl,
    cmd_webhooks_get as cmd_webhooks_get_impl,
    cmd_webhooks_ingest as cmd_webhooks_ingest_impl,
    cmd_webhooks_recipe as cmd_webhooks_recipe_impl,
    cmd_webhooks_rotate as cmd_webhooks_rotate_impl,
    cmd_webhooks_subscriptions_create as cmd_webhooks_subscriptions_create_impl,
    cmd_webhooks_subscriptions_delete as cmd_webhooks_subscriptions_delete_impl,
    cmd_webhooks_subscriptions_get as cmd_webhooks_subscriptions_get_impl,
    cmd_webhooks_subscriptions_list as cmd_webhooks_subscriptions_list_impl,
    cmd_webhooks_subscriptions_pause as cmd_webhooks_subscriptions_pause_impl,
    cmd_webhooks_subscriptions_resume as cmd_webhooks_subscriptions_resume_impl,
    cmd_webhooks_subscriptions_rotate_secret as cmd_webhooks_subscriptions_rotate_secret_impl,
    cmd_webhooks_subscriptions_test as cmd_webhooks_subscriptions_test_impl,
    cmd_webhooks_subscriptions_update as cmd_webhooks_subscriptions_update_impl,
)
from autotouch_cli.commands.prompts import (
    PromptCommandRuntime as PromptCommandHandlerRuntime,
    cmd_prompts_create as cmd_prompts_create_impl,
    cmd_prompts_delete as cmd_prompts_delete_impl,
    cmd_prompts_get as cmd_prompts_get_impl,
    cmd_prompts_list as cmd_prompts_list_impl,
    cmd_prompts_update as cmd_prompts_update_impl,
)
from autotouch_cli.commands.workspace_secrets import (
    WorkspaceSecretCommandRuntime as WorkspaceSecretCommandHandlerRuntime,
    cmd_workspace_secrets_delete as cmd_workspace_secrets_delete_impl,
    cmd_workspace_secrets_list as cmd_workspace_secrets_list_impl,
    cmd_workspace_secrets_set as cmd_workspace_secrets_set_impl,
)
from autotouch_cli.core.auth import (
    auth_headers as auth_headers_impl,
    env_api_key as env_api_key_impl,
    env_user_token as env_user_token_impl,
    looks_like_api_key as looks_like_api_key_impl,
    looks_like_jwt as looks_like_jwt_impl,
    mask_api_key as mask_api_key_impl,
    mask_secret as mask_secret_impl,
    print_missing_developer_auth_help as print_missing_developer_auth_help_impl,
    print_missing_user_session_help as print_missing_user_session_help_impl,
    resolve_token as resolve_token_impl,
    resolve_user_session_token as resolve_user_session_token_impl,
)
from autotouch_cli.core.config import (
    DEFAULT_API_URL,
    api_url as api_url_impl,
    config_path as config_path_impl,
    load_config as load_config_impl,
    load_env_files as load_env_files_impl,
    package_version as package_version_impl,
    save_config as save_config_impl,
)
from autotouch_cli.core.http import (
    ApiRequestError,
    request_api as request_api_impl,
    request_multipart_api as request_multipart_api_impl,
)
from autotouch_cli.core.io import (
    chunk_list as chunk_list_impl,
    dedupe_keep_order as dedupe_keep_order_impl,
    load_emails_from_file as load_emails_from_file_impl,
    load_json_input as load_json_input_impl,
    load_required_object_payload as load_required_object_payload_impl,
    load_string_values_from_file as load_string_values_from_file_impl,
    normalize_email_value as normalize_email_value_impl,
    normalize_string_value as normalize_string_value_impl,
    parse_email_payload as parse_email_payload_impl,
    parse_json_string as parse_json_string_impl,
    parse_string_list_payload as parse_string_list_payload_impl,
    split_cli_email_values as split_cli_email_values_impl,
    split_cli_string_values as split_cli_string_values_impl,
)
from autotouch_cli.core.output import (
    OUTPUT_MODES as OUTPUT_MODES_IMPL,
    apply_output_projection as apply_output_projection_impl,
    as_display as as_display_impl,
    emit_text as emit_text_impl,
    normalize_output_mode as normalize_output_mode_impl,
    normalize_output_projection as normalize_output_projection_impl,
    print_human as print_human_impl,
    print_json as print_json_impl,
    resolve_json_pointer_value as resolve_json_pointer_value_impl,
    resolve_output_select_path as resolve_output_select_path_impl,
    supports_color as supports_color_impl,
)
from autotouch_cli.core.csv_import import (
    build_import_verify_query_params as build_import_verify_query_params_impl,
    collect_import_assertions as collect_import_assertions_impl,
    create_rows_and_patch_records as create_rows_and_patch_records_impl,
    evaluate_import_assertions_on_records as evaluate_import_assertions_on_records_impl,
    has_import_assertions as has_import_assertions_impl,
    read_records_from_csv as read_records_from_csv_impl,
)
from autotouch_cli.core.validation import (
    extract_add_to_crm_field_mappings as _extract_add_to_crm_field_mappings,
    inspect_add_to_crm_field_mappings as _inspect_add_to_crm_field_mappings,
    validate_add_to_crm_create_payload as _validate_add_to_crm_create_payload,
    validate_column_contract_payload as _validate_column_contract_payload,
    load_column_for_run_precheck as _load_column_for_run_precheck_impl,
    load_columns_for_table_precheck as _load_columns_for_table_precheck_impl,
    find_add_to_crm_structured_mapping_issues as _find_add_to_crm_structured_mapping_issues,
    run_precheck_for_add_to_crm as _run_precheck_for_add_to_crm_impl,
)
from autotouch_cli.core.polling import (
    poll_import_until_terminal as poll_import_until_terminal_impl,
    poll_job_until_terminal as poll_job_until_terminal_impl,
)
from autotouch_cli.core.run import (
    build_projection_preflight_warnings as build_projection_preflight_warnings_impl,
    execute_run_flow as execute_run_flow_impl,
    extract_columns_list as extract_columns_list_impl,
    extract_jobs_list as extract_jobs_list_impl,
    is_json_enrichment_column as is_json_enrichment_column_impl,
    is_processed_cell_value as is_processed_cell_value_impl,
    normalize_run_payload as normalize_run_payload_impl,
    print_projection_preflight_warnings as print_projection_preflight_warnings_impl,
    resolve_column_key as resolve_column_key_impl,
    select_next_row_ids as select_next_row_ids_impl,
)
from autotouch_cli.mongo_status import mongo_client as _mongo_client
from autotouch_cli.mongo_status import status_counts as _status_counts
from autotouch_cli.sequence_support import (
    SequenceCommandInputError,
    SequenceRuntime,
    build_sequence_enroll_payload as build_sequence_enroll_payload_impl,
    collect_sequence_lead_ids as collect_sequence_lead_ids_impl,
    sequence_mutation_params as sequence_mutation_params_impl,
)
from autotouch_cli.templates import (
    COLUMN_CREATE_RECIPES,
    COLUMN_RECIPE_NOTES,
    COLUMN_RECIPE_TYPES,
    LINKEDIN_RECIPE_TYPES,
    LINKEDIN_RECIPES,
    SEARCH_RECIPE_TYPES,
    SEARCH_RECIPES,
    WORKFLOW_BLUEPRINT_TYPES,
    WORKFLOW_BLUEPRINTS,
    TemplateRuntime,
    emit_auth_schema,
    emit_cells_schema,
    emit_columns_recipe,
    emit_leads_recipe,
    emit_onboarding_schema,
    emit_org_context_schema,
    emit_personal_context_schema,
    emit_prompt_templates_schema,
    emit_workspace_secrets_schema,
    emit_rows_schema,
    emit_schema,
    emit_sequences_recipe,
    emit_tables_schema,
    emit_tasks_recipe,
    emit_webhooks_recipe,
    emit_workflows_list,
    emit_workflows_recipe,
    emit_workflows_scaffold,
)
from autotouch_shared.provider_registry import (
    ProviderContractValidationError,
    validate_column_provider_contract,
)
from autotouch_cli.exceptions import (
    AutotouchAPIError,
    AutotouchInputError,
    AutotouchCreditError,
    AutotouchTimeoutError,
    AutotouchNotFoundError,
)

try:
    from dotenv import load_dotenv  # type: ignore
except Exception:  # pragma: no cover
    load_dotenv = None  # type: ignore

# Global CLI state and bundled contract data.
DEFAULT_TIMEOUT_SECONDS = 30
ASSERTION_FAILURE_EXIT_CODE = 3
TERMINAL_JOB_STATUSES = {
    "completed",
    "partial",
    "error",
    "timed_out",
    "cancelled",
    "failed",
    "completed_with_errors",
}
NON_TERMINAL_JOB_STATUSES = {
    "queued",
    "distributing",
    "processing",
    "running",
    "pending",
}
PROJECTION_READINESS_SAMPLE_SIZE = 50
OUTPUT_MODE = "json"
OUTPUT_MODES = OUTPUT_MODES_IMPL
OUTPUT_SELECT: Optional[str] = None
OUTPUT_JSON_POINTER: Optional[str] = None
ANSI_PURPLE = "\033[38;5;141m"
ANSI_RESET = "\033[0m"
SETUP_BANNER = r"""
 █████  ██    ██ ████████  ██████  ████████  ██████  ██    ██  ██████ ██   ██
██   ██ ██    ██    ██    ██    ██    ██    ██    ██ ██    ██ ██      ██   ██
███████ ██    ██    ██    ██    ██    ██    ██    ██ ██    ██ ██      ███████
██   ██ ██    ██    ██    ██    ██    ██    ██    ██ ██    ██ ██      ██   ██
██   ██  ██████     ██     ██████     ██     ██████   ██████   ██████ ██   ██

                         AI CLI SETUP
"""

def _package_version() -> str:
    return package_version_impl()


CLI_VERSION = _package_version()


RUN_SOP_GUIDE: Dict[str, Any] = {
    "title": "Credit-safe enrichment SOP",
    "rules": [
        "Use --output json for automation; avoid parsing human-format output.",
        "Filter first before running billable columns.",
        "Estimate first with the same payload you plan to run.",
        "Start with a small firstN pilot before scaling.",
        "For exact bounded batches, prefer run-next with a small count.",
        "For add_to_crm, generate payload from columns recipe before create/update.",
        "Keep unprocessedOnly enabled unless you intentionally reprocess rows.",
        "Treat bulk job state as source of truth: queued/distributing/processing are non-terminal; completed/partial/error/cancelled are terminal.",
        "For JSON enrichment columns, run with --wait and verify terminal status before creating projection splits.",
        "Always inspect raw outputs before summary counts; parse by column dataType (json vs scalar).",
        "When summarizing enrichment outputs, parse by key precedence (phone: mobile_number -> phone_numbers[0].number -> primary_phone; email: response -> email -> work_email).",
    ],
    "clarifications": [
        "Email/phone enrichment does not require add_to_crm.",
        "add_to_crm is an optional export action to Leads CRM (non-billable).",
        "Projection columns backfill existing rows automatically after creation; no separate projection run is required.",
        "For custom LLM schemas, use strict field-map shape (no root type/properties wrapper; arrays must use {type:\"array\",items:...}).",
        "Do not report not_found from a single missing key when fallback keys exist.",
    ],
    "default_run_payload": {
        "scope": "filtered",
        "filters": {
            "mode": "and",
            "filters": [
                {"columnKey": "company_domain", "operator": "isNotEmpty"},
                {"columnKey": "country", "operator": "equals", "value": "United States"},
            ],
        },
        "unprocessedOnly": True,
        "firstN": 25,
    },
}


# -- Validation shims (logic lives in autotouch_cli.core.validation) ----------
# _extract_add_to_crm_field_mappings  — imported directly above
# _inspect_add_to_crm_field_mappings  — imported directly above
# _validate_add_to_crm_create_payload — imported directly above
# _find_add_to_crm_structured_mapping_issues — imported directly above


def _validate_column_contract_payload_or_exit(payload: Dict[str, Any]) -> None:
    _validate_column_contract_payload(payload)


def _load_columns_for_table_precheck(
    *,
    table_id: str,
    base_url: Optional[str],
    token: str,
    use_x_api_key: bool,
    timeout: int,
    verbose: bool,
) -> Optional[List[Dict[str, Any]]]:
    return _load_columns_for_table_precheck_impl(
        table_id=table_id,
        base_url=base_url,
        token=token,
        use_x_api_key=use_x_api_key,
        timeout=timeout,
        verbose=verbose,
        request_api=_request_api,
    )


def _load_column_for_run_precheck(
    *,
    table_id: str,
    column_id: str,
    base_url: Optional[str],
    token: str,
    use_x_api_key: bool,
    timeout: int,
    verbose: bool,
) -> Optional[Dict[str, Any]]:
    return _load_column_for_run_precheck_impl(
        table_id=table_id,
        column_id=column_id,
        base_url=base_url,
        token=token,
        use_x_api_key=use_x_api_key,
        timeout=timeout,
        verbose=verbose,
        request_api=_request_api,
    )


def _run_precheck_for_add_to_crm(
    *,
    args: argparse.Namespace,
    token: str,
    payload: Dict[str, Any],
) -> None:
    _run_precheck_for_add_to_crm_impl(
        args=args,
        token=token,
        payload=payload,
        request_api=_request_api,
        default_timeout=DEFAULT_TIMEOUT_SECONDS,
    )


# Compatibility shims over extracted `core/*` helpers.
def _load_env_files() -> None:
    load_env_files_impl(load_dotenv)


_load_env_files()


def _config_path() -> Path:
    return config_path_impl()


def _load_config() -> Dict[str, Any]:
    return load_config_impl()


def _save_config(data: Dict[str, Any]) -> Path:
    return save_config_impl(data)


def _mask_api_key(value: Optional[str]) -> str:
    return mask_api_key_impl(value)


def _mask_secret(value: Optional[str]) -> str:
    return mask_secret_impl(value)


def _api_url(value: Optional[str] = None) -> str:
    return api_url_impl(value, load_config_fn=_load_config, default_api_url=DEFAULT_API_URL)


def _looks_like_api_key(value: Optional[str]) -> bool:
    return looks_like_api_key_impl(value)


def _looks_like_jwt(value: Optional[str]) -> bool:
    return looks_like_jwt_impl(value)


def _env_user_token() -> Optional[str]:
    return env_user_token_impl()


def _env_api_key() -> Optional[str]:
    return env_api_key_impl()


def _print_missing_developer_auth_help() -> None:
    print_missing_developer_auth_help_impl()


def _print_missing_user_session_help() -> None:
    print_missing_user_session_help_impl()


def _resolve_token(explicit_token: Optional[str], required: bool = True) -> Optional[str]:
    return resolve_token_impl(
        explicit_token,
        required=required,
        load_config_fn=_load_config,
        env_user_token_fn=_env_user_token,
        env_api_key_fn=_env_api_key,
        print_missing_help_fn=_print_missing_developer_auth_help,
    )


def _resolve_user_session_token(explicit_token: Optional[str], required: bool = True) -> Optional[str]:
    return resolve_user_session_token_impl(
        explicit_token,
        required=required,
        load_config_fn=_load_config,
        looks_like_api_key_fn=_looks_like_api_key,
        env_user_token_fn=_env_user_token,
        print_missing_help_fn=_print_missing_user_session_help,
    )


def _auth_headers(token: Optional[str], use_x_api_key: bool = False) -> Dict[str, str]:
    return auth_headers_impl(token, use_x_api_key=use_x_api_key)


def _set_output_mode(mode: Optional[str]) -> None:
    global OUTPUT_MODE
    OUTPUT_MODE = normalize_output_mode_impl(mode, default="json")


def _set_output_projection(select_expr: Optional[str], json_pointer: Optional[str]) -> None:
    global OUTPUT_SELECT, OUTPUT_JSON_POINTER
    OUTPUT_SELECT, OUTPUT_JSON_POINTER = normalize_output_projection_impl(select_expr, json_pointer)


def _supports_color() -> bool:
    return supports_color_impl(isatty=bool(sys.stdout.isatty()), environ=os.environ)


def _as_display(value: Any) -> str:
    return as_display_impl(value)


def _resolve_output_select_path(value: Any, select_expr: str) -> Any:
    return resolve_output_select_path_impl(value, select_expr)


def _resolve_json_pointer_value(value: Any, pointer: str) -> Any:
    return resolve_json_pointer_value_impl(value, pointer)


def _apply_output_projection(data: Any) -> Any:
    return apply_output_projection_impl(
        data,
        output_select=OUTPUT_SELECT,
        output_json_pointer=OUTPUT_JSON_POINTER,
    )


def _emit_text(text: str = "", *, file: Any = None) -> None:
    emit_text_impl(text, file=file)


def _print_human(data: Any, *, file: Any = None) -> None:
    print_human_impl(data, file=file)


def _print_json(data: Any, compact: bool = False, *, file: Any = None, apply_projection: bool = True) -> None:
    print_json_impl(
        data,
        compact=compact,
        file=file,
        apply_projection=apply_projection,
        output_mode=OUTPUT_MODE,
        output_select=OUTPUT_SELECT,
        output_json_pointer=OUTPUT_JSON_POINTER,
    )


def _emit_progress_event(data: Any, *, compact: bool = False) -> None:
    if OUTPUT_MODE == "json":
        return
    _print_json(data, compact=compact, apply_projection=False)


def _parse_json_string(raw: str, context: str) -> Any:
    return parse_json_string_impl(raw, context)


def _load_json_input(
    *,
    inline_json: Optional[str],
    file_path: Optional[str],
    context: str,
    default: Any = None,
) -> Any:
    return load_json_input_impl(
        inline_json=inline_json,
        file_path=file_path,
        context=context,
        default=default,
    )


def _load_required_object_payload(args: argparse.Namespace, *, noun: str) -> Dict[str, Any]:
    return load_required_object_payload_impl(args, noun=noun)


def _normalize_email_value(value: Any) -> Optional[str]:
    return normalize_email_value_impl(value)


def _split_cli_email_values(raw_values: Optional[List[str]]) -> List[str]:
    return split_cli_email_values_impl(raw_values)


def _parse_email_payload(payload: Any, *, context: str) -> List[str]:
    return parse_email_payload_impl(payload, context=context)


def _load_emails_from_file(file_path: str, *, csv_column: Optional[str]) -> List[str]:
    return load_emails_from_file_impl(file_path, csv_column=csv_column)


def _dedupe_keep_order(values: List[str]) -> List[str]:
    return dedupe_keep_order_impl(values)


def _collect_blacklist_emails(args: argparse.Namespace) -> List[str]:
    emails: List[str] = []
    emails.extend(_split_cli_email_values(getattr(args, "email", None)))

    raw_emails_json = getattr(args, "emails_json", None)
    if raw_emails_json:
        payload = _parse_json_string(str(raw_emails_json), "emails")
        emails.extend(_parse_email_payload(payload, context="--emails-json"))

    emails_file = getattr(args, "emails_file", None)
    if emails_file:
        emails.extend(
            _load_emails_from_file(
                str(emails_file),
                csv_column=getattr(args, "emails_column", None),
            )
        )

    deduped = _dedupe_keep_order([email for email in emails if email])
    if not deduped:
        raise AutotouchInputError(
            "provide emails via --email, --emails-json, or --emails-file"
        )
    return deduped


def _chunk_list(values: List[str], size: int) -> List[List[str]]:
    return chunk_list_impl(values, size)


def _request_api(
    method: str,
    path: str,
    *,
    base_url: str,
    token: Optional[str],
    use_x_api_key: bool = False,
    params: Optional[Dict[str, Any]] = None,
    payload: Optional[Any] = None,
    form_payload: Optional[Dict[str, Any]] = None,
    files: Optional[Any] = None,
    extra_headers: Optional[Dict[str, str]] = None,
    timeout: int = DEFAULT_TIMEOUT_SECONDS,
    verbose: bool = False,
    raise_on_error: bool = False,
) -> Any:
    return request_api_impl(
        method,
        path,
        base_url=base_url,
        token=token,
        api_url_fn=_api_url,
        auth_headers_fn=_auth_headers,
        requests_module=requests,
        use_x_api_key=use_x_api_key,
        params=params,
        payload=payload,
        form_payload=form_payload,
        files=files,
        extra_headers=extra_headers,
        timeout=timeout,
        default_timeout_seconds=DEFAULT_TIMEOUT_SECONDS,
        verbose=verbose,
        error_body_writer=lambda body: _print_json(
            body,
            compact=False,
            file=sys.stderr,
            apply_projection=False,
        ),
        raise_on_error=raise_on_error,
    )


def _normalize_string_value(value: Any) -> Optional[str]:
    return normalize_string_value_impl(value)


def _split_cli_string_values(raw_values: Optional[List[str]]) -> List[str]:
    return split_cli_string_values_impl(raw_values)


def _parse_string_list_payload(
    payload: Any,
    *,
    context: str,
    array_key: Optional[str] = None,
    key: Optional[str] = None,
) -> List[str]:
    return parse_string_list_payload_impl(
        payload,
        context=context,
        array_key=array_key,
        key=key,
    )


def _load_string_values_from_file(
    file_path: str,
    *,
    context: str,
    array_key: str,
    csv_column: Optional[str] = None,
) -> List[str]:
    return load_string_values_from_file_impl(
        file_path,
        context=context,
        array_key=array_key,
        csv_column=csv_column,
    )


def _request_multipart_api(
    method: str,
    path: str,
    *,
    base_url: str,
    token: Optional[str],
    file_path: str,
    file_field: str = "file",
    form_fields: Optional[Dict[str, Any]] = None,
    params: Optional[Dict[str, Any]] = None,
    use_x_api_key: bool = False,
    timeout: int = DEFAULT_TIMEOUT_SECONDS,
    verbose: bool = False,
) -> Any:
    return request_multipart_api_impl(
        method,
        path,
        base_url=base_url,
        token=token,
        file_path=file_path,
        api_url_fn=_api_url,
        auth_headers_fn=_auth_headers,
        requests_module=requests,
        file_field=file_field,
        form_fields=form_fields,
        params=params,
        use_x_api_key=use_x_api_key,
        timeout=timeout,
        default_timeout_seconds=DEFAULT_TIMEOUT_SECONDS,
        verbose=verbose,
        error_body_writer=lambda body: _print_json(
            body,
            compact=False,
            file=sys.stderr,
            apply_projection=False,
        ),
    )


def _normalize_run_payload(args: argparse.Namespace) -> Dict[str, Any]:
    return normalize_run_payload_impl(args, load_json_input_fn=_load_json_input)


def _resolve_column_key(
    *,
    table_id: str,
    column_id: str,
    base_url: str,
    token: str,
    use_x_api_key: bool,
    timeout: int,
    verbose: bool,
) -> str:
    return resolve_column_key_impl(
        table_id=table_id,
        column_id=column_id,
        base_url=base_url,
        token=token,
        use_x_api_key=use_x_api_key,
        timeout=timeout,
        verbose=verbose,
        request_api_fn=_request_api,
    )


def _extract_columns_list(columns_raw: Any) -> List[Dict[str, Any]]:
    return extract_columns_list_impl(columns_raw)


def _extract_jobs_list(jobs_raw: Any) -> List[Dict[str, Any]]:
    return extract_jobs_list_impl(jobs_raw)


def _is_json_enrichment_column(column: Dict[str, Any]) -> bool:
    return is_json_enrichment_column_impl(column)


def _is_processed_cell_value(value: Any) -> bool:
    return is_processed_cell_value_impl(value)


def _print_projection_preflight_warnings(warnings: List[Dict[str, Any]]) -> None:
    print_projection_preflight_warnings_impl(warnings)


def _build_projection_preflight_warnings(
    *,
    table_id: str,
    payload: Dict[str, Any],
    base_url: str,
    token: str,
    use_x_api_key: bool,
    timeout: int,
    verbose: bool,
) -> List[Dict[str, Any]]:
    return build_projection_preflight_warnings_impl(
        table_id=table_id,
        payload=payload,
        base_url=base_url,
        token=token,
        use_x_api_key=use_x_api_key,
        timeout=timeout,
        verbose=verbose,
        request_api_fn=_request_api,
        non_terminal_job_statuses=NON_TERMINAL_JOB_STATUSES,
        terminal_job_statuses=TERMINAL_JOB_STATUSES,
        projection_readiness_sample_size=PROJECTION_READINESS_SAMPLE_SIZE,
    )


def _select_next_row_ids(
    *,
    table_id: str,
    column_id: str,
    count: int,
    filters: Optional[Dict[str, Any]],
    unprocessed_only: bool,
    page_size: int,
    base_url: str,
    token: str,
    use_x_api_key: bool,
    timeout: int,
    verbose: bool,
) -> Dict[str, Any]:
    return select_next_row_ids_impl(
        table_id=table_id,
        column_id=column_id,
        count=count,
        filters=filters,
        unprocessed_only=unprocessed_only,
        page_size=page_size,
        base_url=base_url,
        token=token,
        use_x_api_key=use_x_api_key,
        timeout=timeout,
        verbose=verbose,
        request_api_fn=_request_api,
    )


def _execute_run_flow(
    *,
    args: argparse.Namespace,
    token: str,
    payload: Dict[str, Any],
    context: Optional[Dict[str, Any]] = None,
) -> None:
    execute_run_flow_impl(
        args=args,
        token=token,
        payload=payload,
        context=context,
        request_api_fn=_request_api,
        print_json_fn=_print_json,
        emit_progress_event_fn=_emit_progress_event,
        api_url_fn=_api_url,
        poll_job_fn=_poll_job,
        default_timeout_seconds=DEFAULT_TIMEOUT_SECONDS,
    )


def _create_rows_and_patch_records(
    *,
    table_id: str,
    records: Optional[List[Dict[str, Any]]],
    blank_count: int,
    base_url: str,
    token: str,
    use_x_api_key: bool,
    timeout: int,
    verbose: bool,
    detect_types: bool,
) -> Dict[str, Any]:
    return create_rows_and_patch_records_impl(
        table_id=table_id,
        records=records,
        blank_count=blank_count,
        base_url=base_url,
        token=token,
        use_x_api_key=use_x_api_key,
        timeout=timeout,
        verbose=verbose,
        detect_types=detect_types,
        request_api=_request_api,
    )


def _read_records_from_csv(
    file_path: str,
    *,
    delimiter: str = ",",
    encoding: str = "utf-8",
    trim_headers: bool = True,
    trim_values: bool = False,
    empty_as_null: bool = True,
    skip_empty_rows: bool = True,
    limit: Optional[int] = None,
) -> List[Dict[str, Any]]:
    return read_records_from_csv_impl(
        file_path,
        delimiter=delimiter,
        encoding=encoding,
        trim_headers=trim_headers,
        trim_values=trim_values,
        empty_as_null=empty_as_null,
        skip_empty_rows=skip_empty_rows,
        limit=limit,
    )


def _collect_import_assertions(args: argparse.Namespace) -> Dict[str, Any]:
    return collect_import_assertions_impl(args)


def _has_import_assertions(assertions: Dict[str, Any]) -> bool:
    return has_import_assertions_impl(assertions)


def _evaluate_import_assertions_on_records(
    *,
    records: List[Dict[str, Any]],
    assertions: Dict[str, Any],
    source: str,
) -> Dict[str, Any]:
    return evaluate_import_assertions_on_records_impl(
        records=records,
        assertions=assertions,
        source=source,
    )


def _build_import_verify_query_params(assertions: Dict[str, Any]) -> Dict[str, Any]:
    return build_import_verify_query_params_impl(assertions)


def _job_snapshot(job_doc: Dict[str, Any]) -> Dict[str, Any]:
    return {
        "job_id": job_doc.get("job_id") or job_doc.get("jobId"),
        "status": job_doc.get("status"),
        "provider": job_doc.get("provider"),
        "processed_rows": int(job_doc.get("processed_rows") or 0),
        "total_rows": int(job_doc.get("total_rows") or 0),
        "error_rows": int(job_doc.get("error_rows") or 0),
        "skipped_rows": int(job_doc.get("skipped_rows") or 0),
        "pending_batches": int(job_doc.get("pending_batches") or 0),
        "terminal_reason": job_doc.get("terminal_reason"),
        "credits_used": float(job_doc.get("credits_used") or 0.0),
        "billable": bool(job_doc.get("billable", False)),
        "updated_at": job_doc.get("updated_at"),
    }


def _poll_job(
    *,
    job_id: str,
    base_url: str,
    token: str,
    use_x_api_key: bool,
    interval_seconds: int,
    wait_timeout_seconds: int,
    request_timeout_seconds: int,
    verbose: bool,
    compact: bool,
    once: bool = False,
    print_updates: bool = True,
) -> Dict[str, Any]:
    url = f"{_api_url(base_url)}/api/bulk-jobs/{job_id}"
    headers = _auth_headers(token, use_x_api_key=use_x_api_key)
    return poll_job_until_terminal_impl(
        job_id=job_id,
        url=url,
        headers=headers,
        interval_seconds=interval_seconds,
        wait_timeout_seconds=wait_timeout_seconds,
        request_timeout_seconds=request_timeout_seconds,
        verbose=verbose,
        compact=compact,
        once=once,
        print_updates=print_updates,
        terminal_statuses=TERMINAL_JOB_STATUSES,
        job_snapshot_fn=_job_snapshot,
        emit_progress_fn=lambda data, compact_value: _emit_progress_event(data, compact=compact_value),
        error_body_writer=lambda body: _print_json(
            body,
            compact=False,
            file=sys.stderr,
            apply_projection=False,
        )
        if isinstance(body, (dict, list))
        else print(str(body), file=sys.stderr),
        requests_get=requests.get,
    )


def _poll_import_status(
    *,
    table_id: str,
    task_id: str,
    base_url: str,
    token: str,
    use_x_api_key: bool,
    interval_seconds: int,
    wait_timeout_seconds: int,
    request_timeout_seconds: int,
    verbose: bool,
    compact: bool,
) -> Dict[str, Any]:
    return poll_import_until_terminal_impl(
        table_id=table_id,
        task_id=task_id,
        base_url=base_url,
        token=token,
        use_x_api_key=use_x_api_key,
        interval_seconds=interval_seconds,
        wait_timeout_seconds=wait_timeout_seconds,
        request_timeout_seconds=request_timeout_seconds,
        verbose=verbose,
        compact=compact,
        output_mode=OUTPUT_MODE,
        emit_progress_fn=lambda data, compact_value: _emit_progress_event(data, compact=compact_value),
        request_api_fn=_request_api,
    )


# ---------------------------------------------------------------------------
# Runtime factories
# ---------------------------------------------------------------------------

def _template_runtime() -> TemplateRuntime:
    return TemplateRuntime(
        output_mode=OUTPUT_MODE,
        print_json=lambda data, compact=False: _print_json(data, compact=compact),
        emit_text=_emit_text,
    )


def _sequence_runtime() -> SequenceRuntime:
    return SequenceRuntime(
        load_json_input=_load_json_input,
        split_cli_string_values=_split_cli_string_values,
        parse_json_string=_parse_json_string,
        parse_string_list_payload=_parse_string_list_payload,
        load_string_values_from_file=_load_string_values_from_file,
        dedupe_keep_order=_dedupe_keep_order,
    )


def _sequence_mutation_params(args: argparse.Namespace) -> Optional[Dict[str, Any]]:
    return sequence_mutation_params_impl(args)


def _collect_sequence_lead_ids(args: argparse.Namespace) -> List[str]:
    return collect_sequence_lead_ids_impl(args, runtime=_sequence_runtime())


def _build_sequence_enroll_payload(args: argparse.Namespace) -> Dict[str, Any]:
    return build_sequence_enroll_payload_impl(args, runtime=_sequence_runtime())


def _sequence_command_runtime() -> SequenceCommandHandlerRuntime:
    return SequenceCommandHandlerRuntime(
        resolve_token=_resolve_token,
        request_api=_request_api,
        print_json=lambda data, compact=False: _print_json(data, compact=compact),
        load_json_input=_load_json_input,
        api_url=_api_url,
        emit_progress_event=lambda data, compact=False: _emit_progress_event(data, compact=compact),
        poll_job=_poll_job,
        default_timeout_seconds=DEFAULT_TIMEOUT_SECONDS,
        sequence_runtime_factory=_sequence_runtime,
    )


def _column_command_runtime() -> ColumnCommandHandlerRuntime:
    return ColumnCommandHandlerRuntime(
        resolve_token=_resolve_token,
        request_api=_request_api,
        print_json=lambda data, compact=False: _print_json(data, compact=compact),
        load_json_input=_load_json_input,
        validate_column_contract_payload_or_exit=_validate_column_contract_payload_or_exit,
        validate_add_to_crm_create_payload=_validate_add_to_crm_create_payload,
        build_projection_preflight_warnings=_build_projection_preflight_warnings,
        print_projection_preflight_warnings=_print_projection_preflight_warnings,
        normalize_run_payload=_normalize_run_payload,
        run_precheck_for_add_to_crm=_run_precheck_for_add_to_crm,
        execute_run_flow=_execute_run_flow,
        select_next_row_ids=_select_next_row_ids,
    )


def _cell_command_runtime() -> CellCommandHandlerRuntime:
    return CellCommandHandlerRuntime(
        resolve_token=_resolve_token,
        request_api=_request_api,
        print_json=lambda data, compact=False: _print_json(data, compact=compact),
        load_json_input=_load_json_input,
        emit_cells_schema=emit_cells_schema,
        template_runtime_factory=_template_runtime,
    )


def _row_command_runtime() -> RowCommandHandlerRuntime:
    return RowCommandHandlerRuntime(
        resolve_token=_resolve_token,
        request_api=_request_api,
        request_multipart_api=_request_multipart_api,
        print_json=lambda data, compact=False: _print_json(data, compact=compact),
        load_json_input=_load_json_input,
        api_url=_api_url,
        create_rows_and_patch_records=_create_rows_and_patch_records,
        read_records_from_csv=_read_records_from_csv,
        collect_import_assertions=_collect_import_assertions,
        has_import_assertions=_has_import_assertions,
        evaluate_import_assertions_on_records=_evaluate_import_assertions_on_records,
        build_import_verify_query_params=_build_import_verify_query_params,
        poll_import_status=_poll_import_status,
        assertion_failure_exit_code=ASSERTION_FAILURE_EXIT_CODE,
        default_timeout_seconds=DEFAULT_TIMEOUT_SECONDS,
    )


def _table_command_runtime() -> TableCommandHandlerRuntime:
    return TableCommandHandlerRuntime(
        resolve_token=_resolve_token,
        request_api=_request_api,
        print_json=lambda data, compact=False: _print_json(data, compact=compact),
        load_json_input=_load_json_input,
    )


def _task_command_runtime() -> TaskCommandHandlerRuntime:
    return TaskCommandHandlerRuntime(
        resolve_token=_resolve_token,
        request_api=_request_api,
        print_json=lambda data, compact=False: _print_json(data, compact=compact),
        load_required_object_payload=_load_required_object_payload,
        load_json_input=_load_json_input,
        emit_tasks_recipe=emit_tasks_recipe,
        template_runtime_factory=_template_runtime,
    )


def _webhook_command_runtime() -> WebhookCommandHandlerRuntime:
    return WebhookCommandHandlerRuntime(
        resolve_token=_resolve_token,
        request_api=_request_api,
        print_json=lambda data, compact=False: _print_json(data, compact=compact),
        load_json_input=_load_json_input,
        emit_webhooks_recipe=emit_webhooks_recipe,
        template_runtime_factory=_template_runtime,
    )


def _auth_command_runtime() -> AuthCommandHandlerRuntime:
    return AuthCommandHandlerRuntime(
        resolve_token=_resolve_token,
        resolve_user_session_token=_resolve_user_session_token,
        request_api=_request_api,
        print_json=lambda data, compact=False: _print_json(data, compact=compact),
        load_json_input=_load_json_input,
        api_url=_api_url,
        load_config=_load_config,
        save_config=_save_config,
        config_path=_config_path,
        mask_api_key=_mask_api_key,
        mask_secret=_mask_secret,
        normalize_string_value=_normalize_string_value,
        env_api_key=_env_api_key,
        env_user_token=_env_user_token,
    )


def _prompt_command_runtime() -> PromptCommandHandlerRuntime:
    return PromptCommandHandlerRuntime(
        resolve_token=_resolve_token,
        request_api=_request_api,
        print_json=lambda data, compact=False: _print_json(data, compact=compact),
        load_json_input=_load_json_input,
    )


def _workspace_secret_command_runtime() -> WorkspaceSecretCommandHandlerRuntime:
    return WorkspaceSecretCommandHandlerRuntime(
        resolve_token=_resolve_token,
        request_api=_request_api,
        print_json=lambda data, compact=False: _print_json(data, compact=compact),
        load_json_input=_load_json_input,
        normalize_string_value=_normalize_string_value,
    )


def _job_command_runtime() -> JobCommandHandlerRuntime:
    return JobCommandHandlerRuntime(
        resolve_token=_resolve_token,
        request_api=_request_api,
        print_json=lambda data, compact=False: _print_json(data, compact=compact),
        poll_job=_poll_job,
        api_url=_api_url,
        default_timeout_seconds=DEFAULT_TIMEOUT_SECONDS,
    )


def _lead_command_runtime() -> LeadCommandHandlerRuntime:
    return LeadCommandHandlerRuntime(
        resolve_token=_resolve_token,
        request_api=_request_api,
        print_json=lambda data, compact=False: _print_json(data, compact=compact),
        load_required_object_payload=_load_required_object_payload,
        load_json_input=_load_json_input,
        split_cli_string_values=_split_cli_string_values,
        parse_string_list_payload=_parse_string_list_payload,
        dedupe_keep_order=_dedupe_keep_order,
        normalize_string_value=_normalize_string_value,
    )


def _search_command_runtime() -> SearchCommandHandlerRuntime:
    return SearchCommandHandlerRuntime(
        resolve_token=_resolve_token,
        request_api=_request_api,
        print_json=lambda data, compact=False: _print_json(data, compact=compact),
        load_json_input=_load_json_input,
    )


def _linkedin_command_runtime() -> LinkedInCommandHandlerRuntime:
    return LinkedInCommandHandlerRuntime(
        resolve_token=_resolve_token,
        request_api=_request_api,
        print_json=lambda data, compact=False: _print_json(data, compact=compact),
        load_json_input=_load_json_input,
    )


# ---------------------------------------------------------------------------
# Command registry — replaces ~110 thin shim functions
# ---------------------------------------------------------------------------

_COMMAND_REGISTRY: Dict[str, Tuple[Callable, Callable]] = {}


def _register(name: str, handler: Callable, runtime_factory: Callable) -> None:
    """Register *handler* under *name* with the given *runtime_factory*."""
    _COMMAND_REGISTRY[name] = (handler, runtime_factory)


def _dispatch(args: argparse.Namespace) -> None:
    """Generic dispatcher set as ``func=`` by the parser.

    Looks up ``args._command_name`` in the registry, instantiates the
    runtime, and delegates to the handler.
    """
    name = getattr(args, "_command_name", None)
    if name is None:
        raise AutotouchInputError("internal error: missing _command_name on parsed args")
    entry = _COMMAND_REGISTRY.get(name)
    if entry is None:
        raise AutotouchInputError(f"internal error: unknown command {name!r}")
    handler, runtime_factory = entry
    handler(args, runtime=runtime_factory())


def _bound(name: str) -> Callable:
    """Return a callable ``(args) -> None`` that dispatches *name* via the registry."""
    def _fn(args: argparse.Namespace) -> None:
        args._command_name = name  # type: ignore[attr-defined]
        _dispatch(args)
    return _fn


# -- Auth commands --
_register("auth_check", cmd_auth_check_impl, _auth_command_runtime)
_register("auth_status", cmd_auth_status_impl, _auth_command_runtime)
_register("auth_set_key", cmd_auth_set_key_impl, _auth_command_runtime)
_register("auth_bootstrap", cmd_auth_bootstrap_impl, _auth_command_runtime)
_register("auth_login", cmd_auth_login_impl, _auth_command_runtime)
_register("auth_show", cmd_auth_show_impl, _auth_command_runtime)
_register("auth_whoami", cmd_auth_whoami_impl, _auth_command_runtime)
_register("auth_clear", cmd_auth_clear_impl, _auth_command_runtime)

# -- Prompts commands --
_register("prompts_list", cmd_prompts_list_impl, _prompt_command_runtime)
_register("prompts_get", cmd_prompts_get_impl, _prompt_command_runtime)
_register("prompts_create", cmd_prompts_create_impl, _prompt_command_runtime)
_register("prompts_update", cmd_prompts_update_impl, _prompt_command_runtime)
_register("prompts_delete", cmd_prompts_delete_impl, _prompt_command_runtime)

# -- Workspace secrets commands --
_register("workspace_secrets_list", cmd_workspace_secrets_list_impl, _workspace_secret_command_runtime)
_register("workspace_secrets_set", cmd_workspace_secrets_set_impl, _workspace_secret_command_runtime)
_register("workspace_secrets_delete", cmd_workspace_secrets_delete_impl, _workspace_secret_command_runtime)

# -- Table commands --
_register("tables_list", cmd_tables_list_impl, _table_command_runtime)
_register("tables_create", cmd_tables_create_impl, _table_command_runtime)
_register("tables_update", cmd_tables_update_impl, _table_command_runtime)
_register("tables_identity_get", cmd_tables_identity_get_impl, _table_command_runtime)
_register("tables_identity_set", cmd_tables_identity_set_impl, _table_command_runtime)
_register("tables_identity_clear", cmd_tables_identity_clear_impl, _table_command_runtime)

# -- Row commands --
_register("rows_list", cmd_rows_list_impl, _row_command_runtime)
_register("rows_get", cmd_rows_get_impl, _row_command_runtime)
_register("rows_add", cmd_rows_add_impl, _row_command_runtime)
_register("rows_delete", cmd_rows_delete_impl, _row_command_runtime)
_register("rows_import_csv", cmd_rows_import_csv_impl, _row_command_runtime)
_register("rows_import_status", cmd_rows_import_status_impl, _row_command_runtime)
_register("rows_import_verify", cmd_rows_import_verify_impl, _row_command_runtime)
_register("rows_import_rollback", cmd_rows_import_rollback_impl, _row_command_runtime)
_register("rows_dedupe", cmd_rows_dedupe_impl, _row_command_runtime)

# -- Cell commands --
_register("cells_get", cmd_cells_get_impl, _cell_command_runtime)
_register("cells_patch", cmd_cells_patch_impl, _cell_command_runtime)
_register("cells_schema", cmd_cells_schema_impl, _cell_command_runtime)

# -- Column commands --
_register("columns_list", cmd_columns_list_impl, _column_command_runtime)
_register("columns_create", cmd_columns_create_impl, _column_command_runtime)
_register("columns_update", cmd_columns_update_impl, _column_command_runtime)
_register("columns_move", cmd_columns_move_impl, _column_command_runtime)
_register("columns_delete", cmd_columns_delete_impl, _column_command_runtime)
_register("columns_projections", cmd_columns_projections_impl, _column_command_runtime)
_register("columns_test_http_request", cmd_columns_test_http_request_impl, _column_command_runtime)
_register("columns_run", cmd_columns_run_impl, _column_command_runtime)
_register("columns_run_next", cmd_columns_run_next_impl, _column_command_runtime)
_register("columns_stop", cmd_columns_stop_impl, _column_command_runtime)
_register("columns_estimate", cmd_columns_estimate_impl, _column_command_runtime)

# -- Sequence commands --
_register("sequences_list", cmd_sequences_list_impl, _sequence_command_runtime)
_register("sequences_get", cmd_sequences_get_impl, _sequence_command_runtime)
_register("sequences_stats", cmd_sequences_stats_impl, _sequence_command_runtime)
_register("sequences_delivery_status", cmd_sequences_delivery_status_impl, _sequence_command_runtime)
_register("sequences_create", cmd_sequences_create_impl, _sequence_command_runtime)
_register("sequences_update", cmd_sequences_update_impl, _sequence_command_runtime)
_register("sequences_delete", cmd_sequences_delete_impl, _sequence_command_runtime)
_register("sequences_clone", cmd_sequences_clone_impl, _sequence_command_runtime)
_register("sequences_status", cmd_sequences_status_impl, _sequence_command_runtime)
_register("sequences_enroll", cmd_sequences_enroll_impl, _sequence_command_runtime)
_register("sequences_pause_enrollment", cmd_sequences_pause_enrollment_impl, _sequence_command_runtime)

# -- Job commands --
_register("jobs_get", cmd_jobs_get_impl, _job_command_runtime)
_register("jobs_list", cmd_jobs_list_impl, _job_command_runtime)
_register("jobs_watch", cmd_jobs_watch_impl, _job_command_runtime)

# -- Lead commands --
_register("leads_query", cmd_leads_query_impl, _lead_command_runtime)
_register("leads_get", cmd_leads_get_impl, _lead_command_runtime)
_register("leads_create", cmd_leads_create_impl, _lead_command_runtime)
_register("leads_update", cmd_leads_update_impl, _lead_command_runtime)
_register("leads_upsert_contact_channels", cmd_leads_upsert_contact_channels_impl, _lead_command_runtime)
_register("leads_count", cmd_leads_count_impl, _lead_command_runtime)
_register("leads_stats", cmd_leads_stats_impl, _lead_command_runtime)
_register("leads_research", cmd_leads_research_impl, _lead_command_runtime)
_register("leads_filters_metadata", cmd_leads_filters_metadata_impl, _lead_command_runtime)
_register("leads_research_tables", cmd_leads_research_tables_impl, _lead_command_runtime)
_register("leads_update_status", cmd_leads_update_status_impl, _lead_command_runtime)
_register("leads_reassign_owner", cmd_leads_reassign_owner_impl, _lead_command_runtime)

# -- Task commands --
_register("tasks_query", cmd_tasks_query_impl, _task_command_runtime)
_register("tasks_count", cmd_tasks_count_impl, _task_command_runtime)
_register("tasks_assignee_counts", cmd_tasks_assignee_counts_impl, _task_command_runtime)
_register("tasks_get", cmd_tasks_get_impl, _task_command_runtime)
_register("tasks_stats", cmd_tasks_stats_impl, _task_command_runtime)
_register("tasks_create", cmd_tasks_create_impl, _task_command_runtime)
_register("tasks_update", cmd_tasks_update_impl, _task_command_runtime)
_register("tasks_delete", cmd_tasks_delete_impl, _task_command_runtime)
_register("tasks_bulk_update", cmd_tasks_bulk_update_impl, _task_command_runtime)
_register("tasks_bulk_delete", cmd_tasks_bulk_delete_impl, _task_command_runtime)
_register("tasks_draft", cmd_tasks_draft_impl, _task_command_runtime)
_register("tasks_schedule_email", cmd_tasks_schedule_email_impl, _task_command_runtime)
_register("tasks_recipe", cmd_tasks_recipe_impl, _task_command_runtime)

# -- Webhook commands --
_register("webhooks_get", cmd_webhooks_get_impl, _webhook_command_runtime)
_register("webhooks_rotate", cmd_webhooks_rotate_impl, _webhook_command_runtime)
_register("webhooks_ingest", cmd_webhooks_ingest_impl, _webhook_command_runtime)
_register("webhooks_recipe", cmd_webhooks_recipe_impl, _webhook_command_runtime)
_register("webhooks_configure_ingest", cmd_webhooks_configure_ingest_impl, _webhook_command_runtime)
_register("webhooks_subscriptions_list", cmd_webhooks_subscriptions_list_impl, _webhook_command_runtime)
_register("webhooks_subscriptions_create", cmd_webhooks_subscriptions_create_impl, _webhook_command_runtime)
_register("webhooks_subscriptions_get", cmd_webhooks_subscriptions_get_impl, _webhook_command_runtime)
_register("webhooks_subscriptions_update", cmd_webhooks_subscriptions_update_impl, _webhook_command_runtime)
_register("webhooks_subscriptions_delete", cmd_webhooks_subscriptions_delete_impl, _webhook_command_runtime)
_register("webhooks_subscriptions_pause", cmd_webhooks_subscriptions_pause_impl, _webhook_command_runtime)
_register("webhooks_subscriptions_resume", cmd_webhooks_subscriptions_resume_impl, _webhook_command_runtime)
_register("webhooks_subscriptions_rotate_secret", cmd_webhooks_subscriptions_rotate_secret_impl, _webhook_command_runtime)
_register("webhooks_subscriptions_test", cmd_webhooks_subscriptions_test_impl, _webhook_command_runtime)
_register("webhooks_deliveries_list", cmd_webhooks_deliveries_list_impl, _webhook_command_runtime)
_register("webhooks_deliveries_attempts", cmd_webhooks_deliveries_attempts_impl, _webhook_command_runtime)

# -- Search commands --
_register("search_companies", cmd_search_companies_impl, _search_command_runtime)
_register("search_people", cmd_search_people_impl, _search_command_runtime)
_register("search_similar_companies", cmd_search_similar_companies_impl, _search_command_runtime)
_register("search_news", cmd_search_news_impl, _search_command_runtime)
_register("search_local_businesses", cmd_search_local_businesses_impl, _search_command_runtime)
_register("search_reviews", cmd_search_reviews_impl, _search_command_runtime)
_register("search_filings", cmd_search_filings_impl, _search_command_runtime)

# -- LinkedIn commands --
_register("linkedin_status", cmd_linkedin_status_impl, _linkedin_command_runtime)
_register("linkedin_limits", cmd_linkedin_limits_impl, _linkedin_command_runtime)
_register("linkedin_search", cmd_linkedin_search_impl, _linkedin_command_runtime)
_register("linkedin_search_params", cmd_linkedin_search_params_impl, _linkedin_command_runtime)
_register("linkedin_posts", cmd_linkedin_posts_impl, _linkedin_command_runtime)
_register("linkedin_post", cmd_linkedin_post_impl, _linkedin_command_runtime)
_register("linkedin_comments", cmd_linkedin_comments_impl, _linkedin_command_runtime)
_register("linkedin_reactions", cmd_linkedin_reactions_impl, _linkedin_command_runtime)

# -- Template emitter commands (use _template_runtime) --
_register("schema", emit_schema, _template_runtime)
_register("auth_schema", emit_auth_schema, _template_runtime)
_register("onboarding_schema", emit_onboarding_schema, _template_runtime)
_register("org_context_schema", emit_org_context_schema, _template_runtime)
_register("personal_context_schema", emit_personal_context_schema, _template_runtime)
_register("prompts_schema", emit_prompt_templates_schema, _template_runtime)
_register("workspace_secrets_schema", emit_workspace_secrets_schema, _template_runtime)
_register("tables_schema", emit_tables_schema, _template_runtime)
_register("rows_schema", emit_rows_schema, _template_runtime)
_register("columns_recipe", emit_columns_recipe, _template_runtime)
_register("sequences_recipe", emit_sequences_recipe, _template_runtime)
_register("leads_recipe", emit_leads_recipe, _template_runtime)
_register("workflows_list", emit_workflows_list, _template_runtime)
_register("workflows_recipe", emit_workflows_recipe, _template_runtime)
_register("workflows_scaffold", emit_workflows_scaffold, _template_runtime)

# -- Special-case commands that pass extra kwargs --


def _cmd_search_recipe_dispatch(args: argparse.Namespace, *, runtime: SearchCommandHandlerRuntime) -> None:
    cmd_search_recipe_impl(args, runtime=runtime, recipes=SEARCH_RECIPES)


def _cmd_linkedin_recipe_dispatch(args: argparse.Namespace, *, runtime: LinkedInCommandHandlerRuntime) -> None:
    cmd_linkedin_recipe_impl(args, runtime=runtime, recipes=LINKEDIN_RECIPES)


_register("search_recipe", _cmd_search_recipe_dispatch, _search_command_runtime)
_register("linkedin_recipe", _cmd_linkedin_recipe_dispatch, _linkedin_command_runtime)


# ---------------------------------------------------------------------------
# Standalone command functions (not shims — have their own logic)
# ---------------------------------------------------------------------------

# Standalone inline command helpers and payload builders.
def _print_setup_banner() -> None:
    banner = SETUP_BANNER.rstrip()
    if _supports_color():
        for line in banner.splitlines():
            if line.strip():
                print(f"{ANSI_PURPLE}{line}{ANSI_RESET}")
            else:
                print("")
    else:
        print(banner)
    print("Preview: configure your key once, then drive Smart Table from CLI or agents.")
    print("")


def _resolve_setup_api_key(args: argparse.Namespace) -> str:
    explicit = str(getattr(args, "api_key", "") or "").strip()
    if explicit:
        return explicit
    env_token = (
        os.environ.get("SMARTTABLE_TOKEN")
        or os.environ.get("AUTOTOUCH_API_KEY")
        or os.environ.get("AUTOTOUCH_TOKEN")
    )
    if env_token:
        return str(env_token).strip()

    cfg = _load_config()
    existing = str(cfg.get("api_key") or "").strip()
    if existing:
        return existing

    if sys.stdin.isatty():
        entered = getpass.getpass("Paste developer API key (stk_...): ").strip()
        if entered:
            return entered

    raise AutotouchInputError(
        "missing api key. Pass --api-key, set AUTOTOUCH_API_KEY, or run interactively."
    )


def cmd_setup(args: argparse.Namespace) -> None:
    if OUTPUT_MODE == "human" and not args.no_banner:
        _print_setup_banner()

    cfg = _load_config()
    existing_key = str(cfg.get("api_key") or "").strip()
    api_key = _resolve_setup_api_key(args)
    base_url = str(args.base_url or cfg.get("base_url") or _api_url()).rstrip("/")

    if existing_key and existing_key != api_key and not args.overwrite:
        raise AutotouchInputError(
            "config already has an API key. Pass --overwrite to replace it."
        )

    capabilities = _request_api(
        "GET",
        "/api/capabilities",
        base_url=base_url,
        token=api_key,
        use_x_api_key=bool(args.use_x_api_key),
        timeout=int(args.timeout or DEFAULT_TIMEOUT_SECONDS),
        verbose=bool(args.verbose),
    )

    cfg["api_key"] = api_key
    cfg["base_url"] = base_url
    cfg["updated_at_epoch"] = int(time.time())
    path = _save_config(cfg)

    output = {
        "setup_complete": True,
        "base_url": base_url,
        "auth_header_mode": "x-api-key" if args.use_x_api_key else "authorization",
        "config_path": str(path),
        "api_key_masked": _mask_api_key(api_key),
        "api_version": capabilities.get("api_version") if isinstance(capabilities, dict) else None,
        "recommended_next_steps": [
            "autotouch capabilities --output human",
            "autotouch tables create --name \"CLI Contacts\" --output human",
            "autotouch rows import-csv --table-id <TABLE_ID> --confirm-table-id <TABLE_ID> --file contacts.csv --checkpoint-file .autotouch-import.json --wait",
            "autotouch columns run-next --table-id <TABLE_ID> --column-id <COLUMN_ID> --count 5 --show-estimate --wait",
        ],
    }

    if OUTPUT_MODE == "human":
        print("✓ Key validated")
        print(f"✓ Config saved: {path}")
        print(f"✓ Base URL: {base_url}")
        print("")
        print("Next steps:")
        for line in output["recommended_next_steps"]:
            print(f"  {line}")
        return

    _print_json(output, compact=args.compact)


def _coerce_string_list(values: Any) -> List[str]:
    if values is None:
        return []
    if isinstance(values, list):
        normalized: List[str] = []
        for item in values:
            for piece in str(item or "").split(","):
                stripped = piece.strip()
                if stripped:
                    normalized.append(stripped)
        return normalized
    if isinstance(values, str):
        stripped = values.strip()
        if not stripped:
            return []
        try:
            parsed = json.loads(stripped)
        except Exception:
            parsed = None
        if isinstance(parsed, list):
            return [str(item).strip() for item in parsed if str(item or "").strip()]
        return [piece.strip() for piece in stripped.split(",") if piece.strip()]
    return []


def _build_org_context_payload(args: argparse.Namespace) -> Dict[str, Any]:
    payload = _load_json_input(
        inline_json=getattr(args, "data_json", None),
        file_path=getattr(args, "data_file", None),
        context="data",
        default=None,
    )
    if payload is not None:
        if not isinstance(payload, dict):
            raise AutotouchInputError("org-context payload must be a JSON object")
        return payload

    payload = {"source": getattr(args, "source", None) or "cli"}
    if getattr(args, "value_proposition", None) is not None:
        payload["value_proposition"] = args.value_proposition
    ideal_titles = _coerce_string_list(getattr(args, "ideal_title", None))
    if ideal_titles:
        payload["ideal_titles"] = ideal_titles
    if getattr(args, "buyer_persona", None) is not None:
        payload["buyer_persona"] = args.buyer_persona
    if getattr(args, "user_persona", None) is not None:
        payload["user_persona"] = args.user_persona
    if getattr(args, "voice_profile", None) is not None:
        payload["voice_profile"] = args.voice_profile
    case_study_urls = _coerce_string_list(getattr(args, "case_study_url", None))
    if case_study_urls:
        payload["case_study_urls"] = case_study_urls
    return payload


def _org_context_form_payload(payload: Dict[str, Any]) -> Dict[str, Any]:
    form_payload: Dict[str, Any] = {
        "source": str(payload.get("source") or "cli"),
        "value_proposition": str(payload.get("value_proposition") or ""),
        "ideal_titles": json.dumps(_coerce_string_list(payload.get("ideal_titles"))),
        "icp_buyer": str(payload.get("icp_buyer") or payload.get("buyer_persona") or ""),
        "icp_users": str(payload.get("icp_users") or payload.get("user_persona") or ""),
        "case_study_urls": json.dumps(_coerce_string_list(payload.get("case_study_urls"))),
        "voice_profile": str(payload.get("voice_profile") or ""),
    }
    return form_payload


def _build_personal_context_payload(args: argparse.Namespace) -> Dict[str, Any]:
    payload = _load_json_input(
        inline_json=getattr(args, "data_json", None),
        file_path=getattr(args, "data_file", None),
        context="data",
        default=None,
    )
    if payload is not None:
        if not isinstance(payload, dict):
            raise AutotouchInputError("personal-context payload must be a JSON object")
        return payload

    payload = {}
    for arg_name, payload_key in {
        "title": "title",
        "linkedin_url": "linkedin_url",
        "personal_value_proposition": "personal_value_proposition",
        "voice_profile": "voice_profile",
    }.items():
        value = getattr(args, arg_name, None)
        if value is not None:
            payload[payload_key] = value

    territories = _coerce_string_list(getattr(args, "territory", None))
    if territories:
        payload["territories"] = territories

    if getattr(args, "use_personal_value_proposition", False):
        payload["use_personal_value_proposition"] = True
    if getattr(args, "use_personal_voice_profile", False):
        payload["use_personal_voice_profile"] = True
    return payload


def cmd_onboarding_submit_domain(args: argparse.Namespace) -> None:
    token = _resolve_user_session_token(args.token, required=True)
    payload = _load_json_input(
        inline_json=getattr(args, "data_json", None),
        file_path=getattr(args, "data_file", None),
        context="data",
        default=None,
    )
    if payload is None:
        target = _normalize_string_value(getattr(args, "website", None) or getattr(args, "domain", None))
        if not target:
            raise AutotouchInputError("pass --website/--domain or --data-json/--data-file")
        payload = {"website": target}
    if not isinstance(payload, dict):
        raise AutotouchInputError("onboarding submit-domain payload must be a JSON object")

    data = _request_api(
        "POST",
        "/api/onboarding/submit-domain",
        base_url=args.base_url,
        token=token,
        use_x_api_key=False,
        form_payload={
            "website": str(payload.get("website") or payload.get("domain") or "").strip(),
            "domain": str(payload.get("domain") or "").strip(),
        },
        timeout=args.timeout,
        verbose=args.verbose,
    )
    _print_json(data, compact=args.compact)


def cmd_org_context_get(args: argparse.Namespace) -> None:
    token = _resolve_user_session_token(args.token, required=True)
    data = _request_api(
        "GET",
        "/api/org/context",
        base_url=args.base_url,
        token=token,
        use_x_api_key=False,
        timeout=args.timeout,
        verbose=args.verbose,
    )
    _print_json(data, compact=args.compact)


def cmd_org_context_set(args: argparse.Namespace) -> None:
    token = _resolve_user_session_token(args.token, required=True)
    payload = _build_org_context_payload(args)
    data = _request_api(
        "POST",
        "/api/org/context",
        base_url=args.base_url,
        token=token,
        use_x_api_key=False,
        form_payload=_org_context_form_payload(payload),
        timeout=args.timeout,
        verbose=args.verbose,
    )
    _print_json(data, compact=args.compact)


def cmd_personal_context_get(args: argparse.Namespace) -> None:
    token = _resolve_user_session_token(args.token, required=True)
    data = _request_api(
        "GET",
        "/api/onboarding/personal-context",
        base_url=args.base_url,
        token=token,
        use_x_api_key=False,
        timeout=args.timeout,
        verbose=args.verbose,
    )
    _print_json(data, compact=args.compact)


def cmd_personal_context_set(args: argparse.Namespace) -> None:
    token = _resolve_user_session_token(args.token, required=True)
    payload = _build_personal_context_payload(args)
    data = _request_api(
        "POST",
        "/api/onboarding/personal-context",
        base_url=args.base_url,
        token=token,
        use_x_api_key=False,
        payload=payload,
        timeout=args.timeout,
        verbose=args.verbose,
    )
    _print_json(data, compact=args.compact)


def cmd_context_resolved(args: argparse.Namespace) -> None:
    token = _resolve_user_session_token(args.token, required=True)
    data = _request_api(
        "GET",
        "/api/llm/company-context",
        base_url=args.base_url,
        token=token,
        use_x_api_key=False,
        timeout=args.timeout,
        verbose=args.verbose,
    )
    _print_json(data, compact=args.compact)


def cmd_capabilities(args: argparse.Namespace) -> None:
    token = _resolve_token(args.token, required=False)
    data = _request_api(
        "GET",
        "/api/capabilities",
        base_url=args.base_url,
        token=token,
        use_x_api_key=args.use_x_api_key,
        timeout=args.timeout,
        verbose=args.verbose,
    )
    if OUTPUT_MODE == "human" and isinstance(data, dict):
        execution = data.get("execution_policies") if isinstance(data.get("execution_policies"), dict) else {}
        llm = execution.get("llm") if isinstance(execution.get("llm"), dict) else {}
        output_contract = llm.get("output_contract") if isinstance(llm.get("output_contract"), dict) else {}
        agent_contract = output_contract.get("agent") if isinstance(output_contract.get("agent"), dict) else {}
        basic_contract = output_contract.get("basic") if isinstance(output_contract.get("basic"), dict) else {}
        automation = data.get("automation") if isinstance(data.get("automation"), dict) else {}
        providers = automation.get("providers") if isinstance(automation.get("providers"), dict) else {}

        print(f"API version : {_as_display(data.get('api_version'))}")
        print(f"Base URL    : {_as_display(data.get('base_url'))}")

        auth = data.get("authentication") if isinstance(data.get("authentication"), dict) else {}
        scopes = auth.get("scopes")
        if isinstance(scopes, list):
            print(f"Scopes      : {', '.join(str(s) for s in scopes)}")

        print("")
        print("LLM output modes")
        print("----------------")
        print(
            "agent : JSON-oriented structured output"
            + (
                f" (default_data_type={agent_contract.get('default_data_type')})"
                if agent_contract.get("default_data_type")
                else ""
            )
        )
        basic_types = basic_contract.get("supported_data_types")
        basic_types_str = ", ".join(str(t) for t in basic_types) if isinstance(basic_types, list) else "text/json"
        print(f"basic : {basic_types_str} (JSON requires dataType=json)")
        if providers:
            print("")
            print("Provider setup")
            print("--------------")
            print(", ".join(sorted(str(name) for name in providers.keys())))
        linkedin = data.get("linkedin") if isinstance(data.get("linkedin"), dict) else None
        search = data.get("search") if isinstance(data.get("search"), dict) else None
        if isinstance(linkedin, dict):
            print("")
            print("LinkedIn list building")
            print("---------------------")
            modes = linkedin.get("api_modes", [])
            print(f"API modes       : {', '.join(str(m) for m in modes)}")
            print(f"Access gate     : {linkedin.get('access_gate', 'linkedin_access flag')}")
            caps = linkedin.get("rate_limits", {}).get("search_daily_caps", {})
            if caps:
                for mode, cap in caps.items():
                    print(f"  {mode:20s}: {cap}")
            best_for = linkedin.get("best_for")
            if isinstance(best_for, list) and best_for:
                print("Best for        :")
                for item in best_for:
                    print(f"  - {item}")
            not_for = linkedin.get("not_for")
            if isinstance(not_for, list) and not_for:
                print("Not for         :")
                for item in not_for:
                    print(f"  - {item}")
            print("Tip: run `autotouch linkedin recipe` for example payloads.")
        if isinstance(search, dict):
            print("")
            print("Provider-hidden search")
            print("----------------------")
            pricing = search.get("pricing", {}).get("families", {})
            if isinstance(pricing, dict) and pricing:
                print("Pricing")
                for family, entry in pricing.items():
                    if isinstance(entry, dict):
                        print(f"  {family:20s}: {entry.get('rule', '-')}")
            chooser = search.get("chooser_guidance") if isinstance(search.get("chooser_guidance"), dict) else {}
            company_first = chooser.get("use_company_search_when")
            if isinstance(company_first, list) and company_first:
                print("Use company search when:")
                for item in company_first:
                    print(f"  - {item}")
            people_first = chooser.get("use_people_search_when")
            if isinstance(people_first, list) and people_first:
                print("Use people search when:")
                for item in people_first:
                    print(f"  - {item}")
            linkedin_instead = chooser.get("use_linkedin_instead_when")
            if isinstance(linkedin_instead, list) and linkedin_instead:
                print("Use LinkedIn instead when:")
                for item in linkedin_instead:
                    print(f"  - {item}")
            print("Tip: run `autotouch search recipe` for example payloads.")
        print("")
        print("Tip: inspect automation.providers in JSON output for provider-specific setup contracts.")
        print("Tip: for JSON enrichment columns, run with `--wait` and verify terminal status before `autotouch columns projections`.")
        return

    _print_json(data, compact=args.compact)


def cmd_sop(args: argparse.Namespace) -> None:
    if args.out_file:
        with open(args.out_file, "w", encoding="utf-8") as f:
            json.dump(RUN_SOP_GUIDE, f, indent=2)

    if OUTPUT_MODE == "human":
        print(RUN_SOP_GUIDE["title"])
        print("-" * len(RUN_SOP_GUIDE["title"]))
        print("Rules:")
        for idx, rule in enumerate(RUN_SOP_GUIDE["rules"], start=1):
            print(f"{idx}. {rule}")
        print("\nClarifications:")
        for note in RUN_SOP_GUIDE["clarifications"]:
            print(f"- {note}")
        print("\nDefault run payload:")
        print(json.dumps(RUN_SOP_GUIDE["default_run_payload"], indent=2))
        if args.out_file:
            print(f"\nSaved: {args.out_file}")
        return

    _print_json(RUN_SOP_GUIDE, compact=args.compact)


def cmd_blacklist_list(args: argparse.Namespace) -> None:
    token = _resolve_token(args.token, required=True)
    params: Dict[str, Any] = {
        "type_filter": str(getattr(args, "type_filter", "all") or "all"),
        "page": max(1, int(getattr(args, "page", 1) or 1)),
        "limit": max(1, int(getattr(args, "limit", 100) or 100)),
    }
    search = str(getattr(args, "search", "") or "").strip()
    if search:
        params["search"] = search

    data = _request_api(
        "GET",
        "/api/blacklist",
        base_url=args.base_url,
        token=token,
        use_x_api_key=args.use_x_api_key,
        params=params,
        timeout=args.timeout,
        verbose=args.verbose,
    )
    _print_json(data, compact=args.compact)


def cmd_blacklist_add(args: argparse.Namespace) -> None:
    token = _resolve_token(args.token, required=True)
    entry_type = str(args.type).strip().lower()
    value = str(args.value or "").strip().lower()

    if entry_type == "domain":
        value = value.replace("https://", "").replace("http://", "")
        value = value.lstrip("@")
        value = value.split("/", 1)[0]

    payload = {
        "type": entry_type,
        "value": value,
        "reason": str(getattr(args, "reason", "") or ""),
        "notes": str(getattr(args, "notes", "") or ""),
    }
    data = _request_api(
        "POST",
        "/api/blacklist",
        base_url=args.base_url,
        token=token,
        use_x_api_key=args.use_x_api_key,
        payload=payload,
        timeout=args.timeout,
        verbose=args.verbose,
    )
    _print_json(data, compact=args.compact)


def cmd_blacklist_remove(args: argparse.Namespace) -> None:
    token = _resolve_token(args.token, required=True)
    data = _request_api(
        "DELETE",
        f"/api/blacklist/{args.entry_id}",
        base_url=args.base_url,
        token=token,
        use_x_api_key=args.use_x_api_key,
        timeout=args.timeout,
        verbose=args.verbose,
    )
    _print_json(data, compact=args.compact)


def cmd_blacklist_import(args: argparse.Namespace) -> None:
    token = _resolve_token(args.token, required=True)
    source_path = os.path.abspath(os.path.expanduser(str(args.file)))
    data = _request_multipart_api(
        "POST",
        "/api/blacklist/import",
        base_url=args.base_url,
        token=token,
        use_x_api_key=args.use_x_api_key,
        file_path=source_path,
        file_field="file",
        timeout=args.timeout,
        verbose=args.verbose,
    )
    if isinstance(data, dict):
        data.setdefault("source", source_path)
    _print_json(data, compact=args.compact)


def cmd_blacklist_check(args: argparse.Namespace) -> None:
    token = _resolve_token(args.token, required=True)
    emails = _collect_blacklist_emails(args)
    chunk_size = max(1, min(int(getattr(args, "chunk_size", 1000) or 1000), 1000))

    all_results: List[Dict[str, Any]] = []
    chunk_summaries: List[Dict[str, Any]] = []
    for chunk_index, chunk in enumerate(_chunk_list(emails, chunk_size), start=1):
        response = _request_api(
            "POST",
            "/api/blacklist/check",
            base_url=args.base_url,
            token=token,
            use_x_api_key=args.use_x_api_key,
            payload={"emails": chunk},
            timeout=args.timeout,
            verbose=args.verbose,
        )
        if not isinstance(response, dict):
            raise AutotouchAPIError("unexpected response from /api/blacklist/check")

        chunk_results = response.get("results") if isinstance(response.get("results"), list) else []
        chunk_summary = response.get("summary") if isinstance(response.get("summary"), dict) else {}
        all_results.extend([result for result in chunk_results if isinstance(result, dict)])
        chunk_summaries.append(
            {
                "chunk": chunk_index,
                "size": len(chunk),
                "summary": chunk_summary,
            }
        )

    total_blacklisted = sum(
        1
        for result in all_results
        if bool(result.get("is_blacklisted"))
    )
    output: Dict[str, Any] = {
        "input_count": len(emails),
        "chunks": len(chunk_summaries),
        "chunk_size": chunk_size,
        "summary": {
            "total_checked": len(all_results),
            "total_blacklisted": total_blacklisted,
            "total_clean": max(0, len(all_results) - total_blacklisted),
        },
    }
    if not bool(getattr(args, "summary_only", False)):
        output["results"] = all_results
        if len(chunk_summaries) > 1:
            output["chunk_summaries"] = chunk_summaries
    _print_json(output, compact=args.compact)


def cmd_blacklist_filter(args: argparse.Namespace) -> None:
    token = _resolve_token(args.token, required=True)
    emails = _collect_blacklist_emails(args)
    chunk_size = max(1, min(int(getattr(args, "chunk_size", 10000) or 10000), 10000))

    clean_emails: List[str] = []
    blacklisted_emails: List[Dict[str, Any]] = []
    chunk_summaries: List[Dict[str, Any]] = []
    for chunk_index, chunk in enumerate(_chunk_list(emails, chunk_size), start=1):
        response = _request_api(
            "POST",
            "/api/blacklist/filter",
            base_url=args.base_url,
            token=token,
            use_x_api_key=args.use_x_api_key,
            payload={"emails": chunk},
            timeout=args.timeout,
            verbose=args.verbose,
        )
        if not isinstance(response, dict):
            raise AutotouchAPIError("unexpected response from /api/blacklist/filter")

        chunk_clean = response.get("clean_emails") if isinstance(response.get("clean_emails"), list) else []
        chunk_blocked = (
            response.get("blacklisted_emails")
            if isinstance(response.get("blacklisted_emails"), list)
            else []
        )
        chunk_summary = response.get("summary") if isinstance(response.get("summary"), dict) else {}
        clean_emails.extend(
            [
                normalized
                for normalized in (_normalize_email_value(item) for item in chunk_clean)
                if normalized
            ]
        )
        blacklisted_emails.extend([item for item in chunk_blocked if isinstance(item, dict)])
        chunk_summaries.append(
            {
                "chunk": chunk_index,
                "size": len(chunk),
                "summary": chunk_summary,
            }
        )

    output: Dict[str, Any] = {
        "input_count": len(emails),
        "chunks": len(chunk_summaries),
        "chunk_size": chunk_size,
        "summary": {
            "total_provided": len(emails),
            "total_clean": len(clean_emails),
            "total_blacklisted": len(blacklisted_emails),
        },
    }
    if not bool(getattr(args, "summary_only", False)):
        output["clean_emails"] = clean_emails
        output["blacklisted_emails"] = blacklisted_emails
        if len(chunk_summaries) > 1:
            output["chunk_summaries"] = chunk_summaries
    _print_json(output, compact=args.compact)


def cmd_status(args: argparse.Namespace) -> None:
    db = _mongo_client(args.mongo_uri, args.db_name)
    counts = _status_counts(db, args.table_id, args.column_id)
    total = sum(counts.values())
    _print_json(
        {"pending": counts["pending"], "done": counts["done"], "error": counts["error"], "totalCells": total},
        compact=bool(getattr(args, "compact", False)),
    )


def cmd_watch(args: argparse.Namespace) -> None:
    if OUTPUT_MODE == "json" and not bool(getattr(args, "once", False)):
        raise AutotouchInputError("watch with --output json requires --once. Use --output ndjson or --output human for streaming.")
    db = _mongo_client(args.mongo_uri, args.db_name)
    interval = max(1, int(args.interval or 5))
    compact = bool(getattr(args, "compact", False))

    def _snapshot() -> Dict[str, int]:
        counts = _status_counts(db, args.table_id, args.column_id)
        total = sum(counts.values())
        return {
            "pending": counts["pending"],
            "done": counts["done"],
            "error": counts["error"],
            "totalCells": total,
        }

    if bool(getattr(args, "once", False)):
        snapshot = _snapshot()
        if OUTPUT_MODE == "human" and not (OUTPUT_SELECT or OUTPUT_JSON_POINTER):
            _emit_text(
                f"pending={snapshot['pending']} done={snapshot['done']} error={snapshot['error']} total={snapshot['totalCells']}"
            )
            return
        _print_json(snapshot, compact=compact)
        return

    try:
        while True:
            snapshot = _snapshot()
            if OUTPUT_MODE == "human" and not (OUTPUT_SELECT or OUTPUT_JSON_POINTER):
                _emit_text(
                    f"pending={snapshot['pending']} done={snapshot['done']} error={snapshot['error']} total={snapshot['totalCells']}"
                )
            else:
                _print_json(snapshot, compact=compact)
            time.sleep(interval)
    except KeyboardInterrupt:
        pass


def cmd_cli_manifest(args: argparse.Namespace) -> None:
    output = _load_bundled_cli_manifest() if getattr(args, "source", "live") == "bundled" else None
    if not isinstance(output, dict):
        output = build_cli_manifest()
    if args.out_file:
        with open(args.out_file, "w", encoding="utf-8") as f:
            json.dump(output, f, indent=2)
    _print_json(output, compact=args.compact)


def cmd_cli_reference(args: argparse.Namespace) -> None:
    output = _load_bundled_cli_reference() if getattr(args, "source", "live") == "bundled" else None
    if not output:
        output = build_cli_reference_markdown(build_cli_manifest())
    if args.out_file:
        with open(args.out_file, "w", encoding="utf-8") as f:
            f.write(output)
    if OUTPUT_MODE == "human":
        _emit_text(output.rstrip())
        return
    _print_json(
        {
            "version": CLI_VERSION,
            "reference_markdown": output,
        },
        compact=args.compact,
    )


# ---------------------------------------------------------------------------
# Manifest and reference helpers
# ---------------------------------------------------------------------------

def build_cli_manifest(parser: Optional[argparse.ArgumentParser] = None) -> Dict[str, Any]:
    manifest_parser = parser or build_parser()
    return build_cli_manifest_payload(CLI_VERSION, manifest_parser)


def _load_bundled_cli_manifest() -> Optional[Dict[str, Any]]:
    try:
        resource = importlib_resources.files("autotouch_cli").joinpath("data/cli-manifest.json")
        return json.loads(resource.read_text(encoding="utf-8"))
    except Exception:
        return None


def _load_bundled_cli_reference() -> Optional[str]:
    try:
        resource = importlib_resources.files("autotouch_cli").joinpath("data/CLI_REFERENCE.md")
        return resource.read_text(encoding="utf-8")
    except Exception:
        return None


# ---------------------------------------------------------------------------
# Compatibility aliases for tests that call cmd_* by name on the module
# ---------------------------------------------------------------------------
cmd_auth_bootstrap = _bound("auth_bootstrap")
cmd_auth_check = _bound("auth_check")
cmd_auth_status = _bound("auth_status")
cmd_auth_schema = _bound("auth_schema")
cmd_auth_whoami = _bound("auth_whoami")
cmd_cells_get = _bound("cells_get")
cmd_columns_create = _bound("columns_create")
cmd_columns_move = _bound("columns_move")
cmd_columns_projections = _bound("columns_projections")
cmd_columns_run_next = _bound("columns_run_next")
cmd_columns_test_http_request = _bound("columns_test_http_request")
cmd_jobs_watch = _bound("jobs_watch")
cmd_rows_get = _bound("rows_get")
cmd_rows_list = _bound("rows_list")
cmd_schema = _bound("schema")
cmd_sequences_enroll = _bound("sequences_enroll")
cmd_sequences_status = _bound("sequences_status")
cmd_tasks_recipe = _bound("tasks_recipe")
cmd_workflows_scaffold = _bound("workflows_scaffold")
cmd_webhooks_recipe = _bound("webhooks_recipe")
cmd_workspace_secrets_delete = _bound("workspace_secrets_delete")
cmd_workspace_secrets_schema = _bound("workspace_secrets_schema")
cmd_workspace_secrets_set = _bound("workspace_secrets_set")

# ---------------------------------------------------------------------------
# Parser assembly — delegates to autotouch_cli.parser
# ---------------------------------------------------------------------------

def _build_commands_dict() -> Dict[str, Callable]:
    """Build the commands dict consumed by ``parser.build_parser``."""
    commands: Dict[str, Callable] = {}

    # Registry-dispatched commands (the bulk of the CLI)
    for name in _COMMAND_REGISTRY:
        commands[name] = _bound(name)

    # Standalone commands (have their own logic, not shims)
    commands["cli_manifest"] = cmd_cli_manifest
    commands["cli_reference"] = cmd_cli_reference
    commands["setup"] = cmd_setup
    commands["onboarding_submit_domain"] = cmd_onboarding_submit_domain
    commands["org_context_get"] = cmd_org_context_get
    commands["org_context_set"] = cmd_org_context_set
    commands["personal_context_get"] = cmd_personal_context_get
    commands["personal_context_set"] = cmd_personal_context_set
    commands["context_resolved"] = cmd_context_resolved
    commands["capabilities"] = cmd_capabilities
    commands["blacklist_list"] = cmd_blacklist_list
    commands["blacklist_add"] = cmd_blacklist_add
    commands["blacklist_remove"] = cmd_blacklist_remove
    commands["blacklist_import"] = cmd_blacklist_import
    commands["blacklist_check"] = cmd_blacklist_check
    commands["blacklist_filter"] = cmd_blacklist_filter
    commands["status"] = cmd_status
    commands["watch"] = cmd_watch
    commands["sop"] = cmd_sop

    return commands


def build_parser() -> argparse.ArgumentParser:
    from autotouch_cli.parser import build_parser as _build_parser_impl

    return _build_parser_impl(
        _build_commands_dict(),
        cli_version=CLI_VERSION,
        default_api_url=_api_url(),
        default_timeout_seconds=DEFAULT_TIMEOUT_SECONDS,
        output_modes=list(OUTPUT_MODES),
    )


# ---------------------------------------------------------------------------
# Entrypoint
# ---------------------------------------------------------------------------

def main(argv: Optional[List[str]] = None) -> None:
    from autotouch_cli.exceptions import AutotouchError

    parser = build_parser()
    args = parser.parse_args(argv)
    _set_output_mode(getattr(args, "output", "json"))
    _set_output_projection(getattr(args, "select", None), getattr(args, "json_pointer", None))
    try:
        args.func(args)
    except AutotouchError as exc:
        message = str(exc)
        if message:
            print(f"ERROR: {message}", file=sys.stderr)
        sys.exit(exc.exit_code)


if __name__ == "__main__":
    main()
