"""
Scan Salesforce for Contacts/Leads with missing key fields.

Respects custom metadata field mappings and FLS permissions.
Filters out B2C domains to stay compliant.
"""

import hashlib
import logging
from dataclasses import dataclass, field
from typing import Dict, List, Optional, Set

from ...services.salesforce_gateway import SalesforceGateway

logger = logging.getLogger(__name__)


@dataclass
class MissingFieldCandidate:
    """A Contact or Lead with missing enrichable fields."""

    # Record identity
    sfdc_id: str
    sfdc_object: str  # "Contact" or "Lead"

    # Name fields (for PDL matching)
    first_name: Optional[str] = None
    last_name: Optional[str] = None
    full_name: Optional[str] = None

    # Company context (improves PDL match rate)
    company_name: Optional[str] = None
    company_domain: Optional[str] = None
    linkedin_url: Optional[str] = None

    # Current values (may be partial)
    email: Optional[str] = None
    phone: Optional[str] = None
    mobile_phone: Optional[str] = None
    title: Optional[str] = None

    # Missing fields to enrich
    missing_fields: Set[str] = field(default_factory=set)

    # For deduplication
    match_key: Optional[str] = None

    def __post_init__(self):
        """Compute match key for deduplication."""
        if not self.match_key:
            # sha256(first+last+domain) for deduping PDL calls
            key_parts = [
                (self.first_name or "").lower().strip(),
                (self.last_name or "").lower().strip(),
                (self.company_domain or "").lower().strip(),
            ]
            key_str = "|".join(key_parts)
            self.match_key = hashlib.sha256(key_str.encode()).hexdigest()


class MissingFieldScanner:
    """
    Scan Salesforce for records with missing contact data.

    Supports:
    - Custom field mappings via metadata
    - FLS validation
    - B2C domain filtering
    - Deduplication by match key
    """

    # Standard field mappings (can be overridden by custom metadata)
    STANDARD_FIELD_MAP = {
        "Contact": {
            "first_name": "FirstName",
            "last_name": "LastName",
            "full_name": "Name",
            "email": "Email",
            "phone": "Phone",
            "mobile_phone": "MobilePhone",
            "title": "Title",
            "company_name": "Account.Name",
            "company_domain": "Account.Website",
        },
        "Lead": {
            "first_name": "FirstName",
            "last_name": "LastName",
            "full_name": "Name",
            "email": "Email",
            "phone": "Phone",
            "mobile_phone": "MobilePhone",
            "title": "Title",
            "company_name": "Company",
            "company_domain": "Website",
        },
    }

    # Core enrichable fields (always supported out of the box)
    CORE_ENRICHABLE_FIELDS: Set[str] = {"email", "phone", "mobile_phone", "title"}
    # Optional fields require explicit mapping before they are scanned/written
    OPTIONAL_ENRICHABLE_FIELDS: Set[str] = {"linkedin_url"}
    # Backwards-compatible superset; prefer instance-level get_enrichable_fields().
    ENRICHABLE_FIELDS: Set[str] = CORE_ENRICHABLE_FIELDS | OPTIONAL_ENRICHABLE_FIELDS

    def __init__(
        self,
        gateway: SalesforceGateway,
        b2c_domains: Optional[Set[str]] = None,
        custom_field_map: Optional[Dict[str, Dict[str, str]]] = None,
    ):
        """
        Initialize scanner.

        Args:
            gateway: Salesforce gateway for queries
            b2c_domains: Set of B2C domains to exclude (gmail.com, yahoo.com, etc.)
            custom_field_map: Override field mappings {"Contact": {"email": "Work_Email__c"}}
        """
        self.gateway = gateway
        self.b2c_domains = b2c_domains or self._load_b2c_domains()
        self.custom_field_map = custom_field_map or {}
        self._enrichable_fields: Set[str] = self._compute_enrichable_fields()

    def _compute_enrichable_fields(self) -> Set[str]:
        """Determine which logical fields are active for this scanner instance."""
        fields = set(self.CORE_ENRICHABLE_FIELDS)
        for mapping in self.custom_field_map.values():
            for logical_field in mapping.keys():
                if logical_field in self.OPTIONAL_ENRICHABLE_FIELDS:
                    fields.add(logical_field)
        return fields

    def get_enrichable_fields(self) -> Set[str]:
        """Return logical fields considered enrichable for this scanner instance."""
        return set(self._enrichable_fields)

    @staticmethod
    def _load_b2c_domains() -> Set[str]:
        """Load B2C domains from bundled resource file."""
        from pathlib import Path

        # Try multiple locations
        candidates = [
            Path(__file__).parent.parent.parent.parent
            / "resources"
            / "b2c_domains_v20250719.txt",
            Path("src/fmatch/resources/b2c_domains_v20250719.txt"),
            Path("src/fmatch/core/resources/b2c_domains_v20250719.txt"),
        ]

        for path in candidates:
            if path.exists():
                logger.info(f"Loading B2C domains from {path}")
                with open(path, "r", encoding="utf-8") as f:
                    domains = {
                        line.strip().lower()
                        for line in f
                        if line.strip() and not line.startswith("#")
                    }
                logger.info(f"Loaded {len(domains)} B2C domains")
                return domains

        logger.warning("B2C domain file not found, using minimal fallback list")
        return {
            "gmail.com",
            "yahoo.com",
            "hotmail.com",
            "outlook.com",
            "aol.com",
            "icloud.com",
            "me.com",
            "mac.com",
            "live.com",
            "msn.com",
        }

    def _get_field_name(self, sfdc_object: str, logical_field: str) -> str:
        """
        Get actual SFDC field name, respecting custom mappings.

        Args:
            sfdc_object: "Contact" or "Lead"
            logical_field: "email", "phone", etc.

        Returns:
            Actual SFDC field name (e.g., "Email" or "Work_Email__c")
        """
        # Check custom mapping first
        if sfdc_object in self.custom_field_map:
            if logical_field in self.custom_field_map[sfdc_object]:
                return self.custom_field_map[sfdc_object][logical_field]

        # Fall back to standard mapping
        if sfdc_object in self.STANDARD_FIELD_MAP:
            return self.STANDARD_FIELD_MAP[sfdc_object].get(
                logical_field, logical_field
            )

        return logical_field

    def _build_scan_query(
        self,
        sfdc_object: str,
        limit: int = 1000,
        extra_conditions: Optional[List[str]] = None,
    ) -> str:
        """
        Build SOQL query to find records with missing fields.

        Args:
            sfdc_object: "Contact" or "Lead"
            limit: Max records to return

        Returns:
            SOQL query string
        """
        # Map logical fields to actual SFDC fields
        fields_to_select = []
        logical_fields = [
            "first_name",
            "last_name",
            "full_name",
            "email",
            "phone",
            "mobile_phone",
            "title",
            "company_name",
            "company_domain",
        ]
        for optional_field in sorted(
            self._enrichable_fields - self.CORE_ENRICHABLE_FIELDS
        ):
            logical_fields.append(optional_field)
        for logical in logical_fields:
            actual = self._get_field_name(sfdc_object, logical)
            if actual.lower() == logical.lower():
                # No mapping available; skip to avoid invalid SOQL column
                continue
            if actual not in fields_to_select:
                fields_to_select.append(actual)

        # Build WHERE clause: at least one enrichable field must be missing
        enrichable_actual = [
            self._get_field_name(sfdc_object, f) for f in self._enrichable_fields
        ]
        where_clauses = [
            f"{field} = NULL" for field in enrichable_actual if "." not in field
        ]  # Skip relationship fields in WHERE
        base_condition = f"({' OR '.join(where_clauses)})"

        and_clauses = [
            base_condition,
            f"{self._get_field_name(sfdc_object, 'first_name')} != NULL",
            f"{self._get_field_name(sfdc_object, 'last_name')} != NULL",
        ]

        if extra_conditions:
            for condition in extra_conditions:
                if condition and isinstance(condition, str):
                    and_clauses.append(condition)

        query = f"""
        SELECT Id, {', '.join(fields_to_select)}
        FROM {sfdc_object}
        WHERE {' AND '.join(and_clauses)}
        LIMIT {limit}
        """

        return query.strip()

    async def scan(
        self,
        account_id: str,
        sfdc_object: str = "Contact",
        limit: int = 1000,
        filter_b2c: bool = True,
        extra_conditions: Optional[List[str]] = None,
    ) -> List[MissingFieldCandidate]:
        """
        Scan Salesforce for records with missing enrichable fields.

        Args:
            account_id: FoundryMatch account ID
            sfdc_object: "Contact" or "Lead"
            limit: Max candidates to return
            filter_b2c: Skip B2C email domains
            extra_conditions: Additional SOQL conditions to AND with the base query

        Returns:
            List of candidates with missing fields
        """
        logger.info(f"Scanning {sfdc_object} for missing fields (limit={limit})")

        # Build query
        query = self._build_scan_query(
            sfdc_object, limit, extra_conditions=extra_conditions
        )
        logger.debug(f"SOQL: {query}")

        # Execute query via gateway
        result = await self.gateway.soql(query)

        if result.get("error"):
            logger.error(f"SOQL query failed: {result['error']}")
            return []

        records = result.get("records", [])
        logger.info(f"Found {len(records)} records with missing fields")

        # Convert to candidates
        candidates = []
        seen_keys = set()

        for rec in records:
            candidate = self._record_to_candidate(rec, sfdc_object)

            if not candidate:
                continue

            # Filter B2C domains
            if filter_b2c and self._is_b2c_domain(candidate.email):
                logger.debug(f"Skipping B2C email: {candidate.email}")
                continue

            # Deduplicate by match key
            if candidate.match_key in seen_keys:
                logger.debug(
                    f"Skipping duplicate: {candidate.first_name} {candidate.last_name}"
                )
                continue

            seen_keys.add(candidate.match_key)
            candidates.append(candidate)

        logger.info(f"Returning {len(candidates)} unique candidates after filtering")
        return candidates

    def _record_to_candidate(
        self, record: Dict, sfdc_object: str
    ) -> Optional[MissingFieldCandidate]:
        """Convert SFDC record to MissingFieldCandidate."""
        try:
            # Extract fields using mapping
            def get_value(logical_field: str) -> Optional[str]:
                actual = self._get_field_name(sfdc_object, logical_field)
                # Handle relationship fields (e.g., Account.Name)
                if "." in actual:
                    parts = actual.split(".")
                    val = record
                    for part in parts:
                        if not val or not isinstance(val, dict):
                            return None
                        val = val.get(part)
                    return val if val else None
                return record.get(actual)

            first_name = get_value("first_name")
            last_name = get_value("last_name")

            # Skip if no name
            if not first_name or not last_name:
                return None

            # Build candidate
            candidate = MissingFieldCandidate(
                sfdc_id=record["Id"],
                sfdc_object=sfdc_object,
                first_name=first_name,
                last_name=last_name,
                full_name=get_value("full_name"),
                company_name=get_value("company_name"),
                company_domain=self._normalize_domain(get_value("company_domain")),
                email=get_value("email"),
                phone=get_value("phone"),
                mobile_phone=get_value("mobile_phone"),
                title=get_value("title"),
                linkedin_url=get_value("linkedin_url"),
            )

            # Identify missing fields
            for logical_field in self._enrichable_fields:
                current_value = getattr(candidate, logical_field, None)
                if not current_value or str(current_value).strip() == "":
                    candidate.missing_fields.add(logical_field)

            # Only return if at least one field is missing
            if not candidate.missing_fields:
                return None

            return candidate

        except Exception as e:
            logger.warning(f"Failed to parse record {record.get('Id')}: {e}")
            return None

    def _is_b2c_domain(self, email: Optional[str]) -> bool:
        """Check if email is from a B2C domain."""
        if not email or "@" not in email:
            return False

        domain = email.split("@")[-1].lower().strip()
        return domain in self.b2c_domains

    @staticmethod
    def _normalize_domain(domain: Optional[str]) -> Optional[str]:
        """Normalize domain for matching (remove www, protocol, etc.)."""
        if not domain:
            return None

        domain = domain.lower().strip()

        # Remove protocol
        if domain.startswith("http://"):
            domain = domain[7:]
        elif domain.startswith("https://"):
            domain = domain[8:]

        # Remove www
        if domain.startswith("www."):
            domain = domain[4:]

        # Remove path
        if "/" in domain:
            domain = domain.split("/")[0]

        return domain if domain else None
