"""
Async worker that processes inbound webhook queue items and invokes
IngestionService to normalize and enqueue records for matching.

Usage:
  python -m fmatch.saas.workers.inbound_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

import redis.asyncio as redis

from fmatch.saas import settings
from fmatch.saas.services.ingestion_service import IngestionService

logger = logging.getLogger(__name__)


INBOUND_QUEUE = "webhook:inbound:queue"
INBOUND_DLQ = "webhook:inbound:dlq"


@dataclass
class InboundItem:
    id: str
    account_id: Optional[str] = None
    format: Optional[str] = None
    integration_id: Optional[str] = None

    @staticmethod
    def from_json(s: str) -> "InboundItem":
        try:
            payload = json.loads(s)
            return InboundItem(
                id=str(payload.get("id")),
                account_id=payload.get("account_id"),
                format=payload.get("format"),
                integration_id=payload.get("integration_id"),
            )
        except Exception as e:
            raise ValueError(f"Invalid inbound item: {e}")


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

    async def start(self) -> None:
        logger.info(
            "Starting InboundWebhookWorker (queue=%s, concurrency=%d)",
            INBOUND_QUEUE,
            self.concurrency,
        )
        # Signals
        try:
            signal.signal(signal.SIGINT, self._on_signal)
            signal.signal(signal.SIGTERM, self._on_signal)
        except Exception:
            # Windows/limited envs may not support signals in same way
            pass

        # Redis
        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("InboundWebhookWorker stopped")

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

    async def _poll_loop(self):
        assert self.redis is not None
        while self.running:
            try:
                # BRPOP returns (key, value) or None on timeout
                item = await self.redis.brpop(INBOUND_QUEUE, timeout=5)
                if not item:
                    continue
                _, value = item
                try:
                    inbound = InboundItem.from_json(value)
                except Exception as e:
                    logger.error("Dropping invalid inbound queue payload: %s", e)
                    continue

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

    async def _process(self, inbound: InboundItem) -> None:
        try:
            # IngestionService opens its own session when session=None
            result = await self.ingestion.process_inbound_webhook(inbound.id)
            count = (result or {}).get("records_processed") or (result or {}).get(
                "count"
            )
            logger.info(
                "Inbound processed id=%s format=%s count=%s",
                inbound.id,
                inbound.format,
                count,
            )
        except Exception as e:
            logger.error("Inbound failed id=%s: %s", inbound.id, e)
            # DLQ details
            try:
                assert self.redis is not None
                await self.redis.zadd(
                    INBOUND_DLQ, {inbound.id: datetime.utcnow().timestamp()}
                )
                await self.redis.hset(
                    f"webhook:inbound:dlq:{inbound.id}",
                    mapping={
                        "error": str(e),
                        "format": inbound.format or "",
                        "integration_id": inbound.integration_id or "",
                        "at": datetime.utcnow().isoformat(),
                    },
                )
            except Exception:
                # Best-effort DLQ
                pass


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


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