"""SQLite-backed event bus implementation."""
from __future__ import annotations

import json
import logging
import sqlite3
from datetime import timedelta
from pathlib import Path
from typing import TYPE_CHECKING, Any, Iterable
from uuid import uuid4

from logos_adk.checkpoint_json import JSONObject, ensure_json_object
from logos_adk.persistence import (
    get_sqlite_schema_version,
    set_sqlite_schema_version,
)
from logos_adk.postgres_utils import safe_identifier

from .base import (
    EventBus,
    _clone_record,
    _iso_after,
    _utcnow_iso,
    _validate_topic,
    _validate_lease_owner,
)
from .models import EVENT_BUS_SCHEMA_KIND, EVENT_BUS_SCHEMA_VERSION, EventRecord

if TYPE_CHECKING:
    pass

_logger = logging.getLogger(__name__)


def _row_to_record(row: Any) -> EventRecord:
    """Convert database row to EventRecord."""
    return EventRecord(
        id=str(row["id"]),
        topic=str(row["topic"]),
        payload=json.loads(row["payload_json"]),
        status=str(row["status"]),
        created_at=str(row["created_at"]),
        updated_at=str(row["updated_at"]),
        available_at=str(row["available_at"]),
        campaign_id=row["campaign_id"],
        correlation_id=row["correlation_id"],
        run_id=row["run_id"],
        publisher=row["publisher"],
        lease_owner=row["lease_owner"],
        lease_expires_at=row["lease_expires_at"],
        attempt=int(row["attempt"]),
        dedupe_key=row["dedupe_key"],
        last_error=row["last_error"],
    )


def _normalize_event_timestamp(value: Any) -> str | None:
    """Normalize event timestamp value."""
    if value is None:
        return None
    if isinstance(value, str):
        return value
    return str(value)


class SQLiteEventBus(EventBus):
    """SQLite-backed event bus."""

    def __init__(
        self,
        path: str = ".logos_adk/event_bus.db",
        table_name: str = "events",
        *,
        max_attempts: int | None = None,
        max_queue_depth: int = 0,
    ) -> None:
        """Initialize SQLite event bus.
        
        Args:
            path: Database file path
            table_name: Table name for events
            max_attempts: Maximum retry attempts before dead-lettering
            max_queue_depth: Maximum queue depth (0 = unlimited)
        """
        super().__init__(max_attempts=max_attempts, max_queue_depth=max_queue_depth)
        self.path = path
        self.table_name = safe_identifier(table_name)
        self._schema_ready = False

    def _connect(self) -> sqlite3.Connection:
        """Create database connection."""
        try:
            Path(self.path).parent.mkdir(parents=True, exist_ok=True)
            connection = sqlite3.connect(self.path)
            connection.row_factory = sqlite3.Row
            return connection
        except Exception as exc:
            from logos_adk.errors import StateBackendError
            raise StateBackendError(f"Failed to open SQLite event bus: {exc}") from exc

    def _ensure_schema(self, connection: sqlite3.Connection) -> None:
        """Ensure database schema is up to date."""
        if self._schema_ready:
            return
        connection.execute(
            f"""
            CREATE TABLE IF NOT EXISTS {self.table_name} (
                id TEXT PRIMARY KEY,
                topic TEXT NOT NULL,
                payload_json TEXT NOT NULL,
                status TEXT NOT NULL,
                created_at TEXT NOT NULL,
                updated_at TEXT NOT NULL,
                available_at TEXT NOT NULL,
                campaign_id TEXT,
                correlation_id TEXT,
                run_id TEXT,
                publisher TEXT,
                lease_owner TEXT,
                lease_expires_at TEXT,
                attempt INTEGER NOT NULL DEFAULT 0,
                dedupe_key TEXT,
                last_error TEXT
            )
            """
        )
        connection.execute(
            f"""
            CREATE INDEX IF NOT EXISTS idx_{self.table_name}_claimable
            ON {self.table_name} (topic, status, available_at, lease_expires_at, created_at)
            """
        )
        connection.execute(
            f"""
            CREATE INDEX IF NOT EXISTS idx_{self.table_name}_correlation
            ON {self.table_name} (correlation_id, topic, created_at)
            """
        )
        connection.execute(
            f"""
            CREATE INDEX IF NOT EXISTS idx_{self.table_name}_campaign
            ON {self.table_name} (campaign_id, topic, created_at)
            """
        )
        columns = {
            row["name"]
            for row in connection.execute(f"PRAGMA table_info({self.table_name})").fetchall()
        }
        if "campaign_id" not in columns:
            connection.execute(f"ALTER TABLE {self.table_name} ADD COLUMN campaign_id TEXT")
        recorded_version = get_sqlite_schema_version(connection, EVENT_BUS_SCHEMA_KIND)
        if recorded_version != EVENT_BUS_SCHEMA_VERSION:
            set_sqlite_schema_version(connection, EVENT_BUS_SCHEMA_KIND, EVENT_BUS_SCHEMA_VERSION)
        connection.commit()
        self._schema_ready = True

    def publish(
        self,
        topic: str,
        payload: JSONObject,
        *,
        campaign_id: str | None = None,
        correlation_id: str | None = None,
        run_id: str | None = None,
        publisher: str | None = None,
        dedupe_key: str | None = None,
        available_in_seconds: float = 0.0,
    ) -> EventRecord:
        """Publish an event to the bus."""
        self._check_backpressure()
        topic_name = _validate_topic(topic)
        created_at = _utcnow_iso()
        record = EventRecord(
            id=uuid4().hex,
            topic=topic_name,
            payload=ensure_json_object(payload, path=f"event[{topic_name}].payload"),
            status="queued",
            created_at=created_at,
            updated_at=created_at,
            available_at=_iso_after(available_in_seconds),
            campaign_id=campaign_id,
            correlation_id=correlation_id,
            run_id=run_id,
            publisher=publisher,
            dedupe_key=dedupe_key,
        )
        with self._connect() as connection:
            self._ensure_schema(connection)
            connection.execute(
                f"""
                INSERT INTO {self.table_name} (
                    id, topic, payload_json, status, created_at, updated_at, available_at,
                    campaign_id, correlation_id, run_id, publisher, lease_owner, lease_expires_at,
                    attempt, dedupe_key, last_error
                ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
                """,
                (
                    record.id,
                    record.topic,
                    json.dumps(record.payload, ensure_ascii=False, allow_nan=False),
                    record.status,
                    record.created_at,
                    record.updated_at,
                    record.available_at,
                    record.campaign_id,
                    record.correlation_id,
                    record.run_id,
                    record.publisher,
                    record.lease_owner,
                    record.lease_expires_at,
                    record.attempt,
                    record.dedupe_key,
                    record.last_error,
                ),
            )
            connection.commit()
        self._notify_observers(record)
        return _clone_record(record)

    def claim(
        self,
        consumer: str,
        topics: Iterable[str],
        *,
        lease_seconds: float = 60.0,
        correlation_id: str | None = None,
        run_id: str | None = None,
    ) -> EventRecord | None:
        """Claim an event for processing."""
        topic_names = [_validate_topic(topic) for topic in topics]
        if not topic_names:
            return None
        placeholders = ", ".join("?" for _ in topic_names)
        now = _utcnow_iso()
        lease_expires_at = _iso_after(lease_seconds)
        where_parts = [
            f"topic IN ({placeholders})",
            "((status = 'queued' AND available_at <= ?) OR (status = 'claimed' AND lease_expires_at IS NOT NULL AND lease_expires_at <= ?))",
        ]
        params: list[object] = [*topic_names, now, now]
        if correlation_id is not None:
            where_parts.append("correlation_id = ?")
            params.append(correlation_id)
        if run_id is not None:
            where_parts.append("run_id = ?")
            params.append(run_id)
        where_sql = " AND ".join(where_parts)

        with self._connect() as connection:
            self._ensure_schema(connection)
            row = connection.execute(
                f"""
                SELECT * FROM {self.table_name}
                WHERE {where_sql}
                ORDER BY available_at ASC, created_at ASC, id ASC
                LIMIT 1
                """,
                tuple(params),
            ).fetchone()
            if row is None:
                return None
            updated = connection.execute(
                f"""
                UPDATE {self.table_name}
                SET status = 'claimed',
                    updated_at = ?,
                    lease_owner = ?,
                    lease_expires_at = ?,
                    attempt = attempt + 1,
                    last_error = NULL
                WHERE id = ?
                  AND (
                    (status = 'queued' AND available_at <= ?)
                    OR
                    (status = 'claimed' AND lease_expires_at IS NOT NULL AND lease_expires_at <= ?)
                  )
                """,
                (now, consumer, lease_expires_at, row["id"], now, now),
            )
            connection.commit()
            if updated.rowcount == 0:
                return None
            claimed_row = connection.execute(
                f"SELECT * FROM {self.table_name} WHERE id = ?",
                (row["id"],),
            ).fetchone()
        return None if claimed_row is None else _row_to_record(claimed_row)

    def ack(self, event_id: str, consumer: str) -> None:
        """Acknowledge event completion."""
        with self._connect() as connection:
            self._ensure_schema(connection)
            row = connection.execute(
                f"SELECT * FROM {self.table_name} WHERE id = ?",
                (event_id,),
            ).fetchone()
            if row is None:
                return
            _validate_lease_owner(_row_to_record(row), consumer)
            connection.execute(
                f"""
                UPDATE {self.table_name}
                SET status = 'completed',
                    updated_at = ?,
                    lease_owner = NULL,
                    lease_expires_at = NULL
                WHERE id = ?
                """,
                (_utcnow_iso(), event_id),
            )
            connection.commit()

    def requeue(
        self,
        event_id: str,
        consumer: str,
        *,
        delay_seconds: float = 0.0,
        error: str | None = None,
    ) -> None:
        """Requeue event for retry."""
        with self._connect() as connection:
            self._ensure_schema(connection)
            row = connection.execute(
                f"SELECT * FROM {self.table_name} WHERE id = ?",
                (event_id,),
            ).fetchone()
            if row is None:
                return
            record = _row_to_record(row)
            _validate_lease_owner(record, consumer)
            # Dead Letter Queue: if max_attempts exceeded, move to dead_letter
            if self.max_attempts > 0 and record.attempt >= self.max_attempts:
                connection.execute(
                    f"""
                    UPDATE {self.table_name}
                    SET status = 'dead_letter',
                        updated_at = ?,
                        lease_owner = NULL,
                        lease_expires_at = NULL,
                        last_error = ?
                    WHERE id = ?
                    """,
                    (_utcnow_iso(), error, event_id),
                )
                connection.commit()
                _logger.warning(
                    "Event '%s' (topic=%s) moved to dead letter queue after %d attempts",
                    event_id, record.topic, record.attempt,
                )
                return
            connection.execute(
                f"""
                UPDATE {self.table_name}
                SET status = 'queued',
                    updated_at = ?,
                    available_at = ?,
                    lease_owner = NULL,
                    lease_expires_at = NULL,
                    last_error = ?
                WHERE id = ?
                """,
                (_utcnow_iso(), _iso_after(delay_seconds), error, event_id),
            )
            connection.commit()

    def requeue_dead_letter(self, event_id: str, *, delay_seconds: float = 0.0) -> bool:
        """Requeue a dead-lettered event."""
        with self._connect() as connection:
            self._ensure_schema(connection)
            row = connection.execute(
                f"SELECT * FROM {self.table_name} WHERE id = ?",
                (event_id,),
            ).fetchone()
            if row is None:
                return False
            record = _row_to_record(row)
            if record.status != "dead_letter":
                return False
            connection.execute(
                f"""
                UPDATE {self.table_name}
                SET status = 'queued',
                    attempt = 0,
                    updated_at = ?,
                    available_at = ?,
                    lease_owner = NULL,
                    lease_expires_at = NULL,
                    last_error = NULL
                WHERE id = ?
                """,
                (_utcnow_iso(), _iso_after(delay_seconds), event_id),
            )
            connection.commit()
        return True

    def purge_completed(self, *, max_age_seconds: float = 86400.0) -> int:
        """Purge old completed events."""
        from datetime import UTC, datetime

        cutoff = (datetime.now(UTC) - timedelta(seconds=max_age_seconds)).isoformat()
        with self._connect() as connection:
            self._ensure_schema(connection)
            cursor = connection.execute(
                f"DELETE FROM {self.table_name} WHERE status IN ('completed', 'dead_letter') AND updated_at < ?",
                (cutoff,),
            )
            connection.commit()
            return cursor.rowcount

    def get_event(self, event_id: str) -> EventRecord | None:
        """Get event by ID."""
        with self._connect() as connection:
            self._ensure_schema(connection)
            row = connection.execute(
                f"SELECT * FROM {self.table_name} WHERE id = ?",
                (event_id,),
            ).fetchone()
        return None if row is None else _row_to_record(row)

    def list_events(
        self,
        *,
        topic: str | None = None,
        status: str | None = None,
        campaign_id: str | None = None,
        correlation_id: str | None = None,
        run_id: str | None = None,
    ) -> list[EventRecord]:
        """List events with filters."""
        where_parts: list[str] = []
        params: list[object] = []
        if topic is not None:
            where_parts.append("topic = ?")
            params.append(_validate_topic(topic))
        if status is not None:
            where_parts.append("status = ?")
            params.append(status)
        if campaign_id is not None:
            where_parts.append("campaign_id = ?")
            params.append(campaign_id)
        if correlation_id is not None:
            where_parts.append("correlation_id = ?")
            params.append(correlation_id)
        if run_id is not None:
            where_parts.append("run_id = ?")
            params.append(run_id)
        where_sql = "" if not where_parts else f"WHERE {' AND '.join(where_parts)}"
        with self._connect() as connection:
            self._ensure_schema(connection)
            rows = connection.execute(
                f"""
                SELECT * FROM {self.table_name}
                {where_sql}
                ORDER BY created_at ASC, id ASC
                """,
                tuple(params),
            ).fetchall()
        return [_row_to_record(row) for row in rows]


__all__ = ["SQLiteEventBus", "_row_to_record"]
