"""
Gremlin Chat Tool Registry.

Defines all operations available to the AI chat agent.
Each tool has safety metadata, parameter schema, and execution hints.
"""

from __future__ import annotations

from enum import Enum
from typing import Any, Dict, List, Optional, Set, Tuple

from pydantic import BaseModel, Field


class Executor(str, Enum):
    """Where the tool executes."""

    CLIENT = "client"  # Runs entirely in Apps Script (sheet operations)
    BACKEND = "backend"  # Runs via API call, client just waits
    HYBRID = "hybrid"  # Client initiates, backend processes, client polls for result


class JobMode(str, Enum):
    """How HYBRID tools handle async work (clarifies HYBRID semantics)."""

    SYNC = "sync"  # Backend returns immediately, client writes
    ASYNC_JOB = "async_job"  # Client submits job, polls for completion, then writes
    STREAM = "stream"  # Backend streams tokens/results via Redis


class ResultChannel(str, Enum):
    """Who writes the final result."""

    CLIENT_WRITE = "client_write"  # Client writes to sheet after receiving data
    SERVER_WRITE = "server_write"  # Server writes directly (not used - Brain/Hands principle)
    NONE = "none"  # No write (e.g., scan returns info only)


class UndoStrategy(str, Enum):
    """How to undo this operation."""

    KEYBOARD = "keyboard"  # Ctrl+Z immediately after
    SNAPSHOT = "snapshot"  # Pre-image stored in Redis
    RESTORE_ROWS = "restore_rows"  # Can re-insert deleted rows
    IRREVERSIBLE = "irreversible"  # Cannot be undone (SFDC writes)


class ToolParam(BaseModel):
    """Parameter definition for a tool."""

    name: str
    type: str  # "string", "integer", "boolean", "array", "column_name", "a1_range"
    description: str
    required: bool = True
    default: Optional[Any] = None
    enum: Optional[List[str]] = None  # For constrained choices


class ToolDefinition(BaseModel):
    """Complete tool definition with safety metadata."""

    name: str
    category: str  # "transform", "match", "salesforce", "repair", "style"
    description: str  # Human-readable, shown to LLM (keep to 1-2 lines!)
    params: List[ToolParam] = Field(default_factory=list)
    executor: Executor = Executor.CLIENT
    destructive: bool = False  # Deletes data or hard to undo
    undo_strategy: UndoStrategy = UndoStrategy.KEYBOARD  # How to recover
    requires: List[str] = Field(default_factory=list)  # Prerequisites: "salesforce_connected", "paid_tier"
    confirm_required: bool = True  # Show proposal card before executing (tool default)
    example_prompt: Optional[str] = None  # Example user request that triggers this tool
    execution_enabled: bool = True  # Tool can be toggled off without removal

    # HYBRID executor clarification
    job_mode: Optional[JobMode] = None  # How async work is handled (for HYBRID)
    result_channel: ResultChannel = ResultChannel.CLIENT_WRITE  # Who writes result

    # Prompt inclusion control (avoid bloating prompt with all tools)
    prompt_priority: int = 1  # 1=always include, 2=include if space, 3=omit from prompt


# Resolve forward references for Pydantic models when using postponed annotations
ToolParam.model_rebuild()
ToolDefinition.model_rebuild()


# =============================================================================
# TOOL REGISTRY
# =============================================================================

TOOL_REGISTRY: Dict[str, ToolDefinition] = {}


def register_tool(tool: ToolDefinition) -> ToolDefinition:
    """Register a tool in the global registry."""
    TOOL_REGISTRY[tool.name] = tool
    return tool


# -----------------------------------------------------------------------------
# TRANSFORM TOOLS
# -----------------------------------------------------------------------------

def _range_param(description: str) -> ToolParam:
    return ToolParam(name="range", type="a1_range", description=description)


def _optional_bool_param(name: str, description: str, default: Optional[bool] = None) -> ToolParam:
    return ToolParam(name=name, type="boolean", description=description, required=False, default=default)


def _optional_string_param(
    name: str, description: str, default: Optional[str] = None, enum: Optional[List[str]] = None
) -> ToolParam:
    return ToolParam(name=name, type="string", description=description, required=False, default=default, enum=enum)


# Company cleanup & domain
TRANSFORM_TOOLS: List[ToolDefinition] = [
    ToolDefinition(
        name="normalize_company",
        category="transform",
        description="Clean and standardize company names. Removes suffixes like Inc/LLC and fixes casing; use force_llm for harder cases.",
        params=[
            _range_param("Range containing company names"),
            _optional_bool_param("force_llm", "Force LLM-based cleanup for ambiguous names", default=False),
        ],
        executor=Executor.CLIENT,
        destructive=False,
        example_prompt="Clean up the company names in column A",
        prompt_priority=1,
    ),
    ToolDefinition(
        name="clean_name",
        category="transform",
        description="Normalize personal names with proper casing and spacing.",
        params=[_range_param("Range containing contact names")],
        executor=Executor.CLIENT,
        destructive=False,
        example_prompt="Fix the casing on the contact names",
        prompt_priority=2,
    ),
    ToolDefinition(
        name="clean_website",
        category="transform",
        description="Normalize websites to a consistent host/path string without scheme, query, or fragment.",
        params=[_range_param("Range containing websites or URLs")],
        executor=Executor.CLIENT,
        destructive=False,
        example_prompt="Standardize the website URLs",
        prompt_priority=2,
    ),
    ToolDefinition(
        name="company_to_domain",
        category="transform",
        description="Resolve company names to domains with confidence and suggestions.",
        params=[
            _range_param("Range containing company names"),
            _optional_string_param("mode", "Resolution mode (fast, balanced, strict)", default="balanced", enum=["fast", "balanced", "strict"]),
            _optional_string_param("auto_link_threshold", "Minimum confidence 0-1 for auto-linking"),
            _optional_string_param("suggest_threshold", "Suggestion threshold 0-1 for soft matches"),
        ],
        executor=Executor.CLIENT,
        destructive=False,
        example_prompt="Find domains for the companies in column A",
        prompt_priority=1,
    ),
    ToolDefinition(
        name="detect_parent_company",
        category="transform",
        description="Detect likely parent companies for each account based on company and domain.",
        params=[_range_param("Range covering company + domain columns (rows)")],
        executor=Executor.CLIENT,
        destructive=False,
        example_prompt="Identify parent companies for these accounts",
        prompt_priority=2,
    ),
    ToolDefinition(
        name="generate_dedup_key",
        category="transform",
        description="Generate composite deduplication keys for each row to prevent duplicates.",
        params=[_range_param("Range covering full rows for dedup key generation")],
        executor=Executor.CLIENT,
        destructive=False,
        example_prompt="Create dedup keys for these contacts",
        prompt_priority=2,
    ),
    ToolDefinition(
        name="split_fullname",
        category="transform",
        description="Split full names into First Name and Last Name columns.",
        params=[_range_param("Range containing full names")],
        executor=Executor.CLIENT,
        destructive=False,
        example_prompt="Split the names into first and last",
        prompt_priority=1,
    ),
    ToolDefinition(
        name="normalize_title",
        category="transform",
        description="Standardize job titles and return best-effort title, department, and seniority. Blank or unrecognized titles default seniority to IC.",
        params=[_range_param("Range containing job titles")],
        executor=Executor.CLIENT,
        destructive=False,
        example_prompt="Standardize these job titles",
        prompt_priority=1,
    ),
    ToolDefinition(
        name="normalize_job_function",
        category="transform",
        description="Normalize job titles into functional departments.",
        params=[
            _range_param("Range containing job titles"),
            _optional_bool_param("force_llm", "Force LLM normalization for ambiguous functions", default=False),
        ],
        executor=Executor.CLIENT,
        destructive=False,
        example_prompt="Map these job titles to departments",
        prompt_priority=2,
    ),
    ToolDefinition(
        name="normalize_industry",
        category="transform",
        description="Normalize industry names to standard categories.",
        params=[
            _range_param("Range containing industry values"),
            _optional_bool_param("force_llm", "Force LLM normalization for ambiguous industries", default=False),
        ],
        executor=Executor.CLIENT,
        destructive=False,
        example_prompt="Categorize these industries",
        prompt_priority=1,
    ),
    ToolDefinition(
        name="company_size_bucket",
        category="transform",
        description="Bucket company size values into standard employee bands.",
        params=[_range_param("Range containing company size or employee counts")],
        executor=Executor.CLIENT,
        destructive=False,
        example_prompt="Bucket these companies by size",
        prompt_priority=1,
    ),
    ToolDefinition(
        name="classify_revenue_band",
        category="transform",
        description="Classify revenue figures into standard revenue bands.",
        params=[_range_param("Range containing revenue values")],
        executor=Executor.CLIENT,
        destructive=False,
        example_prompt="Bucket these revenues into bands",
        prompt_priority=1,
    ),
    ToolDefinition(
        name="classify_lead_source",
        category="transform",
        description="Classify lead source using referrer/UTM context.",
        params=[
            _range_param("Range containing lead source text or URLs"),
            ToolParam(name="utm", type="object", description="Optional UTM parameters", required=False),
            _optional_string_param("referrer", "Original referrer URL", default=""),
        ],
        executor=Executor.CLIENT,
        destructive=False,
        example_prompt="Label the lead source for these rows",
        prompt_priority=2,
    ),
    ToolDefinition(
        name="calculate_lead_score",
        category="transform",
        description="Calculate lead scores using firmographic and behavioral signals.",
        params=[_range_param("Range covering rows to score (uses multiple columns)")],
        executor=Executor.CLIENT,
        destructive=False,
        example_prompt="Score these leads",
        prompt_priority=2,
    ),
    ToolDefinition(
        name="detect_technographics",
        category="transform",
        description="Detect likely technographics such as CRM and marketing platform.",
        params=[_range_param("Range with domains/descriptions to infer technographics")],
        executor=Executor.CLIENT,
        destructive=False,
        example_prompt="Detect CRM and marketing tools for these companies",
        prompt_priority=2,
    ),

    # Email operations
    ToolDefinition(
        name="extract_domain_from_email",
        category="transform",
        description="Extract domain from email addresses; optionally strip B2C domains.",
        params=[
            _range_param("Range containing email addresses"),
            _optional_bool_param("strip_b2c", "Strip common personal email domains", default=False),
        ],
        executor=Executor.CLIENT,
        destructive=False,
        example_prompt="Pull domains from the email column",
        prompt_priority=1,
    ),
    ToolDefinition(
        name="is_business_email",
        category="transform",
        description="Check if emails are business (company) or personal inboxes.",
        params=[_range_param("Range containing email addresses")],
        executor=Executor.CLIENT,
        destructive=False,
        example_prompt="Which of these emails are business vs personal?",
        prompt_priority=1,
    ),
    ToolDefinition(
        name="is_role_based_email",
        category="transform",
        description="Flag role-based or generic inboxes like info@ and sales@.",
        params=[_range_param("Range containing email addresses")],
        executor=Executor.CLIENT,
        destructive=False,
        example_prompt="Find role-based emails like info@ or sales@",
        prompt_priority=1,
    ),
    ToolDefinition(
        name="email_domain_match",
        category="transform",
        description="Check whether email domains align with company domains.",
        params=[_range_param("Two-column range: Email column then Company/Domain column")],
        executor=Executor.CLIENT,
        destructive=False,
        example_prompt="Do these emails match the company domain?",
        prompt_priority=1,
    ),
    ToolDefinition(
        name="parse_email_pattern",
        category="transform",
        description="Detect the email naming pattern (first.last, firstinitiallast, etc.).",
        params=[_range_param("Range containing email addresses")],
        executor=Executor.CLIENT,
        destructive=False,
        example_prompt="What email pattern do these addresses use?",
        prompt_priority=2,
    ),

    # Phone operations
    ToolDefinition(
        name="fix_phone_format",
        category="transform",
        description="Format phone numbers to international E.164 with country defaults.",
        params=[
            _range_param("Range containing phone numbers"),
            _optional_string_param("country", "Default country for formatting", default="US"),
        ],
        executor=Executor.CLIENT,
        destructive=False,
        example_prompt="Format these phone numbers to E.164",
        prompt_priority=1,
    ),
    ToolDefinition(
        name="normalize_phone_to_e164",
        category="transform",
        description="Normalize phone numbers to strict E.164 format (preserves extensions).",
        params=[
            _range_param("Range containing phone numbers"),
            _optional_string_param("country", "Default country for formatting", default="US"),
        ],
        executor=Executor.CLIENT,
        destructive=False,
        example_prompt="Convert phone numbers to E.164 using country context",
        prompt_priority=1,
    ),

    # Location / geo
    ToolDefinition(
        name="normalize_state",
        category="transform",
        description="Standardize state/province names to canonical casing (e.g., California -> CA).",
        params=[
            _range_param("Range containing state or province values"),
            _optional_string_param("country", "Country context", default="US"),
        ],
        executor=Executor.CLIENT,
        destructive=False,
        example_prompt="Fix the state values in this column",
        prompt_priority=1,
    ),
    ToolDefinition(
        name="state_name_to_code",
        category="transform",
        description="Convert state or province names to subdivision codes. Optional country narrows lookup.",
        params=[
            _range_param("Range containing state or province names"),
            _optional_string_param(
                "country",
                "Optional country code to constrain subdivision lookup",
                default="",
                enum=["", "US", "CA", "AU"],
            ),
        ],
        executor=Executor.CLIENT,
        destructive=False,
        example_prompt="Turn state names into two-letter codes",
        prompt_priority=2,
    ),
    ToolDefinition(
        name="normalize_country",
        category="transform",
        description="Normalize country names to canonical formatting.",
        params=[_range_param("Range containing country values")],
        executor=Executor.CLIENT,
        destructive=False,
        example_prompt="Normalize the country values",
        prompt_priority=1,
    ),
    ToolDefinition(
        name="assign_us_region",
        category="transform",
        description="Group US states into sales regions.",
        params=[_range_param("Range containing US states")],
        executor=Executor.CLIENT,
        destructive=False,
        example_prompt="Assign US sales regions for these states",
        prompt_priority=1,
    ),
    ToolDefinition(
        name="assign_global_region",
        category="transform",
        description="Assign global sales regions from country values.",
        params=[_range_param("Range containing country values")],
        executor=Executor.CLIENT,
        destructive=False,
        example_prompt="Tag rows with global regions by country",
        prompt_priority=2,
    ),
    ToolDefinition(
        name="assign_territory",
        category="transform",
        description="Assign sales territories using geography and firmographics.",
        params=[_range_param("Range covering rows with geo/firmographic data")],
        executor=Executor.CLIENT,
        destructive=False,
        example_prompt="Assign territories for these leads",
        prompt_priority=2,
    ),
    ToolDefinition(
        name="parse_address_components",
        category="transform",
        description="Split full addresses into Street, City, State/Region, and Postal Code.",
        params=[_range_param("Range containing full addresses")],
        executor=Executor.CLIENT,
        destructive=False,
        example_prompt="Split the address column into street, city, state, and postal",
        prompt_priority=2,
    ),

    # Domain / URL
    ToolDefinition(
        name="extract_domain_root",
        category="transform",
        description="Extract the registrable root domain from URLs or emails.",
        params=[_range_param("Range containing URLs, domains, or emails")],
        executor=Executor.CLIENT,
        destructive=False,
        example_prompt="Get the root domains from these URLs",
        prompt_priority=2,
    ),
    ToolDefinition(
        name="extract_subdomain",
        category="transform",
        description="Extract the subdomain portion from URLs or domains.",
        params=[_range_param("Range containing URLs or domains")],
        executor=Executor.CLIENT,
        destructive=False,
        example_prompt="Pull the subdomain (like blog) from these URLs",
        prompt_priority=2,
    ),
    ToolDefinition(
        name="extract_id_from_url",
        category="transform",
        description="Parse provider, record type, and identifier from URLs.",
        params=[_range_param("Range containing URLs to parse")],
        executor=Executor.CLIENT,
        destructive=False,
        example_prompt="Extract IDs from these tracking URLs",
        prompt_priority=2,
    ),

    # Classification & parsing
    ToolDefinition(
        name="parse_email_pattern",
        category="transform",
        description="Identify the email naming convention for a list of addresses.",
        params=[_range_param("Range containing email addresses")],
        executor=Executor.CLIENT,
        destructive=False,
        example_prompt="Identify the email pattern used by these addresses",
        prompt_priority=2,
    ),
    ToolDefinition(
        name="parse_naics",
        category="transform",
        description="Map NAICS text to standardized sector, subsector, and detail codes.",
        params=[_range_param("Range containing NAICS descriptions or codes")],
        executor=Executor.CLIENT,
        destructive=False,
        example_prompt="Assign NAICS sectors to these entries",
        prompt_priority=2,
    ),
    ToolDefinition(
        name="parse_sic",
        category="transform",
        description="Map SIC text to standardized division and detail codes.",
        params=[_range_param("Range containing SIC descriptions or codes")],
        executor=Executor.CLIENT,
        destructive=False,
        example_prompt="Assign SIC divisions to these rows",
        prompt_priority=2,
    ),

    # Data quality
    ToolDefinition(
        name="calculate_completeness",
        category="transform",
        description="Score how complete each row is and list missing fields.",
        params=[_range_param("Range covering rows to score for completeness")],
        executor=Executor.CLIENT,
        destructive=False,
        example_prompt="Check completeness for these rows",
        prompt_priority=1,
    ),
    ToolDefinition(
        name="detect_data_quality_issues",
        category="transform",
        description="Flag likely data quality issues (personal emails, invalid phones, suspicious patterns).",
        params=[_range_param("Range covering rows to scan for issues")],
        executor=Executor.CLIENT,
        destructive=False,
        example_prompt="Find data quality issues in this sheet",
        prompt_priority=1,
    ),
    ToolDefinition(
        name="detect_duplicate_likelihood",
        category="transform",
        description="Estimate duplicate likelihood scores for each row.",
        params=[
            _range_param("Range covering rows to score for duplicates"),
            _optional_string_param("mode", "Blocking strategy: speed, balanced, or scale", default="balanced", enum=["speed", "balanced", "scale"]),
            _optional_string_param("threshold", "Minimum score (0-1) to consider a duplicate"),
        ],
        executor=Executor.CLIENT,
        destructive=False,
        example_prompt="Score duplicate likelihood across these rows",
        prompt_priority=1,
    ),
    ToolDefinition(
        name="infer_employee_count",
        category="transform",
        description="Predict employee count ranges with confidence.",
        params=[_range_param("Range covering company rows to estimate employees")],
        executor=Executor.CLIENT,
        destructive=False,
        example_prompt="Estimate employee counts for these companies",
        prompt_priority=2,
    ),

    # Enrichment
    ToolDefinition(
        name="enrich_company_name",
        category="transform",
        description="Resolve canonical company names from domain/website (CRM-first waterfall).",
        params=[_range_param("Range containing domains or company names to resolve")],
        executor=Executor.CLIENT,
        destructive=False,
        example_prompt="Fix the company names using the website column",
        prompt_priority=2,
    ),
    ToolDefinition(
        name="enrich_company_industry",
        category="transform",
        description="Enrich company industry from profiles (primary and all industries).",
        params=[_range_param("Range containing company domains or names")],
        executor=Executor.CLIENT,
        destructive=False,
        example_prompt="Enrich industry for these companies",
        prompt_priority=3,
    ),
    ToolDefinition(
        name="enrich_company_country",
        category="transform",
        description="Enrich company country from profiles (primary and alternates).",
        params=[_range_param("Range containing company domains or names")],
        executor=Executor.CLIENT,
        destructive=False,
        example_prompt="Enrich company country for this list",
        prompt_priority=3,
    ),
    ToolDefinition(
        name="enrich_company_headquarters",
        category="transform",
        description="Enrich company headquarters location from profiles.",
        params=[_range_param("Range containing company domains or names")],
        executor=Executor.CLIENT,
        destructive=False,
        example_prompt="Add HQ locations for these companies",
        prompt_priority=3,
    ),
    ToolDefinition(
        name="enrich_company_revenue",
        category="transform",
        description="Enrich company revenue from profiles (amount, currency, year).",
        params=[_range_param("Range containing company domains or names")],
        executor=Executor.CLIENT,
        destructive=False,
        example_prompt="Add revenue for these companies",
        prompt_priority=3,
    ),
    ToolDefinition(
        name="enrich_from_foundrygraph",
        category="transform",
        description=(
            "Enrich company data with all available fields from FoundryGraph "
            "(revenue, employees, headquarters, parent company, etc.)."
        ),
        params=[
            _range_param("Range containing company domains or names"),
            _optional_string_param(
                "input_type",
                "Whether input is 'domain' or 'company_name'",
                default="domain",
                enum=["domain", "company_name"],
            ),
        ],
        executor=Executor.CLIENT,
        destructive=False,
        example_prompt="Enrich these companies with all FoundryGraph fields",
        prompt_priority=3,
    ),
    ToolDefinition(
        name="enrich_social_handles",
        category="transform",
        description="Enrich company social handles (Twitter, YouTube) from external IDs.",
        params=[_range_param("Range containing company domains or names")],
        executor=Executor.CLIENT,
        destructive=False,
        example_prompt="Look up social handles for these companies",
        prompt_priority=3,
    ),
    ToolDefinition(
        name="enrich_stock_info",
        category="transform",
        description="Enrich stock ticker, exchange, and ISIN for companies.",
        params=[_range_param("Range containing company domains or names")],
        executor=Executor.CLIENT,
        destructive=False,
        example_prompt="Add ticker and exchange info for these companies",
        prompt_priority=3,
    ),
    ToolDefinition(
        name="enrich_subsidiaries",
        category="transform",
        description="Enrich companies with subsidiary counts and names.",
        params=[_range_param("Range containing company domains or names")],
        executor=Executor.CLIENT,
        destructive=False,
        example_prompt="List subsidiaries for these parent companies",
        prompt_priority=3,
    ),
]

for tool_def in TRANSFORM_TOOLS:
    register_tool(tool_def)

# -----------------------------------------------------------------------------
# MATCHING TOOLS
# -----------------------------------------------------------------------------

register_tool(
    ToolDefinition(
        name="match_sheets",
        category="match",
        description="Fuzzy match rows between two ranges. Returns best matches above threshold.",
        params=[
            ToolParam(name="source_range", type="a1_range", description="Source data to match FROM"),
            ToolParam(name="ref_range", type="a1_range", description="Reference data to match TO"),
            ToolParam(name="threshold", type="integer", description="Match threshold 0-100", required=False, default=80),
        ],
        executor=Executor.HYBRID,  # Client submits, backend computes, client polls
        job_mode=JobMode.ASYNC_JOB,  # Client polls for completion
        result_channel=ResultChannel.CLIENT_WRITE,  # Client writes match results
        destructive=False,
        undo_strategy=UndoStrategy.KEYBOARD,
        requires=["paid_tier"],
        example_prompt="Match this sheet to the Accounts tab",
    )
)

register_tool(
    ToolDefinition(
        name="match_to_salesforce",
        category="match",
        description="Match companies to Salesforce Accounts using fuzzy matching.",
        params=[
            ToolParam(name="range", type="a1_range", description="Range with company names"),
            ToolParam(name="match_column", type="column_name", description="Column header with company names"),
            ToolParam(name="threshold", type="integer", description="Match threshold 0-100", required=False, default=80),
        ],
        executor=Executor.HYBRID,
        job_mode=JobMode.ASYNC_JOB,  # Client polls for completion
        result_channel=ResultChannel.CLIENT_WRITE,  # Client writes match results
        destructive=False,
        undo_strategy=UndoStrategy.KEYBOARD,
        requires=["salesforce_connected", "paid_tier"],
        example_prompt="Match these companies to Salesforce",
    )
)

register_tool(
    ToolDefinition(
        name="resolve_company",
        category="match",
        description="Resolve company names using Salesforce and/or FoundryGraph. Writes canonical names and optional SFDC columns.",
        params=[
            ToolParam(name="range", type="a1_range", description="Range with company names", required=False),
            _optional_string_param(
                "target",
                "Resolution source: smart, salesforce, or foundrygraph",
                default="smart",
                enum=["smart", "salesforce", "foundrygraph"],
            ),
            ToolParam(
                name="output_range",
                type="a1_range",
                description="Optional output range for resolved names",
                required=False,
            ),
        ],
        executor=Executor.CLIENT,
        destructive=False,
        undo_strategy=UndoStrategy.KEYBOARD,
        example_prompt="Resolve company names using FoundryGraph",
    )
)

register_tool(
    ToolDefinition(
        name="match_salesforce_account",
        category="match",
        description="Match companies against Salesforce Accounts. Returns Account ID and Name columns.",
        params=[
            ToolParam(name="range", type="a1_range", description="Range with company names"),
            ToolParam(name="company_column", type="column_name", description="Column header with company names"),
            ToolParam(name="domain_column", type="column_name", description="Column header with domains (optional)", required=False),
            ToolParam(name="threshold", type="integer", description="Match threshold 0-100", required=False, default=80),
        ],
        executor=Executor.HYBRID,
        job_mode=JobMode.ASYNC_JOB,
        result_channel=ResultChannel.CLIENT_WRITE,
        destructive=False,
        undo_strategy=UndoStrategy.KEYBOARD,
        requires=["salesforce_connected", "paid_tier"],
        example_prompt="Add Salesforce Account IDs for these companies",
    )
)

register_tool(
    ToolDefinition(
        name="dedupe",
        category="match",
        description="Remove duplicate rows by key columns. Keeps first occurrence.",
        params=[
            ToolParam(name="range", type="a1_range", description="Data range to deduplicate"),
            ToolParam(name="key_columns", type="array", description="Column headers for duplicate key"),
            ToolParam(name="threshold", type="integer", description="Fuzzy threshold (100=exact)", required=False, default=100),
        ],
        executor=Executor.CLIENT,
        destructive=True,
        undo_strategy=UndoStrategy.SNAPSHOT,  # Store pre-image before deletion
        confirm_required=True,
        example_prompt="Remove duplicates by email",
    )
)

# -----------------------------------------------------------------------------
# SALESFORCE TOOLS
# -----------------------------------------------------------------------------

register_tool(
    ToolDefinition(
        name="sfdc_enrich",
        category="salesforce",
        description="Pull fields from Salesforce by ID. Adds columns with enriched data.",
        params=[
            ToolParam(name="range", type="a1_range", description="Range with Salesforce IDs"),
            ToolParam(name="object", type="string", description="Object type", enum=["Account", "Contact", "Lead", "Opportunity"]),
            ToolParam(name="id_column", type="column_name", description="Column with SF IDs"),
            ToolParam(name="fields", type="array", description="Fields to pull (e.g., Industry, Website)"),
        ],
        executor=Executor.HYBRID,
        job_mode=JobMode.ASYNC_JOB,  # Batch lookup, client polls
        result_channel=ResultChannel.CLIENT_WRITE,  # Client writes enriched columns
        destructive=False,
        undo_strategy=UndoStrategy.KEYBOARD,
        requires=["salesforce_connected"],
        example_prompt="Pull Industry and Website from Salesforce",
    )
)

register_tool(
    ToolDefinition(
        name="sfdc_pull_report",
        category="salesforce",
        description="Import a Salesforce report to a new tab.",
        params=[
            ToolParam(name="report_id", type="string", description="Report ID (starts with 00O)"),
            ToolParam(name="tab_name", type="string", description="Name for new tab", required=False),
        ],
        executor=Executor.HYBRID,
        job_mode=JobMode.ASYNC_JOB,  # Report pull can be slow
        result_channel=ResultChannel.CLIENT_WRITE,  # Client creates new tab + writes data
        destructive=False,
        undo_strategy=UndoStrategy.KEYBOARD,  # Can delete the tab
        requires=["salesforce_connected"],
        example_prompt="Pull the Pipeline report from Salesforce",
    )
)

register_tool(
    ToolDefinition(
        name="sfdc_push_campaign",
        category="salesforce",
        description="Add contacts/leads to a Campaign. Creates CampaignMember records.",
        params=[
            ToolParam(name="range", type="a1_range", description="Range with Contact/Lead IDs"),
            ToolParam(name="campaign_id", type="string", description="Campaign ID (starts with 701)"),
            ToolParam(name="status", type="string", description="Member status", required=False, default="Sent"),
        ],
        executor=Executor.HYBRID,
        job_mode=JobMode.ASYNC_JOB,  # Batch insert, client polls
        result_channel=ResultChannel.NONE,  # No sheet write - data goes to SFDC
        destructive=True,  # Creates records in Salesforce - cannot auto-undo
        undo_strategy=UndoStrategy.IRREVERSIBLE,  # Must manually delete in SFDC
        confirm_required=True,
        requires=["salesforce_connected"],
        example_prompt="Add these contacts to the Q1 Campaign",
    )
)

# NOTE: No raw sfdc_query tool. Use templates instead for safety.
register_tool(
    ToolDefinition(
        name="sfdc_query_template",
        category="salesforce",
        description="Run a saved query template. Use template names like 'accounts_by_state', 'open_opportunities'.",
        params=[
            ToolParam(
                name="template",
                type="string",
                description="Template name",
                enum=["accounts_by_state", "accounts_by_industry", "open_opportunities", "recent_contacts", "campaign_members"],
            ),
            ToolParam(
                name="filters",
                type="object",
                description="Filter dict (e.g., {state: 'CA', industry: 'Tech'})",
                required=False,
            ),
            ToolParam(name="tab_name", type="string", description="Name for results tab", required=False),
        ],
        executor=Executor.HYBRID,
        job_mode=JobMode.ASYNC_JOB,  # Query can be slow
        result_channel=ResultChannel.CLIENT_WRITE,  # Client creates tab + writes results
        destructive=False,
        undo_strategy=UndoStrategy.KEYBOARD,
        requires=["salesforce_connected", "paid_tier"],
        prompt_priority=2,  # Include if space (not top-tier tool)
        example_prompt="Get all Accounts in California",
    )
)

# -----------------------------------------------------------------------------
# REPAIR TOOLS
# -----------------------------------------------------------------------------

register_tool(
    ToolDefinition(
        name="scan",
        category="repair",
        description="Analyze sheet for data quality issues. Detects formula errors, missing values, inconsistencies.",
        params=[
            ToolParam(name="range", type="a1_range", description="Range to scan", required=False),
        ],
        executor=Executor.BACKEND,
        destructive=False,
        confirm_required=False,  # Safe to run immediately
        example_prompt="What's wrong with this sheet?",
    )
)

register_tool(
    ToolDefinition(
        name="repair_formulas",
        category="repair",
        description="Fix broken formulas and #REF errors. Analyzes formula patterns and suggests corrections.",
        params=[
            ToolParam(name="range", type="a1_range", description="Range to repair", required=False),
            ToolParam(name="auto", type="boolean", description="Auto-apply safe fixes", required=False, default=False),
        ],
        executor=Executor.CLIENT,
        destructive=False,
        requires=["paid_tier"],
        example_prompt="Fix the broken formulas",
    )
)

register_tool(
    ToolDefinition(
        name="triage",
        category="repair",
        description="Get AI-suggested actions based on sheet content. Shows 'What should I do?' panel.",
        params=[],
        executor=Executor.CLIENT,
        destructive=False,
        confirm_required=False,
        example_prompt="What should I do with this?",
    )
)

# -----------------------------------------------------------------------------
# STYLE TOOLS
# -----------------------------------------------------------------------------

register_tool(
    ToolDefinition(
        name="style_preset",
        category="style",
        description="Apply a predefined style preset to the sheet. Professional formatting with one click.",
        params=[
            ToolParam(
                name="preset", type="string", description="Style preset name", enum=["professional", "minimal", "bold", "colorful"]
            ),
            ToolParam(name="range", type="a1_range", description="Range to style", required=False),
        ],
        executor=Executor.CLIENT,
        destructive=False,
        example_prompt="Make this look professional",
    )
)

register_tool(
    ToolDefinition(
        name="style_custom",
        category="style",
        description="Apply custom styling based on natural language description.",
        params=[
            ToolParam(name="description", type="string", description="Natural language style description"),
            ToolParam(name="range", type="a1_range", description="Range to style", required=False),
        ],
        executor=Executor.BACKEND,  # Uses AI to interpret
        destructive=False,
        requires=["paid_tier"],
        example_prompt="Make the headers blue with white text",
    )
)


# -----------------------------------------------------------------------------
# UTILITY FUNCTIONS
# -----------------------------------------------------------------------------

def get_tools_by_category(category: str) -> List[ToolDefinition]:
    """Get all tools in a category."""
    return [t for t in TOOL_REGISTRY.values() if t.category == category]


def get_tool_prompt_descriptions(
    max_tools: int = 30, allowlist: Optional[Set[str]] = None, category_filter: Optional[str] = None
) -> str:
    """
    Generate tool descriptions for the LLM prompt.

    Args:
        max_tools: Maximum number of tools to include (default 15)
        allowlist: If provided, only include these tool names (bypasses priority)
        category_filter: If provided, only include tools from this category

    Tools are selected by prompt_priority (1=always, 2=include if space, 3=omit).
    """
    lines = ["## Available Tools\n"]

    # Filter tools
    candidates = list(TOOL_REGISTRY.values())

    if allowlist:
        # Allowlist bypasses priority - include all listed tools
        candidates = [t for t in candidates if t.name in allowlist]
    elif category_filter:
        candidates = [t for t in candidates if t.category == category_filter]
    else:
        # Sort by priority, then by name for stability
        candidates.sort(key=lambda t: (t.prompt_priority, t.name))
        # Take top max_tools
        candidates = candidates[:max_tools]

    # Group by category for display
    by_category: Dict[str, List[ToolDefinition]] = {}
    for tool in candidates:
        by_category.setdefault(tool.category, []).append(tool)

    category_labels = {
        "transform": "Data Cleaning & Transforms",
        "match": "Matching & Deduplication",
        "salesforce": "Salesforce Integration",
        "repair": "Sheet Analysis & Repair",
        "style": "Formatting & Styling",
    }

    for cat, tools in sorted(by_category.items()):
        lines.append(f"\n### {category_labels.get(cat, cat.title())}\n")
        for tool in tools:
            params_str = ", ".join(f"{p.name}: {p.type}" + ("?" if not p.required else "") for p in tool.params)
            lines.append(f"- **{tool.name}**({params_str}): {tool.description}")
            if tool.example_prompt:
                lines.append(f"  - Example: \"{tool.example_prompt}\"")
            if tool.destructive:
                lines.append("  - *Destructive: requires confirmation*")
            if tool.requires:
                lines.append(f"  - *Requires: {', '.join(tool.requires)}*")

    return "\n".join(lines)


def validate_tool_available(tool_name: str) -> Tuple[bool, Optional[str]]:
    tool = TOOL_REGISTRY.get(tool_name)
    if not tool:
        return False, f"Tool '{tool_name}' not found"
    if not getattr(tool, "execution_enabled", True):
        return False, f"Tool '{tool_name}' is not available"
    return True, None


def validate_tool_params(tool_name: str, params: Dict[str, Any], context: Dict[str, Any]) -> List[str]:
    """
    Validate tool parameters against schema. Returns list of error messages.

    IMPORTANT: Backend does LIGHT validation only. Client does final A1 parsing.
    - Backend checks: required params, type hints, enum constraints, column existence
    - Client checks: actual A1 range validity, sheet existence for cross-refs

    Args:
        tool_name: Name of the tool
        params: Parameters provided by LLM
        context: Sheet context for validation (headers, row_count, etc.)
    """
    errors: List[str] = []
    tool = TOOL_REGISTRY.get(tool_name)
    if not tool:
        return [f"Unknown tool: {tool_name}"]

    headers = context.get("headers", [])
    other_sheets = context.get("other_sheets", [])

    for param_def in tool.params:
        value = params.get(param_def.name)

        # Check required
        if param_def.required and value is None:
            errors.append(f"Missing required parameter: {param_def.name}")
            continue

        if value is None:
            continue

        # Type-specific validation
        if param_def.type == "column_name":
            if value not in headers:
                matches = [h for h in headers if h.lower() == str(value).lower()]
                if not matches:
                    errors.append(f"Column '{value}' not found. Available: {', '.join(headers[:10])}")

        elif param_def.type == "a1_range":
            # LIGHT validation only - client does real parsing
            val_str = str(value or "").strip()
            if not val_str:
                errors.append(f"{param_def.name} cannot be empty")
            elif len(val_str) > 100:
                errors.append(f"{param_def.name} too long (max 100 chars)")
            elif "!" in val_str:
                sheet_part = val_str.split("!")[0].strip("'\"")
                if sheet_part and sheet_part not in other_sheets:
                    # Could be current sheet or quoted name - allow but warn
                    pass

        elif param_def.type == "integer":
            try:
                int(value)
            except (ValueError, TypeError):
                errors.append(f"{param_def.name} must be an integer, got: {value}")

        elif param_def.type == "array":
            if not isinstance(value, list):
                errors.append(f"{param_def.name} must be a list, got: {type(value).__name__}")

        elif param_def.enum and value not in param_def.enum:
            errors.append(f"{param_def.name} must be one of {param_def.enum}, got: {value}")

    return errors


# =============================================================================
# SAFE IMPORT WRAPPER (used by gremlin_chat.py)
# =============================================================================

def get_registry_safe() -> Dict[str, "ToolDefinition"]:
    """
    Return tool registry, or empty dict if disabled/broken.
    This ensures chat can boot even if tool registry has issues.
    """
    import os

    if os.getenv("FM_GREMLIN_TOOLS_ENABLED", "").lower() not in ("true", "1"):
        return {}
    return TOOL_REGISTRY


def is_execution_enabled() -> bool:
    """Check if tool execution is enabled (kill switch)."""
    import os

    return os.getenv("FM_GREMLIN_EXECUTION_ENABLED", "true").lower() in ("true", "1")
