"""
L2A Matcher - Lead to Account matching using the core engine
==============================================================
Uses the EngineClient for all matching operations.
NO local fuzzy matching - everything goes through the engine.
"""

from typing import Dict, Any, Optional
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from datetime import datetime
import logging

# Use the engine for ALL matching
from .engine_client import get_engine_client
from .context_service import ContextService
from ..model_defs.l2a_models import L2APolicy, L2ASuggestion
from ..models import Integration, IntegrationProvider
from .rate_limiter import SmartRateLimiter, RateLimitConfig

logger = logging.getLogger(__name__)


class L2AMatcher:
    """
    Lead->Account matcher using the core matching engine.
    All matching operations go through EngineClient.
    """

    def __init__(self, db: AsyncSession):
        self.db = db
        self.engine = get_engine_client()  # Use the engine for all matching
        self.context_service = ContextService(db)  # Context-aware matching
        self.limiter = SmartRateLimiter(
            RateLimitConfig(requests_per_second=5, concurrent_limit=10)
        )  # Conservative rate limit for SF API

    async def ensure_policy(self, account_id: str, integration_id: str) -> L2APolicy:
        """Ensure a policy exists for this account."""
        res = await self.db.execute(
            select(L2APolicy).where(
                L2APolicy.account_id == account_id, L2APolicy.status == "ACTIVE"
            )
        )
        pol = res.scalar_one_or_none()
        if pol:
            return pol

        # Detect lead lookup field (reference to Account) if present
        lead_lookup = await self._detect_lead_lookup_field(integration_id)
        pol = L2APolicy(
            account_id=account_id,
            integration_id=integration_id,
            lead_lookup_field=lead_lookup or None,
        )
        self.db.add(pol)
        await self.db.commit()
        await self.db.refresh(pol)
        return pol

    async def _detect_lead_lookup_field(self, integration_id: str) -> Optional[str]:
        """Detect the Lead field that references Account."""
        from .salesforce_gateway import SalesforceGateway

        integ = await self._get_sf_integration(integration_id)
        sf = await SalesforceGateway.for_tenant(integ.account_id, self.db)
        schema = await sf.describe_object("Lead")
        for f in schema.get("fields", []):
            if f.get("type") == "reference" and "Account" in (
                f.get("referenceTo") or []
            ):
                return f["name"]  # e.g., Account__c
        return None

    async def _get_sf_integration(self, integration_id_or_account: str):
        """Get Salesforce integration by ID or account ID."""
        q = select(Integration).where(Integration.id == integration_id_or_account)
        res = await self.db.execute(q)
        integ = res.scalar_one_or_none()
        if integ:
            return integ

        # Fallback by account_id + SALESFORCE
        q = select(Integration).where(
            Integration.account_id == integration_id_or_account,
            Integration.provider == IntegrationProvider.SALESFORCE,
        )
        res = await self.db.execute(q)
        return res.scalar_one()

    async def run(self, account_id: str, progress_cb=None) -> Dict[str, Any]:
        """
        Match Leads to Accounts using the core engine.
        All matching is delegated to EngineClient.
        """
        start_time = datetime.utcnow()

        if progress_cb:
            progress_cb(0.05, "Loading Salesforce integration")

        # Get integration and policy
        integ = await self._get_sf_integration(account_id)
        from .salesforce_gateway import SalesforceGateway

        sf = await SalesforceGateway.for_tenant(integ.account_id, self.db)
        policy = await self.ensure_policy(integ.account_id, integ.id)

        # Get organization context for smarter matching
        org_profile = await self.context_service.get_or_create_profile(integ.account_id)
        weight_adjustments = self.context_service.get_weight_adjustments(org_profile)
        blocking_strategy = self.context_service.get_blocking_strategy(org_profile)
        threshold_adjustments = self.context_service.get_threshold_adjustments(
            org_profile
        )

        # Get existing suggestions to avoid duplicates
        existing_query = select(L2ASuggestion.lead_id).where(
            L2ASuggestion.policy_id == policy.id,
            L2ASuggestion.status.in_(
                ["pending", "linked"]
            ),  # Don't recreate if already processed
        )
        existing_result = await self.db.execute(existing_query)
        existing_lead_ids = {row[0] for row in existing_result.all()}

        logger.info(f"Found {len(existing_lead_ids)} existing suggestions to skip")

        if progress_cb:
            progress_cb(0.20, "Indexing Accounts")

        # Fetch Accounts from Salesforce (target records)
        logger.info("Fetching Accounts from Salesforce...")
        account_fields = [
            "Id",
            "Name",
            "Website",
            "Phone",
            "BillingCity",
            "BillingState",
            "BillingPostalCode",
        ]
        accounts = await self.limiter.execute_with_backoff(
            sf.get_sample_records, "Account", account_fields, n=50000
        )
        logger.info(f"Fetched {len(accounts)} Accounts")

        # Build SOQL query for Leads (source records)
        lead_fields = [
            "Id",
            "FirstName",
            "LastName",
            "Company",
            "Email",
            "Website",
            "Phone",
            "City",
            "State",
            "PostalCode",
        ]
        leads_query = (
            f"SELECT {', '.join(lead_fields)} FROM Lead WHERE IsConverted=false"
        )

        # Only get leads without existing Account link
        if policy.lead_lookup_field:
            leads_query += f" AND {policy.lead_lookup_field}=NULL"

        # Collect all leads using pagination
        logger.info("Fetching Leads from Salesforce...")
        all_leads = []
        processed = 0
        total_estimated = 10000  # Estimate for progress

        async for chunk in sf.paginate_soql(leads_query):
            await self.limiter.wait_for_rate_limit()  # Throttle each page
            all_leads.extend(chunk)
            processed += len(chunk)

            if progress_cb:
                pct = 0.20 + 0.50 * min(processed / total_estimated, 1.0)
                progress_cb(pct, f"Scanning leads {processed}/{total_estimated}")

            if len(all_leads) >= 10000:  # Safety limit
                logger.warning("Reached 10k leads limit, stopping pagination")
                break

        logger.info(f"Fetched {len(all_leads)} unconverted Leads")

        # Filter out existing suggestions
        leads_to_process = [l for l in all_leads if l["Id"] not in existing_lead_ids]
        logger.info(f"Processing {len(leads_to_process)} new Leads")

        if not leads_to_process:
            logger.info("No new leads to process")
            return {
                "considered": 0,
                "suggestions": 0,
                "processing_time_ms": 0,
                "message": "No new leads to process",
            }

        # Create discovery policy for the engine with context-aware adjustments
        # Apply weight adjustments based on org profile
        base_weights = policy.weights.copy()
        for field, adjustment in weight_adjustments.items():
            if field in base_weights:
                base_weights[field] *= adjustment

        # Apply threshold adjustments
        base_thresholds = policy.thresholds.copy()
        base_thresholds.update(threshold_adjustments)

        discovery_policy = {
            "field_mappings": [
                {
                    "source_column": "Company",
                    "reference_column": "Name",
                    "weight": base_weights.get("name", 0.30),
                    "algorithm": "TokenSetRatio",
                },
                {
                    "source_column": "Email",
                    "reference_column": "Website",
                    "weight": base_weights.get("domain", 0.45),
                    "algorithm": "Domain",  # Special algorithm for domain matching
                },
                {
                    "source_column": "Phone",
                    "reference_column": "Phone",
                    "weight": base_weights.get("phone", 0.15),
                    "algorithm": "Exact",
                },
                {
                    "source_column": "City",
                    "reference_column": "BillingCity",
                    "weight": base_weights.get("geo", 0.05),
                    "algorithm": "Exact",
                },
                {
                    "source_column": "State",
                    "reference_column": "BillingState",
                    "weight": base_weights.get("geo", 0.05),
                    "algorithm": "Exact",
                },
            ],
            "threshold": base_thresholds.get("suggest", 0.60),
            "source_id_field": "Id",
            "target_id_field": "Id",
            "blocking_keys": {
                # Use domain as blocking key for efficiency
                "source_column": "Email",
                "reference_column": "Website",
                "key_length": 5,
            },
        }

        # Progress callback for logging
        async def progress_callback(event):
            # Handle both dict and ProgressEvent objects
            if hasattr(event, "message"):
                # It's a ProgressEvent object
                logger.info(f"Matching progress: {getattr(event, 'message', '')}")
            elif isinstance(event, dict):
                # It's a dictionary
                logger.info(f"Matching progress: {event.get('message', '')}")
            else:
                logger.info(f"Matching progress: {str(event)}")

        if progress_cb:
            progress_cb(0.70, "Running engine matching")

        # Use the engine for ALL matching - no local fuzzy logic!
        logger.info("Starting engine-based matching...")
        result = await self.engine.match_salesforce_records(
            source_records=leads_to_process,
            target_records=accounts,
            discovery_policy=discovery_policy,
            batch_size=1000,
            progress_callback=progress_callback,
        )

        logger.info(
            f"Engine found {len(result.matches)} matches in {result.processing_time_ms}ms"
        )

        if progress_cb:
            progress_cb(0.90, "Creating suggestions")

        # Convert engine results to L2ASuggestions
        suggestions = []
        suggestions_created = 0
        certain_count = 0

        for _, match in result.matches.iterrows():
            # Create suggestions for matches above minimum threshold
            score = float(match.get("score", 0))
            # Use threshold from policy (default 0.60 to match UI minimum)
            if score < policy.thresholds.get("suggest", 0.60):
                continue

            tier = self.engine._get_tier(score)
            if tier == "CERTAIN":
                certain_count += 1

            suggestion = L2ASuggestion(
                policy_id=policy.id,
                lead_id=match.get("source_id"),
                account_id=match.get("target_id"),
                score=score,
                tier=tier,
                reasons=match.get("feature_scores", {}),
                explanation=match.get("explanation", ""),
                status="pending",
            )
            suggestions.append(suggestion)

            # Batch commit for performance
            if len(suggestions) >= 500:
                self.db.add_all(suggestions)
                await self.db.commit()
                suggestions_created += len(suggestions)
                suggestions = []
                logger.info(
                    f"Committed batch of 500 suggestions (total: {suggestions_created})"
                )

        # Commit remaining suggestions
        if suggestions:
            self.db.add_all(suggestions)
            await self.db.commit()
            suggestions_created += len(suggestions)
            logger.info(f"Committed final batch of {len(suggestions)} suggestions")

        # Update policy stats
        policy.last_run_at = datetime.utcnow()
        policy.stats["total_runs"] = policy.stats.get("total_runs", 0) + 1
        policy.stats["last_run_suggestions"] = suggestions_created
        policy.stats["last_run_considered"] = len(leads_to_process)
        policy.stats["last_engine_version"] = result.engine_version
        await self.db.commit()

        if progress_cb:
            progress_cb(0.98, "Finalizing")

        duration_seconds = (datetime.utcnow() - start_time).total_seconds()

        return {
            "considered": len(leads_to_process),
            "suggestions_generated": suggestions_created,
            "certain_count": certain_count,
            "processing_time_ms": result.processing_time_ms,
            "engine_version": result.engine_version,
            "total_duration_seconds": duration_seconds,
            "stats": result.stats,
            "message": f"Created {suggestions_created} suggestions from {len(leads_to_process)} leads",
        }

    async def apply_suggestion(
        self, suggestion_id: str, action: str = "link"
    ) -> Dict[str, Any]:
        """Apply a suggestion (link Lead to Account)."""
        # Get suggestion
        res = await self.db.execute(
            select(L2ASuggestion).where(L2ASuggestion.id == suggestion_id)
        )
        suggestion = res.scalar_one_or_none()

        if not suggestion:
            raise ValueError(f"Suggestion {suggestion_id} not found")

        if suggestion.status != "pending":
            raise ValueError(f"Suggestion already processed: {suggestion.status}")

        # Get policy for field name
        res = await self.db.execute(
            select(L2APolicy).where(L2APolicy.id == suggestion.policy_id)
        )
        policy = res.scalar_one()

        if action == "link" and policy.lead_lookup_field:
            # Update Lead in Salesforce
            integ = await self._get_sf_integration(policy.integration_id)
            from .salesforce_gateway import SalesforceGateway

            sf = await SalesforceGateway.for_tenant(integ.account_id, self.db)

            # Link the Lead to the Account via gateway update wrapper
            from .sf_fls import assert_updateable

            await assert_updateable(sf, "Lead", policy.lead_lookup_field)
            await sf.update(
                "Lead",
                suggestion.lead_id,
                {policy.lead_lookup_field: suggestion.account_id},
            )

            suggestion.status = "linked"
            suggestion.applied_at = datetime.utcnow()

        elif action == "reject":
            suggestion.status = "rejected"
            suggestion.applied_at = datetime.utcnow()

        else:
            raise ValueError(f"Invalid action: {action}")

        await self.db.commit()

        return {
            "suggestion_id": suggestion_id,
            "action": action,
            "status": suggestion.status,
        }
