from __future__ import annotations

import argparse
import json
import sys
from pathlib import Path

from autotouch_cli.exceptions import AutotouchInputError
from dataclasses import dataclass
from typing import Any, Callable, Dict, List, Optional, Tuple

from autotouch_shared.linkedin_contract import (
    linkedin_recipe_entries,
    linkedin_recipe_types,
)
from autotouch_shared.provider_registry import (
    automation_workflow_blueprints,
    get_recipe_contract,
    recipe_types,
)
from autotouch_shared.search_contract import (
    search_recipe_entries,
    search_recipe_types,
)


@dataclass(frozen=True)
class TemplateRuntime:
    output_mode: str
    print_json: Callable[[Any, bool], None]
    emit_text: Callable[[str], None]


COLUMN_RECIPE_BASE_TYPES = tuple(recipe_types())
COLUMN_RECIPE_ALIASES: Dict[str, Dict[str, Any]] = {
    "add_to_leads": {
        "canonical": "add_to_crm",
        "key": "add_to_leads",
        "label": "Add to Leads",
        "notes": [
            "CLI alias for the canonical add_to_crm provider recipe.",
            "Use this when the operator language is 'Add to Leads'. Internal provider/config names remain add_to_crm.",
        ],
    },
    "sync_to_leads": {
        "canonical": "add_to_crm",
        "key": "sync_to_leads",
        "label": "Sync to Leads",
        "notes": [
            "CLI alias for the canonical add_to_crm provider recipe.",
            "Use this when the operator language is 'Sync to Leads'. Internal provider/config names remain add_to_crm.",
        ],
    },
}
COLUMN_RECIPE_TYPES = tuple(dict.fromkeys([*COLUMN_RECIPE_BASE_TYPES, *COLUMN_RECIPE_ALIASES.keys()]))
WORKFLOW_BLUEPRINT_TYPES = tuple(sorted(automation_workflow_blueprints().keys()))
WORKFLOW_PROVIDER_RECIPE_TYPES: Dict[str, str] = {
    "llm": "llm_enrichment",
    "lead_finder": "lead_finder",
    "email_finder": "email_finder",
    "phone_finder": "phone_finder",
    "sync_to_table": "sync_to_table",
    "add_to_crm": "sync_to_leads",
    "add_to_sequence": "add_to_sequence",
}

_SINGLE_BUYER_FIELDS: List[Tuple[str, str]] = [
    ("first_name", "First Name"),
    ("last_name", "Last Name"),
    ("title", "Title"),
    ("company_name", "Company Name"),
    ("company_website", "Company Website"),
    ("linkedin_url", "LinkedIn URL"),
]
_SINGLE_BUYER_CRM_FIELDS: List[Tuple[str, str]] = [
    ("first_name", "First Name"),
    ("last_name", "Last Name"),
    ("title", "Title"),
    ("company_name", "Company Name"),
    ("company_website", "Company Website"),
    ("company_domain", "Company Domain"),
    ("linkedin_url", "LinkedIn URL"),
]
_CONTACT_ITEM_FIELDS: List[Tuple[str, str]] = [
    ("first_name", "First Name"),
    ("last_name", "Last Name"),
    ("title", "Title"),
    ("company_name", "Company Name"),
    ("company_domain", "Company Domain"),
    ("linkedin_url", "LinkedIn URL"),
]


def _build_column_recipe_catalog() -> Tuple[Dict[str, Dict[str, Any]], Dict[str, List[str]]]:
    recipes: Dict[str, Dict[str, Any]] = {}
    notes: Dict[str, List[str]] = {}
    for recipe_type in COLUMN_RECIPE_BASE_TYPES:
        contract = get_recipe_contract(recipe_type)
        if not contract:
            continue
        payload = contract.recipe_payload_copy()
        if isinstance(payload, dict):
            recipes[recipe_type] = payload
        notes[recipe_type] = contract.recipe_notes_list()
    for alias, metadata in COLUMN_RECIPE_ALIASES.items():
        canonical = str(metadata.get("canonical") or "").strip()
        canonical_payload = recipes.get(canonical)
        if not isinstance(canonical_payload, dict):
            continue
        alias_payload = json.loads(json.dumps(canonical_payload))
        if metadata.get("key"):
            alias_payload["key"] = metadata["key"]
        if metadata.get("label"):
            alias_payload["label"] = metadata["label"]
        recipes[alias] = alias_payload
        alias_notes = list(metadata.get("notes") or [])
        alias_notes.extend(notes.get(canonical) or [])
        notes[alias] = alias_notes
    return recipes, notes


COLUMN_CREATE_RECIPES, COLUMN_RECIPE_NOTES = _build_column_recipe_catalog()
WORKFLOW_BLUEPRINTS = json.loads(json.dumps(automation_workflow_blueprints()))

SEQUENCE_RECIPE_TYPES = [
    "create",
    "bulk_automated",
    "linkedin_outreach",
    "enroll",
]

SEQUENCE_RECIPES: Dict[str, Dict[str, Any]] = {
    "create": {
        "name": "Outbound v1",
        "sourceTableId": "<TABLE_ID>",
        "sourceTableName": "ICP Prospects",
        "exitRules": {
            "stopOnReply": True,
            "stopOnBounce": True,
            "stopOnManualOptOut": True,
        },
        "schedule": {
            "timezone": "America/New_York",
            "workingDays": [1, 2, 3, 4, 5],
            "dailyWindowStart": "09:00",
            "dailyWindowEnd": "17:00",
            "skipHolidays": False,
        },
        "emailDelivery": {
            "mode": "personal",
        },
        "defaultAppendSignature": True,
        "steps": [
            {
                "id": "email_1",
                "kind": "EMAIL",
                "waitDays": 0,
                "waitHours": 0,
                "waitMinutes": 0,
                "executionMode": "AUTOMATED",
                "emailSendMode": "AUTOMATED",
                "emailIsReply": False,
                "appendSignature": True,
                "aiDraft": True,
                "subjectTemplate": "",
                "bodyTemplate": "",
            },
            {
                "id": "call_1",
                "kind": "CALL",
                "waitDays": 1,
                "waitHours": 0,
                "waitMinutes": 0,
                "executionMode": "AUTOMATED",
                "aiDraft": True,
                "scriptTemplate": "",
            },
            {
                "id": "linkedin_1",
                "kind": "LINKEDIN_CHAT_MESSAGE",
                "waitDays": 2,
                "waitHours": 0,
                "waitMinutes": 0,
                "executionMode": "AUTOMATED",
                "aiDraft": True,
                "scriptTemplate": "",
            },
        ],
    },
    "bulk_automated": {
        "name": "Bulk outbound v1",
        "sourceTableId": "<TABLE_ID>",
        "sourceTableName": "ICP Prospects",
        "exitRules": {
            "stopOnReply": True,
            "stopOnBounce": True,
            "stopOnManualOptOut": True,
        },
        "schedule": {
            "timezone": "America/New_York",
            "workingDays": [1, 2, 3, 4, 5],
            "dailyWindowStart": "09:00",
            "dailyWindowEnd": "17:00",
            "skipHolidays": False,
        },
        "emailDelivery": {
            "mode": "bulk",
            "bulkProvider": "instantly",
        },
        "defaultAppendSignature": True,
        "steps": [
            {
                "id": "email_1",
                "kind": "EMAIL",
                "waitDays": 0,
                "waitHours": 0,
                "waitMinutes": 0,
                "emailSendMode": "AUTOMATED",
                "emailIsReply": False,
                "appendSignature": True,
                "subjectTemplate": "Quick intro",
                "bodyTemplate": "Hi {{first_name}}, ...",
            },
            {
                "id": "email_2",
                "kind": "EMAIL",
                "waitDays": 2,
                "waitHours": 0,
                "waitMinutes": 0,
                "emailSendMode": "AUTOMATED",
                "emailIsReply": False,
                "appendSignature": True,
                "subjectTemplate": "Following up",
                "bodyTemplate": "Hi {{first_name}}, circling back on this.",
            },
        ],
    },
    "linkedin_outreach": {
        "name": "LinkedIn outreach v1",
        "sourceTableId": "<TABLE_ID>",
        "sourceTableName": "ICP Prospects",
        "exitRules": {
            "stopOnReply": True,
            "stopOnBounce": True,
            "stopOnManualOptOut": True,
            "stopOnLinkedInReply": True,
        },
        "schedule": {
            "timezone": "America/New_York",
            "workingDays": [1, 2, 3, 4, 5],
            "dailyWindowStart": "09:00",
            "dailyWindowEnd": "17:00",
            "skipHolidays": False,
        },
        "emailDelivery": {
            "mode": "personal",
        },
        "steps": [
            {
                "id": "visit_1",
                "kind": "LINKEDIN_VISIT_PROFILE",
                "waitDays": 0,
                "waitHours": 0,
                "waitMinutes": 0,
                "executionMode": "AUTOMATED",
            },
            {
                "id": "connect_1",
                "kind": "LINKEDIN_CONNECT",
                "waitDays": 1,
                "waitHours": 0,
                "waitMinutes": 0,
                "executionMode": "AUTOMATED",
                "aiDraft": True,
                "scriptTemplate": "",
            },
            {
                "id": "message_1",
                "kind": "LINKEDIN_CHAT_MESSAGE",
                "waitDays": 2,
                "waitHours": 0,
                "waitMinutes": 0,
                "executionMode": "AUTOMATED",
                "aiDraft": True,
                "scriptTemplate": "",
            },
        ],
    },
    "enroll": {
        "leadIds": ["<LEAD_ID_1>", "<LEAD_ID_2>"],
        "researchContext": {
            "sourceTableId": "<TABLE_ID>",
        },
        "reviewOnly": False,
    },
}

SEQUENCE_RECIPE_NOTES: Dict[str, List[str]] = {
    "create": [
        "Recommended default: personal delivery plus AUTOMATED email with aiDraft=true.",
        "AI draft generates subject + body at execution time.",
        "AI draft for LinkedIn message generates message copy; AI draft for call generates a call script.",
        "External sequence create/update requires sourceTableId.",
        "Pass --actor-user-id on mutating commands when you need explicit actor resolution.",
    ],
    "bulk_automated": [
        "Use this when the provider should send email automatically at scale.",
        "Bulk delivery sequences cannot contain MANUAL email steps, so aiDraft does not apply to those email steps.",
        "Activate only after provider readiness is configured (autotouch sequences delivery-status).",
    ],
    "linkedin_outreach": [
        "LinkedIn outreach sequence: visit profile, connect, then message.",
        "Supported step kinds: LINKEDIN_VISIT_PROFILE, LINKEDIN_CONNECT, LINKEDIN_CHAT_MESSAGE, LINKEDIN_VOICE_MESSAGE.",
        "LINKEDIN_VISIT_PROFILE is always AUTOMATED and visits the lead's profile (notifies by default).",
        "LINKEDIN_CONNECT sends a connection request; scriptTemplate is the invite note (max 300 chars).",
        "LINKEDIN_CHAT_MESSAGE sends a direct message; uses InMail credits for 2nd/3rd degree connections.",
        "LINKEDIN_VOICE_MESSAGE sends a voice note; only works for 1st degree connections.",
        "Connection degree (1st/2nd/3rd/out_of_network) is auto-detected from the LinkedIn profile.",
        "Set stopOnLinkedInReply=true in exitRules to stop the sequence when the lead replies on LinkedIn.",
        "Leads must have a linkedin_url field to be reachable for LinkedIn steps.",
    ],
    "enroll": [
        "task_id is the bulk-job id; use autotouch jobs watch --job-id <TASK_ID> or pass --wait.",
        "Set reviewOnly=true to stage enrollments on DRAFT/PAUSED sequences without starting them yet.",
    ],
}

LEAD_RECIPE_TYPES = [
    "create",
    "update",
    "contact_upsert",
    "query",
    "research",
    "selection",
]

LEAD_RECIPES: Dict[str, Dict[str, Any]] = {
    "create": {
        "company_domain": "acme.com",
        "linkedin_url": "https://www.linkedin.com/in/jane-doe",
        "first_name": "Jane",
        "last_name": "Doe",
        "title": "VP Sales",
        "email_addresses": [
            {
                "address": "jane@acme.com",
                "provider": "manual",
                "type": "work",
                "primary": True,
            }
        ],
        "phone_numbers": [
            {
                "number": "+14155551234",
                "provider": "manual",
                "type": "mobile",
                "primary": True,
            }
        ],
        "notes": "Created by an agent during outbound prospecting.",
    },
    "update": {
        "title": "VP Revenue",
        "notes": "Confirmed role update from LinkedIn.",
        "location": "San Francisco, CA",
        "timezone": "America/Los_Angeles",
    },
    "contact_upsert": {
        "emails": [
            {
                "address": "jane@acme.com",
                "provider": "manual",
                "type": "work",
                "primary": True,
            }
        ],
        "phones": [
            {
                "number": "+14155551234",
                "provider": "manual",
                "type": "mobile",
                "primary": True,
            }
        ],
    },
    "query": {
        "filter": {
            "root": {
                "kind": "group",
                "logic": "and",
                "children": [
                    {"kind": "rule", "field": "status", "operator": "eq", "value": "active"},
                    {"kind": "rule", "field": "company_domain", "operator": "contains", "value": "acme"},
                ],
            }
        },
        "sort": [{"field": "created_at", "direction": "desc"}],
        "pagination": {"limit": 25},
        "scope": "org",
        "include_research_data": False,
    },
    "research": {
        "lead_ids": ["<LEAD_ID_1>", "<LEAD_ID_2>"],
        "source_table_id": "<TABLE_ID>",
        "field_ids": ["<FIELD_ID_1>", "<FIELD_ID_2>"],
    },
    "selection": {
        "selection": {
            "mode": "descriptor",
            "descriptor": {
                "root": {
                    "kind": "group",
                    "logic": "and",
                    "children": [
                        {"kind": "rule", "field": "status", "operator": "eq", "value": "active"},
                    ],
                }
            },
            "sort": [{"field": "created_at", "direction": "desc"}],
            "search": "revops",
            "limit": 100,
        }
    },
}

LEAD_RECIPE_NOTES: Dict[str, List[str]] = {
    "create": [
        "Use canonical lead schema fields when creating a new lead.",
        "Create can include email_addresses and phone_numbers directly.",
    ],
    "update": [
        "Use update for scalar/profile fields only.",
        "Do not pass email/phone fields here; use contact_upsert instead.",
        "Status and owner also have dedicated commands: leads update-status and leads reassign-owner.",
    ],
    "contact_upsert": [
        "This route merges emails/phones into the existing lead instead of replacing the arrays.",
        "Use canonical emails[] / phones[] input, or the equivalent emailAddresses / phoneNumbers aliases in raw API payloads.",
    ],
    "query": [
        "Use this shape for leads query/count/stats.",
        "filter uses the descriptor form accepted by the Leads API.",
    ],
    "research": [
        "Use this shape for leads research when you want specific lead ids and source table fields.",
    ],
    "selection": [
        "Use this selection payload shape for bulk lead actions like update-status and reassign-owner.",
    ],
}

TASK_RECIPE_TYPES = [
    "query",
    "query_linkedin",
    "create",
    "create_linkedin",
    "update",
    "bulk_update",
    "bulk_delete",
    "draft",
    "schedule_email",
]

TASK_RECIPES: Dict[str, Dict[str, Any]] = {
    "query": {
        "filters": [
            {"field": "task_status", "operator": "eq", "value": "pending"},
            {"field": "channel", "operator": "eq", "value": "email"},
        ],
        "sort": {"field": "due_date", "direction": "asc"},
        "include_skipped": False,
        "include_total_count": True,
        "include_research_values": True,
        "research_field_ids": ["<FIELD_ID_1>"],
    },
    "query_linkedin": {
        "filters": [
            {"field": "task_status", "operator": "eq", "value": "pending"},
            {"field": "channel", "operator": "eq", "value": "linkedin"},
        ],
        "sort": {"field": "due_date", "direction": "asc"},
        "include_skipped": False,
        "include_total_count": True,
    },
    "create": {
        "lead_ids": ["<LEAD_ID_1>"],
        "assigned_to_ids": ["<USER_ID>"],
        "channel": "email",
        "priority": "medium",
        "subject": "Intro follow-up",
        "scheduling": {"due_date": "2026-03-12T15:00:00Z"},
        "content": {
            "subject": "Quick intro",
            "body_text": "Hi {{first_name}}, wanted to reach out about your team.",
        },
        "notes": "Created from the CLI recipe.",
        "origin": "manual",
    },
    "create_linkedin": {
        "lead_ids": ["<LEAD_ID_1>"],
        "assigned_to_ids": ["<USER_ID>"],
        "channel": "linkedin",
        "linkedin_action": "message",
        "priority": "medium",
        "scheduling": {"due_date": "2026-03-12T15:00:00Z"},
        "content": {
            "type": "linkedin_message",
            "body": "Hi {{first_name}}, saw your work at {{company_name}} and wanted to connect.",
        },
        "notes": "LinkedIn message task from CLI.",
        "origin": "manual",
    },
    "update": {
        "task_status": "scheduled",
        "priority": "high",
        "due_date": "2026-03-13T16:00:00Z",
        "notes": "Move this task to tomorrow afternoon.",
    },
    "bulk_update": {
        "selection": {"mode": "ids", "ids": ["<TASK_ID_1>", "<TASK_ID_2>"]},
        "status": "scheduled",
        "priority": "high",
        "assigned_to": "<USER_ID>",
    },
    "bulk_delete": {
        "selection": {"mode": "ids", "ids": ["<TASK_ID_1>", "<TASK_ID_2>"]},
    },
    "draft": {
        "force": False,
    },
    "schedule_email": {
        "provider": "gmail",
        "provider_account_email": "rep@example.com",
        "subject": "Quick intro",
        "body_text": "Hi {{first_name}}, reaching out after reviewing your profile.",
        "body_format": "auto",
        "send_at": "2026-03-12T16:30:00Z",
    },
}

TASK_RECIPE_NOTES: Dict[str, List[str]] = {
    "query": [
        "Use this shape for tasks query, count, and assignee-counts.",
        "filters and sort follow the Tasks CRM request contract.",
        "channel values: email, call, linkedin, custom.",
    ],
    "query_linkedin": [
        "Same shape as query, filtered to LinkedIn tasks.",
        "linkedin_action values on returned tasks: connect, message, voice_message, visit_profile.",
    ],
    "create": [
        "lead_ids and assigned_to_ids are required.",
        "scheduling is required and should include at least one due/scheduling value.",
    ],
    "create_linkedin": [
        "channel must be 'linkedin'.",
        "linkedin_action: 'connect' | 'message' | 'voice_message' | 'visit_profile'.",
        "Content type varies by action: linkedin_connect (message field), linkedin_message (body field), linkedin_voice_message (message + voice_message blob).",
        "1st degree connections get free direct messages; 2nd/3rd degree use InMail credits.",
        "Voice messages only work for 1st degree connections.",
    ],
    "update": [
        "Use this for a single task update payload.",
    ],
    "bulk_update": [
        "selection can be ids-based or descriptor-based depending on the backend contract.",
    ],
    "bulk_delete": [
        "Use task_ids or selection; this recipe shows the explicit selection form.",
    ],
    "draft": [
        "Task draft only accepts a small optional payload; pass --force to regenerate content.",
    ],
    "schedule_email": [
        "provider is usually gmail or microsoft for direct scheduling.",
    ],
}

WEBHOOK_RECIPE_TYPES = [
    "ingest",
    "subscription_create",
    "subscription_update",
    "subscription_test",
]

WEBHOOK_RECIPES: Dict[str, Dict[str, Any]] = {
    "ingest": {
        "records": [
            {
                "first_name": "Jane",
                "last_name": "Doe",
                "company_domain": "acme.com",
                "linkedin_url": "https://www.linkedin.com/in/jane-doe",
            }
        ]
    },
    "subscription_create": {
        "url": "https://example.com/webhooks/autotouch",
        "events": ["bulk_job.completed", "lead.created"],
        "enabled": True,
        "filters": {"organization_id": "<ORG_ID>"},
        "metadata": {"environment": "production"},
        "retryPolicy": {
            "max_attempts": 5,
            "base_delay_seconds": 30,
            "max_delay_seconds": 900,
            "timeout_seconds": 15,
        },
    },
    "subscription_update": {
        "events": ["bulk_job.completed", "bulk_job.partial"],
        "enabled": True,
        "filters": {"table_id": "<TABLE_ID>"},
        "metadata": {"owner": "ops"},
    },
    "subscription_test": {
        "eventType": "lead.created",
        "data": {"lead_id": "<LEAD_ID>", "source": "cli-test"},
    },
}

WEBHOOK_RECIPE_NOTES: Dict[str, List[str]] = {
    "ingest": [
        "Use this with webhooks ingest when writing rows through a table webhook token.",
    ],
    "subscription_create": [
        "retryPolicy is optional, but including it makes the request contract explicit for agents.",
    ],
    "subscription_update": [
        "Patch payloads can include only the fields you want to change.",
    ],
    "subscription_test": [
        "Use this to fire a synthetic event at a subscription endpoint.",
    ],
}

SEARCH_RECIPE_TYPES = list(search_recipe_types())
SEARCH_RECIPES: Dict[str, Dict[str, Any]] = search_recipe_entries()
SEARCH_RECIPE_NOTES: Dict[str, List[str]] = {}

LINKEDIN_RECIPE_TYPES = list(linkedin_recipe_types())
LINKEDIN_RECIPES: Dict[str, Dict[str, Any]] = linkedin_recipe_entries()
LINKEDIN_RECIPE_NOTES: Dict[str, List[str]] = {}

SHARED_SCHEMA_TYPES = [
    "filter_descriptor",
    "sort_spec",
    "run_request",
    "selection_descriptor",
]

SHARED_SCHEMAS: Dict[str, Dict[str, Any]] = {
    "filter_descriptor": {
        "mode": "and",
        "filters": [
            {"columnKey": "country", "operator": "equals", "value": "United States"},
            {"columnKey": "work_email", "operator": "isEmpty"},
        ],
    },
    "sort_spec": {"columnKey": "created_at", "direction": "desc"},
    "run_request": {
        "scope": "filtered",
        "filters": {
            "mode": "and",
            "filters": [
                {"columnKey": "company_domain", "operator": "isNotEmpty"},
                {"columnKey": "linkedin_url", "operator": "isNotEmpty"},
            ],
        },
        "unprocessedOnly": True,
        "firstN": 25,
    },
    "selection_descriptor": {
        "selection": {
            "mode": "descriptor",
            "descriptor": {
                "root": {
                    "kind": "group",
                    "logic": "and",
                    "children": [
                        {"kind": "rule", "field": "status", "operator": "eq", "value": "active"},
                    ],
                }
            },
            "sort": [{"field": "created_at", "direction": "desc"}],
            "limit": 100,
        }
    },
}

SHARED_SCHEMA_NOTES: Dict[str, List[str]] = {
    "filter_descriptor": [
        "Used by rows list, column runs, and conditional auto-run filters.",
    ],
    "sort_spec": [
        "Rows list accepts a sort object; some bulk selection APIs accept a list of sort objects.",
    ],
    "run_request": [
        "Use this shape with columns run or columns estimate when you want an explicit payload.",
    ],
    "selection_descriptor": [
        "Use this shape for ids-or-descriptor bulk selection APIs like leads and tasks bulk operations.",
    ],
}

AUTH_SCHEMA_TYPES = ["bootstrap", "login"]
AUTH_SCHEMAS: Dict[str, Dict[str, Any]] = {
    "bootstrap": {
        "first_name": "Ada",
        "last_name": "Lovelace",
        "email": "ada@example.com",
        "password": "$AUTOTOUCH_BOOTSTRAP_PASSWORD",
        "organization_name": "Autotouch Labs",
        "keyName": "Agent bootstrap key",
        "scopes": ["tables:read", "tables:write"],
    },
    "login": {
        "email": "ada@example.com",
        "password": "$AUTOTOUCH_LOGIN_PASSWORD",
    },
}
AUTH_SCHEMA_NOTES = {
    "bootstrap": ["Use this for auth bootstrap when you want to bypass individual flags and send the full payload."],
    "login": ["Use this for auth login when you want to provide the full payload via --data-json/--data-file."],
}

ONBOARDING_SCHEMA_TYPES = ["submit_domain"]
ONBOARDING_SCHEMAS = {
    "submit_domain": {
        "website": "https://www.autotouch.ai",
    }
}
ONBOARDING_SCHEMA_NOTES = {
    "submit_domain": ["Accepts website or domain; website is the most explicit form."],
}

ORG_CONTEXT_SCHEMA_TYPES = ["set"]
ORG_CONTEXT_SCHEMAS = {
    "set": {
        "source": "cli",
        "value_proposition": "We automate SDR research and admin work so reps can spend more time on live selling.",
        "ideal_titles": ["VP Sales", "Head of Growth"],
        "buyer_persona": "Revenue leadership",
        "user_persona": "SDR and AE teams",
        "voice_profile": "Direct, practical, operator-first.",
        "case_study_urls": ["https://example.com/case-study"],
    }
}
ORG_CONTEXT_SCHEMA_NOTES = {
    "set": ["Use this for org-context set when you want to send the full context payload instead of flags."],
}

PERSONAL_CONTEXT_SCHEMA_TYPES = ["set"]
PERSONAL_CONTEXT_SCHEMAS = {
    "set": {
        "title": "Founder",
        "linkedin_url": "https://www.linkedin.com/in/example",
        "territories": ["North America"],
        "personal_value_proposition": "I help GTM teams eliminate repetitive research work.",
        "voice_profile": "Founder-led, concise, technical.",
        "use_personal_value_proposition": True,
        "use_personal_voice_profile": True,
    }
}
PERSONAL_CONTEXT_SCHEMA_NOTES = {
    "set": ["Use this for personal-context set when you want the full JSON payload path."],
}

WORKSPACE_SECRET_SCHEMA_TYPES = ["set"]
WORKSPACE_SECRET_SCHEMAS = {
    "set": {
        "name": "clearbit",
        "value": "$CLEARBIT_API_KEY",
    }
}
WORKSPACE_SECRET_SCHEMA_NOTES = {
    "set": [
        "Use this with workspace-secrets set when you want to pass the full payload via --data-json/--data-file.",
        "Developer API keys need secrets:write (or *) to create/update secrets.",
    ],
}

PROMPT_TEMPLATE_SCHEMA_TYPES = ["create", "update"]
PROMPT_TEMPLATE_SCHEMAS: Dict[str, Dict[str, Any]] = {
    "create": {
        "name": "Company Research Agent",
        "description": "Identifies company domain, name, and summary from a person's title and LinkedIn profile.",
        "content": "Identify the company of the person from their title/headline. Return company_domain, company_name, and a 1-sentence summary.",
        "instructions": "Use people-discovery and company-profile skills to verify the company.",
        "response_schema": {
            "company_domain": "string",
            "company_name": "string",
            "company_summary": "string",
        },
        "type": "simple",
        "mode": "agent",
        "scope": "team",
    },
    "update": {
        "name": "Company Research Agent v2",
        "description": "Updated prompt with improved research strategy.",
    },
}
PROMPT_TEMPLATE_SCHEMA_NOTES: Dict[str, List[str]] = {
    "create": [
        "name and content are required; all other fields are optional.",
        "type is 'simple' (instructions mode) or 'raw' (full advanced prompt).",
        "mode is 'basic' (Quick LLM), 'agent' (Deep Research), or 'any' (both).",
        "scope is 'user' (private) or 'team' (shared with org).",
        "response_schema defines the JSON output structure for structured output columns.",
        "Developer API keys need prompts:write (or *) to create templates.",
    ],
    "update": [
        "Only include the fields you want to change; all fields are optional.",
        "Developer API keys need prompts:write (or *) to update templates.",
    ],
}


TABLE_SCHEMA_TYPES = ["create"]
TABLE_SCHEMAS = {
    "create": {
        "name": "CLI Contacts",
        "metadata": {"source": "csv_import", "owner": "ops"},
    }
}
TABLE_SCHEMA_NOTES = {
    "create": ["Use this with tables create when you want to pass the full payload instead of flags."],
}

ROW_SCHEMA_TYPES = ["add_records", "import_field_mappings"]
ROW_SCHEMAS = {
    "add_records": {
        "records": [
            {"first_name": "Jane", "last_name": "Doe", "company_domain": "acme.com"},
            {"first_name": "John", "last_name": "Smith", "company_domain": "example.com"},
        ]
    },
    "import_field_mappings": {
        "First Name": "first_name",
        "Last Name": "last_name",
        "Company Website": "company_domain",
        "LinkedIn URL": "linkedin_url",
    },
}
ROW_SCHEMA_NOTES = {
    "add_records": ["rows add also accepts a single object or a bare list; this recipe uses the explicit {records:[...]} form."],
    "import_field_mappings": ["Maps CSV headers to target column keys/standard fields for import-csv."],
}

CELL_SCHEMA_TYPES = ["patch_updates"]
CELL_SCHEMAS = {
    "patch_updates": {
        "updates": [
            {"rowId": "<ROW_ID>", "key": "status", "value": "New value"},
            {"rowId": "<ROW_ID_2>", "key": "score", "value": 42},
        ]
    }
}
CELL_SCHEMA_NOTES = {
    "patch_updates": [
        "cells patch accepts a bare list too; this schema uses the explicit updates[] wrapper.",
        "Use column key strings in updates[].key; the API does not accept columnId here.",
    ],
}


def _json_copy(payload: Any) -> Any:
    return json.loads(json.dumps(payload))


def _workflow_blueprint_payload(workflow_type: str) -> Dict[str, Any]:
    payload = WORKFLOW_BLUEPRINTS.get(workflow_type)
    if not payload:
        raise AutotouchInputError(f"unsupported workflow type '{workflow_type}'")
    return _json_copy(payload)


def _recipe_payload(recipe_type: str) -> Dict[str, Any]:
    payload = COLUMN_CREATE_RECIPES.get(recipe_type)
    if not payload:
        raise AutotouchInputError(f"unsupported recipe type '{recipe_type}'")
    return _json_copy(payload)


def _sequence_recipe_payload(recipe_type: str) -> Dict[str, Any]:
    payload = SEQUENCE_RECIPES.get(recipe_type)
    if not payload:
        raise AutotouchInputError(f"unsupported sequence recipe type '{recipe_type}'")
    return _json_copy(payload)


def _lead_recipe_payload(recipe_type: str) -> Dict[str, Any]:
    payload = LEAD_RECIPES.get(recipe_type)
    if not payload:
        raise AutotouchInputError(f"unsupported lead recipe type '{recipe_type}'")
    return _json_copy(payload)


def _task_recipe_payload(recipe_type: str) -> Dict[str, Any]:
    payload = TASK_RECIPES.get(recipe_type)
    if not payload:
        raise AutotouchInputError(f"unsupported task recipe type '{recipe_type}'")
    return _json_copy(payload)


def _webhook_recipe_payload(recipe_type: str) -> Dict[str, Any]:
    payload = WEBHOOK_RECIPES.get(recipe_type)
    if not payload:
        raise AutotouchInputError(f"unsupported webhook recipe type '{recipe_type}'")
    return _json_copy(payload)


def _schema_payload(recipe_type: str, payloads: Dict[str, Dict[str, Any]], *, noun: str) -> Dict[str, Any]:
    payload = payloads.get(recipe_type)
    if not payload:
        raise AutotouchInputError(f"unsupported {noun} type '{recipe_type}'")
    return _json_copy(payload)


def _write_json_file(path: Optional[str], payload: Any) -> None:
    if path:
        with open(path, "w", encoding="utf-8") as handle:
            json.dump(payload, handle, indent=2)


def _write_json_files(out_dir: Optional[str], artifacts: List[Dict[str, Any]]) -> List[str]:
    if not out_dir:
        return []
    target_dir = Path(out_dir).expanduser()
    target_dir.mkdir(parents=True, exist_ok=True)
    saved_paths: List[str] = []
    for artifact in artifacts:
        filename = str(artifact.get("filename") or "").strip()
        if not filename:
            continue
        file_path = target_dir / filename
        with open(file_path, "w", encoding="utf-8") as handle:
            json.dump(artifact.get("payload"), handle, indent=2)
        saved_paths.append(str(file_path))
    return saved_paths


def _schema_field_map(fields: List[Tuple[str, str]]) -> Dict[str, Any]:
    return {field_key: "string" for field_key, _label in fields}


def _projection_payload(*, source_column_id: str, fields: List[Tuple[str, str]]) -> Dict[str, Any]:
    return {
        "items": [
            {
                "key": field_key,
                "label": field_label,
                "sourceColumnId": source_column_id,
                "path": field_key,
                "dataType": "text",
            }
            for field_key, field_label in fields
        ]
    }


def _single_buyer_instructions(*, include_company_domain: bool) -> str:
    keys = ["first_name", "last_name", "title", "company_name", "company_website"]
    if include_company_domain:
        keys.append("company_domain")
    keys.append("linkedin_url")
    return (
        "Find the single most likely buyer of social/cell engagement software for this company. "
        "Return exactly one JSON object with keys: "
        + ", ".join(keys)
        + ". Use empty string when unknown. Do not return arrays or multiple people."
    )


def _single_buyer_agent_payload(*, include_company_domain: bool) -> Dict[str, Any]:
    payload = _recipe_payload("llm_enrichment")
    field_map = _schema_field_map(_SINGLE_BUYER_CRM_FIELDS if include_company_domain else _SINGLE_BUYER_FIELDS)
    payload["key"] = "single_buyer_agent"
    payload["label"] = "Single Buyer Agent"
    payload["config"] = {
        **dict(payload.get("config") or {}),
        "instructions": _single_buyer_instructions(include_company_domain=include_company_domain),
        "mode": "agent",
        "temperature": 0.2,
        "batchSize": 25,
        "promptSource": "generated",
        "useCompanyContext": True,
        "structuredOutput": True,
        "useAutoSchema": False,
        "response_schema": field_map,
        "user_schema": field_map,
    }
    return payload


def _items_agent_payload() -> Dict[str, Any]:
    payload = _recipe_payload("llm_enrichment")
    item_schema = _schema_field_map(_CONTACT_ITEM_FIELDS)
    array_schema = {
        "items": {
            "type": "array",
            "items": {
                "type": "object",
                "properties": item_schema,
            },
        }
    }
    payload["key"] = "contact_items_agent"
    payload["label"] = "Contact Items Agent"
    payload["config"] = {
        **dict(payload.get("config") or {}),
        "instructions": (
            "Research the best-fit contacts for this company and return JSON with a top-level items array. "
            "Each item must include first_name, last_name, title, company_name, company_domain, and linkedin_url. "
            "Return items as an empty array when no credible contact is found."
        ),
        "mode": "agent",
        "temperature": 0.2,
        "batchSize": 25,
        "promptSource": "generated",
        "useCompanyContext": True,
        "structuredOutput": True,
        "useAutoSchema": False,
        "response_schema": array_schema,
        "user_schema": array_schema,
    }
    return payload


def _single_buyer_phone_finder_payload() -> Dict[str, Any]:
    payload = _recipe_payload("phone_finder")
    payload["config"] = {
        **dict(payload.get("config") or {}),
        "firstName": "first_name",
        "lastName": "last_name",
        "company": "company_name",
        "linkedinUrl": "linkedin_url",
    }
    return payload


def _single_buyer_sync_to_leads_payload() -> Dict[str, Any]:
    payload = _recipe_payload("sync_to_leads")
    payload["config"] = {
        **dict(payload.get("config") or {}),
        "fieldMappings": {
            "mode": "single",
            "linkedinUrl": "linkedin_url",
            "companyDomain": "company_domain",
            "firstName": "first_name",
            "lastName": "last_name",
            "title": "title",
            "companyName": "company_name",
            "emailAddresses": [
                {"column": "work_email", "path": "response", "type": "work"},
            ],
            "phoneNumbers": [
                {"column": "mobile_phone", "path": "mobile_number", "type": "mobile"},
            ],
        },
        "sourceColumns": [
            "linkedin_url",
            "company_domain",
            "first_name",
            "last_name",
            "title",
            "company_name",
            "work_email",
            "mobile_phone",
        ],
    }
    return payload


def _items_sync_to_table_payload() -> Dict[str, Any]:
    payload = _recipe_payload("sync_to_table")
    payload["key"] = "sync_contacts_to_table"
    payload["label"] = "Sync Contacts To Table"
    payload["config"] = {
        "provider": "sync_to_table",
        "destinationTableId": "<DESTINATION_TABLE_ID>",
        "syncMode": "list",
        "listSourceColumnId": "<CONTACT_ITEMS_COLUMN_ID>",
        "listPath": "items",
        "syncBehavior": "link",
        "columnMappings": [
            {"sourceScope": "item", "itemKey": "first_name", "destKey": "first_name"},
            {"sourceScope": "item", "itemKey": "last_name", "destKey": "last_name"},
            {"sourceScope": "item", "itemKey": "title", "destKey": "title"},
            {"sourceScope": "item", "itemKey": "company_name", "destKey": "company_name"},
            {"sourceScope": "item", "itemKey": "company_domain", "destKey": "company_domain"},
            {"sourceScope": "item", "itemKey": "linkedin_url", "destKey": "linkedin_url"},
        ],
    }
    return payload


def _items_phone_finder_payload() -> Dict[str, Any]:
    payload = _recipe_payload("phone_finder")
    payload["config"] = {
        **dict(payload.get("config") or {}),
        "firstName": "first_name",
        "lastName": "last_name",
        "company": "company_name",
        "linkedinUrl": "linkedin_url",
    }
    return payload


def _items_sync_to_leads_payload() -> Dict[str, Any]:
    payload = _recipe_payload("sync_to_leads")
    payload["config"] = {
        **dict(payload.get("config") or {}),
        "fieldMappings": {
            "mode": "single",
            "linkedinUrl": "linkedin_url",
            "companyDomain": "company_domain",
            "firstName": "first_name",
            "lastName": "last_name",
            "title": "title",
            "companyName": "company_name",
            "emailAddresses": [
                {"column": "work_email", "path": "response", "type": "work"},
            ],
            "phoneNumbers": [
                {"column": "mobile_phone", "path": "mobile_number", "type": "mobile"},
            ],
        },
        "sourceColumns": [
            "linkedin_url",
            "company_domain",
            "first_name",
            "last_name",
            "title",
            "company_name",
            "work_email",
            "mobile_phone",
        ],
    }
    return payload


def _workflow_artifact(
    *,
    step: int,
    provider: str,
    filename: str,
    payload: Dict[str, Any],
    table_scope: str,
    notes: Optional[List[str]] = None,
    recipe_type: Optional[str] = None,
    kind: str = "column_recipe",
) -> Dict[str, Any]:
    artifact: Dict[str, Any] = {
        "step": step,
        "kind": kind,
        "provider": provider,
        "filename": filename,
        "payload": payload,
        "table_scope": table_scope,
    }
    if recipe_type:
        artifact["recipe_type"] = recipe_type
    if notes:
        artifact["notes"] = notes
    return artifact


def _workflow_scaffold_single_buyer_agent() -> Dict[str, Any]:
    artifacts = [
        _workflow_artifact(
            step=1,
            provider="llm",
            recipe_type="llm_enrichment",
            filename="01-single-buyer-agent.json",
            payload=_single_buyer_agent_payload(include_company_domain=False),
            table_scope="source",
            notes=[
                "This uses the exact one-person output contract from the buyer-discovery guide.",
            ],
        ),
        _workflow_artifact(
            step=2,
            provider="json_projections",
            filename="02-single-buyer-projections.json",
            payload=_projection_payload(
                source_column_id="<SINGLE_BUYER_AGENT_COLUMN_ID>",
                fields=_SINGLE_BUYER_FIELDS,
            ),
            table_scope="source",
            kind="projection_payload",
            notes=[
                "Run projections only after the single_buyer_agent column has finished with terminal status.",
            ],
        ),
    ]
    return {
        "artifacts": artifacts,
        "commands": [
            "autotouch columns create --table-id <TABLE_ID> --data-file 01-single-buyer-agent.json",
            "autotouch columns run --table-id <TABLE_ID> --column-id <SINGLE_BUYER_AGENT_COLUMN_ID> --wait",
            "autotouch columns projections --table-id <TABLE_ID> --after-column-id <SINGLE_BUYER_AGENT_COLUMN_ID> --data-file 02-single-buyer-projections.json",
        ],
        "notes": [
            "This scaffold stops after deterministic flat fields exist on the source table.",
            "Next common step: create email_finder / phone_finder columns on those projected fields.",
        ],
    }


def _workflow_scaffold_single_buyer_agent_to_crm() -> Dict[str, Any]:
    artifacts = [
        _workflow_artifact(
            step=1,
            provider="llm",
            recipe_type="llm_enrichment",
            filename="01-single-buyer-agent.json",
            payload=_single_buyer_agent_payload(include_company_domain=True),
            table_scope="source",
            notes=[
                "This extends the one-person contract with company_domain so Sync to Leads can map companyDomain directly.",
            ],
        ),
        _workflow_artifact(
            step=2,
            provider="json_projections",
            filename="02-single-buyer-projections.json",
            payload=_projection_payload(
                source_column_id="<SINGLE_BUYER_AGENT_COLUMN_ID>",
                fields=_SINGLE_BUYER_CRM_FIELDS,
            ),
            table_scope="source",
            kind="projection_payload",
            notes=[
                "Replace <SINGLE_BUYER_AGENT_COLUMN_ID> after the source column is created.",
            ],
        ),
        _workflow_artifact(
            step=3,
            provider="email_finder",
            recipe_type="email_finder",
            filename="03-email-finder.json",
            payload=_recipe_payload("email_finder"),
            table_scope="source",
        ),
        _workflow_artifact(
            step=4,
            provider="phone_finder",
            recipe_type="phone_finder",
            filename="04-phone-finder.json",
            payload=_single_buyer_phone_finder_payload(),
            table_scope="source",
        ),
        _workflow_artifact(
            step=5,
            provider="add_to_crm",
            recipe_type="sync_to_leads",
            filename="05-sync-to-leads.json",
            payload=_single_buyer_sync_to_leads_payload(),
            table_scope="source",
        ),
    ]
    return {
        "artifacts": artifacts,
        "commands": [
            "autotouch columns create --table-id <TABLE_ID> --data-file 01-single-buyer-agent.json",
            "autotouch columns run --table-id <TABLE_ID> --column-id <SINGLE_BUYER_AGENT_COLUMN_ID> --wait",
            "autotouch columns projections --table-id <TABLE_ID> --after-column-id <SINGLE_BUYER_AGENT_COLUMN_ID> --data-file 02-single-buyer-projections.json",
            "autotouch columns create --table-id <TABLE_ID> --data-file 03-email-finder.json",
            "autotouch columns create --table-id <TABLE_ID> --data-file 04-phone-finder.json",
            "autotouch columns create --table-id <TABLE_ID> --data-file 05-sync-to-leads.json",
            "autotouch columns run --table-id <TABLE_ID> --column-id <WORK_EMAIL_COLUMN_ID> --wait",
            "autotouch columns run --table-id <TABLE_ID> --column-id <MOBILE_PHONE_COLUMN_ID> --wait",
            "autotouch columns run --table-id <TABLE_ID> --column-id <SYNC_TO_LEADS_COLUMN_ID> --wait",
        ],
        "notes": [
            "The agent prompt returns exactly one buyer per row and the downstream commands preserve that dependency order.",
            "If company_domain is still unknown after agent research, derive it before running Sync to Leads.",
        ],
    }


def _workflow_scaffold_lead_finder_to_sequence() -> Dict[str, Any]:
    add_to_sequence_payload = _recipe_payload("add_to_sequence")
    add_to_sequence_payload["config"] = {
        **dict(add_to_sequence_payload.get("config") or {}),
        "sourceLeadColumn": "lead_contacts",
    }
    artifacts = [
        _workflow_artifact(
            step=1,
            provider="lead_finder",
            recipe_type="lead_finder",
            filename="01-lead-finder.json",
            payload=_recipe_payload("lead_finder"),
            table_scope="source",
        ),
        _workflow_artifact(
            step=2,
            provider="add_to_sequence",
            recipe_type="add_to_sequence",
            filename="02-add-to-sequence.json",
            payload=add_to_sequence_payload,
            table_scope="source",
        ),
    ]
    return {
        "artifacts": artifacts,
        "commands": [
            "autotouch columns create --table-id <TABLE_ID> --data-file 01-lead-finder.json",
            "autotouch columns create --table-id <TABLE_ID> --data-file 02-add-to-sequence.json",
            "autotouch columns run --table-id <TABLE_ID> --column-id <LEAD_FINDER_COLUMN_ID> --wait",
            "autotouch columns run --table-id <TABLE_ID> --column-id <ADD_TO_SEQUENCE_COLUMN_ID> --wait",
        ],
        "notes": [
            "The target sequence must already be ACTIVE before the final add_to_sequence run.",
        ],
    }


def _workflow_scaffold_items_to_contacts_then_sequence() -> Dict[str, Any]:
    add_to_sequence_payload = _recipe_payload("add_to_sequence")
    add_to_sequence_payload["config"] = {
        **dict(add_to_sequence_payload.get("config") or {}),
        "sourceLeadColumn": "sync_to_leads",
    }
    artifacts = [
        _workflow_artifact(
            step=1,
            provider="llm",
            recipe_type="llm_enrichment",
            filename="01-contact-items-agent.json",
            payload=_items_agent_payload(),
            table_scope="source",
        ),
        _workflow_artifact(
            step=2,
            provider="sync_to_table",
            recipe_type="sync_to_table",
            filename="02-sync-contacts-to-table.json",
            payload=_items_sync_to_table_payload(),
            table_scope="source",
            notes=[
                "This runs on the source table but writes linked/copied contact rows into <DESTINATION_TABLE_ID>.",
            ],
        ),
        _workflow_artifact(
            step=3,
            provider="email_finder",
            recipe_type="email_finder",
            filename="03-email-finder.json",
            payload=_recipe_payload("email_finder"),
            table_scope="destination",
        ),
        _workflow_artifact(
            step=4,
            provider="add_to_crm",
            recipe_type="sync_to_leads",
            filename="04-sync-to-leads.json",
            payload=_items_sync_to_leads_payload(),
            table_scope="destination",
        ),
        _workflow_artifact(
            step=5,
            provider="add_to_sequence",
            recipe_type="add_to_sequence",
            filename="05-add-to-sequence.json",
            payload=add_to_sequence_payload,
            table_scope="destination",
        ),
    ]
    return {
        "artifacts": artifacts,
        "commands": [
            "autotouch columns create --table-id <SOURCE_TABLE_ID> --data-file 01-contact-items-agent.json",
            "autotouch columns run --table-id <SOURCE_TABLE_ID> --column-id <CONTACT_ITEMS_COLUMN_ID> --wait",
            "autotouch columns create --table-id <SOURCE_TABLE_ID> --data-file 02-sync-contacts-to-table.json",
            "autotouch columns run --table-id <SOURCE_TABLE_ID> --column-id <SYNC_CONTACTS_TO_TABLE_COLUMN_ID> --wait",
            "autotouch columns create --table-id <DESTINATION_TABLE_ID> --data-file 03-email-finder.json",
            "autotouch columns create --table-id <DESTINATION_TABLE_ID> --data-file 04-sync-to-leads.json",
            "autotouch columns create --table-id <DESTINATION_TABLE_ID> --data-file 05-add-to-sequence.json",
            "autotouch columns run --table-id <DESTINATION_TABLE_ID> --column-id <WORK_EMAIL_COLUMN_ID> --wait",
            "autotouch columns run --table-id <DESTINATION_TABLE_ID> --column-id <SYNC_TO_LEADS_COLUMN_ID> --wait",
            "autotouch columns run --table-id <DESTINATION_TABLE_ID> --column-id <ADD_TO_SEQUENCE_COLUMN_ID> --wait",
        ],
        "notes": [
            "This scaffold handles the ordering across the source table and the destination contacts table.",
            "If you also need phone enrichment on the destination table, add a phone_finder column after the sync-to-table step.",
        ],
    }


def _workflow_scaffold_bundle(workflow_type: str) -> Dict[str, Any]:
    builders: Dict[str, Callable[[], Dict[str, Any]]] = {
        "lead_finder_to_sequence": _workflow_scaffold_lead_finder_to_sequence,
        "llm_items_to_contacts_then_sequence": _workflow_scaffold_items_to_contacts_then_sequence,
        "single_buyer_agent": _workflow_scaffold_single_buyer_agent,
        "single_buyer_agent_to_crm": _workflow_scaffold_single_buyer_agent_to_crm,
    }
    builder = builders.get(workflow_type)
    if builder is None:
        raise AutotouchInputError(f"unsupported workflow scaffold type '{workflow_type}'")
    return builder()


def _emit_template_bundle(
    *,
    selected_type: str,
    template_types: List[str],
    payload_getter: Callable[[str], Dict[str, Any]],
    notes_lookup: Dict[str, List[str]],
    usage_lookup: Any,
    label: str,
    out_file: Optional[str],
    compact: bool,
    runtime: TemplateRuntime,
) -> None:
    if selected_type == "all":
        payloads = {name: payload_getter(name) for name in template_types}
        output = {
            "recipes" if label == "recipe" else "schemas": payloads,
            "notes": notes_lookup,
            "usage": usage_lookup,
        }
        _write_json_file(out_file, output)
        if runtime.output_mode == "human":
            title = "Recipes" if label == "recipe" else "Schemas"
            runtime.emit_text(title)
            runtime.emit_text("-" * len(title))
            for name in template_types:
                runtime.emit_text("")
                _print_generic_template_human(
                    label=label,
                    recipe_type=name,
                    payload=payloads[name],
                    notes=notes_lookup.get(name) or [],
                    usage=usage_lookup.get(name) if isinstance(usage_lookup, dict) else usage_lookup,
                    out_file=None,
                    runtime=runtime,
                )
            if out_file:
                runtime.emit_text(f"\nSaved: {out_file}")
            return
        runtime.print_json(output, compact)
        return

    payload = payload_getter(selected_type)
    output = {
        "type": selected_type,
        "payload": payload,
        "notes": notes_lookup.get(selected_type) or [],
        "usage": usage_lookup.get(selected_type) if isinstance(usage_lookup, dict) else usage_lookup,
    }
    _write_json_file(out_file, payload)
    if runtime.output_mode == "human":
        _print_generic_template_human(
            label=label,
            recipe_type=selected_type,
            payload=payload,
            notes=notes_lookup.get(selected_type) or [],
            usage=output["usage"],
            out_file=out_file,
            runtime=runtime,
        )
        return
    runtime.print_json(output, compact)


def _print_generic_template_human(
    *,
    label: str,
    recipe_type: str,
    payload: Dict[str, Any],
    notes: List[str],
    usage: Any,
    out_file: Optional[str],
    runtime: TemplateRuntime,
) -> None:
    runtime.emit_text(f"{recipe_type} {label}")
    runtime.emit_text("----------------")
    runtime.emit_text(json.dumps(payload, indent=2))
    if notes:
        runtime.emit_text("")
        for note in notes:
            runtime.emit_text(f"- {note}")
    if usage:
        runtime.emit_text("\nUse with:")
        if isinstance(usage, dict):
            for command in usage.values():
                runtime.emit_text(str(command))
        else:
            runtime.emit_text(str(usage))
    if out_file:
        runtime.emit_text(f"\nSaved payload file: {out_file}")


def emit_workflows_list(args: argparse.Namespace, runtime: TemplateRuntime) -> None:
    blueprints = {name: _workflow_blueprint_payload(name) for name in WORKFLOW_BLUEPRINT_TYPES}
    output = {
        "workflows": blueprints,
        "usage": {
            "recipe": "autotouch workflows recipe --type <WORKFLOW_TYPE> --out-file <workflow.json>",
            "scaffold": "autotouch workflows scaffold --type <WORKFLOW_TYPE> --out-dir <workflow-dir>",
        },
        "notes": [
            "Workflow blueprints describe recommended multi-step pipelines; they do not create tables/columns automatically.",
            "Use workflow blueprints together with `autotouch workflows scaffold`, `autotouch columns recipe`, `autotouch sequences recipe`, and `autotouch capabilities`.",
        ],
    }
    _write_json_file(getattr(args, "out_file", None), output)
    if runtime.output_mode == "human":
        runtime.emit_text("Workflow blueprints")
        runtime.emit_text("-------------------")
        for name in WORKFLOW_BLUEPRINT_TYPES:
            bp = blueprints[name]
            runtime.emit_text(f"\n[{name}]")
            runtime.emit_text(f"Goal: {bp.get('goal') or 'n/a'}")
            recommended_when = bp.get("recommended_when") or []
            if recommended_when:
                runtime.emit_text("Recommended when:")
                for note in recommended_when:
                    runtime.emit_text(f"- {note}")
            steps = bp.get("steps") or []
            if steps:
                runtime.emit_text("Steps:")
                for idx, step in enumerate(steps, start=1):
                    provider = step.get("provider") or "unknown"
                    purpose = step.get("purpose") or ""
                    runtime.emit_text(f"{idx}. {provider}: {purpose}")
        runtime.emit_text("\nUse with:")
        runtime.emit_text("autotouch workflows recipe --type <WORKFLOW_TYPE> --out-file <workflow.json>")
        runtime.emit_text("autotouch workflows scaffold --type <WORKFLOW_TYPE> --out-dir <workflow-dir>")
        if getattr(args, "out_file", None):
            runtime.emit_text(f"\nSaved: {args.out_file}")
        return
    runtime.print_json(output, getattr(args, "compact", False))


def emit_workflows_recipe(args: argparse.Namespace, runtime: TemplateRuntime) -> None:
    selected_type = str(args.type or "")
    payload = _workflow_blueprint_payload(selected_type)
    output = {
        "type": selected_type,
        "workflow": payload,
        "usage": {
            "columns_recipe": "autotouch columns recipe --type <COLUMN_TYPE> --out-file <payload.json>",
            "sequence_recipe": "autotouch sequences recipe --type create --out-file <payload.json>",
            "capabilities": "autotouch capabilities --output json",
            "scaffold": f"autotouch workflows scaffold --type {selected_type} --out-dir <workflow-dir>",
        },
    }
    _write_json_file(getattr(args, "out_file", None), payload)
    if runtime.output_mode == "human":
        runtime.emit_text(f"{selected_type} workflow")
        runtime.emit_text("----------------")
        runtime.emit_text(json.dumps(payload, indent=2))
        runtime.emit_text("\nUse with:")
        runtime.emit_text("autotouch columns recipe --type <COLUMN_TYPE> --out-file <payload.json>")
        runtime.emit_text("autotouch sequences recipe --type create --out-file <payload.json>")
        runtime.emit_text(f"autotouch workflows scaffold --type {selected_type} --out-dir <workflow-dir>")
        if getattr(args, "out_file", None):
            runtime.emit_text(f"\nSaved workflow file: {args.out_file}")
        return
    runtime.print_json(output, getattr(args, "compact", False))


def emit_workflows_scaffold(args: argparse.Namespace, runtime: TemplateRuntime) -> None:
    selected_type = str(args.type or "")
    bundle = _workflow_scaffold_bundle(selected_type)
    artifacts = list(bundle.get("artifacts") or [])
    saved_files = _write_json_files(getattr(args, "out_dir", None), artifacts)
    output = {
        "type": selected_type,
        "workflow": _workflow_blueprint_payload(selected_type),
        "artifacts": artifacts,
        "commands": list(bundle.get("commands") or []),
        "notes": list(bundle.get("notes") or []),
        "saved_files": saved_files,
    }
    if runtime.output_mode == "human":
        runtime.emit_text(f"{selected_type} scaffold")
        runtime.emit_text("---------------------")
        for artifact in artifacts:
            runtime.emit_text(
                f"{artifact['step']}. {artifact['filename']} [{artifact['provider']}] ({artifact['table_scope']})"
            )
            for note in artifact.get("notes") or []:
                runtime.emit_text(f"- {note}")
        if output["notes"]:
            runtime.emit_text("")
            for note in output["notes"]:
                runtime.emit_text(f"- {note}")
        runtime.emit_text("\nCommands:")
        for command in output["commands"]:
            runtime.emit_text(command)
        if saved_files:
            runtime.emit_text("\nSaved files:")
            for path in saved_files:
                runtime.emit_text(path)
        return
    runtime.print_json(output, getattr(args, "compact", False))


def emit_columns_recipe(args: argparse.Namespace, runtime: TemplateRuntime) -> None:
    selected_type = str(args.type or "all")
    if selected_type == "all":
        recipes = {name: _recipe_payload(name) for name in COLUMN_RECIPE_TYPES}
        output = {
            "recipes": recipes,
            "notes": COLUMN_RECIPE_NOTES,
            "usage": "autotouch columns create --table-id <TABLE_ID> --data-file <payload.json>",
        }
        _write_json_file(getattr(args, "out_file", None), output)
        if runtime.output_mode == "human":
            runtime.emit_text("Column create payload recipes")
            runtime.emit_text("-----------------------------")
            for name in COLUMN_RECIPE_TYPES:
                runtime.emit_text(f"\n[{name}]")
                runtime.emit_text(json.dumps(recipes[name], indent=2))
                for note in COLUMN_RECIPE_NOTES.get(name) or []:
                    runtime.emit_text(f"- {note}")
            runtime.emit_text("\nUse with:")
            runtime.emit_text("autotouch columns create --table-id <TABLE_ID> --data-file <payload.json>")
            if getattr(args, "out_file", None):
                runtime.emit_text(f"\nSaved: {args.out_file}")
            return
        runtime.print_json(output, getattr(args, "compact", False))
        return

    payload = _recipe_payload(selected_type)
    output_single = {
        "type": selected_type,
        "payload": payload,
        "notes": COLUMN_RECIPE_NOTES.get(selected_type) or [],
        "usage": "autotouch columns create --table-id <TABLE_ID> --data-file <payload.json>",
    }
    _write_json_file(getattr(args, "out_file", None), payload)
    if runtime.output_mode == "human":
        runtime.emit_text(f"{selected_type} payload")
        runtime.emit_text("----------------")
        runtime.emit_text(json.dumps(payload, indent=2))
        for note in COLUMN_RECIPE_NOTES.get(selected_type) or []:
            runtime.emit_text(f"- {note}")
        runtime.emit_text("\nUse with:")
        runtime.emit_text("autotouch columns create --table-id <TABLE_ID> --data-file <payload.json>")
        if getattr(args, "out_file", None):
            runtime.emit_text(f"\nSaved payload file: {args.out_file}")
        return
    runtime.print_json(output_single, getattr(args, "compact", False))


def emit_sequences_recipe(args: argparse.Namespace, runtime: TemplateRuntime) -> None:
    _emit_template_bundle(
        selected_type=str(args.type or "all"),
        template_types=SEQUENCE_RECIPE_TYPES,
        payload_getter=_sequence_recipe_payload,
        notes_lookup=SEQUENCE_RECIPE_NOTES,
        usage_lookup={
            "create": "autotouch sequences create --data-file <payload.json>",
            "bulk_automated": "autotouch sequences create --data-file <payload.json>",
            "enroll": "autotouch sequences enroll --sequence-id <SEQUENCE_ID> --data-file <payload.json>",
        },
        label="recipe",
        out_file=getattr(args, "out_file", None),
        compact=getattr(args, "compact", False),
        runtime=runtime,
    )


def emit_leads_recipe(args: argparse.Namespace, runtime: TemplateRuntime) -> None:
    _emit_template_bundle(
        selected_type=str(args.type or "all"),
        template_types=LEAD_RECIPE_TYPES,
        payload_getter=_lead_recipe_payload,
        notes_lookup=LEAD_RECIPE_NOTES,
        usage_lookup={
            "create": "autotouch leads create --data-file <payload.json>",
            "update": "autotouch leads update --lead-id <LEAD_ID> --data-file <payload.json>",
            "contact_upsert": "autotouch leads upsert-contact-channels --lead-id <LEAD_ID> --data-file <payload.json>",
            "query": "autotouch leads query --data-file <payload.json>",
            "research": "autotouch leads research --data-file <payload.json>",
            "selection": "autotouch leads update-status --data-file <payload.json> --status <STATUS>",
        },
        label="recipe",
        out_file=getattr(args, "out_file", None),
        compact=getattr(args, "compact", False),
        runtime=runtime,
    )


def emit_tasks_recipe(args: argparse.Namespace, runtime: TemplateRuntime) -> None:
    _emit_template_bundle(
        selected_type=str(args.type or "all"),
        template_types=TASK_RECIPE_TYPES,
        payload_getter=_task_recipe_payload,
        notes_lookup=TASK_RECIPE_NOTES,
        usage_lookup={
            "query": "autotouch tasks query --data-file <payload.json>",
            "create": "autotouch tasks create --data-file <payload.json>",
            "update": "autotouch tasks update --task-id <TASK_ID> --data-file <payload.json>",
            "bulk_update": "autotouch tasks bulk-update --data-file <payload.json>",
            "bulk_delete": "autotouch tasks bulk-delete --data-file <payload.json>",
            "draft": "autotouch tasks draft --task-id <TASK_ID> --data-file <payload.json>",
            "schedule_email": "autotouch tasks schedule-email --task-id <TASK_ID> --data-file <payload.json>",
        },
        label="recipe",
        out_file=getattr(args, "out_file", None),
        compact=getattr(args, "compact", False),
        runtime=runtime,
    )


def emit_webhooks_recipe(args: argparse.Namespace, runtime: TemplateRuntime) -> None:
    _emit_template_bundle(
        selected_type=str(args.type or "all"),
        template_types=WEBHOOK_RECIPE_TYPES,
        payload_getter=_webhook_recipe_payload,
        notes_lookup=WEBHOOK_RECIPE_NOTES,
        usage_lookup={
            "ingest": "autotouch webhooks ingest --table-id <TABLE_ID> --records-file <payload.json>",
            "subscription_create": "autotouch webhooks subscriptions create --data-file <payload.json>",
            "subscription_update": "autotouch webhooks subscriptions update --subscription-id <SUB_ID> --data-file <payload.json>",
            "subscription_test": "autotouch webhooks subscriptions test --subscription-id <SUB_ID> --data-file <payload.json>",
        },
        label="recipe",
        out_file=getattr(args, "out_file", None),
        compact=getattr(args, "compact", False),
        runtime=runtime,
    )


def emit_schema(args: argparse.Namespace, runtime: TemplateRuntime) -> None:
    _emit_template_bundle(
        selected_type=str(args.type or "all"),
        template_types=SHARED_SCHEMA_TYPES,
        payload_getter=lambda schema_type: _schema_payload(schema_type, SHARED_SCHEMAS, noun="schema"),
        notes_lookup=SHARED_SCHEMA_NOTES,
        usage_lookup={
            "filter_descriptor": "Use with rows list, columns run, or conditional auto-run payloads.",
            "sort_spec": "Use with rows list or descriptor-based APIs that accept sort JSON.",
            "run_request": "autotouch columns run --table-id <TABLE_ID> --column-id <COLUMN_ID> --data-file <payload.json>",
            "selection_descriptor": "Use with tasks/leads bulk operations that accept selection payloads.",
        },
        label="schema",
        out_file=getattr(args, "out_file", None),
        compact=getattr(args, "compact", False),
        runtime=runtime,
    )


def emit_auth_schema(args: argparse.Namespace, runtime: TemplateRuntime) -> None:
    _emit_template_bundle(
        selected_type=str(args.type or "all"),
        template_types=AUTH_SCHEMA_TYPES,
        payload_getter=lambda schema_type: _schema_payload(schema_type, AUTH_SCHEMAS, noun="auth schema"),
        notes_lookup=AUTH_SCHEMA_NOTES,
        usage_lookup={
            "bootstrap": "autotouch auth bootstrap --data-file <payload.json>",
            "login": "autotouch auth login --data-file <payload.json>",
        },
        label="schema",
        out_file=getattr(args, "out_file", None),
        compact=getattr(args, "compact", False),
        runtime=runtime,
    )


def emit_onboarding_schema(args: argparse.Namespace, runtime: TemplateRuntime) -> None:
    _emit_template_bundle(
        selected_type=str(args.type or "all"),
        template_types=ONBOARDING_SCHEMA_TYPES,
        payload_getter=lambda schema_type: _schema_payload(schema_type, ONBOARDING_SCHEMAS, noun="onboarding schema"),
        notes_lookup=ONBOARDING_SCHEMA_NOTES,
        usage_lookup={"submit_domain": "autotouch onboarding submit-domain --data-file <payload.json>"},
        label="schema",
        out_file=getattr(args, "out_file", None),
        compact=getattr(args, "compact", False),
        runtime=runtime,
    )


def emit_org_context_schema(args: argparse.Namespace, runtime: TemplateRuntime) -> None:
    _emit_template_bundle(
        selected_type=str(args.type or "all"),
        template_types=ORG_CONTEXT_SCHEMA_TYPES,
        payload_getter=lambda schema_type: _schema_payload(schema_type, ORG_CONTEXT_SCHEMAS, noun="org-context schema"),
        notes_lookup=ORG_CONTEXT_SCHEMA_NOTES,
        usage_lookup={"set": "autotouch org-context set --data-file <payload.json>"},
        label="schema",
        out_file=getattr(args, "out_file", None),
        compact=getattr(args, "compact", False),
        runtime=runtime,
    )


def emit_personal_context_schema(args: argparse.Namespace, runtime: TemplateRuntime) -> None:
    _emit_template_bundle(
        selected_type=str(args.type or "all"),
        template_types=PERSONAL_CONTEXT_SCHEMA_TYPES,
        payload_getter=lambda schema_type: _schema_payload(schema_type, PERSONAL_CONTEXT_SCHEMAS, noun="personal-context schema"),
        notes_lookup=PERSONAL_CONTEXT_SCHEMA_NOTES,
        usage_lookup={"set": "autotouch personal-context set --data-file <payload.json>"},
        label="schema",
        out_file=getattr(args, "out_file", None),
        compact=getattr(args, "compact", False),
        runtime=runtime,
    )


def emit_workspace_secrets_schema(args: argparse.Namespace, runtime: TemplateRuntime) -> None:
    _emit_template_bundle(
        selected_type=str(args.type or "all"),
        template_types=WORKSPACE_SECRET_SCHEMA_TYPES,
        payload_getter=lambda schema_type: _schema_payload(schema_type, WORKSPACE_SECRET_SCHEMAS, noun="workspace-secrets schema"),
        notes_lookup=WORKSPACE_SECRET_SCHEMA_NOTES,
        usage_lookup={"set": "autotouch workspace-secrets set --data-file <payload.json>"},
        label="schema",
        out_file=getattr(args, "out_file", None),
        compact=getattr(args, "compact", False),
        runtime=runtime,
    )


def emit_prompt_templates_schema(args: argparse.Namespace, runtime: TemplateRuntime) -> None:
    _emit_template_bundle(
        selected_type=str(args.type or "all"),
        template_types=PROMPT_TEMPLATE_SCHEMA_TYPES,
        payload_getter=lambda schema_type: _schema_payload(schema_type, PROMPT_TEMPLATE_SCHEMAS, noun="prompt-template schema"),
        notes_lookup=PROMPT_TEMPLATE_SCHEMA_NOTES,
        usage_lookup={
            "create": "autotouch prompts create --data-file <payload.json>",
            "update": "autotouch prompts update --id <TEMPLATE_ID> --data-file <payload.json>",
        },
        label="schema",
        out_file=getattr(args, "out_file", None),
        compact=getattr(args, "compact", False),
        runtime=runtime,
    )


def emit_tables_schema(args: argparse.Namespace, runtime: TemplateRuntime) -> None:
    _emit_template_bundle(
        selected_type=str(args.type or "all"),
        template_types=TABLE_SCHEMA_TYPES,
        payload_getter=lambda schema_type: _schema_payload(schema_type, TABLE_SCHEMAS, noun="tables schema"),
        notes_lookup=TABLE_SCHEMA_NOTES,
        usage_lookup={"create": "autotouch tables create --data-file <payload.json>"},
        label="schema",
        out_file=getattr(args, "out_file", None),
        compact=getattr(args, "compact", False),
        runtime=runtime,
    )


def emit_rows_schema(args: argparse.Namespace, runtime: TemplateRuntime) -> None:
    _emit_template_bundle(
        selected_type=str(args.type or "all"),
        template_types=ROW_SCHEMA_TYPES,
        payload_getter=lambda schema_type: _schema_payload(schema_type, ROW_SCHEMAS, noun="rows schema"),
        notes_lookup=ROW_SCHEMA_NOTES,
        usage_lookup={
            "add_records": "autotouch rows add --table-id <TABLE_ID> --records-file <payload.json>",
            "import_field_mappings": "autotouch rows import-csv --table-id <TABLE_ID> --file contacts.csv --field-mappings-file <payload.json>",
        },
        label="schema",
        out_file=getattr(args, "out_file", None),
        compact=getattr(args, "compact", False),
        runtime=runtime,
    )


def emit_cells_schema(args: argparse.Namespace, runtime: TemplateRuntime) -> None:
    _emit_template_bundle(
        selected_type=str(args.type or "all"),
        template_types=CELL_SCHEMA_TYPES,
        payload_getter=lambda schema_type: _schema_payload(schema_type, CELL_SCHEMAS, noun="cells schema"),
        notes_lookup=CELL_SCHEMA_NOTES,
        usage_lookup={"patch_updates": "autotouch cells patch --table-id <TABLE_ID> --updates-file <payload.json>"},
        label="schema",
        out_file=getattr(args, "out_file", None),
        compact=getattr(args, "compact", False),
        runtime=runtime,
    )
