from __future__ import annotations

from dataclasses import dataclass
from datetime import date, datetime, timedelta, timezone
from typing import Dict, List, Mapping, Optional, Sequence, Tuple

from sqlalchemy import bindparam, case, func, select, text
from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.sql import Select

from ..models import SaaSEvent, SaaSEventStatus


@dataclass
class SaaSEventSummary:
    org_id: str
    total_events: int
    failures: int
    credits_used: int
    last_event_at: Optional[datetime]
    top_actions: List[Tuple[str, int]]


@dataclass
class SaaSEventFailure:
    id: str
    ts: datetime
    action: str
    status: SaaSEventStatus
    sheet_id: str
    tab_name: Optional[str]
    user_email: str
    source_version: Optional[str]
    attrs: Mapping[str, object]


@dataclass
class SaaSEventOverviewKPIs:
    events_24h: int
    error_rate: float
    unique_orgs: int
    p95_build_duration_ms: Optional[float]


@dataclass
class SaaSEventOverviewDay:
    date: str
    started: int
    success: int
    failed: int


@dataclass
class SaaSEventOverviewFeature:
    feature: str
    count: int


@dataclass
class SaaSEventOverviewFailure:
    ts: datetime
    feature: Optional[str]
    action: str
    error_code: Optional[str]
    duration_ms: Optional[int]
    org_id: str


@dataclass
class SaaSEventOverview:
    kpis: SaaSEventOverviewKPIs
    events_by_day: List[SaaSEventOverviewDay]
    top_features: List[SaaSEventOverviewFeature]
    recent_failures: List[SaaSEventOverviewFailure]


@dataclass
class SaaSEventLogRow:
    id: str
    ts: datetime
    org_id: str
    action: str
    status: str
    feature: Optional[str]
    error_code: Optional[str]
    error_stage: Optional[str]
    error_hint: Optional[str]
    duration_ms: Optional[int]
    rows: Optional[int]
    source_version: Optional[str]


class SaaSEventRepo:
    """Persistence helpers for SaaS activity event logging."""

    def __init__(self, session: AsyncSession):
        self.session = session

    async def _existing_by_org_idem(
        self, org_id: str, keys: Sequence[str]
    ) -> Dict[str, SaaSEvent]:
        if not keys:
            return {}

        stmt = (
            select(SaaSEvent)
            .where(SaaSEvent.org_id == org_id)
            .where(SaaSEvent.idempotency_key.in_(keys))
        )
        result = await self.session.execute(stmt)
        rows = result.scalars().all()
        return {row.idempotency_key: row for row in rows}

    async def _existing_map(
        self, events: Sequence[SaaSEvent]
    ) -> Dict[Tuple[str, str], SaaSEvent]:
        lookup: Dict[Tuple[str, str], SaaSEvent] = {}
        by_org: Dict[str, List[str]] = {}
        for event in events:
            # De-dupe keys within the batch to avoid duplicate fetches
            key = (event.org_id, event.idempotency_key)
            if key in lookup:
                continue
            by_org.setdefault(event.org_id, []).append(event.idempotency_key)

        for org_id, keys in by_org.items():
            existing = await self._existing_by_org_idem(org_id, keys)
            for idem_key, row in existing.items():
                lookup[(org_id, idem_key)] = row

        return lookup

    async def record_batch(self, events: Sequence[SaaSEvent]) -> Tuple[int, int]:
        """Insert events, skipping duplicates by (org_id, idempotency_key)."""
        if not events:
            return 0, 0

        existing = await self._existing_map(events)
        to_insert = [
            event
            for event in events
            if (event.org_id, event.idempotency_key) not in existing
        ]

        inserted = 0
        if to_insert:
            try:
                self.session.add_all(to_insert)
                await self.session.commit()
                inserted = len(to_insert)
            except IntegrityError:
                # Concurrent insert — refresh duplicate map and treat as dedupe.
                await self.session.rollback()
                existing = await self._existing_map(events)
                inserted = 0
        return inserted, len(events) - inserted

    async def summary(
        self,
        org_id: str,
        *,
        since: Optional[datetime] = None,
        actions_limit: int = 5,
    ) -> SaaSEventSummary:
        filters = [SaaSEvent.org_id == org_id]
        if since is not None:
            filters.append(SaaSEvent.ts >= since)

        total_stmt = select(func.count()).where(*filters)
        failures_stmt = select(func.count()).where(
            *filters, SaaSEvent.status == SaaSEventStatus.FAILED
        )
        credits_stmt = select(
            func.coalesce(
                func.sum(
                    case(
                        (
                            SaaSEvent.action.like("enrichment_%"),
                            SaaSEvent.credits_delta,
                        ),
                        else_=0,
                    )
                ),
                0,
            )
        ).where(*filters)
        last_event_stmt = select(func.max(SaaSEvent.ts)).where(*filters)
        top_actions_stmt: Select = (
            select(SaaSEvent.action, func.count().label("count"))
            .where(*filters)
            .group_by(SaaSEvent.action)
            .order_by(func.count().desc())
            .limit(actions_limit)
        )

        total = (await self.session.execute(total_stmt)).scalar_one()
        failures = (await self.session.execute(failures_stmt)).scalar_one()
        credits = (await self.session.execute(credits_stmt)).scalar_one() or 0
        last_event = (await self.session.execute(last_event_stmt)).scalar_one_or_none()
        top_actions_rows = await self.session.execute(top_actions_stmt)
        top_actions = [(row[0], int(row[1])) for row in top_actions_rows.all()]

        return SaaSEventSummary(
            org_id=org_id,
            total_events=int(total or 0),
            failures=int(failures or 0),
            credits_used=int(credits or 0),
            last_event_at=last_event,
            top_actions=top_actions,
        )

    async def recent_failures(
        self,
        org_id: str,
        *,
        since: Optional[datetime] = None,
        action: Optional[str] = None,
        limit: int = 50,
    ) -> List[SaaSEventFailure]:
        filters = [
            SaaSEvent.org_id == org_id,
            SaaSEvent.status == SaaSEventStatus.FAILED,
        ]
        if since is not None:
            filters.append(SaaSEvent.ts >= since)
        if action:
            filters.append(SaaSEvent.action == action)

        stmt = (
            select(SaaSEvent).where(*filters).order_by(SaaSEvent.ts.desc()).limit(limit)
        )
        rows = await self.session.execute(stmt)
        events = rows.scalars().all()
        return [
            SaaSEventFailure(
                id=str(event.id),
                ts=event.ts,
                action=event.action,
                status=event.status,
                sheet_id=event.sheet_id,
                tab_name=event.tab_name,
                user_email=event.user_email,
                source_version=event.source_version,
                attrs=event.attrs or {},
            )
            for event in events
        ]

    async def list_events(
        self,
        *,
        since: datetime,
        until: datetime,
        feature: Optional[str] = None,
        action: Optional[str] = None,
        error_code: Optional[str] = None,
        status: Optional[List[str]] = None,
        org_id: Optional[str] = None,
        source_version: Optional[str] = None,
        limit: int = 100,
        offset: int = 0,
    ) -> Tuple[List[SaaSEventLogRow], int]:
        dialect = getattr(self.session.bind, "dialect", None)
        dialect_name = dialect.name if dialect else ""
        is_sqlite = dialect_name == "sqlite"

        feature_expr = (
            "json_extract(attrs, '$.feature')" if is_sqlite else "attrs->>'feature'"
        )
        error_expr = (
            "json_extract(attrs, '$.error_code')" if is_sqlite else "attrs->>'error_code'"
        )
        stage_expr = (
            "json_extract(attrs, '$.error_stage')" if is_sqlite else "attrs->>'error_stage'"
        )
        hint_expr = (
            "json_extract(attrs, '$.error_hint')" if is_sqlite else "attrs->>'error_hint'"
        )

        where_clauses = ["ts BETWEEN :since AND :until"]
        params: Dict[str, object] = {
            "since": since,
            "until": until,
            "limit": limit,
            "offset": offset,
        }

        if feature:
            where_clauses.append(f"{feature_expr} = :feature")
            params["feature"] = feature
        if action:
            where_clauses.append("action = :action")
            params["action"] = action
        if error_code:
            where_clauses.append(f"{error_expr} = :error_code")
            params["error_code"] = error_code
        if org_id:
            where_clauses.append("org_id = :org_id")
            params["org_id"] = org_id
        if source_version:
            where_clauses.append("source_version = :source_version")
            params["source_version"] = source_version
        if status:
            where_clauses.append("status IN :status")
            params["status"] = status

        where_sql = " AND ".join(where_clauses)
        count_stmt = text(f"SELECT COUNT(*) FROM saas_events WHERE {where_sql}")
        event_stmt = text(
            f"""
            SELECT
              id,
              ts,
              org_id,
              action,
              status,
              duration_ms,
              rows,
              source_version,
              {feature_expr} AS feature,
              {error_expr} AS error_code,
              {stage_expr} AS error_stage,
              {hint_expr} AS error_hint
            FROM saas_events
            WHERE {where_sql}
            ORDER BY ts DESC
            LIMIT :limit OFFSET :offset
            """
        )
        if status:
            count_stmt = count_stmt.bindparams(bindparam("status", expanding=True))
            event_stmt = event_stmt.bindparams(bindparam("status", expanding=True))

        total = int((await self.session.execute(count_stmt, params)).scalar() or 0)
        rows = (await self.session.execute(event_stmt, params)).all()
        events = [
            SaaSEventLogRow(
                id=str(row[0]),
                ts=row[1],
                org_id=row[2],
                action=row[3],
                status=row[4],
                duration_ms=row[5],
                rows=row[6],
                source_version=row[7],
                feature=row[8],
                error_code=row[9],
                error_stage=row[10],
                error_hint=row[11],
            )
            for row in rows
        ]
        return events, total

    async def daily_rollup(
        self,
        org_id: str,
        *,
        days: int = 30,
    ) -> List[Mapping[str, object]]:
        """Return per-day aggregates (credits and failures)."""
        days = max(1, days)
        start = datetime.utcnow() - timedelta(days=days - 1)

        # Try materialized view first (Postgres deployments).
        dialect = getattr(self.session.bind, "dialect", None)
        rows: Optional[List[Mapping[str, object]]] = None

        if dialect and dialect.name == "postgresql":
            try:
                result = await self.session.execute(
                    text(
                        """
                        SELECT day, events, credits_used, failures
                        FROM saas_event_daily
                        WHERE org_id = :org_id
                          AND day >= DATE_TRUNC('day', :start)
                        ORDER BY day
                        """
                    ),
                    {"org_id": org_id, "start": start},
                )
                rows = result.mappings().all()
            except Exception:
                rows = None

        if rows is None:
            # Fallback aggregation from base table (SQLite/dev environments).
            day_expr = (
                func.date(SaaSEvent.ts)
                if not dialect or dialect.name == "sqlite"
                else func.date_trunc("day", SaaSEvent.ts)
            )

            stmt = (
                select(
                    day_expr.label("day"),
                    func.count().label("events"),
                    func.coalesce(
                        func.sum(
                            case(
                                (
                                    SaaSEvent.action.like("enrichment_%"),
                                    SaaSEvent.credits_delta,
                                ),
                                else_=0,
                            )
                        ),
                        0,
                    ).label("credits_used"),
                    func.sum(
                        case(
                            (SaaSEvent.status == SaaSEventStatus.FAILED, 1),
                            else_=0,
                        )
                    ).label("failures"),
                )
                .where(SaaSEvent.org_id == org_id)
                .where(SaaSEvent.ts >= start)
                .group_by(day_expr)
                .order_by(day_expr)
            )
            result = await self.session.execute(stmt)
            rows = result.mappings().all()

        return [
            {
                "day": row["day"],
                "events": int(row["events"] or 0),
                "credits_used": int(row["credits_used"] or 0),
                "failures": int(row["failures"] or 0),
            }
            for row in rows
        ]

    async def overview_stats(
        self,
        *,
        since: datetime,
        until: datetime,
        features: Optional[List[str]] = None,
        status: Optional[List[str]] = None,
        limit: int = 25,
    ) -> SaaSEventOverview:
        dialect = getattr(self.session.bind, "dialect", None)
        dialect_name = dialect.name if dialect else ""
        is_postgres = dialect_name == "postgresql"
        is_sqlite = dialect_name == "sqlite"

        feature_expr = "json_extract(attrs, '$.feature')" if is_sqlite else "attrs->>'feature'"
        error_expr = "json_extract(attrs, '$.error_code')" if is_sqlite else "attrs->>'error_code'"

        where_clauses = ["ts BETWEEN :since AND :until"]
        params: Dict[str, object] = {"since": since, "until": until}

        if features:
            where_clauses.append(f"{feature_expr} IN :features")
            params["features"] = features
        if status:
            where_clauses.append("status IN :status")
            params["status"] = status

        where_sql = " AND ".join(where_clauses)

        kpi_params = dict(params)
        kpi_params["since_24h"] = datetime.now(timezone.utc) - timedelta(hours=24)

        if is_postgres:
            kpi_stmt = text(
                f"""
                SELECT
                  COUNT(*) FILTER (WHERE ts > :since_24h) AS events_24h,
                  COUNT(*) FILTER (WHERE status = 'failed')::float / NULLIF(COUNT(*), 0) AS error_rate,
                  COUNT(DISTINCT org_id) AS unique_orgs,
                  PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY duration_ms)
                    FILTER (WHERE action LIKE '%_build_completed' AND status = 'success') AS p95_build_duration_ms
                FROM saas_events
                WHERE {where_sql}
                """
            )
        else:
            kpi_stmt = text(
                f"""
                SELECT
                  SUM(CASE WHEN ts > :since_24h THEN 1 ELSE 0 END) AS events_24h,
                  SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) * 1.0 / NULLIF(COUNT(*), 0) AS error_rate,
                  COUNT(DISTINCT org_id) AS unique_orgs
                FROM saas_events
                WHERE {where_sql}
                """
            )

        if features:
            kpi_stmt = kpi_stmt.bindparams(bindparam("features", expanding=True))
        if status:
            kpi_stmt = kpi_stmt.bindparams(bindparam("status", expanding=True))

        kpi_row = (await self.session.execute(kpi_stmt, kpi_params)).mappings().first() or {}
        p95_build = kpi_row.get("p95_build_duration_ms")

        if not is_postgres:
            p95_build = await self._compute_p95_build_duration(
                where_sql, params, features=features, status=status
            )

        kpis = SaaSEventOverviewKPIs(
            events_24h=int(kpi_row.get("events_24h") or 0),
            error_rate=float(kpi_row.get("error_rate") or 0),
            unique_orgs=int(kpi_row.get("unique_orgs") or 0),
            p95_build_duration_ms=float(p95_build) if p95_build is not None else None,
        )

        count_started = "COUNT(*) FILTER (WHERE status = 'started')" if is_postgres else "SUM(CASE WHEN status = 'started' THEN 1 ELSE 0 END)"
        count_success = "COUNT(*) FILTER (WHERE status = 'success')" if is_postgres else "SUM(CASE WHEN status = 'success' THEN 1 ELSE 0 END)"
        count_failed = "COUNT(*) FILTER (WHERE status = 'failed')" if is_postgres else "SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END)"

        events_stmt = text(
            f"""
            SELECT
              DATE(ts) AS date,
              {count_started} AS started,
              {count_success} AS success,
              {count_failed} AS failed
            FROM saas_events
            WHERE {where_sql}
            GROUP BY DATE(ts)
            ORDER BY DATE(ts)
            """
        )
        if features:
            events_stmt = events_stmt.bindparams(bindparam("features", expanding=True))
        if status:
            events_stmt = events_stmt.bindparams(bindparam("status", expanding=True))
        events_rows = (await self.session.execute(events_stmt, params)).mappings().all()

        events_by_day = [
            SaaSEventOverviewDay(
                date=_coerce_date(row.get("date")),
                started=int(row.get("started") or 0),
                success=int(row.get("success") or 0),
                failed=int(row.get("failed") or 0),
            )
            for row in events_rows
        ]

        features_stmt = text(
            f"""
            SELECT
              {feature_expr} AS feature,
              COUNT(*) AS count
            FROM saas_events
            WHERE {where_sql}
              AND {feature_expr} IS NOT NULL
            GROUP BY {feature_expr}
            ORDER BY count DESC
            LIMIT 10
            """
        )
        if features:
            features_stmt = features_stmt.bindparams(bindparam("features", expanding=True))
        if status:
            features_stmt = features_stmt.bindparams(bindparam("status", expanding=True))
        feature_rows = (await self.session.execute(features_stmt, params)).mappings().all()

        top_features = [
            SaaSEventOverviewFeature(feature=str(row.get("feature")), count=int(row.get("count") or 0))
            for row in feature_rows
            if row.get("feature") is not None
        ]

        failures_stmt = text(
            f"""
            SELECT
              ts,
              {feature_expr} AS feature,
              action,
              {error_expr} AS error_code,
              duration_ms,
              org_id
            FROM saas_events
            WHERE {where_sql}
              AND status = 'failed'
            ORDER BY ts DESC
            LIMIT :limit
            """
        )
        failures_params = dict(params)
        failures_params["limit"] = limit
        if features:
            failures_stmt = failures_stmt.bindparams(bindparam("features", expanding=True))
        if status:
            failures_stmt = failures_stmt.bindparams(bindparam("status", expanding=True))

        failures_rows = (await self.session.execute(failures_stmt, failures_params)).mappings().all()
        recent_failures = [
            SaaSEventOverviewFailure(
                ts=row.get("ts"),
                feature=row.get("feature"),
                action=row.get("action"),
                error_code=row.get("error_code"),
                duration_ms=row.get("duration_ms"),
                org_id=row.get("org_id"),
            )
            for row in failures_rows
        ]

        return SaaSEventOverview(
            kpis=kpis,
            events_by_day=events_by_day,
            top_features=top_features,
            recent_failures=recent_failures,
        )

    async def _compute_p95_build_duration(
        self,
        where_sql: str,
        params: Mapping[str, object],
        *,
        features: Optional[List[str]] = None,
        status: Optional[List[str]] = None,
    ) -> Optional[float]:
        durations_stmt = text(
            f"""
            SELECT duration_ms
            FROM saas_events
            WHERE {where_sql}
              AND action LIKE '%_build_completed'
              AND status = 'success'
              AND duration_ms IS NOT NULL
            """
        )
        if features:
            durations_stmt = durations_stmt.bindparams(bindparam("features", expanding=True))
        if status:
            durations_stmt = durations_stmt.bindparams(bindparam("status", expanding=True))
        rows = (await self.session.execute(durations_stmt, params)).fetchall()
        durations = [row[0] for row in rows if row[0] is not None]
        return _percentile_cont(durations, 0.95)


def _coerce_date(value: object) -> str:
    if isinstance(value, datetime):
        return value.date().isoformat()
    if isinstance(value, date):
        return value.isoformat()
    return str(value) if value is not None else ""


def _percentile_cont(values: Sequence[float], percentile: float) -> Optional[float]:
    if not values:
        return None
    if percentile <= 0:
        return float(values[0])
    if percentile >= 1:
        return float(values[-1])

    sorted_vals = sorted(float(v) for v in values)
    n = len(sorted_vals)
    if n == 1:
        return sorted_vals[0]

    rank = (n - 1) * percentile
    low = int(rank)
    high = min(low + 1, n - 1)
    if low == high:
        return sorted_vals[low]
    weight = rank - low
    return sorted_vals[low] + (sorted_vals[high] - sorted_vals[low]) * weight
