"""6N CRM Codegen policy. Consumes catalog truth; does not invent a verb registry."""
# Policy prose is intentionally long; wrap would hide the generation rules.
# ruff: noqa: E501
from __future__ import annotations

from dataclasses import dataclass
from typing import Any

from src.domain.connectors.invoke_capabilities import (
    CRM_ASSOCIATION_PHASE_6G,
    CRM_PUBLIC_INVOKE_VERBS,
    INVOKE_CAPABILITIES,
    is_implemented,
)

CRM_PUBLIC_INVOKE_PATH = "POST /api/projects/{project_id}/capabilities/{capability}"

CRM_CODEGEN_POLICY = """\
For generic CRM requirements, use crm.* capabilities.

Do not choose HubSpot or Salesforce actions based solely on which
connector is installed.

Use provider-specific actions only when the requirement explicitly
depends on provider-specific semantics that the canonical CRM
capabilities do not represent.

CRM stages are logical project aliases. Never emit HubSpot dealstage
values or Salesforce StageName values in canonical requests.

Do not expose provider IDs, connection IDs, tokens, portal IDs,
org IDs, instance URLs, SOQL, filterGroups, association type IDs,
or AccountId in generic generated application code.
"""

CRM_CODEGEN_RULES = (
    "Identify product intent in CRM objects (contact, company, deal, association), not providers.",
    "If canonical crm.* covers the intent, generate that capability. Installed HubSpot vs Salesforce must not change the generated contract.",
    "Use crm.contact.create for a new contact. Use crm.contact.upsert only for create-or-update / sync / ensure-exists / save-this-lead-by-email. Email present is not enough to choose upsert.",
    "Canonical upsert identity is contact.email (match-by email). Do not emit match_by, external_id_field, hs_object_id, or Salesforce External ID on generic upsert.",
    "CRMObjectRef is {object_type, external_id}: an opaque id in the resolved CRM connection, not a P2HS record id, HubSpot object id specifically, or Salesforce Id specifically.",
    "Deal stage values are project logical aliases only (for example qualified/won/lost when configured). Never emit appointmentscheduled, closedwon, Qualification, Closed Won, dealstage, or StageName.",
    "Do not invent pipeline: default. Avoid pipeline unless the requirement explicitly needs it; HubSpot has pipeline maps, Salesforce canonical pipeline is rejected and must not be mapped to RecordType.",
    "Search stays in the frozen exact subset: contact email; company name; deal name and/or stage (AND). Do not generate query, filters, filterGroups, SOQL, sorts, projection, LIKE, fuzzy, or domain search on canonical CRM.",
    "Associations: only contact→company and deal→company via crm.association.create. Do not silently generate unsupported pairs (company→contact, company→deal, contact↔deal). Those need HubSpot labels or Salesforce OpportunityContactRole as provider actions.",
    "Public path only: host-relative POST /api/projects/{project_id}/capabilities/{capability}. Never Core, Cloud, HubSpot, or Salesforce hosts. Do not call /api/connectors or /api/integrations to pick a CRM provider.",
    "Do not emit provider_hint unless the user explicitly constrains the provider family. It is not account selection.",
    "crm_unbound and binding_ambiguous are configuration problems. Do not try HubSpot then Salesforce, and do not pick the first connection.",
    "Parse canonical responses (contact/company/deal, items, next_cursor, status). Do not expect hubspot.properties, salesforce.records, portalId, or attributes.url.",
    "Reason about machine-readable codes, grouped as configuration / validation / temporary provider / not found / duplicate. Do not generate giant provider error switches.",
    "Do not generate crm.external_sync as an invoke. Do not treat crm.deal.updated as crm.deal.update. Do not generate inbound CRM event subscriptions unless a generic event-subscription abstraction is already planned.",
    "Compose product from App Blocks + catalog crm.*: a lead-capture form that syncs to CRM is forms.contact / contact-capture with optional crm.contact.upsert. crm.lead / lead-lifecycle stay local Cloud `lead` records — do not rewrite them onto crm.contact.create. HubSpot vs Salesforce must not change Block/Pack selection. Local form success is independent of CRM unless missing_configuration_behavior is block_install.",
)

CRM_CANONICAL_TYPES = ("CRMContact", "CRMCompany", "CRMDeal", "CRMObjectRef", "CRMAssociation")

CRM_CONTACT_EXAMPLE = {
    "email": "customer@example.com",
    "first_name": "Jane",
    "last_name": "Smith",
}
CRM_DEAL_EXAMPLE = {
    "name": "Enterprise Renewal",
    "amount": 25000,
    "stage": "qualified",
}

GENERIC_CRM_LEAK_TOKENS: tuple[str, ...] = (
    "hubspot.create_contact",
    "hubspot.create_deal",
    "hubspot.upsert_contact",
    "salesforce.create_contact",
    "salesforce.create_opportunity",
    "salesforce.upsert_contact",
    "filterGroups",
    "filter_groups",
    "association_type_id",
    "AccountId",
    "dealstage",
    "StageName",
    "RecordTypeId",
    "hs_object_id",
    "portal_id",
    "portalId",
    "org_id",
    "connection_id",
    "instance_url",
    "api.hubapi.com",
    "my.salesforce.com",
    "/api/connectors",
    "/api/integrations",
    "/api/crm/",
    "crm.external_sync",
)

PROVIDER_BRANCH_TOKENS: tuple[str, ...] = (
    'provider === "hubspot"',
    "provider === 'hubspot'",
    'provider === "salesforce"',
    "provider === 'salesforce'",
    "if (provider ===",
    "if (connector ===",
)


@dataclass(frozen=True)
class CrmGenerationDecision:
    surface: str
    capability: str | None = None
    provider_action: str | None = None
    payload: dict[str, Any] | None = None
    notes: tuple[str, ...] = ()

    @property
    def invoke_path(self) -> str | None:
        if not self.capability:
            return None
        return CRM_PUBLIC_INVOKE_PATH.format(
            project_id="{project_id}",
            capability=self.capability,
        )


def canonical_crm_invoke_capabilities() -> tuple[str, ...]:
    """Defaults come from the invoke catalog, not a parallel CRM registry."""
    verbs: list[str] = []
    for capability_id in CRM_PUBLIC_INVOKE_VERBS:
        spec = INVOKE_CAPABILITIES[capability_id]
        if spec.get("status") != "implemented":
            continue
        if not spec.get("invocable"):
            continue
        if not is_implemented(capability_id):
            continue
        verbs.append(capability_id)
    return tuple(verbs)


def supported_association_pairs() -> tuple[str, ...]:
    return tuple(CRM_ASSOCIATION_PHASE_6G["supported_pairs"])


CRM_PROMPT_FIXTURES: tuple[dict[str, Any], ...] = (
    {
        "prompt": "Create a contact in our CRM",
        "connected": None,
        "decision": CrmGenerationDecision(
            surface="canonical",
            capability="crm.contact.create",
            payload={"contact": dict(CRM_CONTACT_EXAMPLE)},
        ),
    },
    {
        "prompt": "Sync form submissions to CRM by email",
        "connected": None,
        "decision": CrmGenerationDecision(
            surface="canonical",
            capability="crm.contact.upsert",
            payload={"contact": dict(CRM_CONTACT_EXAMPLE)},
            notes=("identity is contact.email",),
        ),
    },
    {
        "prompt": "When someone submits the contact form, create/update them in the CRM.",
        "connected": None,
        "decision": CrmGenerationDecision(
            surface="canonical",
            capability="crm.contact.upsert",
            payload={"contact": dict(CRM_CONTACT_EXAMPLE)},
        ),
    },
    {
        "prompt": "Search CRM contact by email",
        "connected": None,
        "decision": CrmGenerationDecision(
            surface="canonical",
            capability="crm.contact.search",
            payload={"email": "customer@example.com"},
        ),
    },
    {
        "prompt": "Create a company",
        "connected": None,
        "decision": CrmGenerationDecision(
            surface="canonical",
            capability="crm.company.create",
            payload={"company": {"name": "Acme Corp", "phone": "555-0100"}},
            notes=("omit required domain for portable generation",),
        ),
    },
    {
        "prompt": "Find company called Acme",
        "connected": None,
        "decision": CrmGenerationDecision(
            surface="canonical",
            capability="crm.company.search",
            payload={"name": "Acme"},
        ),
    },
    {
        "prompt": "Create a deal",
        "connected": None,
        "decision": CrmGenerationDecision(
            surface="canonical",
            capability="crm.deal.create",
            payload={"deal": {"name": "Enterprise Renewal", "amount": 25000}},
            notes=("do not invent pipeline", "omit stage unless aliases are configured"),
        ),
    },
    {
        "prompt": "Move deal to qualified",
        "connected": None,
        "decision": CrmGenerationDecision(
            surface="canonical",
            capability="crm.deal.update",
            payload={
                "deal": {"object_type": "deal", "external_id": "ext-deal-1"},
                "changes": {"stage": "qualified"},
            },
            notes=("qualified is a logical alias, not a provider wire value",),
        ),
    },
    {
        "prompt": "Find qualified deals named Acme",
        "connected": None,
        "decision": CrmGenerationDecision(
            surface="canonical",
            capability="crm.deal.search",
            payload={"name": "Acme", "stage": "qualified"},
        ),
    },
    {
        "prompt": "Associate this contact with the company",
        "connected": None,
        "decision": CrmGenerationDecision(
            surface="canonical",
            capability="crm.association.create",
            payload={
                "association": {
                    "from": {"object_type": "contact", "external_id": "ext-contact-1"},
                    "to": {"object_type": "company", "external_id": "ext-company-1"},
                }
            },
        ),
    },
    {
        "prompt": "Run this Salesforce SOQL query SELECT Id FROM Contact",
        "connected": "salesforce",
        "decision": CrmGenerationDecision(
            surface="provider_action",
            provider_action="salesforce.query_soql",
        ),
    },
    {
        "prompt": "Use HubSpot association label X",
        "connected": "hubspot",
        "decision": CrmGenerationDecision(
            surface="provider_action",
            provider_action="hubspot.associate_objects",
        ),
    },
    {
        "prompt": "Set the HubSpot lifecycle stage to customer",
        "connected": "hubspot",
        "decision": CrmGenerationDecision(
            surface="provider_action",
            provider_action="hubspot.update_contact",
            notes=("lifecycle stage is HubSpot-specific, not canonical deal stage",),
        ),
    },
    {
        "prompt": "Create a CRM contact when this form is submitted.",
        "connected": "hubspot",
        "decision": CrmGenerationDecision(
            surface="canonical",
            capability="crm.contact.create",
            payload={"contact": dict(CRM_CONTACT_EXAMPLE)},
        ),
    },
    {
        "prompt": "Create a CRM contact when this form is submitted.",
        "connected": "salesforce",
        "decision": CrmGenerationDecision(
            surface="canonical",
            capability="crm.contact.create",
            payload={"contact": dict(CRM_CONTACT_EXAMPLE)},
        ),
    },
    {
        "prompt": "Link this customer with Acme Corp.",
        "connected": None,
        "decision": CrmGenerationDecision(
            surface="canonical",
            capability="crm.association.create",
            payload={
                "association": {
                    "from": {"object_type": "contact", "external_id": "ext-contact-1"},
                    "to": {"object_type": "company", "external_id": "ext-company-1"},
                }
            },
        ),
    },
    {
        "prompt": "Attach this deal to the company",
        "connected": None,
        "decision": CrmGenerationDecision(
            surface="canonical",
            capability="crm.association.create",
            payload={
                "association": {
                    "from": {"object_type": "deal", "external_id": "ext-deal-1"},
                    "to": {"object_type": "company", "external_id": "ext-company-1"},
                }
            },
        ),
    },
    {
        "prompt": "Associate this contact with the deal",
        "connected": None,
        "decision": CrmGenerationDecision(
            surface="gap",
            notes=(
                "contact↔deal is not a canonical association pair; use HubSpot labels or Salesforce OpportunityContactRole",
            ),
        ),
    },
)


def select_crm_generation(
    prompt: str,
    *,
    connected_provider: str | None = None,
    composition_selected: bool = False,
) -> CrmGenerationDecision:
    """Fixture-backed generation policy. Connected provider must not change generic intent.

    Once Core's planner has already selected the CRM blocks, codegen follows
    that composition. The canonical fixture sentence is not required.
    """
    needle = " ".join(prompt.split())
    matches = [fixture for fixture in CRM_PROMPT_FIXTURES if fixture["prompt"] == needle]
    if not matches:
        if composition_selected:
            return CrmGenerationDecision(
                surface="canonical",
                notes=("follow_reconciled_composition",),
            )
        return CrmGenerationDecision(
            surface="gap",
            notes=("capability gap / require composition",),
        )
    for fixture in matches:
        if fixture["connected"] == connected_provider:
            return fixture["decision"]
    signatures = {
        (item["decision"].surface, item["decision"].capability, item["decision"].provider_action)
        for item in matches
    }
    if len(signatures) == 1:
        return matches[0]["decision"]
    if connected_provider in {"hubspot", "salesforce"} and _looks_generic_crm(needle):
        raise AssertionError(
            "generic CRM intent must not fall through to the connected provider"
        )
    return CrmGenerationDecision(
        surface="gap",
        notes=("capability gap / require composition",),
    )


def _looks_generic_crm(prompt: str) -> bool:
    lowered = prompt.lower()
    if "soql" in lowered or "filtergroups" in lowered or "lifecycle stage" in lowered:
        return False
    return "crm" in lowered or "contact" in lowered or "deal" in lowered or "company" in lowered


def generic_snippet_leaks(source: str) -> tuple[str, ...]:
    found: list[str] = []
    for token in GENERIC_CRM_LEAK_TOKENS:
        if token in source:
            found.append(token)
    for token in PROVIDER_BRANCH_TOKENS:
        if token in source:
            found.append(token)
    if "SELECT " in source and "FROM " in source:
        found.append("SOQL")
    return tuple(found)


def example_canonical_invoke(capability: str, payload: dict[str, Any]) -> str:
    return (
        f'const result = await invokeCapability("{capability}", {payload!r})\n'
        'if (result.status === "succeeded") {\n'
        "  const record = result.contact || result.company || result.deal || result.association || result.items\n"
        "}\n"
    )
