from __future__ import annotations

import asyncio
import logging
from datetime import datetime, timezone
from typing import Any, Dict, List, Optional, Sequence

from sqlmodel import Session, create_engine

from fmatch.shared.settings import get_settings

from ..db import AsyncSessionLocal
from ..models import Job, JobStatus
from ..services.salesforce_gateway import SalesforceGateway
from ..services.contact_enrichment.missing_field_scanner import (
    MissingFieldCandidate,
    MissingFieldScanner,
)
from ..model_defs.enrichment_vault import EnrichmentVault
from ..worker import (
    ProgressPhase,
    _job_pk,
    _set_job_meta,
    create_redis_connection,
    emit_progress,
)

from sqlalchemy import select

log = logging.getLogger(__name__)

_settings = get_settings()
_DATABASE_URL = _settings.DATABASE_URL

DEFAULT_OBJECT_TYPES = ["Contact", "Lead"]
MAX_RECORDS_PER_OBJECT = 5000
MAX_DETAIL_ROWS = 1000


def _run_async(func, *args, **kwargs):
    """Execute async coroutine in synchronous worker context."""
    try:
        return asyncio.run(func(*args, **kwargs))
    except RuntimeError as exc:
        if "event loop" in str(exc).lower():
            loop = asyncio.new_event_loop()
            try:
                asyncio.set_event_loop(loop)
                return loop.run_until_complete(func(*args, **kwargs))
            finally:
                asyncio.set_event_loop(None)
                loop.close()
        raise


def _utc_now() -> datetime:
    return datetime.now(timezone.utc)


def run_gap_analysis_job(job_id: str) -> None:
    """Worker entry point for proactive CRM gap analysis."""
    redis_conn = create_redis_connection()
    engine = create_engine(_DATABASE_URL)

    emit_progress(
        redis_conn, job_id, ProgressPhase.INITIALIZING, 5, "Preparing CRM gap analysis"
    )
    _set_job_meta(
        redis_conn,
        job_id,
        status="processing",
        progress=5,
        message="Starting CRM gap analysis",
    )

    try:
        with Session(engine) as db:
            job: Optional[Job] = db.get(Job, _job_pk(job_id))
            if not job:
                emit_progress(
                    redis_conn, job_id, ProgressPhase.FAILED, 100, "Job not found"
                )
                _set_job_meta(
                    redis_conn,
                    job_id,
                    status="failed",
                    progress=100,
                    message="Job not found",
                )
                log.error("CRM gap analysis job %s not found", job_id)
                return

            job.status = JobStatus.PROCESSING
            job.updated_at = _utc_now()
            db.add(job)
            db.commit()

            config = job.config if isinstance(job.config, Dict) else {}
            account_id = config.get("account_id") or job.account_id
            user_id = config.get("user_id") or job.user_id
            object_types = config.get("object_types") or DEFAULT_OBJECT_TYPES
            fields = config.get("fields") or ["Email", "MobilePhone"]
            record_limit = int(config.get("record_limit") or MAX_RECORDS_PER_OBJECT)
            detail_limit = int(config.get("detail_limit") or MAX_DETAIL_ROWS)

            if not account_id:
                raise ValueError("Gap analysis job missing account context")

        payload = _run_async(
            _perform_gap_analysis,
            account_id=str(account_id),
            user_id=str(user_id) if user_id else None,
            object_types=list(object_types),
            fields=list(fields),
            record_limit=record_limit,
            detail_limit=detail_limit,
            redis_conn=redis_conn,
            job_id=job_id,
        )

        summary = payload["summary"]
        details = payload["details"]

        with Session(engine) as db:
            job = db.get(Job, _job_pk(job_id))
            if job:
                job.results = {"summary": summary, "details": details}
                job.status = JobStatus.COMPLETED
                job.updated_at = _utc_now()
                db.add(job)
                db.commit()

        emit_progress(
            redis_conn,
            job_id,
            ProgressPhase.COMPLETED,
            100,
            "CRM gap analysis complete",
        )
        _set_job_meta(
            redis_conn,
            job_id,
            status="completed",
            progress=100,
            message="CRM gap analysis complete",
            summary=summary,
            result_rows=summary.get("total_records", 0),
        )

    except Exception as exc:
        log.exception("CRM gap analysis job %s failed: %s", job_id, exc)
        emit_progress(redis_conn, job_id, ProgressPhase.FAILED, 100, f"Failed: {exc}")
        _set_job_meta(
            redis_conn, job_id, status="failed", progress=100, message=str(exc)
        )

        with Session(create_engine(_DATABASE_URL)) as db:
            job = db.get(Job, _job_pk(job_id))
            if job:
                job.status = JobStatus.FAILED
                job.error = str(exc)
                job.updated_at = _utc_now()
                db.add(job)
                db.commit()


async def _perform_gap_analysis(
    account_id: str,
    user_id: Optional[str],
    object_types: Sequence[str],
    fields: Sequence[str],
    record_limit: int,
    detail_limit: int,
    redis_conn,
    job_id: str,
) -> Dict[str, Any]:
    """Async portion of the gap analysis scan."""
    started_at = _utc_now()
    summaries: Dict[str, Any] = {}
    details: List[Dict[str, Any]] = []

    emit_progress(
        redis_conn, job_id, ProgressPhase.PREPROCESSING, 20, "Connecting to Salesforce"
    )
    _set_job_meta(
        redis_conn,
        job_id,
        status="processing",
        progress=20,
        message="Connecting to Salesforce",
    )

    async with AsyncSessionLocal() as db:
        gateway = await SalesforceGateway.for_tenant(account_id, db)
        scanner = MissingFieldScanner(gateway)

        for idx, object_type in enumerate(object_types):
            progress = 25 + int((idx / max(1, len(object_types))) * 50)
            emit_progress(
                redis_conn,
                job_id,
                ProgressPhase.MATCHING,
                progress,
                f"Scanning {object_type} records",
            )
            _set_job_meta(
                redis_conn,
                job_id,
                status="processing",
                progress=progress,
                message=f"Scanning {object_type}",
            )

            candidates = await scanner.scan(
                account_id=account_id,
                sfdc_object=object_type,
                limit=record_limit,
            )

            vault_hits = await _load_enrichment_hits(
                db=db,
                account_id=account_id,
                object_type=object_type,
                scanner=scanner,
                candidates=candidates,
            )

            object_summary, object_details = _summarize_candidates(
                object_type=object_type,
                candidates=candidates,
                fields=fields,
                detail_limit=detail_limit,
                vault_hits=vault_hits,
            )

            summaries[object_type] = object_summary
            details.extend(object_details)

    completed_at = _utc_now()
    overall_summary = _aggregate_summary(summaries, started_at, completed_at)

    emit_progress(
        redis_conn,
        job_id,
        ProgressPhase.SAVING_RESULTS,
        90,
        "Compiling gap analysis results",
    )
    _set_job_meta(
        redis_conn,
        job_id,
        status="processing",
        progress=90,
        message="Compiling gap analysis results",
        summary=overall_summary,
    )

    return {"summary": overall_summary, "details": details}


def _summarize_candidates(
    object_type: str,
    candidates: Sequence[MissingFieldCandidate],
    fields: Sequence[str],
    detail_limit: int,
    vault_hits: Dict[str, set],
) -> tuple[Dict[str, Any], List[Dict[str, Any]]]:
    """Produce summary counters and detail payload for an SFDC object."""
    field_lower = {field.lower() for field in fields}
    track_email = "email" in field_lower
    track_mobile = {"mobilephone", "mobile_phone"} & field_lower

    missing_email = 0
    missing_mobile = 0
    enrichable_count = 0

    detail_rows: List[Dict[str, Any]] = []

    for candidate in candidates:
        email_missing = "email" in candidate.missing_fields and track_email
        mobile_missing = "mobile_phone" in candidate.missing_fields and bool(
            track_mobile
        )

        if email_missing:
            missing_email += 1
        if mobile_missing:
            missing_mobile += 1

        enrichment_possible = bool(vault_hits.get(candidate.sfdc_id))
        if enrichment_possible:
            enrichable_count += 1

        if len(detail_rows) >= detail_limit:
            continue

        detail_rows.append(
            {
                "sfdc_id": candidate.sfdc_id,
                "object_type": object_type,
                "name": candidate.full_name
                or " ".join(filter(None, [candidate.first_name, candidate.last_name]))[
                    :255
                ]
                or "(Unknown)",
                "company": candidate.company_name,
                "email": candidate.email,
                "mobile": candidate.mobile_phone,
                "phone": candidate.phone,
                "title": candidate.title,
                "missing_fields": [
                    _display_field_name(field)
                    for field in sorted(candidate.missing_fields)
                ],
                "enrichment_available": enrichment_possible,
            }
        )

    summary = {
        "object_type": object_type,
        "records_scanned": len(candidates),
        "missing_email": missing_email,
        "missing_mobile": missing_mobile,
        "enrichable": enrichable_count,
    }

    return summary, detail_rows


def _aggregate_summary(
    per_object_summary: Dict[str, Dict[str, Any]],
    started_at: datetime,
    completed_at: datetime,
) -> Dict[str, Any]:
    """Aggregate object summaries into a dashboard-friendly payload."""
    total_records = sum(
        obj.get("records_scanned", 0) for obj in per_object_summary.values()
    )
    total_missing_email = sum(
        obj.get("missing_email", 0) for obj in per_object_summary.values()
    )
    total_missing_mobile = sum(
        obj.get("missing_mobile", 0) for obj in per_object_summary.values()
    )
    total_enrichable = sum(
        obj.get("enrichable", 0) for obj in per_object_summary.values()
    )

    return {
        "objects": per_object_summary,
        "total_records": total_records,
        "total_missing_email": total_missing_email,
        "total_missing_mobile": total_missing_mobile,
        "enrichable_records": total_enrichable,
        "scan_started_at": started_at.isoformat(),
        "scan_completed_at": completed_at.isoformat(),
    }


def _display_field_name(logical_field: str) -> str:
    """Convert logical field identifiers to display labels for the UI."""
    mapping = {
        "email": "Email",
        "phone": "Phone",
        "mobile_phone": "MobilePhone",
        "title": "Title",
        "linkedin_url": "LinkedIn",
    }
    return mapping.get(logical_field, logical_field.title())


async def _load_enrichment_hits(
    db,
    account_id: str,
    object_type: str,
    scanner: MissingFieldScanner,
    candidates: Sequence[MissingFieldCandidate],
) -> Dict[str, set]:
    """
    Load enrichment availability from the field-level vault.

    Returns:
        {record_id: {field_name, ...}} for fields we already hold enriched values for.
    """
    record_ids = [candidate.sfdc_id for candidate in candidates]
    if not record_ids:
        return {}

    record_fields: Dict[str, set] = {}
    field_whitelist: set = set()

    for candidate in candidates:
        fields = set()
        for logical_field in candidate.missing_fields:
            try:
                sf_field = scanner._get_field_name(object_type, logical_field)  # noqa: SLF001
            except Exception:
                sf_field = None
            if not sf_field or "." in sf_field:
                continue
            fields.add(sf_field)
            field_whitelist.add(sf_field)
        if fields:
            record_fields[candidate.sfdc_id] = fields

    if not field_whitelist:
        return {}

    stmt = (
        select(
            EnrichmentVault.sfdc_record_id,
            EnrichmentVault.field_name,
            EnrichmentVault.value_after,
        )
        .where(EnrichmentVault.account_id == account_id)
        .where(EnrichmentVault.sfdc_object == object_type)
        .where(EnrichmentVault.sfdc_record_id.in_(record_ids))
        .where(EnrichmentVault.field_name.in_(list(field_whitelist)))
    )

    result = await db.execute(stmt)

    hits: Dict[str, set] = {}
    for record_id, field_name, value_after in result:
        if not value_after or not str(value_after).strip():
            continue
        if record_id not in record_fields:
            continue
        if field_name not in record_fields[record_id]:
            continue
        hits.setdefault(record_id, set()).add(field_name)

    return hits
