"""Signal emitter — buffers and writes tag signals to the commons database."""

from __future__ import annotations

import hashlib
import json
import logging
import sqlite3
import threading
from datetime import datetime
from pathlib import Path

from chicory.config import ChicoryConfig

logger = logging.getLogger(__name__)


class SignalEmitter:
    """Buffers tag signals and writes them to the commons database.

    Opens a separate SQLite connection to the commons DB (not the project DB).
    Store/retrieve signals are buffered and flushed periodically or at threshold.
    Synchronicity signals bypass the buffer and flush immediately.
    """

    def __init__(self, config: ChicoryConfig) -> None:
        self._config = config
        self._project_id = config.commons_project_id
        self._buffer: list[dict] = []
        self._lock = threading.Lock()
        self._conn: sqlite3.Connection | None = None
        self._table_ensured = False
        self._flush_thread: threading.Thread | None = None
        self._stop_event = threading.Event()

    def start(self) -> None:
        """Open connection and start background flush thread."""
        self._connect()
        self._flush_thread = threading.Thread(
            target=self._periodic_flush,
            daemon=True,
        )
        self._flush_thread.start()

    def stop(self) -> None:
        """Flush remaining buffer and close connection."""
        self._stop_event.set()
        if self._flush_thread:
            self._flush_thread.join(timeout=10)
        self._flush_now()
        if self._conn:
            self._conn.close()
            self._conn = None

    def emit_store(self, tags: list[str]) -> None:
        """Buffer a store signal."""
        self._add_signal("store", tags)

    def emit_retrieve(self, result_tags: list[str]) -> None:
        """Buffer a retrieve signal."""
        self._add_signal("retrieve", result_tags)

    def emit_synchronicity(
        self,
        tags: list[str],
        strength: float,
        event_type: str,
    ) -> None:
        """Immediately flush a synchronicity signal (bypass buffer)."""
        signal = self._make_signal("synchronicity", tags, strength, event_type)
        self._write_signals([signal])

    def _add_signal(
        self,
        op_type: str,
        tags: list[str],
        strength: float | None = None,
        event_type: str | None = None,
    ) -> None:
        """Add a signal to the buffer; flush if buffer is full."""
        signal = self._make_signal(op_type, tags, strength, event_type)
        flush_now = False
        with self._lock:
            self._buffer.append(signal)
            if len(self._buffer) >= self._config.commons_signal_buffer_size:
                to_flush = self._buffer[:]
                self._buffer.clear()
                flush_now = True

        if flush_now:
            self._write_signals(to_flush)

    def _make_signal(
        self,
        op_type: str,
        tags: list[str],
        strength: float | None = None,
        event_type: str | None = None,
    ) -> dict:
        """Construct a signal dict."""
        sorted_tags = sorted(set(tags))
        tag_set_hash = hashlib.sha256(
            ",".join(sorted_tags).encode()
        ).hexdigest()[:16]
        return {
            "project_id": self._project_id,
            "op_type": op_type,
            "tags": json.dumps(sorted_tags),
            "strength": strength,
            "event_type": event_type,
            "tag_set_hash": tag_set_hash,
        }

    def _connect(self) -> None:
        """Open connection to commons DB with WAL mode."""
        db_path = str(self._config.commons_db_path)
        Path(db_path).parent.mkdir(parents=True, exist_ok=True)
        self._conn = sqlite3.connect(db_path, check_same_thread=False)
        self._conn.execute("PRAGMA journal_mode = WAL")
        self._conn.execute("PRAGMA busy_timeout = 5000")

    def _ensure_table(self) -> None:
        """Create pending_signals table if it doesn't exist."""
        if self._table_ensured:
            return
        self._conn.execute("""
            CREATE TABLE IF NOT EXISTS pending_signals (
                id              INTEGER PRIMARY KEY AUTOINCREMENT,
                project_id      TEXT NOT NULL,
                op_type         TEXT NOT NULL,
                tags            TEXT NOT NULL,
                strength        REAL,
                event_type      TEXT,
                created_at      TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now')),
                processed       INTEGER NOT NULL DEFAULT 0,
                tag_set_hash    TEXT NOT NULL,
                UNIQUE(project_id, op_type, tag_set_hash, created_at)
            )
        """)
        self._conn.execute("""
            CREATE INDEX IF NOT EXISTS idx_pending_signals_unprocessed
            ON pending_signals(processed) WHERE processed = 0
        """)
        self._conn.commit()
        self._table_ensured = True

    def _write_signals(self, signals: list[dict]) -> None:
        """Write signals to the commons DB."""
        if not signals or not self._conn:
            return
        try:
            self._ensure_table()
            for sig in signals:
                self._conn.execute(
                    """
                    INSERT OR IGNORE INTO pending_signals
                        (project_id, op_type, tags, strength, event_type, tag_set_hash)
                    VALUES (?, ?, ?, ?, ?, ?)
                    """,
                    (
                        sig["project_id"], sig["op_type"], sig["tags"],
                        sig["strength"], sig["event_type"], sig["tag_set_hash"],
                    ),
                )
            self._conn.commit()
        except Exception:
            logger.exception("Failed to write commons signals")

    def _flush_now(self) -> None:
        """Flush whatever is in the buffer right now."""
        with self._lock:
            to_flush = self._buffer[:]
            self._buffer.clear()
        if to_flush:
            self._write_signals(to_flush)

    def _periodic_flush(self) -> None:
        """Background thread: flush buffer at configured interval."""
        interval = self._config.commons_flush_interval_seconds
        while not self._stop_event.wait(timeout=interval):
            self._flush_now()
