# Compliance audit-event read API (Phase 3 U10).
#
# Read-only browse of compliance.audit_events written by the U4 outbox
# drainer. Backed by the compliance_reader Aurora role, NOT the writer
# pool. No mutations exist on this surface — the audit_events table is
# append-only at the DB layer (audit_events_block_delete trigger).
#
# Auth: graphql-http resolvers gate on Cognito-only callers (apikey
# blocked) + THINKWORK_PLATFORM_OPERATOR_EMAILS for cross-tenant
# browse. Non-operator callers see only their own tenant's events.
#
# Connection shape: relay-style { edges { node, cursor }, pageInfo }
# (NEW pattern in this repo — established here for cursor-stable
# pagination across equal-microsecond timestamps).

# ---------------------------------------------------------------------------
# Enums
# ---------------------------------------------------------------------------

# Mirror of COMPLIANCE_ACTOR_TYPES in
# packages/database-pg/src/schema/compliance.ts. A unit test in
# packages/api asserts the GraphQL values exactly match the runtime
# slate — fails CI if the schema and enum drift.
enum ComplianceActorType {
  USER
  SYSTEM
  AGENT
}

# Mirror of COMPLIANCE_EVENT_TYPES in
# packages/database-pg/src/schema/compliance.ts. The 14 Phase 3 starter
# events plus 5 Phase 6 reserved events. Resolver translates
# UPPER_UNDERSCORE GraphQL values to dotted lowercase DB strings via a
# canonical helper.
enum ComplianceEventType {
  AUTH_SIGNIN_SUCCESS
  AUTH_SIGNIN_FAILURE
  AUTH_SIGNOUT
  USER_INVITED
  USER_CREATED
  USER_DISABLED
  USER_DELETED
  AGENT_CREATED
  AGENT_DELETED
  AGENT_SKILLS_CHANGED
  MCP_ADDED
  MCP_REMOVED
  WORKSPACE_GOVERNANCE_FILE_EDITED
  DATA_EXPORT_INITIATED
  POLICY_EVALUATED
  POLICY_ALLOWED
  POLICY_BLOCKED
  POLICY_BYPASSED
  APPROVAL_RECORDED
  ATTACHMENT_RECEIVED
  SKILL_ACTIVATED
  OUTPUT_ARTIFACT_PRODUCED
}

enum ComplianceAnchorState {
  ANCHORED
  PENDING
}

# ---------------------------------------------------------------------------
# Core types
# ---------------------------------------------------------------------------

# A single compliance.audit_events row decorated with anchor status.
type ComplianceEvent {
  eventId: ID!
  tenantId: ID!
  occurredAt: String!
  recordedAt: String!
  actor: String!
  actorType: ComplianceActorType!
  source: String!
  eventType: ComplianceEventType!
  eventHash: String!
  prevHash: String
  payload: AWSJSON!
  anchorStatus: ComplianceAnchorStatus!
}

# Flat object with discriminant enum (NOT a GraphQL union — one
# consumer doesn't justify union ergonomics in zod / urql).
# When state == ANCHORED, cadenceId + anchoredRecordedAt are non-null.
# When state == PENDING, nextCadenceWithinMinutes is non-null.
type ComplianceAnchorStatus {
  state: ComplianceAnchorState!
  cadenceId: ID
  anchoredRecordedAt: String
  nextCadenceWithinMinutes: Int
}

# ---------------------------------------------------------------------------
# Pagination (relay-style edges + cursor — new pattern in this repo)
# ---------------------------------------------------------------------------

type ComplianceEventEdge {
  node: ComplianceEvent!
  cursor: String!
}

type ComplianceEventConnection {
  edges: [ComplianceEventEdge!]!
  pageInfo: ComplianceEventPageInfo!
}

type ComplianceEventPageInfo {
  hasNextPage: Boolean!
  endCursor: String
}

# ---------------------------------------------------------------------------
# Filter input
# ---------------------------------------------------------------------------

# All fields optional. tenantId is server-side-overridden for
# non-operator callers (whose scope is forced to their own tenant).
# since/until are ISO8601 strings; bounds are half-open [since, until).
input ComplianceEventFilter {
  tenantId: ID
  actorType: ComplianceActorType
  eventType: ComplianceEventType
  since: String
  until: String
}

# ---------------------------------------------------------------------------
# Queries
# ---------------------------------------------------------------------------

extend type Query {
  """
  Paginated audit-event list, sorted by `occurred_at DESC, event_id DESC`
  (matches the existing `(tenant_id, occurred_at DESC)` index on
  `compliance.audit_events`). Cursor encodes
  `{occurred_at_iso_with_microseconds, event_id}` as base64-url JSON.
  Page size capped at 200 server-side; client-recommended default 50.
  """
  complianceEvents(
    filter: ComplianceEventFilter
    after: String
    first: Int
  ): ComplianceEventConnection!

  """
  Single event by event_id. Non-operator callers reading another tenant's
  event_id see null (existence-oracle defense — SQL filter applies in the
  WHERE clause so the timing-side-channel is closed).
  """
  complianceEvent(eventId: ID!): ComplianceEvent

  """
  Single event by event_hash. Used by the chain-position panel's
  prev_hash click-through and the walk-back-N-events iterator.
  Tenant-scoped for non-operators. The eventHash MUST be a 64-char
  lowercase hex SHA-256 digest; malformed input returns null without
  hitting the DB (resolver-level format guard).
  """
  complianceEventByHash(eventHash: String!): ComplianceEvent

  """
  Distinct tenant_ids visible to the caller. Operators get the full
  set (DISTINCT tenant_id from compliance.audit_events); non-operators
  get a 1-element list of their own tenant. Powers the operator
  tenant-filter typeahead in the admin Compliance section.
  """
  complianceTenants: [ID!]!

  """
  Caller's compliance-operator status + dev-environment configuration
  signal. Mirrors the adminRoleCheck pattern (top-level Query field,
  NOT a User-type field — operator status is caller-dependent and
  attaching it to User would leak the semantic across every
  User-returning query).
  """
  complianceOperatorCheck: ComplianceOperatorCheckResult!
}

type ComplianceOperatorCheckResult {
  """True when the caller's email matches THINKWORK_PLATFORM_OPERATOR_EMAILS."""
  isOperator: Boolean!
  """
  True when the env var is non-empty. False means the dev/staging
  environment hasn't configured the allowlist; admin UI surfaces a
  distinct "allowlist not configured" message rather than silently
  flipping to non-operator UI.
  """
  allowlistConfigured: Boolean!
}

# ---------------------------------------------------------------------------
# Async export — Phase 3 U11
# ---------------------------------------------------------------------------
#
# Operators submit a filter via createComplianceExport. The mutation
# inserts a queued row into compliance.export_jobs, sends an SQS message
# to the U11 runner Lambda (shipped in a follow-up PR), emits a
# data.export_initiated audit event, and returns the job. The runner
# streams matching audit_events rows to a 7-day-lifecycle S3 bucket as
# CSV or JSON, then publishes a 15-minute presigned URL.
#
# Hard caps enforced at the resolver:
#   - 90-day max range (until - since)
#   - 4 KB serialized filter byte cap (payload-balloon defense)
#   - 10 exports / hour per actor (DB-counter rate limit)

# Lowercase mirrors the DB constraint:
#   format text CHECK (format IN ('csv', 'json'))
# The schema permits 'json'; the runner emits NDJSON for that format.
enum ComplianceExportFormat {
  CSV
  JSON
}

# Lowercase mirrors the DB constraint:
#   status text CHECK (status IN ('queued', 'running', 'complete', 'failed'))
enum ComplianceExportStatus {
  QUEUED
  RUNNING
  COMPLETE
  FAILED
}

type ComplianceExport {
  jobId: ID!
  tenantId: ID!
  requestedByActorId: ID!
  requestedAt: String!
  status: ComplianceExportStatus!
  format: ComplianceExportFormat!
  filter: AWSJSON!
  s3Key: String
  presignedUrl: String
  presignedUrlExpiresAt: String
  jobError: String
  startedAt: String
  completedAt: String
}

extend type Mutation {
  """
  Queue an async export of audit events matching the filter. Validates:
    - 90-day cap on (until - since)
    - 4 KB serialized filter byte cap
    - 10 exports / hour rate limit per actor
  Throws typed errors with extensions.code in
    {RATE_LIMIT_EXCEEDED, FILTER_RANGE_TOO_WIDE, FILTER_TOO_LARGE,
     FORBIDDEN, UNAUTHENTICATED, INTERNAL_SERVER_ERROR}.
  Inserts the job row + emits data.export_initiated audit event in a
  single transaction; SQS dispatch happens after commit (queue write
  cannot be rolled back). If SQS send fails, the job is marked FAILED
  with jobError set.
  """
  createComplianceExport(
    filter: ComplianceEventFilter!
    format: ComplianceExportFormat!
  ): ComplianceExport!
}

extend type Query {
  """
  Caller's recent export jobs, sorted requested_at DESC, LIMIT 50.
  Operators see all tenants; non-operators are tenant-scoped via the
  same auth model as complianceEvents.
  """
  complianceExports: [ComplianceExport!]!
}
