"""SQLAlchemy models for the Kredo Drift server database.

All tables follow append-only semantics where noted.
Content-addressed: baselines, drift_tests, chain_entries use content hashes as PKs.
"""

from __future__ import annotations

from datetime import datetime, timezone

from sqlalchemy import (
    Column, String, Text, Float, Integer, DateTime, Boolean, JSON,
    ForeignKey, Index, DDL, event,
)
from sqlalchemy.orm import DeclarativeBase, relationship


class Base(DeclarativeBase):
    pass


class Agent(Base):
    """Registered agent."""
    __tablename__ = "agents"

    # WHY pubkey as PK (not agent_id): The pubkey is the agent's cryptographic
    # identity. On key rotation, a *new* Agent row is created with the new
    # pubkey, while the old row is deactivated. This preserves the full key
    # history and lets chain entries reference the exact key that signed them.
    pubkey = Column(String(128), primary_key=True)  # "ed25519:<hex>"
    agent_id = Column(String(255), nullable=False, unique=True, index=True)
    name = Column(String(255), nullable=False)
    api_key_hash = Column(String(128), nullable=False)  # bcrypt hash
    tier = Column(String(32), nullable=False, default="free")  # free | paid
    owner_pubkey = Column(String(128), nullable=True)  # linked human owner
    created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
    active = Column(Boolean, nullable=False, default=True)
    metadata_ = Column("metadata", JSON, nullable=True)

    baselines = relationship("Baseline", back_populates="agent")
    chain_entries = relationship("ChainEntry", back_populates="agent")


class Baseline(Base):
    """Identity baseline. Append-only."""
    __tablename__ = "baselines"

    # WHY content_hash as PK: Content-addressed storage makes baselines
    # intrinsically deduplicated and tamper-evident. If any field changes, the
    # hash changes, creating a new row rather than silently mutating the old one.
    content_hash = Column(String(64), primary_key=True)  # SHA-256
    agent_pubkey = Column(String(128), ForeignKey("agents.pubkey"), nullable=False, index=True)
    encrypted_vector = Column(Text, nullable=False)  # encrypted 384-dim vector
    dimension_vectors = Column(JSON, nullable=True)  # per-dimension encrypted vectors
    response_hashes = Column(JSON, nullable=False)  # list of SHA-256 hashes
    prompt_ids = Column(JSON, nullable=False)
    prompt_count = Column(Integer, nullable=False)
    model_name = Column(String(128), nullable=False, default="all-MiniLM-L6-v2")
    model_version = Column(String(32), nullable=False, default="1")
    created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
    agent_signature = Column(Text, nullable=True)  # agent's Ed25519 signature
    service_signature = Column(Text, nullable=True)  # service's Ed25519 signature
    chain_hash = Column(String(64), nullable=True)  # link to chain_entries

    agent = relationship("Agent", back_populates="baselines")

    __table_args__ = (
        Index("ix_baselines_agent_created", "agent_pubkey", "created_at"),
    )


class DriftTestRecord(Base):
    """Single drift test run. Append-only."""
    __tablename__ = "drift_tests"

    content_hash = Column(String(64), primary_key=True)
    agent_pubkey = Column(String(128), ForeignKey("agents.pubkey"), nullable=False, index=True)
    baseline_hash = Column(String(64), ForeignKey("baselines.content_hash"), nullable=False)
    encrypted_vector = Column(Text, nullable=False)
    dimension_vectors = Column(JSON, nullable=True)
    response_hashes = Column(JSON, nullable=False)
    prompt_ids = Column(JSON, nullable=False)
    prompt_count = Column(Integer, nullable=False)
    score = Column(Float, nullable=False)
    dimension_scores = Column(JSON, nullable=False)  # list of {dimension, score, similarity}
    classification = Column(String(64), nullable=False)
    model_name = Column(String(128), nullable=False, default="all-MiniLM-L6-v2")
    model_version = Column(String(32), nullable=False, default="1")
    created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
    agent_signature = Column(Text, nullable=True)
    service_signature = Column(Text, nullable=True)
    chain_hash = Column(String(64), nullable=True)

    __table_args__ = (
        Index("ix_drift_tests_agent_created", "agent_pubkey", "created_at"),
    )


class DriftAttestationRecord(Base):
    """Signed drift attestation. Append-only."""
    __tablename__ = "drift_attestations"

    id = Column(String(64), primary_key=True)
    content_hash = Column(String(64), nullable=False, unique=True, index=True)
    agent_pubkey = Column(String(128), ForeignKey("agents.pubkey"), nullable=False, index=True)
    version = Column(String(32), nullable=False, default="drift-attestation-v1")
    attestor = Column(String(255), nullable=True)  # service or agent
    test_hash = Column(String(64), nullable=True)
    score = Column(Float, nullable=False)
    dimension_scores = Column(JSON, nullable=False)
    classification = Column(String(64), nullable=False)
    prompt_selection_hash = Column(String(64), nullable=True)
    chain_position = Column(Integer, nullable=True)
    chain_hash = Column(String(64), nullable=True)
    raw_json = Column(Text, nullable=False)  # canonical JSON of attestation
    agent_signature = Column(Text, nullable=True)
    service_signature = Column(Text, nullable=True)
    expires = Column(DateTime, nullable=True)
    created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))


class ChainEntry(Base):
    """Per-agent hash chain entry. Append-only."""
    __tablename__ = "chain_entries"

    chain_hash = Column(String(64), primary_key=True)  # SHA-256(content_hash || prev_hash)
    agent_pubkey = Column(String(128), ForeignKey("agents.pubkey"), nullable=False, index=True)
    sequence = Column(Integer, nullable=False)
    event_type = Column(String(64), nullable=False)  # baseline, test, key_rotation, recovery
    content_hash = Column(String(64), nullable=False)  # hash of the referenced object
    prev_hash = Column(String(64), nullable=True)  # previous chain_hash (None for genesis)
    created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))

    agent = relationship("Agent", back_populates="chain_entries")

    __table_args__ = (
        Index("ix_chain_agent_seq", "agent_pubkey", "sequence", unique=True),
    )


class AuditEvent(Base):
    """Audit log entry. Append-only, never modified."""
    __tablename__ = "audit_events"

    id = Column(Integer, primary_key=True, autoincrement=True)
    event_type = Column(String(64), nullable=False)
    agent_pubkey = Column(String(128), nullable=True)
    detail = Column(JSON, nullable=True)
    ip_address = Column(String(45), nullable=True)
    created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc), index=True)


# ── Append-only enforcement via DDL triggers (PostgreSQL) ──
# These triggers reject UPDATE and DELETE on immutable tables.

# WHY append-only enforcement via DB triggers: Application-level immutability
# can be bypassed by bugs or direct DB access. PostgreSQL triggers provide a
# defense-in-depth guarantee: even a compromised application server or rogue
# admin query cannot silently modify historical records. This is essential
# for audit credibility -- the chain of evidence must be provably intact.
# WHY allowlist, not user input: table names are hardcoded constants below —
# never from user input. DDL statements don't support bind parameters, so
# string formatting is the only option. The frozenset enforces the allowlist.
_APPEND_ONLY_TABLES = frozenset(["baselines", "drift_tests", "drift_attestations", "chain_entries", "audit_events"])

for _table_name in sorted(_APPEND_ONLY_TABLES):
    assert _table_name.isidentifier(), f"Invalid table name: {_table_name}"
    _trigger_name = f"prevent_modify_{_table_name}"
    _func_name = f"reject_modify_{_table_name}"

    _create_func = DDL(
        "CREATE OR REPLACE FUNCTION %(func)s() RETURNS TRIGGER AS $$ "
        "BEGIN RAISE EXCEPTION 'Table %(table)s is append-only: %%%% not allowed', TG_OP; END; "
        "$$ LANGUAGE plpgsql;" % {"func": _func_name, "table": _table_name}
    )

    _create_trigger = DDL(
        "DO $$ BEGIN "
        "IF NOT EXISTS (SELECT 1 FROM pg_trigger WHERE tgname = '%(trigger)s') THEN "
        "CREATE TRIGGER %(trigger)s BEFORE UPDATE OR DELETE ON %(table)s "
        "FOR EACH ROW EXECUTE FUNCTION %(func)s(); "
        "END IF; END $$;" % {"trigger": _trigger_name, "table": _table_name, "func": _func_name}
    )

    # Attach to the table's after_create event
    event.listen(
        Base.metadata.tables.get(_table_name, Base.metadata),
        "after_create",
        _create_func.execute_if(dialect="postgresql"),
    )
    event.listen(
        Base.metadata.tables.get(_table_name, Base.metadata),
        "after_create",
        _create_trigger.execute_if(dialect="postgresql"),
    )
