# src/fmatch/saas/services/reporting_emitter.py
from __future__ import annotations
from typing import Any, Dict, List, Optional
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import text, insert, select, update
from datetime import datetime
import asyncio
import uuid
import zlib
import base64
import hashlib
from ..model_defs.reporting_models import ChangeEvent, SequenceCounter


def _partition_key(dt: datetime) -> str:
    # ISO week partition (YYYY-WW)
    iso_year, iso_week, _ = dt.isocalendar()
    return f"{iso_year}-{iso_week:02d}"


def _compress(v: Any) -> Optional[str]:
    if v is None:
        return None
    s = v if isinstance(v, str) else str(v)
    if len(s) <= 1000:
        return s
    b = zlib.compress(s.encode("utf-8"))
    return "COMPRESSED:" + base64.b64encode(b).decode("ascii")


def decompress(v: Optional[str]) -> Optional[str]:
    """Decompress a value if it was compressed"""
    if v is None or not v.startswith("COMPRESSED:"):
        return v
    try:
        b64_data = v[11:]  # Skip "COMPRESSED:" prefix
        compressed = base64.b64decode(b64_data)
        return zlib.decompress(compressed).decode("utf-8")
    except Exception:
        return v  # Return as-is if decompression fails


def _sha256(payload: Dict[str, Any]) -> str:
    raw = "|".join(
        str(payload.get(k, ""))
        for k in (
            "changeset_id",
            "object_type",
            "object_id",
            "op",
            "field",
            "after_value",
        )
    )
    return hashlib.sha256(raw.encode("utf-8")).hexdigest()


async def _dialect(session: AsyncSession) -> str:
    return session.bind.dialect.name  # 'postgresql' | 'sqlite' | 'mysql'...


class EventEmitter:
    """Batch-friendly event emitter with circuit breaker + cross-dialect sequence allocation."""

    def __init__(self, reset_timeout: int = 60):
        self.failure_count = 0
        self.circuit_open = False
        self.last_failure_time: Optional[datetime] = None
        self.reset_timeout = reset_timeout
        self._lock = asyncio.Lock()

    async def _next_sequence(self, db: AsyncSession, count: int = 1) -> int:
        d = await _dialect(db)
        if d == "postgresql":
            # Prefer a DB sequence if created: CREATE SEQUENCE IF NOT EXISTS change_events_seq
            q = text("SELECT nextval('change_events_seq') AS n")
            res = await db.execute(q)
            base = int(res.scalar_one())
            if count == 1:
                return base
            # For a block, setval to reserve range
            await db.execute(
                text("SELECT setval('change_events_seq', :v)"), {"v": base + count - 1}
            )
            return base
        # Fallback: sequence_counters table (transactionally safe)
        async with self._lock:
            row = (
                await db.execute(
                    select(SequenceCounter).where(
                        SequenceCounter.name == "change_events"
                    )
                )
            ).scalar_one_or_none()
            if not row:
                await db.execute(
                    insert(SequenceCounter).values(name="change_events", value=0)
                )
                await db.flush()
                base = 1
            else:
                base = row.value + 1
            # reserve block
            await db.execute(
                update(SequenceCounter)
                .where(SequenceCounter.name == "change_events")
                .values(value=base + count - 1)
            )
            return base

    async def emit(
        self, db: AsyncSession, account_id: str, event: Dict[str, Any]
    ) -> Optional[str]:
        ids = await self.emit_batch(db, account_id, [event])
        return ids[0] if ids else None

    async def emit_batch(
        self, db: AsyncSession, account_id: str, events: List[Dict[str, Any]]
    ) -> List[str]:
        now = datetime.utcnow()
        # auto-reset circuit after cool-down
        if (
            self.circuit_open
            and self.last_failure_time
            and (now - self.last_failure_time).seconds > self.reset_timeout
        ):
            self.circuit_open, self.failure_count = False, 0

        if self.circuit_open or not events:
            return []

        try:
            base_seq = await self._next_sequence(db, len(events))
            rows = []
            for i, e in enumerate(events):
                eid = e.get("id") or str(uuid.uuid4())
                payload = dict(e)
                payload.update(
                    {
                        "id": eid,
                        "account_id": account_id,
                        "sequence_num": base_seq + i,
                        "partition_key": _partition_key(now),
                        "before_value": _compress(e.get("before_value")),
                        "after_value": _compress(e.get("after_value")),
                    }
                )
                payload["checksum"] = _sha256(payload)
                rows.append(payload)
            await db.execute(insert(ChangeEvent), rows)

            # Check for subscriptions (placeholder for notification fanout)
            for row in rows:
                await self._check_subscriptions(db, row)

            return [r["id"] for r in rows]
        except Exception:
            # open circuit after repeated failures
            self.failure_count += 1
            self.last_failure_time = now
            if self.failure_count >= 5:
                self.circuit_open = True
            return []

    async def _check_subscriptions(self, db: AsyncSession, event: Dict[str, Any]):
        """Check if any users are subscribed to this change (placeholder)"""
        # In production, this would:
        # 1. Query ChangeSubscription for matching object_type/object_id
        # 2. Filter by notify_on containing the event op
        # 3. Queue notifications for matching subscribers
        # For now, just log it
        pass
