"""Argparse tree assembly for the Smart Table CLI.

Extracted from cli.py. Exposes ``build_parser(commands)`` which returns a
fully-wired ``argparse.ArgumentParser``.  The *commands* dict maps command
names to callables that accept a single ``argparse.Namespace`` argument.
"""
from __future__ import annotations

import argparse
from typing import Any, Callable, Dict, List, Optional

from autotouch_cli.commands.auth import register_auth_subcommands
from autotouch_cli.commands.cells import register_cells_subcommands
from autotouch_cli.commands.columns import register_columns_subcommands
from autotouch_cli.commands.jobs import register_jobs_subcommands
from autotouch_cli.commands.leads import register_leads_subcommands
from autotouch_cli.commands.linkedin import register_linkedin_subcommands
from autotouch_cli.commands.prompts import register_prompts_subcommands
from autotouch_cli.commands.rows import register_rows_subcommands
from autotouch_cli.commands.search import register_search_subcommands
from autotouch_cli.commands.sequences import register_sequences_subcommands
from autotouch_cli.commands.tables import register_tables_subcommands
from autotouch_cli.commands.workspace_secrets import register_workspace_secrets_subcommands
from autotouch_cli.parser_groups import add_tasks_subcommands, add_webhooks_subcommands
from autotouch_cli.templates import (
    AUTH_SCHEMA_TYPES,
    CELL_SCHEMA_TYPES,
    COLUMN_RECIPE_TYPES,
    LEAD_RECIPE_TYPES,
    LINKEDIN_RECIPE_TYPES,
    ONBOARDING_SCHEMA_TYPES,
    ORG_CONTEXT_SCHEMA_TYPES,
    PERSONAL_CONTEXT_SCHEMA_TYPES,
    PROMPT_TEMPLATE_SCHEMA_TYPES,
    ROW_SCHEMA_TYPES,
    SEARCH_RECIPE_TYPES,
    SEQUENCE_RECIPE_TYPES,
    SHARED_SCHEMA_TYPES,
    TABLE_SCHEMA_TYPES,
    TASK_RECIPE_TYPES,
    WEBHOOK_RECIPE_TYPES,
    WORKSPACE_SECRET_SCHEMA_TYPES,
    WORKFLOW_BLUEPRINT_TYPES,
)


# ---------------------------------------------------------------------------
# Shared argument-group helpers
# ---------------------------------------------------------------------------

def _add_output_formatting_arguments(
    parser: argparse.ArgumentParser,
    *,
    default_output: str = "json",
    compact_help: str = "Print compact JSON",
    output_modes: List[str],
) -> None:
    parser.add_argument("--output", choices=list(output_modes), default=default_output, help="Output mode")
    parser.add_argument("--compact", action="store_true", help=compact_help)
    projection_group = parser.add_mutually_exclusive_group()
    projection_group.add_argument("--select", help="Extract a dotted field path from the final result (for example: id or items.0.id)")
    projection_group.add_argument("--json-pointer", help="Extract a JSON pointer from the final result (for example: /id)")


def _add_api_common_arguments(
    parser: argparse.ArgumentParser,
    *,
    default_api_url: str,
    default_timeout_seconds: int,
    output_modes: List[str],
) -> None:
    parser.add_argument("--base-url", default=default_api_url, help=f"API base URL (default: {default_api_url})")
    parser.add_argument("--token", help="Developer API key / JWT token")
    parser.add_argument("--use-x-api-key", action="store_true", help="Send token via X-API-Key header")
    parser.add_argument("--timeout", type=int, default=default_timeout_seconds, help="HTTP timeout in seconds")
    _add_output_formatting_arguments(parser, output_modes=output_modes)
    parser.add_argument("--verbose", action="store_true", help="Print request metadata to stderr")


def _add_run_scope_arguments(parser: argparse.ArgumentParser) -> None:
    parser.add_argument(
        "--scope",
        choices=["all", "subset", "row", "firstN", "filtered"],
        default="all",
        help="Run scope: row=one explicit ID, subset=explicit many IDs, filtered=current filters, firstN=first N rows, all=entire table",
    )
    parser.add_argument("--row-id", help="Single row ID for scope=row")
    parser.add_argument("--row-ids", nargs="*", help="Row IDs for subset scope (or a single row when scope=row)")
    parser.add_argument(
        "--first-n",
        type=int,
        help="First N rows (for scope=firstN). Use with --unprocessed-only to process the next rows without reruns",
    )
    run_mode = parser.add_mutually_exclusive_group()
    run_mode.add_argument(
        "--unprocessed-only",
        dest="unprocessed_only",
        action="store_true",
        help="Only process rows without values (default)",
    )
    run_mode.add_argument(
        "--include-processed",
        dest="unprocessed_only",
        action="store_false",
        help="Include rows that already have output",
    )
    parser.set_defaults(unprocessed_only=True)
    parser.add_argument("--filters-json", help="JSON object for scope=filtered")
    parser.add_argument("--filters-file", help="Path to JSON file for scope=filtered")
    parser.add_argument("--data-json", help="Explicit RunRequest payload JSON (overrides scope flags)")
    parser.add_argument("--data-file", help="Path to RunRequest payload JSON file (overrides scope flags)")


def _add_run_execution_arguments(parser: argparse.ArgumentParser) -> None:
    parser.add_argument("--show-estimate", action="store_true", help="Include estimate response in output")
    parser.add_argument("--dry-run", action="store_true", help="Only estimate; do not execute run")
    parser.add_argument("--max-credits", type=float, help="Block run when estimate exceeds this value")
    parser.add_argument(
        "--allow-unknown-max",
        action="store_true",
        help="Allow run when estimated_credits_max is null (max-credits guard uses min estimate)",
    )
    parser.add_argument(
        "--wait",
        action="store_true",
        help="Wait for bulk job terminal status by polling /api/bulk-jobs/{job_id}",
    )
    parser.add_argument("--quiet-wait", action="store_true", help="Suppress per-poll output in --wait mode")
    parser.add_argument("--poll-interval", type=int, default=2, help="Polling interval seconds for --wait")
    parser.add_argument("--wait-timeout", type=int, default=0, help="Max seconds to wait (0 = no timeout)")
    parser.add_argument("--fail-on-error", action="store_true", help="Exit non-zero when final status is error/cancelled")
    parser.add_argument("--fail-on-partial", action="store_true", help="Exit non-zero when final status is partial")


def _add_leads_query_arguments(parser: argparse.ArgumentParser) -> None:
    parser.add_argument("--data-json", help="Explicit leads query payload JSON")
    parser.add_argument("--data-file", help="Path to leads query payload JSON file")
    parser.add_argument("--filter-json", help="Filter descriptor JSON object")
    parser.add_argument("--filter-file", help="Path to filter descriptor JSON file")
    parser.add_argument("--sort-json", help="Sort spec JSON object/array")
    parser.add_argument("--sort-file", help="Path to sort spec JSON file")
    parser.add_argument("--search", help="Free-text lead search")
    parser.add_argument("--limit", type=int, help="Pagination limit")
    parser.add_argument("--cursor", help="Pagination cursor")
    parser.add_argument("--scope", choices=["org", "user"], help="Lead scope")
    parser.add_argument("--source-table-id", help="Research table id used for scope/research context")
    parser.add_argument(
        "--source-table-scope",
        choices=["company", "lead"],
        help="Research scope resolution mode",
    )
    parser.add_argument(
        "--related-tables-mode",
        choices=["linked", "direct", "none"],
        help="How related_tables should be populated",
    )
    parser.add_argument(
        "--include-research-data",
        action="store_true",
        help="Inline research_data when --source-table-id is provided",
    )
    parser.add_argument(
        "--include-placeholder-leads",
        action="store_true",
        help="Include placeholder/incomplete lead records",
    )


def _add_leads_research_arguments(parser: argparse.ArgumentParser) -> None:
    parser.add_argument("--data-json", help="Explicit leads research payload JSON")
    parser.add_argument("--data-file", help="Path to leads research payload JSON file")
    parser.add_argument(
        "--lead-id",
        action="append",
        help="Lead id (repeatable; comma-separated also supported)",
    )
    parser.add_argument("--lead-ids-json", help="JSON array or object with lead_ids[]")
    parser.add_argument("--lead-ids-file", help="Path to JSON file with lead_ids[]")
    parser.add_argument("--source-table-id", help="Research table id")
    parser.add_argument(
        "--field-id",
        action="append",
        help="Research field/column id to include (repeatable; comma-separated also supported)",
    )
    parser.add_argument("--field-ids-json", help="JSON array or object with field_ids[]")
    parser.add_argument("--field-ids-file", help="Path to JSON file with field_ids[]")


def _add_leads_selection_arguments(parser: argparse.ArgumentParser) -> None:
    parser.add_argument("--data-json", help="Explicit bulk lead action payload JSON")
    parser.add_argument("--data-file", help="Path to bulk lead action payload JSON file")
    parser.add_argument(
        "--lead-id",
        action="append",
        help="Lead id (repeatable; comma-separated also supported)",
    )
    parser.add_argument("--lead-ids-json", help="JSON array or object with lead_ids[]")
    parser.add_argument("--lead-ids-file", help="Path to JSON/TXT/CSV file with lead_ids[]")
    parser.add_argument("--lead-ids-column", default="lead_id", help="CSV column for lead ids (default: lead_id)")
    parser.add_argument("--filter-json", help="Filter descriptor JSON object")
    parser.add_argument("--filter-file", help="Path to filter descriptor JSON file")
    parser.add_argument("--sort-json", help="Sort spec JSON object/array")
    parser.add_argument("--sort-file", help="Path to sort spec JSON file")
    parser.add_argument("--search", help="Free-text lead search")
    parser.add_argument("--limit", type=int, help="Optional descriptor selection limit")
    parser.add_argument("--scope", choices=["org", "user"], help="Lead scope")
    parser.add_argument("--source-table-id", help="Optional research table id for descriptor selection")
    parser.add_argument(
        "--source-table-scope",
        choices=["company", "lead"],
        help="Optional research scope resolution mode",
    )


def _add_actor_override_argument(parser: argparse.ArgumentParser) -> None:
    parser.add_argument(
        "--actor-user-id",
        help="Optional actor override; must resolve to an active user in the same organization",
    )


# ---------------------------------------------------------------------------
# Parser assembly entrypoint
# ---------------------------------------------------------------------------

def build_parser(
    commands: Dict[str, Callable],
    *,
    cli_version: str,
    default_api_url: str,
    default_timeout_seconds: int,
    output_modes: List[str],
) -> argparse.ArgumentParser:
    """Build and return the complete CLI argument parser.

    Parameters
    ----------
    commands:
        Maps command names to callables ``(args) -> None``.  Every command
        referenced in this parser must have an entry.
    cli_version:
        Package version string shown by ``--version``.
    default_api_url:
        Default API base URL.
    default_timeout_seconds:
        Default HTTP timeout.
    output_modes:
        Valid ``--output`` choices.
    """

    # Convenience closures that capture config constants.
    def add_api(parser: argparse.ArgumentParser) -> None:
        _add_api_common_arguments(
            parser,
            default_api_url=default_api_url,
            default_timeout_seconds=default_timeout_seconds,
            output_modes=output_modes,
        )

    def add_output(
        parser: argparse.ArgumentParser,
        *,
        default_output: str = "json",
        compact_help: str = "Print compact JSON",
    ) -> None:
        _add_output_formatting_arguments(
            parser,
            default_output=default_output,
            compact_help=compact_help,
            output_modes=output_modes,
        )

    p = argparse.ArgumentParser(prog="autotouch", description="Smart Table CLI", allow_abbrev=False)
    p.add_argument("--version", action="version", version=f"%(prog)s {cli_version}")
    sub = p.add_subparsers(dest="cmd", required=True)

    # cli-manifest
    pcm = sub.add_parser("cli-manifest", help="Print the machine-readable CLI command manifest")
    pcm.add_argument("--source", choices=["live", "bundled"], default="live", help="Manifest source")
    pcm.add_argument("--out-file", help="Write manifest JSON to file")
    add_output(pcm)
    pcm.set_defaults(func=commands["cli_manifest"])

    # cli-reference
    pcr = sub.add_parser("cli-reference", help="Print the generated CLI reference")
    pcr.add_argument("--source", choices=["live", "bundled"], default="live", help="Reference source")
    pcr.add_argument("--out-file", help="Write reference markdown to file")
    add_output(pcr, default_output="human", compact_help="Print compact JSON when using JSON output")
    pcr.set_defaults(func=commands["cli_reference"])

    # schema
    pshared = sub.add_parser("schema", help="Print shared descriptor schemas used across commands")
    pshared.add_argument("--type", choices=["all", *SHARED_SCHEMA_TYPES], default="all")
    pshared.add_argument("--out-file", help="Write schema JSON to file")
    add_output(pshared)
    pshared.set_defaults(func=commands["schema"])

    # setup
    psetup = sub.add_parser("setup", help="First-run setup with key validation")
    psetup.add_argument("--api-key", help="Developer API key (stk_...)")
    psetup.add_argument("--base-url", default=default_api_url, help=f"API base URL (default: {default_api_url})")
    psetup.add_argument("--overwrite", action="store_true", help="Replace existing stored API key")
    psetup.add_argument("--use-x-api-key", action="store_true", help="Validate using X-API-Key header")
    psetup.add_argument("--timeout", type=int, default=default_timeout_seconds, help="HTTP timeout in seconds")
    add_output(psetup, default_output="human")
    psetup.add_argument("--verbose", action="store_true", help="Print request metadata to stderr")
    psetup.add_argument("--no-banner", action="store_true", help="Skip banner in human mode")
    psetup.set_defaults(func=commands["setup"])

    # auth
    register_auth_subcommands(
        sub,
        add_api_common_arguments=add_api,
        add_output_formatting_arguments=add_output,
        handlers={
            "check": commands["auth_check"],
            "status": commands["auth_status"],
            "bootstrap": commands["auth_bootstrap"],
            "login": commands["auth_login"],
            "set_key": commands["auth_set_key"],
            "show": commands["auth_show"],
            "whoami": commands["auth_whoami"],
            "clear": commands["auth_clear"],
            "schema": commands["auth_schema"],
        },
        auth_schema_types=AUTH_SCHEMA_TYPES,
        default_api_url=default_api_url,
        default_timeout_seconds=default_timeout_seconds,
    )

    # onboarding
    ponboard = sub.add_parser("onboarding", help="Onboarding helpers")
    onboarding_sub = ponboard.add_subparsers(dest="onboarding_cmd", required=True)

    posubmit = onboarding_sub.add_parser("submit-domain", help="Analyze company website and seed org context")
    posubmit.add_argument("--website", help="Company website/domain")
    posubmit.add_argument("--domain", help="Alias for --website")
    posubmit.add_argument("--data-json", help="Explicit submit-domain payload JSON")
    posubmit.add_argument("--data-file", help="Path to submit-domain payload JSON file")
    add_api(posubmit)
    posubmit.set_defaults(func=commands["onboarding_submit_domain"])

    poschema = onboarding_sub.add_parser("schema", help="Print onboarding payload schemas")
    poschema.add_argument("--type", choices=["all", *ONBOARDING_SCHEMA_TYPES], default="all")
    poschema.add_argument("--out-file", help="Write schema JSON to file")
    add_output(poschema)
    poschema.set_defaults(func=commands["onboarding_schema"])

    # org context
    porg = sub.add_parser("org-context", help="Organization value prop / ICP context")
    org_sub = porg.add_subparsers(dest="org_context_cmd", required=True)

    porgg = org_sub.add_parser("get", help="Fetch organization context")
    add_api(porgg)
    porgg.set_defaults(func=commands["org_context_get"])

    porgs = org_sub.add_parser("set", help="Save organization context")
    porgs.add_argument("--source", default="cli", help="Context source label (default: cli)")
    porgs.add_argument("--value-proposition", help="Organization value proposition")
    porgs.add_argument("--ideal-title", action="append", help="Ideal title (repeatable; comma-separated also supported)")
    porgs.add_argument("--buyer-persona", help="Buyer persona / ICP buyer")
    porgs.add_argument("--user-persona", help="User persona / ICP users")
    porgs.add_argument("--voice-profile", help="Voice profile guidance")
    porgs.add_argument("--case-study-url", action="append", help="Case-study URL (repeatable)")
    porgs.add_argument("--data-json", help="Explicit org context payload JSON")
    porgs.add_argument("--data-file", help="Path to org context payload JSON file")
    add_api(porgs)
    porgs.set_defaults(func=commands["org_context_set"])

    porgschema = org_sub.add_parser("schema", help="Print org-context payload schemas")
    porgschema.add_argument("--type", choices=["all", *ORG_CONTEXT_SCHEMA_TYPES], default="all")
    porgschema.add_argument("--out-file", help="Write schema JSON to file")
    add_output(porgschema)
    porgschema.set_defaults(func=commands["org_context_schema"])

    # personal context
    ppersonal = sub.add_parser("personal-context", help="Personal sender / rep context")
    personal_sub = ppersonal.add_subparsers(dest="personal_context_cmd", required=True)

    ppersonalg = personal_sub.add_parser("get", help="Fetch personal context")
    add_api(ppersonalg)
    ppersonalg.set_defaults(func=commands["personal_context_get"])

    ppersonals = personal_sub.add_parser("set", help="Save personal context")
    ppersonals.add_argument("--title", help="User title")
    ppersonals.add_argument("--linkedin-url", help="User LinkedIn URL")
    ppersonals.add_argument("--territory", action="append", help="Territory or region (repeatable)")
    ppersonals.add_argument("--use-personal-value-proposition", action="store_true", help="Prefer the personal value proposition over org default")
    ppersonals.add_argument("--personal-value-proposition", help="Personal value proposition")
    ppersonals.add_argument("--voice-profile", help="Personal voice profile guidance")
    ppersonals.add_argument("--use-personal-voice-profile", action="store_true", help="Prefer the personal voice profile over org default")
    ppersonals.add_argument("--data-json", help="Explicit personal context payload JSON")
    ppersonals.add_argument("--data-file", help="Path to personal context payload JSON file")
    add_api(ppersonals)
    ppersonals.set_defaults(func=commands["personal_context_set"])

    ppersonalschema = personal_sub.add_parser("schema", help="Print personal-context payload schemas")
    ppersonalschema.add_argument("--type", choices=["all", *PERSONAL_CONTEXT_SCHEMA_TYPES], default="all")
    ppersonalschema.add_argument("--out-file", help="Write schema JSON to file")
    add_output(ppersonalschema)
    ppersonalschema.set_defaults(func=commands["personal_context_schema"])

    # workspace secrets
    register_workspace_secrets_subcommands(
        sub,
        add_api_common_arguments=add_api,
        add_output_formatting_arguments=add_output,
        handlers={
            "list": commands["workspace_secrets_list"],
            "set": commands["workspace_secrets_set"],
            "delete": commands["workspace_secrets_delete"],
            "schema": commands["workspace_secrets_schema"],
        },
        workspace_secret_schema_types=WORKSPACE_SECRET_SCHEMA_TYPES,
    )

    # prompts
    register_prompts_subcommands(
        sub,
        add_api_common_arguments=add_api,
        add_output_formatting_arguments=add_output,
        handlers={
            "list": commands["prompts_list"],
            "get": commands["prompts_get"],
            "create": commands["prompts_create"],
            "update": commands["prompts_update"],
            "delete": commands["prompts_delete"],
            "schema": commands["prompts_schema"],
        },
        prompt_template_schema_types=PROMPT_TEMPLATE_SCHEMA_TYPES,
    )

    # resolved context
    pctx = sub.add_parser("context", help="Resolved requester context used by enrichment/runtime")
    ctx_sub = pctx.add_subparsers(dest="context_cmd", required=True)
    pctxr = ctx_sub.add_parser("resolved", help="Show effective requester context after org/personal overrides")
    add_api(pctxr)
    pctxr.set_defaults(func=commands["context_resolved"])

    # capabilities
    pc = sub.add_parser("capabilities", help="Get machine-readable API capabilities")
    add_api(pc)
    pc.set_defaults(func=commands["capabilities"])

    # search
    register_search_subcommands(
        sub,
        add_api_common_arguments=add_api,
        handlers={
            "recipe": commands["search_recipe"],
            "companies": commands["search_companies"],
            "people": commands["search_people"],
            "similar_companies": commands["search_similar_companies"],
            "news": commands["search_news"],
            "local_businesses": commands["search_local_businesses"],
            "reviews": commands["search_reviews"],
            "filings": commands["search_filings"],
        },
        recipe_types=SEARCH_RECIPE_TYPES,
    )

    # workflows
    pw = sub.add_parser("workflows", help="Common multi-step workflow blueprints")
    workflows_sub = pw.add_subparsers(dest="workflows_cmd", required=True)

    pwl = workflows_sub.add_parser("list", help="List common workflow blueprints")
    pwl.add_argument("--out-file", help="Save JSON payload to file")
    add_output(pwl)
    pwl.set_defaults(func=commands["workflows_list"])

    pwr = workflows_sub.add_parser("recipe", help="Emit one workflow blueprint payload")
    pwr.add_argument("--type", required=True, choices=WORKFLOW_BLUEPRINT_TYPES, help="Workflow blueprint type")
    pwr.add_argument("--out-file", help="Save raw workflow payload to file")
    add_output(pwr)
    pwr.set_defaults(func=commands["workflows_recipe"])

    pws = workflows_sub.add_parser("scaffold", help="Emit ordered workflow files and commands for a blueprint")
    pws.add_argument("--type", required=True, choices=WORKFLOW_BLUEPRINT_TYPES, help="Workflow blueprint type")
    pws.add_argument("--out-dir", help="Directory to save scaffold payload files")
    add_output(pws)
    pws.set_defaults(func=commands["workflows_scaffold"])

    # tables
    register_tables_subcommands(
        sub,
        add_api_common_arguments=add_api,
        add_output_formatting_arguments=add_output,
        handlers={
            "list": commands["tables_list"],
            "create": commands["tables_create"],
            "update": commands["tables_update"],
            "identity_get": commands["tables_identity_get"],
            "identity_set": commands["tables_identity_set"],
            "identity_clear": commands["tables_identity_clear"],
            "schema": commands["tables_schema"],
        },
        table_schema_types=TABLE_SCHEMA_TYPES,
    )

    # blacklist
    pbl = sub.add_parser("blacklist", help="Organization blacklist operations (admin)")
    blacklist_sub = pbl.add_subparsers(dest="blacklist_cmd", required=True)

    pbll = blacklist_sub.add_parser("list", help="List blacklist entries")
    pbll.add_argument("--type-filter", choices=["all", "email", "domain"], default="all")
    pbll.add_argument("--search", help="Filter by value/reason/notes")
    pbll.add_argument("--page", type=int, default=1, help="Page number (default: 1)")
    pbll.add_argument("--limit", type=int, default=100, help="Entries per page (default: 100)")
    add_api(pbll)
    pbll.set_defaults(func=commands["blacklist_list"])

    pbla = blacklist_sub.add_parser("add", help="Add a blacklist entry")
    pbla.add_argument("--type", choices=["email", "domain"], required=True, help="Entry type")
    pbla.add_argument("--value", required=True, help="Email or domain value")
    pbla.add_argument("--reason", default="", help="Optional reason")
    pbla.add_argument("--notes", default="", help="Optional notes")
    add_api(pbla)
    pbla.set_defaults(func=commands["blacklist_add"])

    pblr = blacklist_sub.add_parser("remove", help="Remove a blacklist entry by id")
    pblr.add_argument("--entry-id", required=True)
    add_api(pblr)
    pblr.set_defaults(func=commands["blacklist_remove"])

    pbli = blacklist_sub.add_parser("import", help="Import blacklist entries from CSV/TXT")
    pbli.add_argument("--file", required=True, help="Path to CSV/TXT file")
    add_api(pbli)
    pbli.set_defaults(func=commands["blacklist_import"])

    pblc = blacklist_sub.add_parser("check", help="Check whether emails are blacklisted")
    pblc.add_argument(
        "--email",
        action="append",
        help="Email value (repeatable; comma-separated also supported)",
    )
    pblc.add_argument("--emails-json", help="JSON array or object with emails[]")
    pblc.add_argument("--emails-file", help="Path to .txt/.csv/.json email list")
    pblc.add_argument("--emails-column", default="email", help="CSV column for emails (default: email)")
    pblc.add_argument(
        "--chunk-size",
        type=int,
        default=1000,
        help="Request chunk size (max 1000, default: 1000)",
    )
    pblc.add_argument("--summary-only", action="store_true", help="Omit per-email results")
    add_api(pblc)
    pblc.set_defaults(func=commands["blacklist_check"])

    pblf = blacklist_sub.add_parser("filter", help="Filter blacklisted emails from a list")
    pblf.add_argument(
        "--email",
        action="append",
        help="Email value (repeatable; comma-separated also supported)",
    )
    pblf.add_argument("--emails-json", help="JSON array or object with emails[]")
    pblf.add_argument("--emails-file", help="Path to .txt/.csv/.json email list")
    pblf.add_argument("--emails-column", default="email", help="CSV column for emails (default: email)")
    pblf.add_argument(
        "--chunk-size",
        type=int,
        default=10000,
        help="Request chunk size (max 10000, default: 10000)",
    )
    pblf.add_argument("--summary-only", action="store_true", help="Omit full clean/blocked lists")
    add_api(pblf)
    pblf.set_defaults(func=commands["blacklist_filter"])

    # rows
    register_rows_subcommands(
        sub,
        add_api_common_arguments=add_api,
        add_output_formatting_arguments=add_output,
        handlers={
            "list": commands["rows_list"],
            "get": commands["rows_get"],
            "add": commands["rows_add"],
            "import_csv": commands["rows_import_csv"],
            "schema": commands["rows_schema"],
            "import_status": commands["rows_import_status"],
            "import_verify": commands["rows_import_verify"],
            "import_rollback": commands["rows_import_rollback"],
            "delete": commands["rows_delete"],
            "dedupe": commands["rows_dedupe"],
        },
        row_schema_types=ROW_SCHEMA_TYPES,
    )

    # cells
    register_cells_subcommands(
        sub,
        add_api_common_arguments=add_api,
        add_output_formatting_arguments=add_output,
        handlers={
            "get": commands["cells_get"],
            "patch": commands["cells_patch"],
            "schema": commands["cells_schema"],
        },
        schema_types=CELL_SCHEMA_TYPES,
    )

    # columns
    register_columns_subcommands(
        sub,
        add_api_common_arguments=add_api,
        add_output_formatting_arguments=add_output,
        add_run_scope_arguments=_add_run_scope_arguments,
        add_run_execution_arguments=_add_run_execution_arguments,
        handlers={
            "list": commands["columns_list"],
            "create": commands["columns_create"],
            "update": commands["columns_update"],
            "move": commands["columns_move"],
            "delete": commands["columns_delete"],
            "projections": commands["columns_projections"],
            "test_http_request": commands["columns_test_http_request"],
            "run": commands["columns_run"],
            "stop": commands["columns_stop"],
            "run_next": commands["columns_run_next"],
            "estimate": commands["columns_estimate"],
            "recipe": commands["columns_recipe"],
        },
        column_recipe_types=COLUMN_RECIPE_TYPES,
    )

    # sequences
    register_sequences_subcommands(
        sub,
        add_api_common_arguments=add_api,
        add_output_formatting_arguments=add_output,
        add_actor_override_argument=_add_actor_override_argument,
        handlers={
            "list": commands["sequences_list"],
            "get": commands["sequences_get"],
            "stats": commands["sequences_stats"],
            "delivery_status": commands["sequences_delivery_status"],
            "recipe": commands["sequences_recipe"],
            "create": commands["sequences_create"],
            "update": commands["sequences_update"],
            "delete": commands["sequences_delete"],
            "clone": commands["sequences_clone"],
            "status": commands["sequences_status"],
            "enroll": commands["sequences_enroll"],
            "pause_enrollment": commands["sequences_pause_enrollment"],
        },
        sequence_recipe_types=SEQUENCE_RECIPE_TYPES,
    )

    # leads
    register_leads_subcommands(
        sub,
        add_api_common_arguments=add_api,
        add_leads_selection_arguments=_add_leads_selection_arguments,
        add_leads_query_arguments=_add_leads_query_arguments,
        add_leads_research_arguments=_add_leads_research_arguments,
        handlers={
            "recipe": commands["leads_recipe"],
            "get": commands["leads_get"],
            "create": commands["leads_create"],
            "update": commands["leads_update"],
            "upsert_contact_channels": commands["leads_upsert_contact_channels"],
            "update_status": commands["leads_update_status"],
            "reassign_owner": commands["leads_reassign_owner"],
            "query": commands["leads_query"],
            "count": commands["leads_count"],
            "stats": commands["leads_stats"],
            "research": commands["leads_research"],
            "filters_metadata": commands["leads_filters_metadata"],
            "research_tables": commands["leads_research_tables"],
        },
        lead_recipe_types=LEAD_RECIPE_TYPES,
    )

    # tasks
    ptask = sub.add_parser("tasks", help="Task queue operations")
    tasks_sub = ptask.add_subparsers(dest="tasks_cmd", required=True)
    add_tasks_subcommands(
        tasks_sub,
        add_api_common_arguments=add_api,
        add_actor_override_argument=_add_actor_override_argument,
        task_recipe_types=TASK_RECIPE_TYPES,
        handlers={
            "recipe": commands["tasks_recipe"],
            "query": commands["tasks_query"],
            "count": commands["tasks_count"],
            "assignee_counts": commands["tasks_assignee_counts"],
            "get": commands["tasks_get"],
            "stats": commands["tasks_stats"],
            "create": commands["tasks_create"],
            "update": commands["tasks_update"],
            "delete": commands["tasks_delete"],
            "bulk_update": commands["tasks_bulk_update"],
            "bulk_delete": commands["tasks_bulk_delete"],
            "draft": commands["tasks_draft"],
            "schedule_email": commands["tasks_schedule_email"],
        },
    )

    # jobs
    register_jobs_subcommands(
        sub,
        add_api_common_arguments=add_api,
        handlers={
            "get": commands["jobs_get"],
            "list": commands["jobs_list"],
            "watch": commands["jobs_watch"],
        },
    )

    # webhooks
    pwh = sub.add_parser("webhooks", help="Table webhook operations")
    wh_sub = pwh.add_subparsers(dest="webhooks_cmd", required=True)
    add_webhooks_subcommands(
        wh_sub,
        add_api_common_arguments=add_api,
        webhook_recipe_types=WEBHOOK_RECIPE_TYPES,
        handlers={
            "recipe": commands["webhooks_recipe"],
            "get": commands["webhooks_get"],
            "rotate": commands["webhooks_rotate"],
            "ingest": commands["webhooks_ingest"],
            "configure_ingest": commands["webhooks_configure_ingest"],
            "subscriptions_list": commands["webhooks_subscriptions_list"],
            "subscriptions_create": commands["webhooks_subscriptions_create"],
            "subscriptions_get": commands["webhooks_subscriptions_get"],
            "subscriptions_update": commands["webhooks_subscriptions_update"],
            "subscriptions_delete": commands["webhooks_subscriptions_delete"],
            "subscriptions_pause": commands["webhooks_subscriptions_pause"],
            "subscriptions_resume": commands["webhooks_subscriptions_resume"],
            "subscriptions_rotate_secret": commands["webhooks_subscriptions_rotate_secret"],
            "subscriptions_test": commands["webhooks_subscriptions_test"],
            "deliveries_list": commands["webhooks_deliveries_list"],
            "deliveries_attempts": commands["webhooks_deliveries_attempts"],
        },
    )

    # linkedin
    register_linkedin_subcommands(
        sub,
        add_api_common_arguments=add_api,
        handlers={
            "recipe": commands["linkedin_recipe"],
            "status": commands["linkedin_status"],
            "limits": commands["linkedin_limits"],
            "search": commands["linkedin_search"],
            "search_params": commands["linkedin_search_params"],
            "posts": commands["linkedin_posts"],
            "post": commands["linkedin_post"],
            "comments": commands["linkedin_comments"],
            "reactions": commands["linkedin_reactions"],
        },
        recipe_types=LINKEDIN_RECIPE_TYPES,
    )

    # Backward-compatible aliases
    # run
    palias_run = sub.add_parser("run", help="Alias for: columns run")
    palias_run.add_argument("--table-id", required=True)
    palias_run.add_argument("--column-id", required=True)
    _add_run_scope_arguments(palias_run)
    _add_run_execution_arguments(palias_run)
    add_api(palias_run)
    palias_run.set_defaults(func=commands["columns_run"])

    palias_run_next = sub.add_parser("run-next", help="Alias for: columns run-next")
    palias_run_next.add_argument("--table-id", required=True)
    palias_run_next.add_argument("--column-id", required=True)
    palias_run_next.add_argument("--count", type=int, required=True, help="Maximum number of rows to queue")
    palias_run_next.add_argument("--filters-json", help="Optional JSON object to select from filtered rows")
    palias_run_next.add_argument("--filters-file", help="Optional JSON file to select from filtered rows")
    palias_run_next.add_argument("--page-size", type=int, default=200, help="Rows page size while selecting candidates (max 1000)")
    palias_run_next.add_argument(
        "--include-processed",
        dest="unprocessed_only",
        action="store_false",
        help="Include rows that already have output in candidate selection",
    )
    palias_run_next.add_argument("--fail-if-empty", action="store_true", help="Exit non-zero when no eligible rows are selected")
    palias_run_next.set_defaults(unprocessed_only=True)
    _add_run_execution_arguments(palias_run_next)
    add_api(palias_run_next)
    palias_run_next.set_defaults(func=commands["columns_run_next"])

    # status
    ps = sub.add_parser("status", help="Show pending/done/error counts for a table/column (requires pymongo + MONGODB_URI)")
    ps.add_argument("--table-id", required=True)
    ps.add_argument("--column-id", required=True)
    ps.add_argument("--mongo-uri", help="Override MONGODB_URI")
    ps.add_argument("--db-name", help="Override MONGODB_DB_NAME")
    add_output(ps)
    ps.set_defaults(func=commands["status"])

    # watch
    pw = sub.add_parser("watch", help="Watch status periodically (requires pymongo + MONGODB_URI)")
    pw.add_argument("--table-id", required=True)
    pw.add_argument("--column-id", required=True)
    pw.add_argument("--interval", type=int, default=5)
    pw.add_argument("--once", action="store_true", help="Fetch one snapshot and exit")
    pw.add_argument("--mongo-uri", help="Override MONGODB_URI")
    pw.add_argument("--db-name", help="Override MONGODB_DB_NAME")
    add_output(pw, default_output="human")
    pw.set_defaults(func=commands["watch"])

    # SOP guidance
    psop = sub.add_parser("sop", help="Print credit-safe execution SOP + default filtered run payload")
    psop.add_argument("--out-file", help="Write SOP JSON to file")
    add_output(psop)
    psop.set_defaults(func=commands["sop"])

    return p
