"""SQLAlchemy models for self-registration drift API.

Separate from v1 models (models.py) which require Ed25519 keypairs.
Self-registration uses bearer token auth for zero-friction onboarding.
"""

from __future__ import annotations

from datetime import datetime, timezone
from uuid import uuid4

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

from kredo_drift.server.db.models import Base


def _uuid() -> str:
    return uuid4().hex


def _utcnow() -> datetime:
    return datetime.now(timezone.utc)


class DriftAgent(Base):
    """Self-registered agent."""
    __tablename__ = "drift_agents"

    # WHY hex UUID not auto-increment: agent_id doubles as the public identifier
    # in URLs and API responses — sequential IDs leak fleet size and ordering.
    agent_id = Column(String(32), primary_key=True, default=_uuid)
    name = Column(String(255), nullable=False)
    # WHY bcrypt not SHA-256: bearer tokens need slow hashing to resist brute-force
    # if the DB leaks — SHA-256 is too fast for credential storage.
    bearer_token_hash = Column(String(128), nullable=False)  # bcrypt
    # WHY prefix index: bcrypt hashes are salted (can't lookup by hash), so we
    # store the first 8 chars of the raw token for O(1) candidate filtering
    # before the slow bcrypt.checkpw call. Not secret — just narrows the scan.
    token_prefix = Column(String(8), nullable=True, index=True)
    # WHY three-state status: gates API access — retest endpoint rejects "pending"
    # agents who haven't completed their baseline yet.
    status = Column(String(32), nullable=False, default="pending")  # pending, baselined, active
    model = Column(String(255), nullable=True)  # optional: what model the agent runs on
    created_at = Column(DateTime, nullable=False, default=_utcnow)
    active = Column(Boolean, nullable=False, default=True)
    metadata_ = Column("metadata", JSON, nullable=True)
    # Ed25519 public key — sent at registration, used to verify signed requests.
    # NULL for legacy agents that registered before Ed25519 support.
    public_key_hex = Column(String(64), nullable=True)
    # WHY identity_hash: compositional hash that binds behavioral identity (baseline
    # vectors), cryptographic ownership (public key), temporal uniqueness (timestamp),
    # and model lineage into a single non-forgeable identifier. Provisional until
    # baseline completes, then crystallized with baseline_vector_hash.
    identity_hash = Column(String(64), nullable=True, index=True)

    sessions = relationship("DriftSession", back_populates="agent")
    baselines = relationship("SelfRegBaseline", back_populates="agent")


class DriftSession(Base):
    """Assessment session (baseline or retest)."""
    __tablename__ = "drift_sessions"

    session_id = Column(String(32), primary_key=True, default=_uuid)
    agent_id = Column(String(32), ForeignKey("drift_agents.agent_id"), nullable=False, index=True)
    session_type = Column(String(16), nullable=False)  # "baseline" or "retest"
    # WHY store full prompt metadata, not just IDs: scoring pipeline needs
    # dimension/type/pair info at response time — can't round-trip to the prompt
    # pool because pool composition may change between sessions.
    prompts_json = Column(JSON, nullable=False)  # list of prompt dicts
    prompt_count = Column(Integer, nullable=False)
    status = Column(String(32), nullable=False, default="pending")  # pending, complete, expired
    sequence = Column(Integer, nullable=False, default=0)
    # WHY model_id per session: every assessment records which LLM the agent
    # was running. Enables model-matched baseline selection and cross-model
    # coherence analysis. NULL for legacy sessions before model-aware scoring.
    model_id = Column(String(128), nullable=True)
    created_at = Column(DateTime, nullable=False, default=_utcnow)
    expires_at = Column(DateTime, nullable=True)
    completed_at = Column(DateTime, nullable=True)

    agent = relationship("DriftAgent", back_populates="sessions")
    responses = relationship("DriftResponseRecord", back_populates="session")

    __table_args__ = (
        Index("ix_drift_sessions_agent_status", "agent_id", "status"),
    )


class DriftResponseRecord(Base):
    """Individual prompt response within a session."""
    __tablename__ = "drift_responses"

    id = Column(Integer, primary_key=True, autoincrement=True)
    session_id = Column(String(32), ForeignKey("drift_sessions.session_id"), nullable=False, index=True)
    agent_id = Column(String(32), ForeignKey("drift_agents.agent_id"), nullable=False, index=True)
    prompt_id = Column(String(16), nullable=False)
    # WHY hash only, no raw text: privacy model — raw responses are vectorized
    # then discarded. Hash enables dedup/replay detection without storing content.
    response_hash = Column(String(64), nullable=False)  # SHA-256
    created_at = Column(DateTime, nullable=False, default=_utcnow)

    session = relationship("DriftSession", back_populates="responses")


class SelfRegBaseline(Base):
    """Identity baseline created via self-registration."""
    __tablename__ = "selfreg_baselines"

    baseline_id = Column(String(32), primary_key=True, default=_uuid)
    agent_id = Column(String(32), ForeignKey("drift_agents.agent_id"), nullable=False, index=True)
    # WHY Text not JSON for vector: SQLite JSON functions choke on large float
    # arrays — Text column with json.dumps/loads is more portable.
    vector_json = Column(Text, nullable=False)
    dimension_vectors_json = Column(JSON, nullable=True)
    response_hashes_json = Column(JSON, nullable=False)
    prompt_ids_json = Column(JSON, nullable=False)
    prompt_count = Column(Integer, nullable=False)
    # WHY persist the model name: baselines created with different embedding models
    # are incompatible for comparison — need to reject cross-model retests.
    embedding_model = Column(String(128), nullable=False, default="all-MiniLM-L6-v2")
    pair_similarities_json = Column(JSON, nullable=True)
    anchor_values_json = Column(JSON, nullable=True)
    fidelity_vector_json = Column(Text, nullable=True)
    # Per-dimension coherence scores — how internally consistent the agent's
    # responses are within each dimension at baseline time. This is the
    # agent's identity shape before any drift testing.
    dimension_coherence_json = Column(JSON, nullable=True)
    # WHY model_id on baseline: baselines are now keyed by (agent_id, model_id).
    # An agent can have multiple active baselines — one per LLM it runs on.
    # NULL for legacy baselines created before model-aware scoring.
    model_id = Column(String(128), nullable=True)
    # WHY metametric_hash: SHA-256 of the 91-pair correlation fingerprint.
    # Enables fast structural comparison without recomputing from vectors.
    metametric_hash = Column(String(64), nullable=True)
    created_at = Column(DateTime, nullable=False, default=_utcnow)

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

    __table_args__ = (
        Index("ix_selfreg_baselines_agent", "agent_id", "created_at"),
    )


class SelfRegTestResult(Base):
    """Drift test result from self-registration retest."""
    __tablename__ = "selfreg_test_results"

    result_id = Column(String(32), primary_key=True, default=_uuid)
    agent_id = Column(String(32), ForeignKey("drift_agents.agent_id"), nullable=False, index=True)
    baseline_id = Column(String(32), ForeignKey("selfreg_baselines.baseline_id"), nullable=False)
    session_id = Column(String(32), ForeignKey("drift_sessions.session_id"), nullable=False)
    score = Column(Float, nullable=False)
    dimension_scores_json = Column(JSON, nullable=False)
    classification = Column(String(64), nullable=False)
    velocity = Column(Float, nullable=True)
    acceleration = Column(Float, nullable=True)
    trend_direction = Column(String(32), nullable=True)
    contradictions_json = Column(JSON, nullable=True)
    anchors_json = Column(JSON, nullable=True)
    fidelity_score = Column(Float, nullable=True)
    created_at = Column(DateTime, nullable=False, default=_utcnow)

    __table_args__ = (
        Index("ix_selfreg_results_agent", "agent_id", "created_at"),
    )


class DriftResponseText(Base):
    """Temporary storage for response texts during baseline flow.
    WHY separate table: privacy model stores only hashes long-term.
    Texts are held here until all prompts are answered, then vectorized
    and deleted. Never persisted beyond session completion."""
    __tablename__ = "drift_response_texts"

    id = Column(Integer, primary_key=True, autoincrement=True)
    session_id = Column(String(32), nullable=False, index=True)
    agent_id = Column(String(32), nullable=False)
    prompt_id = Column(String(16), nullable=False)
    response_text = Column(Text, nullable=False)
    created_at = Column(DateTime, nullable=False, default=_utcnow)
