"""
Async worker that processes FullEnrich webhook queue items and updates
BYO enrichment job status/results.

Usage:
  python -m fmatch.saas.workers.fullenrich_webhook_worker
"""

from __future__ import annotations

import asyncio
import json
import logging
import signal
from dataclasses import dataclass
from datetime import datetime
from typing import Optional
from uuid import UUID

import redis.asyncio as redis
from sqlalchemy import select

from fmatch.saas import settings
from fmatch.saas.database import session_context
from fmatch.saas.model_defs.webhook_models import InboundWebhook
from fmatch.saas.models import BYOEnrichmentJob, EnrichmentJobStatus
from fmatch.saas.services.enrichment_adapters import (
    ContactEnrichmentInput,
    EnrichmentStatus,
    FullEnrichAdapter,
)
from fmatch.saas.services.email_notifications import send_enrichment_complete_notification
from fmatch.saas.services.sheets_writer import SheetsWriter
from fmatch.core.fullenrich import constants as fe_constants

logger = logging.getLogger(__name__)

FULLENRICH_QUEUE = "webhook:fullenrich:queue"
FULLENRICH_DLQ = "webhook:fullenrich:dlq"


@dataclass
class QueueItem:
    id: str
    workspace_id: Optional[str] = None

    @staticmethod
    def from_json(payload: str) -> "QueueItem":
        data = json.loads(payload)
        return QueueItem(
            id=str(data.get("id")),
            workspace_id=data.get("workspace_id"),
        )


class FullEnrichWebhookWorker:
    def __init__(
        self,
        redis_url: Optional[str] = None,
        concurrency: int = 5,
        manage_signals: bool = True,
    ):
        self.redis_url = redis_url or settings.REDIS_URL
        self.concurrency = max(concurrency, 1)
        self.manage_signals = manage_signals
        self.redis: Optional[redis.Redis] = None
        self.running = False
        self._shutdown = asyncio.Event()
        self._semaphore = asyncio.Semaphore(self.concurrency)

    async def start(self) -> None:
        logger.info(
            "Starting FullEnrichWebhookWorker (queue=%s, concurrency=%d)",
            FULLENRICH_QUEUE,
            self.concurrency,
        )
        if self.manage_signals:
            try:
                signal.signal(signal.SIGINT, self._on_signal)
                signal.signal(signal.SIGTERM, self._on_signal)
            except Exception:
                pass

        self.redis = await redis.from_url(
            self.redis_url, encoding="utf-8", decode_responses=True
        )
        self.running = True

        workers = [
            asyncio.create_task(self._poll_loop()) for _ in range(self.concurrency)
        ]
        try:
            await self._shutdown.wait()
        finally:
            self.running = False
            for t in workers:
                t.cancel()
            await asyncio.gather(*workers, return_exceptions=True)
            if self.redis:
                await self.redis.aclose()
            logger.info("FullEnrichWebhookWorker stopped")

    def _on_signal(self, signum, frame):
        logger.info("Received signal %s; shutting down FullEnrich worker...", signum)
        self._shutdown.set()

    def stop(self) -> None:
        self._shutdown.set()

    async def _poll_loop(self) -> None:
        assert self.redis is not None
        while self.running:
            try:
                item = await self.redis.brpop(FULLENRICH_QUEUE, timeout=5)
                if not item:
                    continue
                _, value = item
                try:
                    inbound = QueueItem.from_json(value)
                except Exception as exc:
                    logger.error("Dropping invalid FullEnrich queue payload: %s", exc)
                    continue

                async with self._semaphore:
                    await self._process(inbound)
            except asyncio.CancelledError:
                break
            except Exception as exc:
                logger.error("FullEnrich worker loop error: %s", exc)
                await asyncio.sleep(1)

    async def _process(self, inbound: QueueItem) -> None:
        async with session_context() as session:
            result = await session.execute(
                select(InboundWebhook).where(InboundWebhook.id == inbound.id)
            )
            webhook = result.scalar_one_or_none()
            if not webhook:
                logger.error("Inbound webhook %s not found", inbound.id)
                return

            webhook.status = "processing"
            webhook.processed_at = datetime.utcnow()
            await session.flush()

            try:
                payload = webhook.payload or {}
                status_raw = str(payload.get("status") or "").upper()
                raw_candidates = payload.get("data") or payload.get("datas") or payload.get("contacts") or []

                job_id = None
                run_id = None
                for entry in raw_candidates:
                    custom = entry.get("custom") or {}
                    if not job_id and custom.get("job_id"):
                        job_id = custom.get("job_id")
                    if not run_id and custom.get("run_id"):
                        run_id = custom.get("run_id")

                job = None
                if job_id:
                    try:
                        job_uuid = UUID(str(job_id))
                    except Exception:
                        job_uuid = None
                    if job_uuid:
                        job_result = await session.execute(
                            select(BYOEnrichmentJob).where(
                                BYOEnrichmentJob.id == job_uuid,
                                BYOEnrichmentJob.workspace_id == (inbound.workspace_id or webhook.account_id),
                            )
                        )
                        job = job_result.scalar_one_or_none()

                if not job and run_id and (inbound.workspace_id or webhook.account_id):
                    job_result = await session.execute(
                        select(BYOEnrichmentJob).where(
                            BYOEnrichmentJob.workspace_id == (inbound.workspace_id or webhook.account_id),
                            BYOEnrichmentJob.run_id == str(run_id),
                        )
                    )
                    job = job_result.scalar_one_or_none()

                if not job:
                    raise ValueError("Unable to resolve enrichment job for webhook payload")

                summary = dict(job.summary or {})
                api_version = summary.get("fullenrich_api_version")
                if not api_version:
                    for entry in raw_candidates:
                        custom = entry.get("custom") or {}
                        if custom.get("api_version"):
                            api_version = custom.get("api_version")
                            break
                if not api_version:
                    logger.warning("FullEnrich webhook missing api_version; defaulting to v1.")
                    api_version = fe_constants.FullEnrichAPIVersion.V1.value

                if api_version == fe_constants.FullEnrichAPIVersion.V2.value:
                    raw_contacts = payload.get("data") or []
                else:
                    raw_contacts = payload.get("datas") or []

                adapter = FullEnrichAdapter(api_key="webhook", api_version=api_version)
                existing_results = summary.get("results") or []
                skipped_results = [
                    r for r in existing_results if r.get("status") == EnrichmentStatus.SKIPPED.value
                ]
                results_by_index = {}
                for row in existing_results:
                    if row.get("status") == EnrichmentStatus.SKIPPED.value:
                        continue
                    idx = row.get("row_index")
                    try:
                        idx = int(idx) if idx is not None else None
                    except (TypeError, ValueError):
                        idx = None
                    if idx is None:
                        continue
                    results_by_index[idx] = dict(row)

                for entry in raw_contacts:
                    custom = entry.get("custom") or {}
                    row_index = custom.get("row_index")
                    try:
                        row_index = int(row_index)
                    except (TypeError, ValueError):
                        row_index = None
                    if row_index is None:
                        continue
                    fallback = ContactEnrichmentInput(
                        first_name=custom.get("first_name") or custom.get("firstname"),
                        last_name=custom.get("last_name") or custom.get("lastname"),
                        domain=custom.get("domain") or custom.get("company_domain"),
                        company_name=custom.get("company_name") or custom.get("company"),
                        email=custom.get("email"),
                        linkedin_url=custom.get("linkedin_url"),
                        row_index=row_index,
                        custom=custom,
                    )
                    parsed = adapter._parse_contact_payload(entry, fallback, api_version=api_version)
                    results_by_index[row_index] = parsed.to_dict()

                submitted_rows = summary.get("submitted_rows") or []
                is_pending = status_raw in fe_constants.JOB_PENDING_STATUSES or not status_raw
                if not is_pending:
                    for row in submitted_rows:
                        idx = row.get("row_index")
                        try:
                            idx = int(idx) if idx is not None else None
                        except (TypeError, ValueError):
                            idx = None
                        if idx is None or idx in results_by_index:
                            continue
                        results_by_index[idx] = {
                            "row_index": idx,
                            "status": EnrichmentStatus.NOT_FOUND.value,
                            "provider": "fullenrich",
                            "first_name": row.get("first_name"),
                            "last_name": row.get("last_name"),
                            "firstname": row.get("firstname") or row.get("first_name"),
                            "lastname": row.get("lastname") or row.get("last_name"),
                            "domain": row.get("domain"),
                            "company_name": row.get("company_name"),
                            "email": row.get("email"),
                            "linkedin_url": row.get("linkedin_url"),
                        }

                results = skipped_results + list(results_by_index.values())
                rows_enriched = sum(
                    1 for r in results_by_index.values() if r.get("status") == EnrichmentStatus.ENRICHED.value
                )
                rows_errored = sum(
                    1 for r in results_by_index.values() if r.get("status") == EnrichmentStatus.ERROR.value
                )
                rows_skipped = len(skipped_results)
                rows_processed = rows_skipped + len(results_by_index)
                credits_consumed = rows_enriched

                errors = []
                for r in results_by_index.values():
                    if r.get("status") == EnrichmentStatus.ERROR.value and r.get("error_message"):
                        errors.append({"row_index": r.get("row_index"), "error": r.get("error_message")})
                    if len(errors) >= 5:
                        break

                summary.update(
                    {
                        "provider_breakdown": {"fullenrich": rows_enriched},
                        "top_errors": errors,
                        "results": results,
                        "fullenrich_api_version": api_version,
                        "fullenrich_status": status_raw or summary.get("fullenrich_status"),
                        "fullenrich_enrichment_id": payload.get("enrichment_id") or payload.get("id")
                        or summary.get("fullenrich_enrichment_id"),
                    }
                )

                job.rows_processed = rows_processed
                job.rows_enriched = rows_enriched
                job.rows_skipped = rows_skipped
                job.rows_errored = rows_errored
                job.credits_consumed = credits_consumed

                if is_pending:
                    job.status = EnrichmentJobStatus.RUNNING.value
                    job.summary = summary
                    await session.commit()
                    webhook.status = "processed"
                    await session.commit()
                    return

                if status_raw in fe_constants.JOB_FAILURE_STATUSES:
                    job.status = EnrichmentJobStatus.FAILED.value
                    job.error_message = f"FullEnrich status: {status_raw}"
                    job.completed_at = datetime.utcnow()
                else:
                    job.status = EnrichmentJobStatus.COMPLETED.value
                    job.completed_at = datetime.utcnow()

                job.rows_processed = rows_processed
                job.rows_enriched = rows_enriched
                job.rows_skipped = rows_skipped
                job.rows_errored = rows_errored
                job.credits_consumed = credits_consumed
                job.summary = summary
                await session.commit()

                webhook.status = "processed"
                await session.commit()
                logger.info("FullEnrich webhook processed for job %s", job.id)

                if job.status == EnrichmentJobStatus.COMPLETED.value and self.redis:
                    try:
                        meta_key = f"byo_enrichment_meta:{job.id}"
                        meta_raw = await self.redis.get(meta_key)
                        meta = json.loads(meta_raw) if meta_raw else {}
                        sheet_id = meta.get("sheet_id")
                        sheet_name = meta.get("sheet_name")
                        if sheet_id and sheet_name:
                            def _sanitize_results(values):
                                cleaned = []
                                for value in values or []:
                                    if not isinstance(value, dict):
                                        continue
                                    sanitized = dict(value)
                                    sanitized.pop("raw", None)
                                    sanitized.pop("custom", None)
                                    cleaned.append(sanitized)
                                return cleaned

                            config = {
                                "output_fields": job.output_fields or [],
                                "enrichment_type": job.enrichment_type,
                                "force_reenrich": bool(meta.get("force_reenrich")),
                                "sheet_name": sheet_name,
                            }
                            writer = SheetsWriter(
                                sheet_id=sheet_id,
                                account_id=str(job.workspace_id),
                                api_key="",
                            )
                            await writer.store_enrichment_results(
                                sheet_name=sheet_name,
                                results=_sanitize_results(results),
                                config=config,
                                metadata={"job_id": str(job.id)},
                            )
                    except Exception as write_err:
                        logger.warning("Failed to store scheduled results for job %s: %s", job.id, write_err)

                # Send email notification if configured
                if job.notify_email and job.status == EnrichmentJobStatus.COMPLETED.value:
                    if summary.get("notification_sent"):
                        logger.info("Completion notification already sent for job %s", job.id)
                    else:
                        try:
                            await send_enrichment_complete_notification(
                                user_email=job.notify_email,
                                job_name=f"Contact Enrichment ({job.row_count} contacts)",
                                job_id=str(job.id),
                                total_contacts=job.row_count,
                                enriched_count=rows_enriched,
                                not_found_count=rows_processed - rows_enriched - rows_errored,
                                error_count=rows_errored,
                                spreadsheet_url=job.spreadsheet_url,
                            )
                            summary["notification_sent"] = True
                            job.summary = summary
                            await session.commit()
                        except Exception as notify_err:
                            logger.warning("Failed to send completion notification: %s", notify_err)
            except Exception as exc:
                logger.error("FullEnrich webhook processing failed: %s", exc)
                webhook.status = "failed"
                await session.commit()
                try:
                    assert self.redis is not None
                    await self.redis.zadd(
                        FULLENRICH_DLQ, {inbound.id: datetime.utcnow().timestamp()}
                    )
                    await self.redis.hset(
                        f"webhook:fullenrich:dlq:{inbound.id}",
                        mapping={
                            "error": str(exc),
                            "at": datetime.utcnow().isoformat(),
                        },
                    )
                except Exception:
                    pass


async def run_worker():
    logging.basicConfig(
        level=logging.INFO,
        format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
    )
    worker = FullEnrichWebhookWorker(
        concurrency=int(settings.WEBHOOK_WORKER_CONCURRENCY or 5)
    )
    await worker.start()


if __name__ == "__main__":
    asyncio.run(run_worker())
