"""
AwoooP Control Plane Models
============================
Phase 1 新表：六合約 control plane、tenant 隔離、principal mapping。
ADR-111~118，2026-05-04 ogt + Claude Sonnet 4.6
"""

from __future__ import annotations

from datetime import datetime
from decimal import Decimal
from typing import Any
from uuid import UUID

from sqlalchemy import (
    Boolean,
    CheckConstraint,
    ForeignKey,
    Index,
    Integer,
    Numeric,
    SmallInteger,
    String,
    Text,
    UniqueConstraint,
    text,
)
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import Mapped, mapped_column

from src.db.base import Base


class AwoooPProject(Base):
    """租戶主表（ADR-111 bootstrap，ADR-115 tenant onboarding）"""

    __tablename__ = "awooop_projects"
    __table_args__ = (
        CheckConstraint(
            "migration_mode IN ('legacy_awoooi_default','shadow','canary','active')",
            name="chk_migration_mode",
        ),
        CheckConstraint(
            "budget_limit_usd IS NULL OR budget_limit_usd >= 0",
            name="chk_budget_non_negative",
        ),
        CheckConstraint(
            "jsonb_typeof(allowed_channels) = 'array'",
            name="chk_allowed_channels_array",
        ),
    )

    project_id: Mapped[str] = mapped_column(String(64), primary_key=True)
    display_name: Mapped[str] = mapped_column(String(256), nullable=False)
    migration_mode: Mapped[str] = mapped_column(
        String(32), nullable=False, default="legacy_awoooi_default"
    )
    budget_limit_usd: Mapped[Decimal | None] = mapped_column(
        Numeric(14, 4), nullable=True
    )
    allowed_channels: Mapped[list[Any]] = mapped_column(
        JSONB, nullable=False, server_default=text("'[]'::jsonb")
    )
    is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
    created_at: Mapped[datetime] = mapped_column(
        nullable=False, server_default=text("NOW()")
    )
    updated_at: Mapped[datetime] = mapped_column(
        nullable=False, server_default=text("NOW()")
    )


class AwoooPContractRevision(Base):
    """六合約共用 revision 表（append-only，ADR-107/ADR-112）"""

    __tablename__ = "awooop_contract_revisions"
    __table_args__ = (
        UniqueConstraint(
            "project_id", "contract_family", "contract_id",
            "version_major", "version_minor",
            name="uq_revision_version",
        ),
        CheckConstraint(
            "contract_family IN ("
            "'project_tenant','agent','mcp_gateway','policy_routing',"
            "'runtime_run_state','channel_event','platform_resource')",
            name="chk_contract_family",
        ),
        CheckConstraint(
            "lifecycle_status IN ('draft','published','active','revoked')",
            name="chk_lifecycle",
        ),
        CheckConstraint("version_major >= 0", name="chk_version_major_non_neg"),
        CheckConstraint("version_minor >= 0", name="chk_version_minor_non_neg"),
        CheckConstraint(
            r"body_hash ~ '^[0-9a-f]{64}$'", name="chk_body_hash_format"
        ),
        Index(
            "idx_revisions_lookup",
            "project_id", "contract_family", "contract_id",
            "lifecycle_status", "version_major", "version_minor",
        ),
        Index("idx_revisions_hash", "body_hash"),
    )

    revision_id: Mapped[UUID] = mapped_column(
        primary_key=True, server_default=text("gen_random_uuid()")
    )
    project_id: Mapped[str] = mapped_column(
        String(64), ForeignKey("awooop_projects.project_id"), nullable=False
    )
    contract_family: Mapped[str] = mapped_column(String(32), nullable=False)
    contract_id: Mapped[str] = mapped_column(String(128), nullable=False)
    version_major: Mapped[int] = mapped_column(SmallInteger, nullable=False, default=1)
    version_minor: Mapped[int] = mapped_column(SmallInteger, nullable=False, default=0)
    lifecycle_status: Mapped[str] = mapped_column(
        String(16), nullable=False, default="draft"
    )
    body_json: Mapped[dict[str, Any]] = mapped_column(JSONB, nullable=False)
    body_hash: Mapped[str] = mapped_column(String(64), nullable=False)
    body_schema_version: Mapped[str] = mapped_column(
        String(16), nullable=False, default="v1.0"
    )
    publish_signature: Mapped[str | None] = mapped_column(String(128), nullable=True)
    publisher_id: Mapped[str | None] = mapped_column(String(128), nullable=True)
    published_at: Mapped[datetime | None] = mapped_column(nullable=True)
    created_at: Mapped[datetime] = mapped_column(
        nullable=False, server_default=text("NOW()")
    )


class AwoooPActiveRevision(Base):
    """Active revision pointer（ADR-107/ADR-113）"""

    __tablename__ = "awooop_active_revisions"
    __table_args__ = (
        UniqueConstraint(
            "project_id", "contract_family", "contract_id",
            name="uq_active_pointer",
        ),
    )

    pointer_id: Mapped[UUID] = mapped_column(
        primary_key=True, server_default=text("gen_random_uuid()")
    )
    project_id: Mapped[str] = mapped_column(
        String(64), ForeignKey("awooop_projects.project_id"), nullable=False
    )
    contract_family: Mapped[str] = mapped_column(String(32), nullable=False)
    contract_id: Mapped[str] = mapped_column(String(128), nullable=False)
    active_revision_id: Mapped[UUID] = mapped_column(
        ForeignKey("awooop_contract_revisions.revision_id", ondelete="RESTRICT"),
        nullable=False,
    )
    updated_at: Mapped[datetime] = mapped_column(
        nullable=False, server_default=text("NOW()")
    )


class AwoooPContractOutbox(Base):
    """Transactional outbox for contract revision invalidation（ADR-113）"""

    __tablename__ = "awooop_contract_outbox"
    __table_args__ = (
        UniqueConstraint("new_revision_id", "event_type", name="uq_outbox_event"),
        Index(
            "idx_outbox_pending",
            "next_retry_at", "created_at",
            postgresql_where=text("delivered_at IS NULL"),
        ),
        Index(
            "idx_outbox_backlog_per_project",
            "project_id", "created_at",
            postgresql_where=text("delivered_at IS NULL"),
        ),
    )

    event_id: Mapped[UUID] = mapped_column(
        primary_key=True, server_default=text("gen_random_uuid()")
    )
    event_type: Mapped[str] = mapped_column(String(64), nullable=False)
    project_id: Mapped[str] = mapped_column(
        String(64), ForeignKey("awooop_projects.project_id"), nullable=False
    )
    contract_family: Mapped[str] = mapped_column(String(32), nullable=False)
    contract_id: Mapped[str] = mapped_column(String(128), nullable=False)
    old_revision_id: Mapped[UUID | None] = mapped_column(
        ForeignKey("awooop_contract_revisions.revision_id"), nullable=True
    )
    new_revision_id: Mapped[UUID] = mapped_column(
        ForeignKey("awooop_contract_revisions.revision_id"), nullable=False
    )
    created_at: Mapped[datetime] = mapped_column(
        nullable=False, server_default=text("NOW()")
    )
    delivered_at: Mapped[datetime | None] = mapped_column(nullable=True)
    relay_attempts: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
    next_retry_at: Mapped[datetime | None] = mapped_column(nullable=True)
    last_error: Mapped[str | None] = mapped_column(Text, nullable=True)


class AwoooPChannelEventDedupe(Base):
    """Channel event idempotency key（ADR-114，partitioned by created_at）"""

    __tablename__ = "awooop_channel_event_dedupe"
    __table_args__ = (
        UniqueConstraint(
            "project_id", "channel_type", "provider_event_id", "created_at",
            name="uq_channel_event_dedupe",
        ),
        Index("idx_dedupe_run", "run_id"),
    )

    # Composite PK（partition key 必須是 PK 一部分）
    # SQLAlchemy 2.x 要求 primary_key=True 標在 mapped_column，不能用 __mapper_args__ 字串 list
    dedupe_id: Mapped[UUID] = mapped_column(
        primary_key=True, server_default=text("gen_random_uuid()")
    )
    project_id: Mapped[str] = mapped_column(String(64), nullable=False)
    channel_type: Mapped[str] = mapped_column(String(32), nullable=False)
    provider_event_id: Mapped[str] = mapped_column(String(256), nullable=False)
    run_id: Mapped[UUID] = mapped_column(nullable=False)
    created_at: Mapped[datetime] = mapped_column(
        primary_key=True, server_default=text("NOW()")
    )


class AwoooPPlatformSubject(Base):
    """Canonical principal mapping（ADR-115）"""

    __tablename__ = "awooop_platform_subjects"
    __table_args__ = (
        UniqueConstraint(
            "project_id", "channel_type", "channel_user_id",
            name="uq_platform_subject",
        ),
        CheckConstraint(
            "jsonb_typeof(roles) = 'array'", name="chk_roles_array"
        ),
        Index(
            "idx_platform_subjects_lookup",
            "project_id", "channel_type", "channel_user_id",
        ),
        Index(
            "idx_platform_subjects_resolve",
            "project_id", "platform_subject_id",
        ),
        Index(
            "idx_platform_subjects_last_seen",
            "project_id", "last_seen_at",
        ),
    )

    subject_id: Mapped[UUID] = mapped_column(
        primary_key=True, server_default=text("gen_random_uuid()")
    )
    project_id: Mapped[str] = mapped_column(
        String(64), ForeignKey("awooop_projects.project_id"), nullable=False
    )
    channel_type: Mapped[str] = mapped_column(String(32), nullable=False)
    channel_user_id: Mapped[str] = mapped_column(String(256), nullable=False)
    channel_chat_id: Mapped[str | None] = mapped_column(String(256), nullable=True)
    platform_subject_id: Mapped[str] = mapped_column(String(128), nullable=False)
    display_name: Mapped[str | None] = mapped_column(String(256), nullable=True)
    roles: Mapped[list[str]] = mapped_column(
        JSONB, nullable=False, server_default=text("'[]'::jsonb")
    )
    first_seen_at: Mapped[datetime] = mapped_column(
        nullable=False, server_default=text("NOW()")
    )
    last_seen_at: Mapped[datetime] = mapped_column(
        nullable=False, server_default=text("NOW()")
    )


class AwoooPProjectMigrationState(Base):
    """Strangler Fig migration state per project × capability（ADR-106 遷移追蹤）"""

    __tablename__ = "awooop_project_migration_state"
    __table_args__ = (
        UniqueConstraint("project_id", "capability", name="uq_project_capability"),
        CheckConstraint(
            "capability IN ("
            "'run_execution','contract_governance',"
            "'budget_tracking','principal_mapping')",
            name="chk_capability",
        ),
        CheckConstraint(
            "current_phase IN ("
            "'legacy_awoooi_default','shadow','canary',"
            "'read_only','suggest','auto_remediate')",
            name="chk_phase",
        ),
    )

    state_id: Mapped[UUID] = mapped_column(
        primary_key=True, server_default=text("gen_random_uuid()")
    )
    project_id: Mapped[str] = mapped_column(
        String(64), ForeignKey("awooop_projects.project_id"), nullable=False
    )
    capability: Mapped[str] = mapped_column(String(64), nullable=False)
    current_phase: Mapped[str] = mapped_column(
        String(32), nullable=False, default="legacy_awoooi_default"
    )
    phase_entered_at: Mapped[datetime] = mapped_column(
        nullable=False, server_default=text("NOW()")
    )
    updated_at: Mapped[datetime] = mapped_column(
        nullable=False, server_default=text("NOW()")
    )


# ─────────────────────────────────────────────────────────────────────────────
# Phase 4: Run State Machine（ADR-114/ADR-119）
# 2026-05-04 ogt + Claude Sonnet 4.6
# ─────────────────────────────────────────────────────────────────────────────

class AwoooPRunState(Base):
    """Run FSM 主表（SKIP LOCKED worker lease，ADR-114）"""

    __tablename__ = "awooop_run_state"
    __table_args__ = (
        CheckConstraint(
            "state IN ("
            "'pending','running','waiting_tool',"
            "'waiting_approval','completed','failed','cancelled','timeout')",
            name="chk_run_state",
        ),
        Index("idx_run_state_pending", "project_id", "created_at",
              postgresql_where=text("state = 'pending' AND lease_until IS NULL")),
        Index("idx_run_state_stale", "lease_until",
              postgresql_where=text("state = 'running' AND lease_until IS NOT NULL")),
        Index("idx_run_state_project_timeline", "project_id", "created_at"),
        Index("idx_run_state_trace_id", "trace_id",
              postgresql_where=text("trace_id IS NOT NULL")),
    )

    run_id: Mapped[UUID] = mapped_column(primary_key=True)
    project_id: Mapped[str] = mapped_column(
        String(64), ForeignKey("awooop_projects.project_id"), nullable=False
    )
    agent_id: Mapped[str] = mapped_column(String(128), nullable=False)
    state: Mapped[str] = mapped_column(String(32), nullable=False, default="pending")
    lease_until: Mapped[datetime | None] = mapped_column(nullable=True)
    heartbeat_at: Mapped[datetime | None] = mapped_column(nullable=True)
    worker_id: Mapped[str | None] = mapped_column(String(128), nullable=True)
    attempt_count: Mapped[int] = mapped_column(SmallInteger, nullable=False, default=0)
    max_attempts: Mapped[int] = mapped_column(SmallInteger, nullable=False, default=3)
    trace_id: Mapped[str | None] = mapped_column(String(128), nullable=True)
    trigger_type: Mapped[str | None] = mapped_column(String(32), nullable=True)
    trigger_ref: Mapped[str | None] = mapped_column(String(256), nullable=True)
    is_shadow: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
    input_sha256: Mapped[str | None] = mapped_column(String(64), nullable=True)
    output_sha256: Mapped[str | None] = mapped_column(String(64), nullable=True)
    cost_usd: Mapped[Decimal] = mapped_column(
        Numeric(10, 4), nullable=False, default=Decimal("0.0000")
    )
    step_count: Mapped[int] = mapped_column(SmallInteger, nullable=False, default=0)
    error_code: Mapped[str | None] = mapped_column(String(64), nullable=True)
    error_detail: Mapped[str | None] = mapped_column(Text, nullable=True)
    created_at: Mapped[datetime] = mapped_column(
        nullable=False, server_default=text("NOW()")
    )
    started_at: Mapped[datetime | None] = mapped_column(nullable=True)
    completed_at: Mapped[datetime | None] = mapped_column(nullable=True)
    timeout_at: Mapped[datetime | None] = mapped_column(nullable=True)


class AwoooPRunStepJournal(Base):
    """SAGA step journal（ADR-119）— 每個 tool call 獨立記錄"""

    __tablename__ = "awooop_run_step_journal"
    __table_args__ = (
        UniqueConstraint("run_id", "step_seq", name="uix_run_step_seq"),
        CheckConstraint(
            "result_status IN ('pending','success','failed','compensated')",
            name="chk_step_result_status",
        ),
        Index("idx_run_step_run_id", "run_id", "step_seq"),
    )

    step_id: Mapped[UUID] = mapped_column(
        primary_key=True, server_default=text("gen_random_uuid()")
    )
    run_id: Mapped[UUID] = mapped_column(
        ForeignKey("awooop_run_state.run_id", ondelete="CASCADE"), nullable=False
    )
    project_id: Mapped[str] = mapped_column(String(64), nullable=False)
    step_seq: Mapped[int] = mapped_column(SmallInteger, nullable=False)
    tool_name: Mapped[str] = mapped_column(String(128), nullable=False)
    mcp_gateway_id: Mapped[str | None] = mapped_column(String(128), nullable=True)
    input_hash: Mapped[str | None] = mapped_column(String(64), nullable=True)
    output_hash: Mapped[str | None] = mapped_column(String(64), nullable=True)
    compensation_json: Mapped[dict[str, Any] | None] = mapped_column(JSONB, nullable=True)
    result_status: Mapped[str] = mapped_column(String(16), nullable=False, default="pending")
    error_code: Mapped[str | None] = mapped_column(String(64), nullable=True)
    was_blocked: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
    block_reason: Mapped[str | None] = mapped_column(String(128), nullable=True)
    created_at: Mapped[datetime] = mapped_column(
        nullable=False, server_default=text("NOW()")
    )
    completed_at: Mapped[datetime | None] = mapped_column(nullable=True)
    latency_ms: Mapped[int | None] = mapped_column(Integer, nullable=True)


class AwoooPRunIdempotency(Base):
    """Run 去重冪等表（ADR-114）— (project_id, channel_type, provider_event_id) → run_id"""

    __tablename__ = "awooop_run_idempotency"
    __table_args__ = (
        UniqueConstraint(
            "project_id", "channel_type", "provider_event_id",
            name="uix_run_idempotency_key",
        ),
        Index("idx_run_idempotency_run_id", "run_id"),
    )

    idempotency_id: Mapped[UUID] = mapped_column(
        primary_key=True, server_default=text("gen_random_uuid()")
    )
    project_id: Mapped[str] = mapped_column(String(64), nullable=False)
    channel_type: Mapped[str] = mapped_column(String(32), nullable=False)
    provider_event_id: Mapped[str] = mapped_column(String(256), nullable=False)
    run_id: Mapped[UUID] = mapped_column(
        ForeignKey("awooop_run_state.run_id"), nullable=False
    )
    created_at: Mapped[datetime] = mapped_column(
        nullable=False, server_default=text("NOW()")
    )


# =============================================================================
# Phase 5: MCP Gateway 四表（ADR-116/ADR-118，2026-05-04）
# =============================================================================


class AwoooPMcpToolRegistry(Base):
    """MCP Tool 白名單（Gate 3: Tool）"""

    __tablename__ = "awooop_mcp_tool_registry"
    __table_args__ = (
        CheckConstraint(
            "tool_type IN ('builtin','mcp_server','custom')",
            name="chk_tool_type",
        ),
        CheckConstraint(
            "jsonb_typeof(allowed_scopes) = 'array'",
            name="chk_allowed_scopes_array",
        ),
        UniqueConstraint("project_id", "tool_name", name="uix_tool_registry_project_name"),
        Index("idx_mcp_tool_registry_project", "project_id", "is_active"),
    )

    tool_id: Mapped[UUID] = mapped_column(
        primary_key=True, server_default=text("gen_random_uuid()")
    )
    project_id: Mapped[str] = mapped_column(
        String(64), ForeignKey("awooop_projects.project_id", ondelete="CASCADE"), nullable=False
    )
    tool_name: Mapped[str] = mapped_column(String(128), nullable=False)
    tool_type: Mapped[str] = mapped_column(String(32), nullable=False)
    description: Mapped[str | None] = mapped_column(Text, nullable=True)
    allowed_scopes: Mapped[list[Any]] = mapped_column(JSONB, nullable=False, default=list)
    environment_tags: Mapped[dict[str, Any]] = mapped_column(JSONB, nullable=False, default=dict)
    is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
    created_at: Mapped[datetime] = mapped_column(
        nullable=False, server_default=text("NOW()")
    )
    updated_at: Mapped[datetime] = mapped_column(
        nullable=False, server_default=text("NOW()")
    )


class AwoooPMcpGrant(Base):
    """Agent × Tool 授權記錄（Gate 2 + Gate 3）"""

    __tablename__ = "awooop_mcp_grants"
    __table_args__ = (
        CheckConstraint(
            "jsonb_typeof(granted_scopes) = 'array'",
            name="chk_grant_scopes_array",
        ),
        CheckConstraint(
            "(is_revoked = FALSE AND revoked_at IS NULL AND revoked_by IS NULL)"
            " OR (is_revoked = TRUE AND revoked_at IS NOT NULL)",
            name="chk_revoke_consistency",
        ),
        UniqueConstraint("project_id", "agent_id", "tool_id", name="uix_mcp_grant_agent_tool"),
        Index(
            "idx_mcp_grants_lookup", "project_id", "agent_id", "tool_id",
            postgresql_where=text("is_revoked = FALSE"),
        ),
    )

    grant_id: Mapped[UUID] = mapped_column(
        primary_key=True, server_default=text("gen_random_uuid()")
    )
    project_id: Mapped[str] = mapped_column(
        String(64), ForeignKey("awooop_projects.project_id", ondelete="CASCADE"), nullable=False
    )
    agent_id: Mapped[str] = mapped_column(String(128), nullable=False)
    tool_id: Mapped[UUID] = mapped_column(
        ForeignKey("awooop_mcp_tool_registry.tool_id", ondelete="CASCADE"), nullable=False
    )
    granted_by: Mapped[str] = mapped_column(String(128), nullable=False)
    granted_scopes: Mapped[list[Any]] = mapped_column(JSONB, nullable=False, default=list)
    expires_at: Mapped[datetime | None] = mapped_column(nullable=True)
    is_revoked: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
    revoked_at: Mapped[datetime | None] = mapped_column(nullable=True)
    revoked_by: Mapped[str | None] = mapped_column(String(128), nullable=True)
    created_at: Mapped[datetime] = mapped_column(
        nullable=False, server_default=text("NOW()")
    )


class AwoooPMcpCredentialRef(Base):
    """k8s Secret 參照（ADR-118 credential isolation）— 只存路徑，不存明文"""

    __tablename__ = "awooop_mcp_credential_refs"
    __table_args__ = (
        CheckConstraint(
            r"k8s_secret_ref ~ '^[a-z0-9-]+/[a-z0-9-]+#[a-zA-Z0-9_-]+$'",
            name="chk_k8s_ref_format",
        ),
        CheckConstraint(
            r"value_sha256 IS NULL OR value_sha256 ~ '^[0-9a-f]{64}$'",
            name="chk_value_sha256_hex",
        ),
        UniqueConstraint("tool_id", "k8s_secret_ref", name="uix_credential_ref_tool"),
        Index("idx_mcp_cred_refs_tool", "tool_id", postgresql_where=text("is_active = TRUE")),
    )

    ref_id: Mapped[UUID] = mapped_column(
        primary_key=True, server_default=text("gen_random_uuid()")
    )
    tool_id: Mapped[UUID] = mapped_column(
        ForeignKey("awooop_mcp_tool_registry.tool_id", ondelete="CASCADE"), nullable=False
    )
    project_id: Mapped[str] = mapped_column(
        String(64), ForeignKey("awooop_projects.project_id", ondelete="CASCADE"), nullable=False
    )
    k8s_secret_ref: Mapped[str] = mapped_column(String(256), nullable=False)
    value_sha256: Mapped[str | None] = mapped_column(String(64), nullable=True)
    description: Mapped[str | None] = mapped_column(Text, nullable=True)
    is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
    created_at: Mapped[datetime] = mapped_column(
        nullable=False, server_default=text("NOW()")
    )
    rotated_at: Mapped[datetime | None] = mapped_column(nullable=True)


class AwoooPMcpGatewayAudit(Base):
    """MCP Gateway call 稽核日誌（ADR-116 P1-09）"""

    __tablename__ = "awooop_mcp_gateway_audit"
    __table_args__ = (
        CheckConstraint(
            "result_status IN ('success','blocked','failed','timeout')",
            name="chk_gateway_result_status",
        ),
        CheckConstraint(
            "block_gate IS NULL OR (block_gate >= 1 AND block_gate <= 5)",
            name="chk_block_gate_range",
        ),
        Index("idx_mcp_audit_run", "project_id", "run_id", "created_at"),
        Index(
            "idx_mcp_audit_blocked", "project_id", "block_gate", "created_at",
            postgresql_where=text("result_status = 'blocked'"),
        ),
    )

    call_id: Mapped[UUID] = mapped_column(
        primary_key=True, server_default=text("gen_random_uuid()")
    )
    project_id: Mapped[str] = mapped_column(String(64), nullable=False)
    run_id: Mapped[UUID | None] = mapped_column(nullable=True)
    trace_id: Mapped[str | None] = mapped_column(String(128), nullable=True)
    agent_id: Mapped[str | None] = mapped_column(String(128), nullable=True)
    tool_id: Mapped[UUID | None] = mapped_column(
        ForeignKey("awooop_mcp_tool_registry.tool_id"), nullable=True
    )
    tool_name: Mapped[str] = mapped_column(String(128), nullable=False)
    credential_ref: Mapped[str | None] = mapped_column(String(256), nullable=True)
    input_hash: Mapped[str | None] = mapped_column(String(64), nullable=True)
    output_hash: Mapped[str | None] = mapped_column(String(64), nullable=True)
    gate_result: Mapped[dict[str, Any]] = mapped_column(JSONB, nullable=False, default=dict)
    result_status: Mapped[str] = mapped_column(String(16), nullable=False)
    block_gate: Mapped[int | None] = mapped_column(SmallInteger, nullable=True)
    block_reason: Mapped[str | None] = mapped_column(String(256), nullable=True)
    latency_ms: Mapped[int | None] = mapped_column(Integer, nullable=True)
    created_at: Mapped[datetime] = mapped_column(
        nullable=False, server_default=text("NOW()")
    )


# =============================================================================
# Phase 7: Channel Hub 雙表（ADR-106 channel_event family，2026-05-04）
# =============================================================================


class AwoooPConversationEvent(Base):
    """入站 Channel Event 鏡像（Telegram/LINE inbound，不儲存明文）"""

    __tablename__ = "awooop_conversation_event"
    __table_args__ = (
        CheckConstraint(
            "channel_type IN ('telegram','line','slack','api','internal')",
            name="chk_conv_event_channel_type",
        ),
        CheckConstraint(
            "content_type IN ('text','photo','document','command','callback_query')",
            name="chk_conv_event_content_type",
        ),
        UniqueConstraint(
            "project_id", "channel_type", "provider_event_id",
            name="uix_conv_event_dedup",
        ),
        Index("idx_conv_event_run", "project_id", "run_id", "received_at"),
        Index("idx_conv_event_subject", "project_id", "platform_subject_id", "received_at"),
    )

    event_id: Mapped[UUID] = mapped_column(
        primary_key=True, server_default=text("gen_random_uuid()")
    )
    project_id: Mapped[str] = mapped_column(
        String(64), ForeignKey("awooop_projects.project_id", ondelete="CASCADE"), nullable=False
    )
    channel_type: Mapped[str] = mapped_column(String(32), nullable=False)
    provider_event_id: Mapped[str] = mapped_column(String(256), nullable=False)
    platform_subject_id: Mapped[str | None] = mapped_column(String(128), nullable=True)
    channel_user_id: Mapped[str | None] = mapped_column(String(256), nullable=True)
    channel_chat_id: Mapped[str | None] = mapped_column(String(256), nullable=True)
    run_id: Mapped[UUID | None] = mapped_column(nullable=True)
    content_type: Mapped[str] = mapped_column(String(32), nullable=False, default="text")
    content_hash: Mapped[str | None] = mapped_column(String(64), nullable=True)
    content_preview: Mapped[str | None] = mapped_column(String(256), nullable=True)
    content_redacted: Mapped[str | None] = mapped_column(Text, nullable=True)
    redaction_version: Mapped[str] = mapped_column(
        String(32), nullable=False, server_default=text("'audit_sink_v1'")
    )
    source_envelope: Mapped[dict[str, Any]] = mapped_column(
        JSONB, nullable=False, server_default=text("'{}'::jsonb")
    )
    attachment_sha256: Mapped[str | None] = mapped_column(String(64), nullable=True)
    is_duplicate: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
    provider_ts: Mapped[datetime | None] = mapped_column(nullable=True)
    received_at: Mapped[datetime] = mapped_column(
        nullable=False, server_default=text("NOW()")
    )


class AwoooPOutboundMessage(Base):
    """出站訊息記錄（interim/final/approval_request + shadow status）"""

    __tablename__ = "awooop_outbound_message"
    __table_args__ = (
        CheckConstraint(
            "channel_type IN ('telegram','line','slack','api','internal')",
            name="chk_outbound_channel_type",
        ),
        CheckConstraint(
            "message_type IN ('interim','final','error','approval_request')",
            name="chk_outbound_message_type",
        ),
        CheckConstraint(
            "send_status IN ('pending','sent','failed','shadow')",
            name="chk_outbound_send_status",
        ),
        Index("idx_outbound_msg_run", "project_id", "run_id", "queued_at"),
        Index(
            "idx_outbound_msg_pending", "project_id", "channel_type", "queued_at",
            postgresql_where=text("send_status = 'pending'"),
        ),
    )

    message_id: Mapped[UUID] = mapped_column(
        primary_key=True, server_default=text("gen_random_uuid()")
    )
    project_id: Mapped[str] = mapped_column(
        String(64), ForeignKey("awooop_projects.project_id", ondelete="CASCADE"), nullable=False
    )
    run_id: Mapped[UUID] = mapped_column(nullable=False)
    conversation_event_id: Mapped[UUID | None] = mapped_column(nullable=True)
    channel_type: Mapped[str] = mapped_column(String(32), nullable=False)
    channel_chat_id: Mapped[str] = mapped_column(String(256), nullable=False)
    message_type: Mapped[str] = mapped_column(String(32), nullable=False)
    content_hash: Mapped[str | None] = mapped_column(String(64), nullable=True)
    content_preview: Mapped[str | None] = mapped_column(String(256), nullable=True)
    content_redacted: Mapped[str | None] = mapped_column(Text, nullable=True)
    redaction_version: Mapped[str] = mapped_column(
        String(32), nullable=False, server_default=text("'audit_sink_v1'")
    )
    source_envelope: Mapped[dict[str, Any]] = mapped_column(
        JSONB, nullable=False, server_default=text("'{}'::jsonb")
    )
    provider_message_id: Mapped[str | None] = mapped_column(String(64), nullable=True)
    send_status: Mapped[str] = mapped_column(String(16), nullable=False, default="pending")
    send_error: Mapped[str | None] = mapped_column(Text, nullable=True)
    queued_at: Mapped[datetime] = mapped_column(
        nullable=False, server_default=text("NOW()")
    )
    sent_at: Mapped[datetime | None] = mapped_column(nullable=True)
    triggered_by_state: Mapped[str | None] = mapped_column(String(32), nullable=True)
    waiting_since: Mapped[datetime | None] = mapped_column(nullable=True)
