"""Seed notifications-in-app: a governed, per-recipient projection over
canonical domain events -- never a second event system, never a workflow
lifecycle, never a generated-app write target. Phase 11C1. Extended in Phase
11C2 with an optional email delivery channel on the same row (email_status/
email_error) -- reusing this pack rather than a second one, since the email
channel shares the exact same notification intent and recipient identity.

Revision ID: 140
Revises: 139
"""
from datetime import datetime, timezone
from uuid import uuid4

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

revision = "140"
down_revision = "139"
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),
)

# Recipient identity: Cloud's own authenticated app_user id (JWT `sub`, exposed
# as CallerAuth.userId in P2HSCloud apps/pc-gateway/src/lib/auth-context.ts),
# the SAME identity resolveRecordScope() already uses for every owner-scoped
# entity. No new relation type, no new identity primitive -- recipient_user_id
# is a plain field carrying it for display, and this record's Cloud envelope
# OWNER is set to the same value at creation, which is the actual enforcement
# boundary (schema.access_policy below), not the field.
#
# schema.access_policy is hand-embedded here, not derived from `security`
# (PackSecurity) via stamp_composition -- confirmed by reading normalize.py
# end to end that no code path ever translates PackSecurity/ownership_mode
# into schema.access_policy for catalog_v2 packs (stamp_composition's own
# `access_policy` parameter is discarded: `_ = access_policy`). That is a
# pre-existing gap shared by every hand-authored pack before this one
# (lead-lifecycle, case-lifecycle, approval-workflow, assignment-tracking,
# task-management, activity-timeline) and is NOT fixed generally here -- doing
# so would mean re-touching frozen Phase 6/9/10 packs without a demonstrated
# need. It is fixed narrowly, for this one entity, because per-user data
# isolation is the actual security boundary a notification inbox needs, and it
# is the first Phase 11 capability where that gap could leak one user's data
# to another rather than merely under-restricting staff-only entities.
# Shape matches Cloud's CompiledAccessPolicy exactly (packages/shared/src/
# records.ts:checkAccessPolicy / composer.py's AccessPolicyEntry): no role is
# granted "create" -- only a non-app_user (internal/service) caller may create
# a notification record, which is exactly how the platform, not Codegen, is
# meant to populate this entity.
_NOTIFICATION_ACCESS_POLICY = {
    "version": 1,
    "roles": {
        "member": {
            "read": {"effect": "allow", "scope": "owner"},
            "update": {"effect": "allow", "scope": "owner"},
        },
        "admin": {"read": {"effect": "allow", "scope": "all"}},
        "super_admin": {"read": {"effect": "allow", "scope": "all"}},
    },
}

_PLATFORM_NOTIFICATION_ENTITY = {
    "entity_key": "platform_notification",
    "name": "Notification",
    "plural_name": "Notifications",
    "schema": {
        "fields": [
            {
                "key": "recipient_user_id", "name": "Recipient", "type": "text",
                "required": True, "indexed": True,
            },
            {"key": "source_event_id", "name": "Source Event", "type": "text", "required": True},
            {
                "key": "canonical_event_id", "name": "Event Type", "type": "text",
                "required": True, "indexed": True,
            },
            {
                "key": "subject_entity_key", "name": "Subject Entity", "type": "text",
                "required": True, "indexed": True,
            },
            {
                "key": "subject_record_id", "name": "Subject Record", "type": "text",
                "required": True, "indexed": True,
            },
            {"key": "title", "name": "Title", "type": "text", "required": True},
            {"key": "message", "name": "Message", "type": "text"},
            {
                "key": "category", "name": "Category", "type": "select",
                "required": True, "indexed": True,
                "options": ["created", "status_changed", "completed", "other"],
            },
            {"key": "producer_capability", "name": "Source Capability", "type": "text"},
            {"key": "payload", "name": "Payload", "type": "json"},
            # Deliberately NOT schema.workflow -- a nullable timestamp is
            # application state (has the recipient seen this), not a business
            # lifecycle. See docs/audits/NOTIFICATIONS_IN_APP_11C1.md Sec 2.
            {"key": "read_at", "name": "Read At", "type": "text"},
            # Phase 11C2 (email channel). Not schema.workflow, same reasoning
            # as read_at, and additionally: field.changed producers cannot
            # carry a to_state (only workflow.transition can -- confirmed by
            # PackEventProducer.shape_matches_kind), so the sent/failed/
            # configuration_required distinction is carried in the event's
            # bounded payload, not as three separate producer identities that
            # would collide on (kind, entity, field) uniqueness. Semantic
            # capability is provider-neutral: email.send always has a working
            # default delivery path (never disclosed to Codegen or the
            # generated app as a specific provider) unless a project has
            # configured its own -- "configuration_required" here means the
            # platform's own default path is unavailable, not "the customer
            # forgot to connect something".
            {
                "key": "email_status", "name": "Email Status", "type": "select",
                "options": ["not_requested", "sent", "failed", "configuration_required"],
            },
            {"key": "email_error", "name": "Email Error", "type": "text"},
        ],
        "access_policy": _NOTIFICATION_ACCESS_POLICY,
    },
}


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

    now = datetime.now(timezone.utc)
    raw_composition = {
        "function_slugs": [],
        "entities": [_PLATFORM_NOTIFICATION_ENTITY],
        "connector_actions": [],
        "requires": [],
    }
    stamped = stamp_composition("notifications-in-app", raw_composition)
    # stamp_composition doesn't touch access_policy (see comment above) --
    # confirm it survived untouched from the raw blob rather than assume it.
    assert stamped["entities"][0]["schema"]["access_policy"] == _NOTIFICATION_ACCESS_POLICY
    op.bulk_insert(
        feature_packs,
        [
            {
                "id": uuid4(),
                "slug": "notifications-in-app",
                "name": "In-App Notifications",
                "description": (
                    "Governed, per-recipient projection over canonical domain "
                    "events (10D-10D4). Not a second event bus: it emits nothing "
                    "of its own. Recipient identity is the platform's existing "
                    "authenticated app_user id (JWT sub / CallerAuth.userId), "
                    "reused via record ownership + access_policy scope=owner -- "
                    "no new individual-user relation type. Rows exist only via "
                    "canonical event consumption by a non-app_user (internal) "
                    "caller; no role is granted create."
                ),
                "version": "1.0.0",
                "tags": ["notifications", "in_app", "operations"],
                "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 = 'notifications-in-app'")
