"""Validation helpers for column payloads and add-to-CRM mappings.

Extracted from cli.py to keep the main entry-point lean.
"""

from __future__ import annotations

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

from autotouch_cli.exceptions import AutotouchInputError
from autotouch_shared.provider_registry import (
    ProviderContractValidationError,
    validate_column_provider_contract,
)

__all__ = [
    "extract_add_to_crm_field_mappings",
    "inspect_add_to_crm_field_mappings",
    "validate_add_to_crm_create_payload",
    "validate_column_contract_payload",
    "load_column_for_run_precheck",
    "load_columns_for_table_precheck",
    "find_add_to_crm_structured_mapping_issues",
    "run_precheck_for_add_to_crm",
]

# ---------------------------------------------------------------------------
# Type alias for the request_api callable injected by callers.
# ---------------------------------------------------------------------------
RequestApiFn = Callable[..., Any]

# Default timeout used when the caller doesn't supply one.
DEFAULT_TIMEOUT_SECONDS = 30


# ---------------------------------------------------------------------------
# Pure helpers (no I/O)
# ---------------------------------------------------------------------------


def extract_add_to_crm_field_mappings(payload: Any) -> Optional[Dict[str, Any]]:
    if not isinstance(payload, dict):
        return None
    config = payload.get("config")
    if not isinstance(config, dict):
        return None
    provider = str(config.get("provider") or "").strip().lower()
    if provider != "add_to_crm":
        return None
    field_mappings = config.get("fieldMappings")
    if isinstance(field_mappings, dict):
        return field_mappings
    return {}


def inspect_add_to_crm_field_mappings(field_mappings: Dict[str, Any]) -> Dict[str, Any]:
    mode = "multi" if str(field_mappings.get("mode") or "").strip().lower() == "multi" else "single"
    required_missing: List[str] = []
    optional_missing: List[str] = []

    def _has_identity_mapping(lead_mapping: Dict[str, Any]) -> bool:
        if str(lead_mapping.get("linkedinUrl") or "").strip():
            return True
        if isinstance(lead_mapping.get("emailAddresses"), list) and any(
            isinstance(item, dict) and str(item.get("column") or "").strip()
            for item in (lead_mapping.get("emailAddresses") or [])
        ):
            return True
        if isinstance(lead_mapping.get("phoneNumbers"), list) and any(
            isinstance(item, dict) and str(item.get("column") or "").strip()
            for item in (lead_mapping.get("phoneNumbers") or [])
        ):
            return True
        return False

    if mode == "multi":
        common = field_mappings.get("common") if isinstance(field_mappings.get("common"), dict) else {}
        leads = field_mappings.get("leads") if isinstance(field_mappings.get("leads"), list) else []

        common_domain = str((common or {}).get("companyDomain") or field_mappings.get("companyDomain") or "").strip()
        if not common_domain:
            required_missing.append("common.companyDomain")

        has_lead_identity = False
        optional_lead_slots = ["firstName", "lastName", "title", "companyName"]
        for idx, lead in enumerate(leads):
            if not isinstance(lead, dict):
                continue
            if _has_identity_mapping(lead):
                has_lead_identity = True
            for slot in optional_lead_slots:
                if not str(lead.get(slot) or "").strip():
                    optional_missing.append(f"leads[{idx}].{slot}")
        if not has_lead_identity:
            required_missing.append("leads[].identityFields")
    else:
        company_domain_key = str(field_mappings.get("companyDomain") or "").strip()
        if not company_domain_key:
            required_missing.append("companyDomain")
        if not _has_identity_mapping(field_mappings):
            required_missing.append("identityFields")
        for slot in ["firstName", "lastName", "title", "companyName"]:
            if not str(field_mappings.get(slot) or "").strip():
                optional_missing.append(slot)

    return {
        "mode": mode,
        "required_missing": required_missing,
        "optional_missing": optional_missing,
    }


def validate_add_to_crm_create_payload(payload: Dict[str, Any]) -> None:
    field_mappings = extract_add_to_crm_field_mappings(payload)
    if field_mappings is None:
        return

    kind_value = str(payload.get("kind") or "").strip().lower()
    if kind_value != "enrichment":
        raise AutotouchInputError(
            "add_to_crm column payload must use kind='enrichment'. "
            "Tip: run `autotouch columns recipe --type add_to_crm`."
        )

    if not field_mappings:
        raise AutotouchInputError(
            "add_to_crm requires config.fieldMappings. "
            "Tip: run `autotouch columns recipe --type add_to_crm`."
        )

    issues = inspect_add_to_crm_field_mappings(field_mappings)
    required_missing = issues.get("required_missing") or []
    optional_missing = issues.get("optional_missing") or []
    if required_missing:
        missing = ", ".join(str(v) for v in required_missing)
        raise AutotouchInputError(
            f"add_to_crm fieldMappings missing required keys: {missing}. "
            "Tip: run `autotouch columns recipe --type add_to_crm`."
        )
    if optional_missing:
        missing = ", ".join(str(v) for v in optional_missing)
        print(
            f"WARN: add_to_crm optional mappings missing: {missing}. "
            "Leads can still be created but profile fields may be sparse.",
            file=sys.stderr,
        )


def validate_column_contract_payload(payload: Dict[str, Any]) -> None:
    config = payload.get("config") if isinstance(payload.get("config"), dict) else {}
    try:
        validate_column_provider_contract(payload, config)
    except ProviderContractValidationError as exc:
        msg = exc.message
        if exc.hint:
            msg = f"{msg} TIP: {exc.hint}"
        raise AutotouchInputError(msg) from exc


# ---------------------------------------------------------------------------
# Precheck helpers (need I/O via injected request_api callable)
# ---------------------------------------------------------------------------


def load_columns_for_table_precheck(
    *,
    table_id: str,
    base_url: Optional[str],
    token: str,
    use_x_api_key: bool,
    timeout: int,
    verbose: bool,
    request_api: RequestApiFn,
) -> Optional[List[Dict[str, Any]]]:
    columns = request_api(
        "GET",
        f"/api/tables/{table_id}/columns",
        base_url=base_url,
        token=token,
        use_x_api_key=use_x_api_key,
        timeout=timeout,
        verbose=verbose,
    )
    return columns if isinstance(columns, list) else None


def load_column_for_run_precheck(
    *,
    table_id: str,
    column_id: str,
    base_url: Optional[str],
    token: str,
    use_x_api_key: bool,
    timeout: int,
    verbose: bool,
    request_api: RequestApiFn,
) -> Optional[Dict[str, Any]]:
    columns = load_columns_for_table_precheck(
        table_id=table_id,
        base_url=base_url,
        token=token,
        use_x_api_key=use_x_api_key,
        timeout=timeout,
        verbose=verbose,
        request_api=request_api,
    )
    if not isinstance(columns, list):
        return None
    for column in columns:
        if not isinstance(column, dict):
            continue
        if str(column.get("id") or "") == str(column_id):
            return column
    return None


def find_add_to_crm_structured_mapping_issues(
    field_mappings: Dict[str, Any],
    columns: List[Dict[str, Any]],
) -> List[str]:
    columns_by_key = {
        str(column.get("key") or "").strip(): column
        for column in columns
        if isinstance(column, dict)
    }
    issues: List[str] = []

    def inspect_contact_array(label: str, values: Any) -> None:
        if not isinstance(values, list):
            return
        for idx, item in enumerate(values):
            if not isinstance(item, dict):
                continue
            column_key = str(item.get("column") or "").strip()
            if not column_key:
                continue
            source_column = columns_by_key.get(column_key)
            source_type = str((source_column or {}).get("dataType") or "").strip().lower()
            path = str(item.get("path") or "").strip()
            if source_type == "json" and not path:
                issues.append(f"{label}[{idx}] -> {column_key}")

    mode = "multi" if str(field_mappings.get("mode") or "").strip().lower() == "multi" else "single"
    if mode == "multi":
        leads = field_mappings.get("leads")
        if isinstance(leads, list):
            for idx, lead in enumerate(leads):
                if not isinstance(lead, dict):
                    continue
                inspect_contact_array(f"leads[{idx}].emailAddresses", lead.get("emailAddresses"))
                inspect_contact_array(f"leads[{idx}].phoneNumbers", lead.get("phoneNumbers"))
    else:
        inspect_contact_array("emailAddresses", field_mappings.get("emailAddresses"))
        inspect_contact_array("phoneNumbers", field_mappings.get("phoneNumbers"))

    return issues


def run_precheck_for_add_to_crm(
    *,
    args: argparse.Namespace,
    token: str,
    payload: Dict[str, Any],
    request_api: RequestApiFn,
    default_timeout: int = DEFAULT_TIMEOUT_SECONDS,
) -> None:
    try:
        column = load_column_for_run_precheck(
            table_id=args.table_id,
            column_id=args.column_id,
            base_url=args.base_url,
            token=token,
            use_x_api_key=bool(args.use_x_api_key),
            timeout=int(args.timeout or default_timeout),
            verbose=bool(args.verbose),
            request_api=request_api,
        )
    except Exception as exc:
        print(f"WARN: add_to_crm precheck skipped (failed to load column): {exc}", file=sys.stderr)
        return

    if not isinstance(column, dict):
        return

    try:
        table_columns = load_columns_for_table_precheck(
            table_id=args.table_id,
            base_url=args.base_url,
            token=token,
            use_x_api_key=bool(args.use_x_api_key),
            timeout=int(args.timeout or default_timeout),
            verbose=bool(args.verbose),
            request_api=request_api,
        )
    except Exception:
        table_columns = None

    field_mappings = extract_add_to_crm_field_mappings(column)
    if field_mappings is None:
        return

    issues = inspect_add_to_crm_field_mappings(field_mappings)
    required_missing = issues.get("required_missing") or []
    optional_missing = issues.get("optional_missing") or []

    if required_missing:
        missing = ", ".join(str(v) for v in required_missing)
        raise AutotouchInputError(
            f"add_to_crm column is misconfigured (missing required mappings: {missing}). "
            "Recreate/update from `autotouch columns recipe --type add_to_crm` before running."
        )

    if optional_missing:
        missing = ", ".join(str(v) for v in optional_missing)
        print(
            f"WARN: add_to_crm optional mappings missing: {missing}. "
            "Run will work, but created Leads records may miss profile fields.",
            file=sys.stderr,
        )

    if isinstance(table_columns, list):
        structured_mapping_issues = find_add_to_crm_structured_mapping_issues(field_mappings, table_columns)
        if structured_mapping_issues:
            missing = ", ".join(structured_mapping_issues)
            raise AutotouchInputError(
                f"add_to_crm structured source mappings require explicit path values: {missing}. "
                "Example: work_email.response or mobile_phone.mobile_number."
            )

    scope = str(payload.get("scope") or "").strip()
    has_filters = isinstance(payload.get("filters"), dict)
    if scope in {"all", "firstN"} and not has_filters:
        print(
            "WARN: add_to_crm run is broad (scope without filters). "
            "Prefer `run-next --count N` or `scope=filtered` to avoid unintended large exports.",
            file=sys.stderr,
        )
