"""
Federation log — append-only record of every sync event between nodes.

This table is the OSS foundation for the commercial two-phase commit machinery.
The schema is intentionally stable: commercial code will write new event_types
and read this log without schema changes.

Event lifecycle (two-phase propagation):
  authority node: INSERT event_type='push',  status='pending'
  child node ack: UPDATE status='ack'
  authority:      INSERT event_type='commit', status='committed'   (all acked)
  or on timeout:  INSERT event_type='reject', status='rejected'    (governance alert)

Isolation events:
  INSERT event_type='isolation_start' when a node loses upstream contact
  INSERT event_type='isolation_end'   when contact is restored
"""

import json
import logging
import sqlite3
from datetime import datetime, timezone
from typing import Optional

import config
from core.storage import FederationLogBackend

logger = logging.getLogger(__name__)

# Valid event types — OSS subset; commercial tier adds workflow events
FEDERATION_EVENT_TYPES = frozenset({
    "push",           # authority pushes contract change to child
    "pull",           # child pulls from upstream
    "ack",            # child acknowledges a pending push
    "commit",         # authority commits after all acks received
    "reject",         # push rejected (constraint violation or timeout)
    "isolation_start",  # node lost contact with upstream
    "isolation_end",    # node regained contact
})

FEDERATION_STATUSES = frozenset({"pending", "ack", "committed", "rejected"})


class FederationLog(FederationLogBackend):
    """
    Append-only federation event log, persisted in SQLite.

    Each row captures one sync event between two nodes. The `lsn` (log sequence
    number) is the monotonically increasing primary key — downstream nodes use it
    as a replication cursor: "give me all events since lsn=N".
    """

    def __init__(self, db_path: str = None):
        if db_path is None:
            db_path = config.DB_PATH
        self.db_path = db_path
        self._mem_conn = sqlite3.connect(":memory:") if db_path == ":memory:" else None
        self._init_db()

    def _connect(self) -> sqlite3.Connection:
        if self._mem_conn is not None:
            return self._mem_conn
        return sqlite3.connect(self.db_path, check_same_thread=False)

    def _init_db(self):
        conn = self._connect()
        conn.execute(
            "CREATE TABLE IF NOT EXISTS federation_log ("
            "lsn INTEGER PRIMARY KEY AUTOINCREMENT, "
            "event_type TEXT NOT NULL, "
            "contract_name TEXT NOT NULL, "
            "contract_version TEXT NOT NULL, "
            "source_node TEXT NOT NULL, "
            "target_node TEXT, "          # NULL = broadcast
            "payload TEXT NOT NULL DEFAULT '{}', "
            "status TEXT NOT NULL DEFAULT 'pending', "
            "created_at TEXT NOT NULL)"
        )
        conn.execute("PRAGMA journal_mode=WAL")
        conn.execute("PRAGMA synchronous=NORMAL")
        conn.execute(
            "CREATE INDEX IF NOT EXISTS idx_federation_log_contract "
            "ON federation_log(contract_name)"
        )
        conn.execute(
            "CREATE INDEX IF NOT EXISTS idx_federation_log_lsn_status "
            "ON federation_log(lsn, status)"
        )
        conn.commit()
        if self._mem_conn is None:
            conn.close()

    def record_event(
        self,
        event_type: str,
        contract_name: str,
        contract_version: str,
        source_node: str,
        target_node: Optional[str] = None,
        payload: Optional[dict] = None,
        status: str = "pending",
    ) -> int:
        """
        Insert a federation event. Returns the assigned lsn.

        Raises ValueError for unknown event_type or status values so callers
        fail fast rather than silently writing garbage into the audit trail.
        """
        if event_type not in FEDERATION_EVENT_TYPES:
            raise ValueError(f"Unknown federation event_type: {event_type!r}. "
                             f"Valid types: {sorted(FEDERATION_EVENT_TYPES)}")
        if status not in FEDERATION_STATUSES:
            raise ValueError(f"Unknown federation status: {status!r}. "
                             f"Valid statuses: {sorted(FEDERATION_STATUSES)}")

        payload_json = json.dumps(payload or {}, sort_keys=True)
        created_at = datetime.now(timezone.utc).isoformat()

        conn = self._connect()
        is_shared = self._mem_conn is not None
        try:
            cur = conn.execute(
                "INSERT INTO federation_log "
                "(event_type, contract_name, contract_version, source_node, "
                "target_node, payload, status, created_at) "
                "VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
                (event_type, contract_name, contract_version, source_node,
                 target_node, payload_json, status, created_at),
            )
            conn.commit()
            return cur.lastrowid
        finally:
            if not is_shared:
                conn.close()

    def update_status(self, lsn: int, status: str) -> bool:
        """
        Update the status of an existing event. Returns True if a row was updated.

        Used by the 2PC machinery to transition: pending → ack → committed/rejected.
        """
        if status not in FEDERATION_STATUSES:
            raise ValueError(f"Unknown federation status: {status!r}")

        conn = self._connect()
        is_shared = self._mem_conn is not None
        try:
            cur = conn.execute(
                "UPDATE federation_log SET status = ? WHERE lsn = ?",
                (status, lsn),
            )
            conn.commit()
            return cur.rowcount == 1
        finally:
            if not is_shared:
                conn.close()

    def get_since(self, lsn: int, contract_name: Optional[str] = None) -> list[dict]:
        """
        Return all events with lsn > given value (replication cursor).

        Optionally filter by contract_name. Results are ordered by lsn ascending
        so callers can process them in order and advance their cursor.
        """
        conn = self._connect()
        is_shared = self._mem_conn is not None
        try:
            if contract_name:
                rows = conn.execute(
                    "SELECT lsn, event_type, contract_name, contract_version, "
                    "source_node, target_node, payload, status, created_at "
                    "FROM federation_log "
                    "WHERE lsn > ? AND contract_name = ? ORDER BY lsn",
                    (lsn, contract_name),
                ).fetchall()
            else:
                rows = conn.execute(
                    "SELECT lsn, event_type, contract_name, contract_version, "
                    "source_node, target_node, payload, status, created_at "
                    "FROM federation_log WHERE lsn > ? ORDER BY lsn",
                    (lsn,),
                ).fetchall()
        finally:
            if not is_shared:
                conn.close()

        return [
            {
                "lsn": row[0],
                "event_type": row[1],
                "contract_name": row[2],
                "contract_version": row[3],
                "source_node": row[4],
                "target_node": row[5],
                "payload": json.loads(row[6]),
                "status": row[7],
                "created_at": row[8],
            }
            for row in rows
        ]

    def get_pending(self, contract_name: Optional[str] = None) -> list[dict]:
        """Return all events with status='pending', optionally filtered by contract."""
        conn = self._connect()
        is_shared = self._mem_conn is not None
        try:
            if contract_name:
                rows = conn.execute(
                    "SELECT lsn, event_type, contract_name, contract_version, "
                    "source_node, target_node, payload, status, created_at "
                    "FROM federation_log "
                    "WHERE status = 'pending' AND contract_name = ? ORDER BY lsn",
                    (contract_name,),
                ).fetchall()
            else:
                rows = conn.execute(
                    "SELECT lsn, event_type, contract_name, contract_version, "
                    "source_node, target_node, payload, status, created_at "
                    "FROM federation_log WHERE status = 'pending' ORDER BY lsn",
                ).fetchall()
        finally:
            if not is_shared:
                conn.close()

        return [
            {
                "lsn": row[0],
                "event_type": row[1],
                "contract_name": row[2],
                "contract_version": row[3],
                "source_node": row[4],
                "target_node": row[5],
                "payload": json.loads(row[6]),
                "status": row[7],
                "created_at": row[8],
            }
            for row in rows
        ]
