// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.

import { z } from 'zod';
import { FieldSchema } from './field.zod';
import { ValidationRuleSchema } from './validation.zod';
import { ActionSchema } from '../ui/action.zod';
import { ListViewSchema } from '../ui/view.zod';

/**
 * API Operations Enum
 */
import { ExpressionInputSchema , TemplateExpressionInputSchema } from '../shared/expression.zod';
import { lazySchema } from '../shared/lazy-schema';
import { MetadataProtectionFields } from '../kernel/metadata-protection.zod';
import { ProtectionSchema } from '../shared/protection.zod';
export const ApiMethod = z.enum([
  'get', 'list',          // Read
  'create', 'update', 'delete', // Write
  'upsert',               // Idempotent Write
  'bulk',                 // Batch operations
  'aggregate',            // Analytics (count, sum)
  'history',              // Audit access
  'search',               // Search access
  'restore', 'purge',     // Trash management
  'import', 'export',     // Data portability
]);
export type ApiMethod = z.infer<typeof ApiMethod>;

/**
 * Capability Flags
 * Defines what system features are enabled for this object.
 * 
 * Optimized based on industry standards (Salesforce, ServiceNow):
 * - Added `activities` (Tasks/Events)
 * - Added `mru` (Recent Items)
 * - Added `feeds` (Social/Chatter)
 * - Grouped API permissions
 * 
 * @example
 * {
 *   trackHistory: true,
 *   searchable: true,
 *   apiEnabled: true,
 *   files: true
 * }
 */
export const ObjectCapabilities = z.object({
  /** Enable history tracking (Audit Trail) */
  trackHistory: z.boolean().default(false).describe('Enable field history tracking for audit compliance'),
  
  /** Enable global search indexing */
  searchable: z.boolean().default(true).describe('Index records for global search'),
  
  /** Enable REST/GraphQL API access */
  apiEnabled: z.boolean().default(true).describe('Expose object via automatic APIs'),

  /** 
   * API Supported Operations
   * Granular control over API exposure.
   */
  apiMethods: z.array(ApiMethod).optional().describe('Whitelist of allowed API operations'),
  
  /** Enable standard attachments/files engine */
  files: z.boolean().default(false).describe('Enable file attachments and document management'),
  
  /** Enable social collaboration (Comments, Mentions, Feeds) */
  feeds: z.boolean().default(false).describe('Enable social feed, comments, and mentions (Chatter-like)'),
  
  /** Enable standard Activity suite (Tasks, Calendars, Events) */
  activities: z.boolean().default(false).describe('Enable standard tasks and events tracking'),
  
  /** Enable Recycle Bin / Soft Delete */
  trash: z.boolean().default(true).describe('Enable soft-delete with restore capability'),

  /** Enable "Recently Viewed" tracking */
  mru: z.boolean().default(true).describe('Track Most Recently Used (MRU) list for users'),
  
  /** Allow cloning records */
  clone: z.boolean().default(true).describe('Allow record deep cloning'),
});

/**
 * Schema for database indexes.
 * Enhanced with additional index types and configuration options
 * 
 * @example
 * {
 *   name: "idx_account_name",
 *   fields: ["name"],
 *   type: "btree",
 *   unique: true
 * }
 */
export const IndexSchema = lazySchema(() => z.object({
  name: z.string().optional().describe('Index name (auto-generated if not provided)'),
  fields: z.array(z.string()).describe('Fields included in the index'),
  type: z.enum(['btree', 'hash', 'gin', 'gist', 'fulltext']).optional().default('btree').describe('Index algorithm type'),
  unique: z.boolean().optional().default(false).describe('Whether the index enforces uniqueness'),
  partial: z.string().optional().describe('Partial index condition (SQL WHERE clause for conditional indexes)'),
}));

/**
 * Search Configuration
 * Defines how this object behaves in search results.
 * 
 * @example
 * {
 *   fields: ["name", "email", "phone"],
 *   displayFields: ["name", "title"],
 *   filters: ["status = 'active'"]
 * }
 */
export const SearchConfigSchema = lazySchema(() => z.object({
  fields: z.array(z.string()).describe('Fields to index for full-text search weighting'),
  displayFields: z.array(z.string()).optional().describe('Fields to display in search result cards'),
  filters: z.array(z.string()).optional().describe('Default filters for search results'),
}));

/**
 * Multi-Tenancy Configuration Schema
 * Configures tenant isolation strategy for SaaS applications
 * 
 * @example Shared database with tenant_id isolation
 * {
 *   enabled: true,
 *   strategy: 'shared',
 *   tenantField: 'tenant_id',
 *   crossTenantAccess: false
 * }
 */
export const TenancyConfigSchema = lazySchema(() => z.object({
  enabled: z.boolean().describe('Enable multi-tenancy for this object'),
  strategy: z.enum(['shared', 'isolated', 'hybrid']).describe('Tenant isolation strategy: shared (single DB, row-level), isolated (separate DB per tenant), hybrid (mix)'),
  tenantField: z.string().default('tenant_id').describe('Field name for tenant identifier'),
  crossTenantAccess: z.boolean().default(false).describe('Allow cross-tenant data access (with explicit permission)'),
}));

/**
 * Soft Delete Configuration Schema
 * Implements recycle bin / trash functionality
 * 
 * @example Standard soft delete with cascade
 * {
 *   enabled: true,
 *   field: 'deleted_at',
 *   cascadeDelete: true
 * }
 */
export const SoftDeleteConfigSchema = lazySchema(() => z.object({
  enabled: z.boolean().describe('Enable soft delete (trash/recycle bin)'),
  field: z.string().default('deleted_at').describe('Field name for soft delete timestamp'),
  cascadeDelete: z.boolean().default(false).describe('Cascade soft delete to related records'),
}));

/**
 * Versioning Configuration Schema
 * Implements record versioning and history tracking
 * 
 * @example Snapshot versioning with 90-day retention
 * {
 *   enabled: true,
 *   strategy: 'snapshot',
 *   retentionDays: 90,
 *   versionField: 'version'
 * }
 */
export const VersioningConfigSchema = lazySchema(() => z.object({
  enabled: z.boolean().describe('Enable record versioning'),
  strategy: z.enum(['snapshot', 'delta', 'event-sourcing']).describe('Versioning strategy: snapshot (full copy), delta (changes only), event-sourcing (event log)'),
  retentionDays: z.number().min(1).optional().describe('Number of days to retain old versions (undefined = infinite)'),
  versionField: z.string().default('version').describe('Field name for version number/timestamp'),
}));

/**
 * Partitioning Strategy Schema
 * Configures table partitioning for performance at scale
 * 
 * @example Range partitioning by date (monthly)
 * {
 *   enabled: true,
 *   strategy: 'range',
 *   key: 'created_at',
 *   interval: '1 month'
 * }
 */
export const PartitioningConfigSchema = lazySchema(() => z.object({
  enabled: z.boolean().describe('Enable table partitioning'),
  strategy: z.enum(['range', 'hash', 'list']).describe('Partitioning strategy: range (date ranges), hash (consistent hashing), list (predefined values)'),
  key: z.string().describe('Field name to partition by'),
  interval: z.string().optional().describe('Partition interval for range strategy (e.g., "1 month", "1 year")'),
}).refine((data) => {
  // If strategy is 'range', interval must be provided
  if (data.strategy === 'range' && !data.interval) {
    return false;
  }
  return true;
}, {
  message: 'interval is required when strategy is "range"',
}));

/**
 * Change Data Capture (CDC) Configuration Schema
 * Enables real-time data streaming to external systems
 * 
 * @example Stream all changes to Kafka
 * {
 *   enabled: true,
 *   events: ['insert', 'update', 'delete'],
 *   destination: 'kafka://events.objectstack'
 * }
 */
export const CDCConfigSchema = lazySchema(() => z.object({
  enabled: z.boolean().describe('Enable Change Data Capture'),
  events: z.array(z.enum(['insert', 'update', 'delete'])).describe('Event types to capture'),
  destination: z.string().describe('Destination endpoint (e.g., "kafka://topic", "webhook://url")'),
}));

/**
 * Object Field Group Schema — MVP (data-layer protocol)
 * 
 * Declares the set of logical field groups for an object. A group bundles
 * related fields together for presentation in forms, detail pages, and
 * editors (e.g., "Contact Info", "Billing", "System").
 * 
 * Design rules (MVP):
 * - Group **order** is the declaration order of this array — no `order` property.
 * - Field → group mapping is derived automatically from `Field.group`
 *   matching `ObjectFieldGroup.key`; the **in-group display order** equals
 *   the traversal order of `ObjectSchema.fields`.
 * - Fields whose `group` is unset (or references an undeclared key) are
 *   considered ungrouped and must be rendered by consumers in a default
 *   bucket after the declared groups, preserving their field declaration order.
 * - Extension packages and runtime code use `Field.group` to assign fields
 *   to an existing group — no per-field order property is introduced at this
 *   layer.
 * 
 * Migration operations supported by this MVP:
 *   - add / rename / delete / reorder groups (via the array)
 *   - assign an existing field to a group (via `Field.group`)
 * 
 * Deferred (not part of MVP):
 *   - explicit per-field in-group ordering
 *   - nested groups / sub-groups
 *   - permission-scoped group visibility beyond `visibleOn`
 * 
 * @example
 * ```ts
 * fieldGroups: [
 *   { key: 'contact_info', label: 'Contact Information', icon: 'user' },
 *   { key: 'billing',      label: 'Billing',             defaultExpanded: false },
 *   { key: 'system',       label: 'System',              visibleOn: '$user.isAdmin' },
 * ]
 * ```
 */
export const ObjectFieldGroupSchema = lazySchema(() => z.object({
  /** Group key — referenced by `Field.group` to assign a field to this group. Must be snake_case. */
  key: z.string().regex(/^[a-z_][a-z0-9_]*$/, {
    message: 'Field group key must be lowercase snake_case (e.g., "contact_info", "billing", "system")',
  }).describe('Group machine key (snake_case). Referenced by Field.group.'),

  /** Human-readable label displayed as the group header. */
  label: z.string().describe('Group display label'),

  /** Optional Lucide/Material icon name for the group header. */
  icon: z.string().optional().describe('Icon name (Lucide/Material) for the group header'),

  /** Optional description / help text shown under the group header. */
  description: z.string().optional().describe('Optional description shown under the group header'),

  /** Whether the group is expanded by default. Defaults to `true`. */
  defaultExpanded: z.boolean().optional().default(true).describe('Whether the group is expanded by default'),

  /** Optional visibility expression — when false, the entire group is hidden (e.g., "$user.isAdmin", "status == \'closed\'"). */
  visibleOn: ExpressionInputSchema.optional().describe('Visibility predicate (CEL); group is hidden when FALSE.'),
}));

export type ObjectFieldGroup = z.infer<typeof ObjectFieldGroupSchema>;
export type ObjectFieldGroupInput = z.input<typeof ObjectFieldGroupSchema>;

/**
 * Base Object Schema Definition
 * 
 * The Blueprint of a Business Object.
 * Represents a table, a collection, or a virtual entity.
 * 
 * @example
 * ```yaml
 * name: project_task
 * label: Project Task
 * icon: task
 * fields:
 *   project:
 *     type: lookup
 *     reference: project
 *   status:
 *     type: select
 *     options: [todo, in_progress, done]
 * enable:
 *   trackHistory: true
 *   files: true
 * ```
 */

/**
 * External Binding (ADR-0015)
 *
 * Optional per-object descriptor that binds this object to a remote table
 * on a federated datasource (one whose `schemaMode !== 'managed'`). When
 * present, the object is "external": DDL is forbidden, the table is
 * validated against the remote schema at boot, and writes require a double
 * opt-in (`datasource.external.allowWrites` **and** this `writable`).
 *
 * The cross-field invariant ("`external` only when the object's datasource
 * has `schemaMode !== 'managed'`") is enforced at metadata-load time, not
 * in this schema, because the datasource may live in another artefact.
 */
export const ObjectExternalBindingSchema = z.object({
  remoteName: z.string().optional()
    .describe('Remote table/view name. Defaults to object.name.'),
  remoteSchema: z.string().optional()
    .describe('Remote schema/database qualifier.'),
  writable: z.boolean().default(false)
    .describe('Per-object write opt-in (also requires datasource.external.allowWrites).'),
  columnMap: z.record(z.string(), z.string()).optional()
    .describe('Remote column name → local field name.'),
  introspectedAt: z.string().datetime().optional()
    .describe('Set by `os datasource introspect`; informational.'),
  ignoreColumns: z.array(z.string()).optional()
    .describe('Remote columns to skip during validation (dev convenience).'),
}).describe('External datasource binding (ADR-0015)');

export type ObjectExternalBinding = z.infer<typeof ObjectExternalBindingSchema>;

const ObjectSchemaBase = z.object({
  /** 
   * Identity & Metadata 
   */
  name: z.string().regex(/^[a-z_][a-z0-9_]*$/).describe('Machine unique key (snake_case). Immutable.'),
  label: z.string().optional().describe('Human readable singular label (e.g. "Account")'),
  pluralLabel: z.string().optional().describe('Human readable plural label (e.g. "Accounts")'),
  description: z.string().optional().describe('Developer documentation / description'),
  icon: z.string().optional().describe('Icon name (Lucide/Material) for UI representation'),

  /**
   * Taxonomy & Organization
   */
  tags: z.array(z.string()).optional().describe('Categorization tags (e.g. "sales", "system", "reference")'),
  active: z.boolean().optional().default(true).describe('Is the object active and usable'),
  isSystem: z.boolean().optional().default(false).describe('Is system object (protected from deletion)'),
  abstract: z.boolean().optional().default(false).describe('Is abstract base object (cannot be instantiated)'),

  /**
   * Managed-by hint — declares which lifecycle bucket the object belongs
   * to so UI clients render the appropriate set of CRUD affordances and
   * the security layer can enforce matching defaults. Modelled after the
   * way Salesforce / ServiceNow / Workday segregate user-owned business
   * data from admin-authored configuration, system-driven runtime rows,
   * and append-only audit trails.
   *
   * - `platform`     — **Default.** User-owned business data. Generic
   *   New / Import / Edit / Delete affordances are all shown. Example:
   *   the user's own `sys_attachment`, `sys_comment`, `sys_saved_report`.
   * - `config`       — Admin-authored metadata / configuration. Generic
   *   New / Edit / Delete shown (admins author via wizard or form), but
   *   CSV Import is suppressed (config rows have nested JSON envelopes
   *   that don't round-trip through a flat sheet; clients should offer a
   *   purpose-built "Import definition (JSON)" action instead). Example:
   *   `sys_sharing_rule`, `sys_role`, `sys_permission_set`, `sys_view`,
   *   `sys_app`.
   * - `system`       — Runtime rows whose lifecycle is owned by a
   *   platform service (the approval engine, the sharing engine, the
   *   invitation service, …). Generic CRUD is hidden — users interact
   *   with these via *domain actions* invoked from the source record
   *   (e.g. "Submit for Approval" on an Opportunity creates an
   *   `sys_approval_request`; "Recall" on the request changes its
   *   state). Example: `sys_approval_request`, `sys_record_share`,
   *   `sys_notification`, `sys_invitation`,
   *   `sys_user_permission_set` / `sys_role_permission_set`.
   * - `append-only`  — Immutable audit log. No New / Import / Edit /
   *   Delete; only View and Export. Example: `sys_approval_action`,
   *   `sys_audit_log`, `sys_activity`, `sys_email`, `sys_presence`.
   * - `better-auth`  — Identity tables owned by the better-auth driver
   *   (sys_user, sys_session, sys_account, sys_member, sys_organization,
   *   sys_api_key, sys_jwks, sys_verification, sys_two_factor,
   *   sys_oauth_*, sys_device_code). Mutations must flow through the
   *   better-auth API so password hashing, token signing, email
   *   verification, and invitation flows fire correctly. Generic CRUD
   *   suppressed; replaced by purpose-built actions
   *   (Invite User, Reset Password, Revoke Session, Rotate Key, …).
   *
   * The flag is purely declarative on the schema. Enforcement happens in
   * two places:
   *   1. Default permission sets ({@link packages/platform-objects/src/security/default-permission-sets.ts})
   *      deny direct CRUD for `system` / `append-only` / `better-auth`.
   *   2. UI clients honour {@link resolveCrudAffordances} to gate the
   *      New / Import / Edit / Delete / Export buttons accordingly.
   *
   * Use {@link userActions} to override the default matrix for a single
   * field (e.g. an "append-only" table that should still allow Export).
   */
  managedBy: z.enum(['platform', 'config', 'system', 'append-only', 'better-auth']).optional().describe(
    'Lifecycle bucket — platform (user CRUD) | config (admin authored) | system (engine-managed) | append-only (audit) | better-auth (identity). UI clients honour the resolved affordance matrix.',
  ),

  /**
   * Per-object override of the generic CRUD affordances that the UI
   * surfaces. Each flag overrides the default derived from
   * {@link managedBy} via {@link resolveCrudAffordances}. Useful for the
   * handful of objects whose lifecycle doesn't cleanly fit a single
   * bucket — e.g. an `append-only` table that should still expose CSV
   * Export, or a `config` table that admins legitimately want to bulk
   * import via CSV.
   *
   * Omitting the block (or leaving individual flags `undefined`) keeps
   * the {@link managedBy}-derived default.
   */
  userActions: z.object({
    create: z.boolean().optional().describe('Show generic "New" button.'),
    import: z.boolean().optional().describe('Show CSV import wizard entry.'),
    edit: z.boolean().optional().describe('Allow inline / form edit of existing rows.'),
    delete: z.boolean().optional().describe('Show row-level delete + bulk delete.'),
    exportCsv: z.boolean().optional().describe('Show CSV export entry.'),
  }).optional().describe('Per-object override of the resolved CRUD affordance matrix.'),

  /**
   * System-field auto-injection control.
   *
   * The `SchemaRegistry` augments every user object with a small set of
   * implicit system fields at registration time so authors don't have to
   * declare them per-object (Salesforce-style). Currently injected:
   *
   *   - `organization_id` — `lookup → sys_organization`. Injected only when
   *     the kernel runs in multi-tenant mode (`OS_MULTI_TENANT === 'true'`;
   *     default is off — single-tenant).
   *     Required for the default `tenant_isolation` RLS policy and the
   *     SecurityPlugin's auto-fill on insert to take effect.
   *
   * Author-declared fields with the same name always win over injection
   * (no overwrite). Objects with `managedBy` set are skipped entirely —
   * better-auth/system/platform tables already declare what they need.
   *
   * Set `systemFields: false` to opt the object out completely. Pass an
   * options object to selectively disable individual injections (currently
   * only `tenant`, but reserved keys `owner`/`audit` are pre-defined for
   * future expansion).
   *
   * @default undefined (= injection enabled, gated by kernel mode)
   */
  systemFields: z
    .union([
      z.literal(false),
      z.object({
        tenant: z.boolean().optional().describe('Inject organization_id (multi-tenant only). Default true.'),
        owner: z.boolean().optional().describe('Reserved for future owner_id auto-injection.'),
        audit: z.boolean().optional().describe('Reserved for future created_by/updated_by auto-injection.'),
      }),
    ])
    .optional()
    .describe('Opt out of, or selectively disable, registry-level system-field auto-injection.'),

  /** 
   * Storage & Virtualization 
   */
  datasource: z.string().optional().default('default').describe('Target Datasource ID. "default" is the primary DB.'),

  /**
   * External Binding (ADR-0015)
   * Present only for federated objects routed to a datasource whose
   * `schemaMode !== 'managed'`. Describes the remote table binding and
   * per-object writability. See {@link ObjectExternalBindingSchema}.
   */
  external: ObjectExternalBindingSchema.optional()
    .describe('Remote table binding for federated (external) objects.'),


  /**
   * Data Model
   */
  fields: z.record(z.string().regex(/^[a-z_][a-z0-9_]*$/, {
    message: 'Field names must be lowercase snake_case (e.g., "first_name", "company", "annual_revenue")',
  }), FieldSchema).describe('Field definitions map. Keys must be snake_case identifiers.'),
  indexes: z.array(IndexSchema).optional().describe('Database performance indexes'),

  /**
   * Field Groups (MVP)
   * 
   * Declares logical groups for presenting fields in forms and detail
   * pages. The **array order is the group display order**. Each field's
   * `Field.group` references an entry's `key` to assign it to a group;
   * within a group, fields are displayed in their `ObjectSchema.fields`
   * declaration order.
   * 
   * See {@link ObjectFieldGroupSchema} for the full MVP contract and
   * deferred features.
   */
  fieldGroups: z.array(ObjectFieldGroupSchema).refine(
    (groups) => new Set(groups.map(g => g.key)).size === groups.length,
    { message: 'fieldGroups[].key must be unique within an object' },
  ).optional().describe('Ordered list of field groups (array order = display order). See ObjectFieldGroupSchema.'),
  
  /**
   * Advanced Data Management
   */
  
  // Multi-tenancy configuration
  tenancy: TenancyConfigSchema.optional().describe('Multi-tenancy configuration for SaaS applications'),
  
  // Soft delete configuration
  softDelete: SoftDeleteConfigSchema.optional().describe('Soft delete (trash/recycle bin) configuration'),
  
  // Versioning configuration
  versioning: VersioningConfigSchema.optional().describe('Record versioning and history tracking configuration'),
  
  // Partitioning strategy
  partitioning: PartitioningConfigSchema.optional().describe('Table partitioning configuration for performance'),
  
  // Change Data Capture
  cdc: CDCConfigSchema.optional().describe('Change Data Capture (CDC) configuration for real-time data streaming'),
  
  /**
   * Logic & Validation (Co-located)
   * Best Practice: Define rules close to data.
   */
  validations: z.array(ValidationRuleSchema).optional().describe('Object-level validation rules'),

  // ADR-0020: record state machines are not a separate `stateMachines` map —
  // each lifecycle is a `state_machine` rule in `validations` above (one rule
  // per state field). Parallel lifecycles = multiple rules. The write path
  // enforces the transition table; UIs read the legal next states via the
  // `/meta/objects/:name/state/:field?from=` introspection endpoint.

  /**
   * Display & UI Hints (Data-Layer)
   */
  displayNameField: z.string().optional().describe('Field to use as the record display name (e.g., "name", "title"). Defaults to "name" if present.'),
  recordName: z.object({
    type: z.enum(['text', 'autonumber']).describe('Record name type: text (user-entered) or autonumber (system-generated)'),
    displayFormat: z.string().optional().describe('Auto-number format pattern (e.g., "CASE-{0000}", "INV-{YYYY}-{0000}")'),
    startNumber: z.number().int().min(0).optional().describe('Starting number for autonumber (default: 1)'),
  }).optional().describe('Record name generation configuration (Salesforce pattern)'),
  titleFormat: TemplateExpressionInputSchema.optional().describe('Title template — supports {{record.field}} interpolation. Overrides displayNameField.'),
  compactLayout: z.array(z.string()).optional().describe('Primary fields for hover/cards/lookups'),

  /**
   * Built-in List Views
   *
   * Curated, platform-shipped list views (grid / kanban / calendar / …)
   * keyed by view name. Rendered as segmented tabs in the console list page
   * **before** any user-saved `sys_view` rows. Use this for system objects
   * (audit, runtime, config) where the default "All records" grid lacks
   * business context — e.g. an approval-request list should ship with
   * "My pending", "I submitted", "Completed" tabs out of the box.
   *
   * Each value is a `ListViewSchema` (see `@objectstack/spec/ui`) so authors
   * get the full tab/filter/sort/grouping vocabulary.
   *
   * @example
   * ```ts
   * listViews: {
   *   my_pending: {
   *     type: 'grid',
   *     label: 'My Pending',
   *     filter: [{ field: 'pending_approvers', operator: 'contains', value: '{current_user_id}' }],
   *     sort: [{ field: 'updated_at', order: 'desc' }],
   *   },
   * }
   * ```
   */
  listViews: z.record(z.string(), ListViewSchema).optional().describe('Built-in named list views (segmented tabs) shipped with the object schema'),

  /**
   * Default FormView used when displaying a single record's detail page.
   *
   * Matches the named FormView under the same object's `ViewSchema.formViews`.
   * When unset, the runtime falls back to the unnamed `form` view, then to
   * an auto-generated layout. This is the Airtable-style "Interface form"
   * binding — every record-detail screen renders through a FormView, so the
   * same metadata drives both the public collection form and the operator's
   * edit panel.
   */
  defaultDetailForm: z.string().optional().describe('Name of the default FormView for record detail / edit screens'),

  /** 
   * Search Engine Config 
   */
  search: SearchConfigSchema.optional().describe('Search engine configuration'),
  
  /** 
   * System Capabilities 
   */
  enable: ObjectCapabilities.optional().describe('Enabled system features modules'),

  /** Record Types */
  recordTypes: z.array(z.string()).optional().describe('Record type names for this object'),

  /** Sharing Model */
  sharingModel: z.enum(['private', 'read', 'read_write', 'full']).optional().describe('Default sharing model'),

  /**
   * Public Share-Link Policy
   *
   * Opt-in declaration that records of this object MAY be published via
   * an opaque capability token (Notion / Google Docs / Figma "anyone with
   * the link" style). When omitted or `enabled:false`, the platform
   * refuses to create share-link rows for this object — independent of
   * any permission the caller holds.
   *
   * Distinct from {@link sharingModel}, which governs *principal-based*
   * sharing (share with specific users / teams / roles). A single object
   * can opt into both: principals get full edit, link recipients get
   * read-only with redaction.
   *
   * Defaults are conservative: when `enabled:true` and no other field is
   * provided, the plugin allows `link_only` audience + `view` permission
   * (the safest combination — caller still needs the URL to access).
   *
   * @see packages/plugins/plugin-sharing/src/share-link-service.ts
   */
  publicSharing: z.object({
    /** Master switch. When false (default), no share links can be issued for this object. */
    enabled: z.boolean().default(false).describe('Allow records of this object to be published via share link'),
    /**
     * Audiences the platform will accept when issuing a link.
     * - `public`       — search engines may index; no token check (rare)
     * - `link_only`    — anyone holding the token (default)
     * - `signed_in`    — token + an authenticated session of any tenant user
     * - `email`        — token + recipient's email matches an allowlist
     */
    allowedAudiences: z.array(z.enum(['public', 'link_only', 'signed_in', 'email'])).optional().describe('Audiences callers may select when creating a link'),
    /** Permission levels callers may grant via a link. Defaults to `['view']`. */
    allowedPermissions: z.array(z.enum(['view', 'comment', 'edit'])).optional().describe('Permission levels selectable on the share dialog'),
    /** Hard cap on requested expiry, in days. Links with `expires_at` further out are rejected. */
    maxExpiryDays: z.number().int().positive().optional().describe('Reject links with expiry beyond this many days'),
    /**
     * Fields stripped from every response served via a share token,
     * regardless of audience. Use for prompts, raw model output,
     * internal metadata, PII, etc. The owner's normal API access is
     * unaffected — redaction is applied only when the request principal
     * is `kind:'share-link'`.
     */
    redactFields: z.array(z.string()).optional().describe('Field names removed from records served via a share token'),
    /**
     * Optional CEL/JSONLogic predicate evaluated against the candidate
     * record when a link is created. When the predicate returns false,
     * the create call fails with 422 (e.g. "draft records cannot be
     * shared"). Evaluator is the same one used by sharing rules.
     */
    eligibility: z.string().optional().describe('CEL expression that must evaluate to true on the target record'),
  }).optional().describe('Public share-link policy (Notion/Figma-style link sharing)'),

  /** Key Prefix */
  keyPrefix: z.string().max(5).optional().describe('Short prefix for record IDs (e.g., "001" for Account)'),

  /**
   * Detail-page UI hints
   *
   * Per-object overrides for the synthesized record detail page (kind:'auto'
   * / slotted defaults). Each flag is consumed by the UI runtime
   * (`@object-ui/plugin-detail`'s `buildDefaultPageSchema`) when no explicit
   * `Page` covers this object. Authored full Pages should set these regions
   * directly instead.
   *
   * - `renderViaSchema=false` opts the object out of the schema-driven
   *   rendering path entirely (legacy `RecordDetailView`).
   * - `hideReferenceRail=true` suppresses the right-hand reference-rail
   *   (related-list summary cards) so the form gets full width — useful for
   *   catalog/atomic objects (Product, Task) where lateral relationships
   *   aren't the user's main job.
   * - `hideRelatedTab=true` suppresses the "Related" tab. By default the
   *   synth removes the Related tab automatically when the rail is shown to
   *   avoid duplication; set this flag to force hide/show regardless.
   */
  detail: z.object({
    renderViaSchema: z.boolean().optional().describe('Opt this object out of the schema-driven detail renderer'),
    hideReferenceRail: z.boolean().optional().describe('Suppress the right-hand reference-rail on the detail page'),
    hideRelatedTab: z.boolean().optional().describe('Suppress the Related tab on the detail page'),
  }).passthrough().optional().describe('Detail-page UI hints consumed by @object-ui/plugin-detail synth'),

  /**
   * Object Actions
   * 
   * Actions associated with this object. Populated automatically by `defineStack()`
   * when top-level actions specify `objectName` matching this object.
   * Can also be defined directly on the object.
   * 
   * Aligns with Salesforce/ServiceNow patterns where actions are part of the
   * object schema, so API responses (e.g., `/api/v1/meta/objects/:name`)
   * include the action list without requiring downstream merge.
   */
  actions: z.array(ActionSchema).optional().describe('Actions associated with this object (auto-populated from top-level actions via objectName)'),

  /**
   * ADR-0010 §3.7 — Package-level protection envelope. Package
   * authors declare lock policy here; the loader translates it
   * into the private `_lock` envelope at registration time and
   * strips this block before persistence. See
   * `shared/protection.zod.ts`.
   */
  protection: ProtectionSchema.optional().describe(
    'Package author protection block — lock policy for this object.',
  ),

  // ADR-0010 — runtime protection envelope (internal — set by loader).
  ...MetadataProtectionFields,
});

/**
 * Converts a snake_case name to a human-readable Title Case label.
 * @example snakeCaseToLabel('project_task') → 'Project Task'
 */
function snakeCaseToLabel(name: string): string {
  return name
    .split('_')
    .map(word => word.charAt(0).toUpperCase() + word.slice(1))
    .join(' ');
}

/**
 * Known-confusable schema keys → precise authoring guidance.
 *
 * ADR-0032's "no silent failure" principle applied to metadata *shape*: an
 * unknown top-level key on `ObjectSchema.create()` used to be discarded by
 * Zod's default `.strip()`, so a misauthored schema key vanished with no
 * error, no warning, and a green `tsc` — shipping dead metadata the author
 * believed they had wired up (issue #1535, object-level `workflows: [...]`).
 *
 * These entries turn the most likely mistakes into a fixable error that points
 * at the *supported* mechanism rather than a generic "unknown key".
 */
const UNKNOWN_KEY_GUIDANCE: Record<string, string> = {
  workflows:
    '`workflows` is not an ObjectSchema field. Object-level, record-triggered ' +
    'automation is authored as a lifecycle hook (`src/objects/<name>.hook.ts`, ' +
    'registered via `defineHook()`) or as a top-level `record_change` flow — ' +
    'not as `workflows[]` on the object schema.',
  workflow:
    '`workflow` is not an ObjectSchema field. Record-triggered automation is ' +
    'authored as a lifecycle hook (`src/objects/<name>.hook.ts`) or a top-level ' +
    '`record_change` flow.',
  hooks:
    '`hooks` is not an ObjectSchema field. Lifecycle hooks live in their own ' +
    '`src/objects/<name>.hook.ts` module, registered via `defineHook()`.',
  triggers:
    '`triggers` is not an ObjectSchema field. Use a lifecycle hook ' +
    '(`src/objects/<name>.hook.ts`) or a top-level `record_change` flow.',
};

/** Levenshtein edit distance — backs the "did you mean" hint for typo'd keys. */
function editDistance(a: string, b: string): number {
  const dp: number[][] = Array.from({ length: a.length + 1 }, () =>
    new Array<number>(b.length + 1).fill(0),
  );
  for (let i = 0; i <= a.length; i++) dp[i][0] = i;
  for (let j = 0; j <= b.length; j++) dp[0][j] = j;
  for (let i = 1; i <= a.length; i++) {
    for (let j = 1; j <= b.length; j++) {
      dp[i][j] = a[i - 1] === b[j - 1]
        ? dp[i - 1][j - 1]
        : 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
    }
  }
  return dp[a.length][b.length];
}

/** Closest known key within a small edit distance, for typo hints (`indexs` → `indexes`). */
function suggestKey(unknown: string, knownKeys: string[]): string | undefined {
  let best: string | undefined;
  let bestDist = Infinity;
  for (const key of knownKeys) {
    const d = editDistance(unknown.toLowerCase(), key.toLowerCase());
    if (d < bestDist) {
      bestDist = d;
      best = key;
    }
  }
  // Only suggest when the keys are genuinely close (guards against noise).
  return best !== undefined && bestDist <= Math.max(2, Math.floor(unknown.length / 3))
    ? best
    : undefined;
}

/**
 * Builds a precise, fixable error for unknown top-level keys on
 * `ObjectSchema.create()` — the metadata-shape analogue of ADR-0032's "no
 * silent failure" (issue #1535). Because authored `*.object.ts` modules call
 * `create()`, this surfaces as a located build error instead of a silently
 * stripped field.
 */
function unknownKeyError(objectName: unknown, unknownKeys: string[], knownKeys: string[]): Error {
  const name = typeof objectName === 'string' && objectName.length > 0 ? objectName : '<unnamed>';
  const lines = unknownKeys.map((key) => {
    const guidance = UNKNOWN_KEY_GUIDANCE[key];
    if (guidance) return `  • ${guidance}`;
    const suggestion = suggestKey(key, knownKeys);
    return suggestion
      ? `  • \`${key}\` is not an ObjectSchema field — did you mean \`${suggestion}\`?`
      : `  • \`${key}\` is not an ObjectSchema field.`;
  });
  return new Error(
    `ObjectSchema.create('${name}'): unknown key(s) — ${unknownKeys.join(', ')}.\n` +
    'These keys would previously have been stripped silently at build, shipping ' +
    'dead metadata with no diagnostic (ADR-0032 "no silent failure", issue #1535).\n\n' +
    `${lines.join('\n')}\n\n` +
    'Remove the unknown key(s), fix the typo, or move the logic to a supported mechanism.',
  );
}

/**
 * Rejects excess top-level keys at compile time: any key of `T` that is not a
 * key of the ObjectSchema input shape is constrained to `never`, turning the
 * silent strip into a `tsc` error at the authoring site as well as at build.
 */
type NoExcessObjectKeys<T> = T &
  Record<Exclude<keyof T, keyof z.input<typeof ObjectSchemaBase>>, never>;

/**
 * Enhanced ObjectSchema with Factory
 */
export const ObjectSchema = lazySchema(() => Object.assign(ObjectSchemaBase, {
  /**
   * Type-safe factory for creating business object definitions.
   * 
   * Enhancements over raw schema:
   * - **Auto-label**: Generates `label` from `name` if not provided (snake_case → Title Case).
   * - **Validation**: Runs Zod `.parse()` to validate the config at creation time.
   * - **No silent strip** (ADR-0032 / #1535): unknown top-level keys (e.g. a
   *   typo'd `validation`, or an object-level `workflows[]`) are rejected with a
   *   precise, fixable error instead of being discarded by Zod's `.strip()`.
   *
   * @example
   * ```ts
   * const Task = ObjectSchema.create({
   *   name: 'project_task',
   *   // label auto-generated as 'Project Task'
   *   fields: {
   *     subject: { type: 'text', label: 'Subject', required: true },
   *   },
   * });
   * ```
   */
  create: <const T extends z.input<typeof ObjectSchemaBase>>(config: NoExcessObjectKeys<T>): Omit<ServiceObject, 'fields'> & Pick<T, 'fields'> => {
    // ADR-0032 "no silent failure" for schema shape (issue #1535): an unknown
    // top-level key here used to be discarded silently by Zod's `.strip()`. We
    // reject it with a located, fixable message *before* parsing so authors get
    // a build error instead of vanished metadata.
    const cfg = config as T & Record<string, unknown>;
    const knownKeys = Object.keys(ObjectSchemaBase.shape);
    const unknownKeys = Object.keys(cfg).filter((k) => !knownKeys.includes(k));
    if (unknownKeys.length > 0) {
      throw unknownKeyError(cfg.name, unknownKeys, knownKeys);
    }
    const withDefaults = {
      ...cfg,
      label: cfg.label ?? snakeCaseToLabel(cfg.name as string),
    };
    return ObjectSchemaBase.parse(withDefaults) as Omit<ServiceObject, 'fields'> & Pick<T, 'fields'>;
  },
}));

export type ServiceObject = z.infer<typeof ObjectSchemaBase>;
export type ServiceObjectInput = z.input<typeof ObjectSchemaBase>;
export type ObjectCapabilities = z.infer<typeof ObjectCapabilities>;
export type ObjectIndex = z.infer<typeof IndexSchema>;
export type TenancyConfig = z.infer<typeof TenancyConfigSchema>;
export type SoftDeleteConfig = z.infer<typeof SoftDeleteConfigSchema>;
export type VersioningConfig = z.infer<typeof VersioningConfigSchema>;
export type PartitioningConfig = z.infer<typeof PartitioningConfigSchema>;
export type CDCConfig = z.infer<typeof CDCConfigSchema>;

/**
 * Resolved CRUD affordance matrix for an object — what generic
 * lifecycle actions UI clients should expose in their toolbars.
 *
 * Use {@link resolveCrudAffordances} to compute this from a schema; the
 * `managedBy` flag drives the defaults, and the optional `userActions`
 * block per-flag-overrides them. UI clients (`ObjectView`,
 * `RecordDetailView`, `RecordFormPage`, …) gate their buttons on this
 * matrix in combination with the user's permissions.
 *
 * The presence of an affordance here means "the *object* permits this
 * action conceptually"; the user still needs the matching permission
 * grant to execute it.
 */
export interface CrudAffordances {
  /** Generic "New" button (single record creation form). */
  create: boolean;
  /** CSV bulk-import wizard. Disabled for config / system / append-only / better-auth by default. */
  import: boolean;
  /** Inline + form editing of existing rows. */
  edit: boolean;
  /** Row-level + bulk delete. */
  delete: boolean;
  /** CSV / clipboard export. Allowed even on append-only audit tables by default. */
  exportCsv: boolean;
}

/**
 * Default affordance matrix per {@link ObjectSchemaBase.managedBy} bucket.
 * Mirrors how Salesforce / ServiceNow / Workday / Notion expose CRUD on
 * different categories of system tables.
 *
 *   platform     — full CRUD (user-owned business data)
 *   config       — admin authored: New/Edit/Delete OK, no CSV import
 *                  (definitions have nested envelopes; admins should use
 *                  a purpose-built "Import definition" action instead)
 *   system       — engine-managed runtime rows: no generic CRUD; users
 *                  interact via domain actions on the source record
 *   append-only  — audit log: View + Export only
 *   better-auth  — identity tables owned by better-auth driver; CRUD
 *                  routed through purpose-built actions (Invite, Reset
 *                  PW, Revoke, …)
 */
const CRUD_AFFORDANCE_DEFAULTS: Record<NonNullable<ServiceObject['managedBy']> | 'platform', CrudAffordances> = {
  platform:      { create: true,  import: true,  edit: true,  delete: true,  exportCsv: true },
  config:        { create: true,  import: false, edit: true,  delete: true,  exportCsv: true },
  system:        { create: false, import: false, edit: false, delete: false, exportCsv: true },
  'append-only': { create: false, import: false, edit: false, delete: false, exportCsv: true },
  'better-auth': { create: false, import: false, edit: false, delete: false, exportCsv: true },
};

/**
 * Resolve the effective CRUD affordance matrix for an object schema.
 *
 * Starts from the bucket default keyed off `managedBy` (defaulting to
 * `'platform'` if unset) and applies the per-flag overrides in
 * `userActions`. Returns a fresh object so callers can mutate safely.
 *
 * @example
 * ```ts
 * const aff = resolveCrudAffordances(sysApprovalRequestSchema);
 * // → { create:false, import:false, edit:false, delete:false, exportCsv:true }
 * ```
 */
export function resolveCrudAffordances(
  obj: Pick<ServiceObject, 'managedBy' | 'userActions'> | { managedBy?: string; userActions?: ServiceObject['userActions'] },
): CrudAffordances {
  const bucket = (obj?.managedBy ?? 'platform') as keyof typeof CRUD_AFFORDANCE_DEFAULTS;
  const base = CRUD_AFFORDANCE_DEFAULTS[bucket] ?? CRUD_AFFORDANCE_DEFAULTS.platform;
  const overrides = obj?.userActions ?? {};
  return {
    create:    overrides.create    ?? base.create,
    import:    overrides.import    ?? base.import,
    edit:      overrides.edit      ?? base.edit,
    delete:    overrides.delete    ?? base.delete,
    exportCsv: overrides.exportCsv ?? base.exportCsv,
  };
}

// =================================================================
// Object Ownership Model
// =================================================================

/**
 * How a package relates to an object it references.
 * 
 * - `own`: This package is the original author/owner of the object.
 *   Only one package may own a given object name. The owner defines
 *   the base schema (table name, primary key, core fields).
 * 
 * - `extend`: This package adds fields, views, or actions to an
 *   existing object owned by another package. Multiple packages
 *   may extend the same object. Extensions are merged at boot time.
 * 
 * Follows Salesforce/ServiceNow patterns:
 *   object name = database table name, globally unique, no namespace prefix.
 */
export const ObjectOwnershipEnum = z.enum(['own', 'extend']);
export type ObjectOwnership = z.infer<typeof ObjectOwnershipEnum>;

/**
 * Object Extension Entry — used in `objectExtensions` array.
 * Declares fields/config to merge into an existing object owned by another package.
 * 
 * @example
 * ```ts
 * objectExtensions: [{
 *   extend: 'contact',               // target object FQN
 *   fields: { sales_stage: Field.select([...]) },
 * }]
 * ```
 */
export const ObjectExtensionSchema = lazySchema(() => z.object({
  /** The target object name (FQN) to extend */
  extend: z.string().describe('Target object name (FQN) to extend'),
  
  /** Fields to merge into the target object (additive) */
  fields: z.record(z.string(), FieldSchema).optional().describe('Fields to add/override'),
  
  /** Override label */
  label: z.string().optional().describe('Override label for the extended object'),
  
  /** Override plural label */
  pluralLabel: z.string().optional().describe('Override plural label for the extended object'),
  
  /** Override description */
  description: z.string().optional().describe('Override description for the extended object'),
  
  /** Additional validation rules to add */
  validations: z.array(ValidationRuleSchema).optional().describe('Additional validation rules to merge into the target object'),
  
  /** Additional indexes to add */
  indexes: z.array(IndexSchema).optional().describe('Additional indexes to merge into the target object'),
  
  /** Merge priority. Higher number applied later (wins on conflict). Default: 200 */
  priority: z.number().int().min(0).max(999).default(200).describe('Merge priority (higher = applied later)'),
}));

export type ObjectExtension = z.infer<typeof ObjectExtensionSchema>;
