"""Scout Service - Fetch Salesforce metadata for Flow Builder."""

from __future__ import annotations

import logging
import re
from typing import Any, Dict, List, Optional

from ..api.v2.flow_builder_schemas import (
    ExistingFlowMetadata,
    FieldMetadata,
    RecordTypeInfo,
    SalesforceFieldType,
    ScoutContext,
    ValidationRuleMetadata,
)
from .cache import HybridCache, make_cache_key
from .salesforce_gateway import SalesforceGateway

log = logging.getLogger(__name__)

FIELD_TYPE_MAP = {
    "string": SalesforceFieldType.STRING,
    "picklist": SalesforceFieldType.PICKLIST,
    "multipicklist": SalesforceFieldType.MULTIPICKLIST,
    "boolean": SalesforceFieldType.BOOLEAN,
    "double": SalesforceFieldType.DOUBLE,
    "currency": SalesforceFieldType.CURRENCY,
    "int": SalesforceFieldType.INTEGER,
    "datetime": SalesforceFieldType.DATETIME,
    "date": SalesforceFieldType.DATE,
    "reference": SalesforceFieldType.REFERENCE,
    "percent": SalesforceFieldType.PERCENT,
    "phone": SalesforceFieldType.PHONE,
    "email": SalesforceFieldType.EMAIL,
    "url": SalesforceFieldType.URL,
    "textarea": SalesforceFieldType.TEXTAREA,
    "longtextarea": SalesforceFieldType.TEXTAREA,
    "richtextarea": SalesforceFieldType.TEXTAREA,
    "encryptedstring": SalesforceFieldType.TEXTAREA,
    "id": SalesforceFieldType.ID,
    "combobox": SalesforceFieldType.PICKLIST,
}

_INVALID_FIELD_RE = re.compile(r"No such column '([^']+)'", re.IGNORECASE)

EXPECTED_STANDARD_FIELDS = {
    "Lead": ["Description", "Name", "Email", "Phone", "Company", "Status"],
    "Contact": ["Description", "Name", "Email", "Phone", "AccountId"],
    "Account": ["Description", "Name", "Industry", "Type", "Website"],
    "Opportunity": ["Description", "Name", "StageName", "Amount", "CloseDate"],
    "Case": ["Description", "Subject", "Status", "Priority", "Origin"],
}


class FlowScout:
    """Fetch and normalize Salesforce metadata for Flow Builder."""

    DEFAULT_CACHE_TTL_S = 600  # 10 minutes

    def __init__(
        self,
        gateway: SalesforceGateway,
        cache: Optional[HybridCache] = None,
        cache_ttl_s: int = DEFAULT_CACHE_TTL_S,
    ) -> None:
        self.gateway = gateway
        self.cache = cache or HybridCache(
            namespace="flow_scout", lru_size=128, ttl=cache_ttl_s
        )
        self.cache_ttl_s = cache_ttl_s

    async def fetch_context(
        self,
        object_api_name: str,
        *,
        include_validation_rules: bool = True,
        include_existing_flows: bool = True,
    ) -> ScoutContext:
        cache_key = make_cache_key(
            "flow_scout",
            {
                "object": object_api_name,
                "validation_rules": include_validation_rules,
                "existing_flows": include_existing_flows,
            },
        )
        cached = await self.cache.get(cache_key)
        if cached:
            try:
                return ScoutContext.model_validate(cached)
            except Exception:
                log.warning("FlowScout cache entry invalid, refetching", exc_info=True)

        describe = await self.gateway.describe_object(object_api_name)
        canonical_object = describe.get("name") or object_api_name
        object_label = describe.get("label") or canonical_object

        fields = self._build_fields(describe)
        required_fields = self._build_required_fields(describe, fields)
        record_types = self._build_record_types(describe)
        picklist_restrictions = await self._build_picklist_restrictions(
            canonical_object, record_types, fields
        )
        fls_warnings = self._check_fls_visibility(canonical_object, fields)

        validation_rules: List[ValidationRuleMetadata] = []
        if include_validation_rules:
            validation_rules = await self._fetch_validation_rules(canonical_object)

        existing_flows: List[ExistingFlowMetadata] = []
        if include_existing_flows:
            existing_flows = await self._fetch_existing_flows(canonical_object)

        context = ScoutContext(
            object_api_name=canonical_object,
            object_label=object_label,
            fields=fields,
            required_fields=required_fields,
            validation_rules=validation_rules,
            existing_flows=existing_flows,
            record_types=record_types,
            picklist_restrictions=picklist_restrictions,
            fls_warnings=fls_warnings,
        )

        await self.cache.set(cache_key, context.model_dump(mode="json"))
        return context

    def _build_fields(self, describe: Dict[str, Any]) -> Dict[str, FieldMetadata]:
        fields: Dict[str, FieldMetadata] = {}
        for field in describe.get("fields", []) or []:
            name = (field.get("name") or "").strip()
            if not name:
                continue
            sf_type = str(field.get("type") or "").lower()
            mapped_type = FIELD_TYPE_MAP.get(sf_type, SalesforceFieldType.STRING)
            fields[name] = FieldMetadata(
                name=name,
                label=field.get("label") or name,
                type=mapped_type,
                updateable=bool(field.get("updateable", True)),
                createable=bool(field.get("createable", True)),
                nillable=bool(field.get("nillable", True)),
                length=field.get("length"),
                precision=field.get("precision"),
                scale=field.get("scale"),
                reference_to=field.get("referenceTo") or None,
                picklist_values=self._extract_picklist_values(field),
                default_value=field.get("defaultValue"),
                is_formula=bool(field.get("calculated", False)),
            )
        return fields

    def _build_required_fields(
        self, describe: Dict[str, Any], fields: Dict[str, FieldMetadata]
    ) -> List[str]:
        required: List[str] = []
        for field in describe.get("fields", []) or []:
            name = (field.get("name") or "").strip()
            if not name or name not in fields:
                continue
            if field.get("defaultedOnCreate"):
                continue
            if not field.get("nillable", True) and field.get("createable", True):
                required.append(name)
        return required

    def _build_record_types(self, describe: Dict[str, Any]) -> List[RecordTypeInfo]:
        record_types: List[RecordTypeInfo] = []
        for rt in describe.get("recordTypeInfos", []) or []:
            if rt.get("available") is False:
                continue
            rt_id = rt.get("recordTypeId") or rt.get("id") or rt.get("Id")
            if not rt_id:
                continue
            developer_name = rt.get("developerName") or rt.get("DeveloperName")
            label = rt.get("name") or rt.get("label") or developer_name or rt_id
            record_types.append(
                RecordTypeInfo(
                    id=rt_id,
                    developer_name=developer_name or label,
                    name=label,
                    is_default=bool(
                        rt.get("defaultRecordTypeMapping")
                        or rt.get("default")
                        or rt.get("isDefault")
                    ),
                )
            )
        return record_types

    async def _build_picklist_restrictions(
        self,
        object_api_name: str,
        record_types: List[RecordTypeInfo],
        fields: Dict[str, FieldMetadata],
    ) -> Dict[str, Dict[str, List[str]]]:
        restrictions: Dict[str, Dict[str, List[str]]] = {}
        if not record_types:
            return restrictions

        picklist_fields = {
            name
            for name, meta in fields.items()
            if meta.type
            in (SalesforceFieldType.PICKLIST, SalesforceFieldType.MULTIPICKLIST)
        }
        if not picklist_fields:
            return restrictions

        for record_type in record_types:
            try:
                payload = await self.gateway.ui_api_picklist_values_by_record_type(
                    object_api_name, record_type.id
                )
            except Exception as exc:
                log.warning(
                    "FlowScout: picklist fetch failed for %s/%s: %s",
                    object_api_name,
                    record_type.id,
                    exc,
                )
                continue

            field_values = payload.get("picklistFieldValues") or {}
            for field_name, info in field_values.items():
                if field_name not in picklist_fields:
                    continue
                values = self._extract_ui_api_picklist_values(info.get("values") or [])
                if not values:
                    continue
                restrictions.setdefault(field_name, {})[record_type.id] = values

        return restrictions

    def _extract_picklist_values(self, field: Dict[str, Any]) -> Optional[List[str]]:
        values = []
        seen = set()
        for entry in field.get("picklistValues", []) or []:
            if entry is None:
                continue
            if entry.get("active") is False:
                continue
            raw = entry.get("value") or entry.get("label")
            if raw is None:
                continue
            value = str(raw).strip()
            if value and value not in seen:
                seen.add(value)
                values.append(value)
        return values or None

    def _extract_ui_api_picklist_values(self, entries: List[Dict[str, Any]]) -> List[str]:
        values = []
        seen = set()
        for entry in entries:
            raw = entry.get("value") or entry.get("label")
            if raw is None:
                continue
            value = str(raw).strip()
            if value and value not in seen:
                seen.add(value)
                values.append(value)
        return values

    def _check_fls_visibility(
        self, object_name: str, fields: Dict[str, FieldMetadata]
    ) -> List[str]:
        """Warn if expected standard fields are missing (likely FLS issue)."""
        warnings: List[str] = []
        expected = EXPECTED_STANDARD_FIELDS.get(object_name, [])
        for field in expected:
            if field not in fields:
                warnings.append(
                    f"Standard field '{field}' not visible on {object_name}. "
                    "Check Field-Level Security for integration user."
                )
        return warnings

    def _extract_invalid_field(self, exc: Exception) -> Optional[str]:
        match = _INVALID_FIELD_RE.search(str(exc))
        if match:
            return match.group(1)
        return None

    async def _tooling_query_with_field_pruning(
        self,
        *,
        sobject: str,
        fields: List[str],
        where_clause: str,
    ) -> Dict[str, Any]:
        remaining = [field for field in fields if field]
        attempts = 0

        while remaining:
            query = (
                f"SELECT {', '.join(remaining)} "
                f"FROM {sobject} WHERE {where_clause}"
            )
            try:
                return await self.gateway.tooling_query(query)
            except ValueError as exc:
                invalid_field = self._extract_invalid_field(exc)
                if not invalid_field:
                    log.warning("FlowScout: tooling query failed: %s", exc)
                    return {"records": []}

                removed = False
                for field in list(remaining):
                    if field.lower() == invalid_field.lower():
                        if field == "Id":
                            log.warning(
                                "FlowScout: invalid field '%s' cannot be removed from %s query",
                                field,
                                sobject,
                            )
                            return {"records": []}
                        remaining.remove(field)
                        removed = True
                        log.warning(
                            "FlowScout: removing invalid field '%s' from %s query",
                            field,
                            sobject,
                        )
                        break

                if not removed:
                    log.warning(
                        "FlowScout: invalid field '%s' not found in %s query fields",
                        invalid_field,
                        sobject,
                    )
                    return {"records": []}

                attempts += 1
                if attempts > 10:
                    log.warning(
                        "FlowScout: too many invalid fields for %s query; aborting",
                        sobject,
                    )
                    return {"records": []}
            except Exception as exc:
                log.warning("FlowScout: tooling query failed: %s", exc)
                return {"records": []}

        return {"records": []}

    async def _fetch_validation_rules(
        self, object_api_name: str
    ) -> List[ValidationRuleMetadata]:
        rules: List[ValidationRuleMetadata] = []
        base_where = f"EntityDefinition.QualifiedApiName = '{object_api_name}'"
        fields = [
            "Id",
            "ValidationName",
            "FullName",
            "Active",
            "ErrorConditionFormula",
            "ErrorMessage",
            "ErrorDisplayField",
            "EntityDefinition.QualifiedApiName",
        ]
        data = await self._tooling_query_with_field_pruning(
            sobject="ValidationRule",
            fields=fields,
            where_clause=base_where,
        )

        for rec in data.get("records", []) if isinstance(data, dict) else []:
            name = (
                rec.get("ValidationName")
                or rec.get("FullName")
                or rec.get("Name")
                or rec.get("Id")
            )
            rules.append(
                ValidationRuleMetadata(
                    name=name or "ValidationRule",
                    active=bool(rec.get("Active", False)),
                    error_condition_formula=rec.get("ErrorConditionFormula") or "",
                    error_message=rec.get("ErrorMessage") or "",
                    referenced_fields=[],
                )
            )
        return rules

    async def _fetch_existing_flows(
        self, object_api_name: str
    ) -> List[ExistingFlowMetadata]:
        flows: List[ExistingFlowMetadata] = []
        where_clause = f"TableEnumOrId = '{object_api_name}'"
        fields = [
            "Id",
            "DeveloperName",
            "ApiName",
            "FullName",
            "Name",
            "MasterLabel",
            "ProcessType",
            "Status",
            "TriggerType",
            "TableEnumOrId",
        ]
        data = await self._tooling_query_with_field_pruning(
            sobject="Flow",
            fields=fields,
            where_clause=where_clause,
        )

        for rec in data.get("records", []) if isinstance(data, dict) else []:
            api_name = (
                rec.get("DeveloperName")
                or rec.get("ApiName")
                or rec.get("FullName")
                or rec.get("Name")
            )
            flows.append(
                ExistingFlowMetadata(
                    name=rec.get("MasterLabel") or api_name or "Flow",
                    api_name=api_name or rec.get("Id") or "Flow",
                    process_type=rec.get("ProcessType") or "",
                    status=rec.get("Status") or "",
                    trigger_type=rec.get("TriggerType"),
                )
            )
        return flows
