"""Seed documents-lifecycle: member-owned structured content records.
Phase 11D5B (composition contract) + 11D5C (content size ceiling added to
this same migration file, since it never shipped/ran against a real DB --
see docs/audits/DOCUMENTS_ARCHITECTURE_AUDIT_11D5A.md,
DOCUMENTS_COMPOSITION_CONTRACT_11D5B.md, and
DOCUMENTS_RUNTIME_STABILIZATION_11D5C.md). files.attachments wiring and
generated-app UI are still 11D5D/E.

Deliberate choices, all traceable to the 11D5A audit + the user's frozen
11D5 decisions, not defaults:

- No `workflow` key. `status` is a plain select field (draft/active/
  archived), NOT workflow.transition-governed. Explicitly rejected in the
  11D5A exit report: document status has no proven gated-transition need
  (approval/review workflows are an explicit non-goal), unlike
  case_record's real open->in_review->... business process.
- No `requires` beyond the implicit data.entity every pack already has --
  same shape as activity-timeline (138), the first pack that didn't need
  workflow.transition either.
- `content_format` is a select field with a single option, "plain_text".
  Not a free-text field: kept as select so a future rich-text slice can
  extend the option set without a field-type migration, without
  pretending polymorphism exists today. Markdown/rich_text/html are
  explicitly deferred to a dedicated future capability slice (its own
  editor + sanitizer + render-boundary work) -- never silently added
  here. See 11D5A audit Sec 4.
- `content.max_bytes = 262144` (256 KiB, UTF-8 bytes not characters). 11D5B
  found ProvisionField / the Cloud entity field schema had no max-length
  concept at all (confirmed by reading provision_manifest.py's ProvisionField
  end to end). 11D5C closed that gap generically: EntityFieldDefinition
  gained an opt-in `max_bytes` property, enforced by validateRecordData
  (P2HSCloud packages/shared/src/records.ts) for any field that declares
  it -- no other existing field/entity is affected. This is what stops
  document.content from becoming an alternate blob store.
- access_policy is HAND-EMBEDDED directly on the entity schema, exactly
  like 140 (notifications-in-app) and 141 (file-attachments) -- not left
  implicit the way case_record/platform_task/assignment are. Confirmed by
  reading normalize.py and catalog_v2.py end to end: PackSecurity/
  ownership_mode in the catalog_v2 overlay is planner/UI-projection
  metadata only ("App Blocks may project this into UI; they must not
  treat it as the security boundary" -- contract.py:229-230) and is NEVER
  translated into schema.access_policy for catalog_v2 packs
  (stamp_composition's own access_policy parameter is discarded:
  `_ = access_policy` in normalize.py). Documents is exactly the
  per-member-data-isolation case 140's own comment warns about ("the
  first Phase 11 capability where that gap could leak one user's data to
  another") -- a member must only ever see/edit their OWN documents,
  never another member's, so this cannot be left to an unenforced
  `security` block the way the staff-only packs (case/task/assignment) do.
  member gets full CRUD at scope=owner (not read-only) even though 11D5B
  ships no create/edit UI -- 11D5C proves CRUD through the existing
  record API directly, and 11D5E's generated-app UI depends on this
  access_policy already being correct, not on a later change to it.

Revision ID: 142
Revises: 141
"""
from datetime import datetime, timezone
from uuid import uuid4

from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql

revision = "142"
down_revision = "141"
branch_labels = None
depends_on = None

feature_packs = sa.table(
    "feature_packs",
    sa.column("id", postgresql.UUID(as_uuid=True)),
    sa.column("slug", sa.String),
    sa.column("name", sa.String),
    sa.column("description", sa.Text),
    sa.column("version", sa.String),
    sa.column("tags", postgresql.JSONB),
    sa.column("status", sa.String),
    sa.column("composition", postgresql.JSONB),
    sa.column("input_schema", postgresql.JSONB),
    sa.column("access_policy", postgresql.JSONB),
)

_DOCUMENT_ACCESS_POLICY = {
    "version": 1,
    "roles": {
        "member": {
            "create": {"effect": "allow", "scope": "owner"},
            "read": {"effect": "allow", "scope": "owner"},
            "update": {"effect": "allow", "scope": "owner"},
            "delete": {"effect": "allow", "scope": "owner"},
        },
        "admin": {
            "create": {"effect": "allow", "scope": "all"},
            "read": {"effect": "allow", "scope": "all"},
            "update": {"effect": "allow", "scope": "all"},
            "delete": {"effect": "allow", "scope": "all"},
        },
        "super_admin": {
            "create": {"effect": "allow", "scope": "all"},
            "read": {"effect": "allow", "scope": "all"},
            "update": {"effect": "allow", "scope": "all"},
            "delete": {"effect": "allow", "scope": "all"},
        },
    },
}

_DOCUMENT_ENTITY = {
    "entity_key": "document",
    "name": "Document",
    "plural_name": "Documents",
    # 11D5C finding, load-bearing: this top-level flag is REQUIRED for member-
    # created documents to be readable by their own creator, and is entirely
    # separate from schema.access_policy above. P2HSCloud's CREATE-path owner
    # stamping (resolveCreateOwner, apps/pc-gateway/src/routes/records.ts)
    # gates purely on entity.rls_enabled/owner_managed -- unlike its sibling
    # resolveRecordScope (auth-context.ts, used for READ/UPDATE scoping),
    # which already prefers schema.access_policy when present. Without this
    # flag, resolveCreateOwner takes its `!rls_enabled && !owner_managed`
    # branch and stamps owner_user_id=null on every record an app_user
    # creates, regardless of what access_policy says -- and scope=owner reads
    # would then match nothing. Every existing catalog pack with an access_
    # policy (file-attachments: scope=all only; notifications-in-app: rows
    # are only ever created by an internal caller, never an app_user) never
    # exercised this path, so the gap was latent, not previously proven safe.
    # documents-lifecycle is the first pack where an app_user CREATEs an
    # owner-scoped record, which is what surfaced it. This is a narrow,
    # already-established, purely additive flag (see FeaturePackInstallService
    # in src/domain/feature_packs/service.py, which already reconciles this
    # exact flag for other packs on reinstall) -- not a kernel change.
    "rls_enabled": True,
    "schema": {
        "fields": [
            {"key": "title", "name": "Title", "type": "text", "required": True},
            {
                "key": "content",
                "name": "Content",
                "type": "text",
                "required": True,
                # 11D5C: UTF-8 byte ceiling, not a character count -- enforced
                # generically by validateRecordData (P2HSCloud packages/shared/
                # src/records.ts) against EntityFieldDefinition.max_bytes, a new
                # opt-in field property with no default (every other text field
                # on every other entity is unaffected). Prevents document.content
                # from becoming an alternate blob-storage path (11D5A/B finding).
                "max_bytes": 262144,
            },
            {
                "key": "content_format",
                "name": "Content Format",
                "type": "select",
                "required": True,
                "options": ["plain_text"],
            },
            {
                "key": "status",
                "name": "Status",
                "type": "select",
                "required": True,
                "indexed": True,
                "options": ["draft", "active", "archived"],
            },
        ],
        "access_policy": _DOCUMENT_ACCESS_POLICY,
    },
}


def upgrade() -> None:
    from src.domain.feature_packs.normalize import stamp_composition

    now = datetime.now(timezone.utc)
    raw_composition = {
        "function_slugs": [],
        "entities": [_DOCUMENT_ENTITY],
        "connector_actions": [],
        "requires": [],
    }
    stamped = stamp_composition("documents-lifecycle", raw_composition)
    # stamp_composition doesn't touch access_policy (see module docstring) --
    # confirm it survived untouched from the raw blob rather than assume it.
    assert stamped["entities"][0]["schema"]["access_policy"] == _DOCUMENT_ACCESS_POLICY
    op.bulk_insert(
        feature_packs,
        [
            {
                "id": uuid4(),
                "slug": "documents-lifecycle",
                "name": "Documents Lifecycle",
                "description": (
                    "Member-owned structured content records (title/content/"
                    "content_format/status). Plain text only in this slice -- "
                    "rich text/markdown is a deliberately deferred future "
                    "capability, not a limitation of the persistence model. "
                    "document_id is not a file_id: binary attachments compose "
                    "via the existing files.attachments capability (11D5D), "
                    "never by extending this entity."
                ),
                "version": "1.0.0",
                "tags": ["documents", "content"],
                "status": "active",
                "composition": stamped,
                "input_schema": {"type": "object", "properties": {}},
                "access_policy": {"minimum_plan": "free"},
                "created_at": now,
                "updated_at": now,
            },
        ],
    )


def downgrade() -> None:
    op.execute("DELETE FROM feature_packs WHERE slug = 'documents-lifecycle'")
