// contracts.ts — Proposed versioned contracts (capture/1, accepted-change/1, resolution.job/1, resolution.replay/1, projection-snapshot/1).
// Generated from assets/fixtures-contracts.js. Status: PROP unless marked EST.

export type ISODateTime = string;
export type Sha256 = string;

export interface CaptureContext {
  storefront_market: string;        // e.g. 'US' — what the storefront serves
  currency: string;                 // ISO 4217
  locale: string;                   // BCP 47
  execution_region: string | null;  // where the collector ran — never a market
  shipping_destination: string | null;
}

export interface ArtifactRef {
  artifact_id: string; ref: string; sha256: Sha256; media_type: string; bytes: number;
}

export interface StructuredClaim {
  attribute: string;                // namespace:key from an attribute definition
  value: unknown;
  evidence_locator: string;         // CSS selector, JSON pointer, or text offset
  method: string;                   // e.g. 'collector:json-ld@1'
}

// EST: capture example fields
export interface CaptureV1 {
  schema_version: 'capture/1';
  capture_id: string; batch_id: string;
  worker_id: string; instance_id: string; collector_version: string;
  source: { source_id: string; native_record_id: string; original_url: string };
  captured_at: ISODateTime;
  context: CaptureContext;
  capabilities: string[];
  artifacts: ArtifactRef[];
  structured_claims?: StructuredClaim[];
}

// Legacy receipt stage name 'published' means relay delivery, never public release.
export type ReceiptStatus = 'received' | 'stored' | 'processing' | 'review' | 'accepted' | 'published' | 'applied' | 'failed';
export interface ReceiptStage {
  stage: 'received' | 'stored' | 'processing' | 'review' | 'accepted' | 'published' | 'applied';
  at: ISODateTime | null;
  outcome: 'ok' | 'partial' | 'waiting' | 'pending' | 'failed' | 'skipped';
  detail: string | null;
}
// status is derived from stages: 'accepted' (resolver) is never 'applied' (product service).
export interface Receipt {
  receipt_id: string; capture_id: string | null;
  owner_instance_id: string; // stable across credential rotation
  status: ReceiptStatus;
  received_at: ISODateTime;
  idempotency_key?: string;
  stages?: ReceiptStage[];          // present on GET /v1/receipts/{id}
  idempotent_replay?: boolean;
}

export interface CollectContext { sourceId: string; market: string; locale: string; now: ISODateTime; signal: AbortSignal }

// EST: SourceCollector.collect
export interface SourceCollector {
  readonly workerId: string;
  readonly version: string;
  readonly capabilities: readonly string[];
  collect(ctx: CollectContext): AsyncIterable<CaptureV1>;
}

export interface ParseInput { capture: CaptureV1; artifact: ArtifactRef; body: Uint8Array; templateFingerprint?: string }
export interface ParseResult { claims: StructuredClaim[]; unmapped: { field: string; raw: string; locator: string }[]; diagnostics: string[] }

// EST: ParserAdapter.supports / parse
export interface ParserAdapter {
  readonly id: string;              // 'pa_lumen_specgrid'
  readonly version: string;         // semver (DEC-03)
  supports(input: { sourceId: string; mediaType: string; schema?: string; template?: string }): boolean;
  parse(input: ParseInput): Promise<ParseResult>;
}

// PROP: authenticated context. Tenant/catalog/sources come from the credential;
// client-supplied ids are only checked against these grants (403 TENANT_SCOPE_DENIED).
export interface AuthContext {
  tenant_id: string;                // 'ten_primary'
  principal: string;                // 'prn_collector_instance' | 'prn_operator_reviewer' | 'prn_product_service'
  principal_kind: 'collector' | 'operator' | 'service';
  client_id?: string;               // per-instance machine client, e.g. 'ci_lumen_cf'
  scopes: Permission[];
  instance_id?: string; worker_id?: string;                 // e.g. ['captures:write', 'artifacts:write']
  sources: string[];                // granted src_* (collectors); [] for operators
  catalogs?: string[];              // granted cat_* (product service exports)
  roles?: ('viewer' | 'reviewer' | 'operator_admin')[];
}

// PROP: artifact upload (two-step, content-addressed per tenant)
export interface ArtifactUploadRequest {
  source_id: string; // required granted source; worker/instance derived from authenticated context
  sha256: Sha256; bytes: number; media_type: string;
  capture_hint?: string;
  mode: 'presigned' | 'streamed';
}
export interface ArtifactUploadResponse {
  upload_id: string | null;
  status: 'pending' | 'already_stored' | 'stored';
  ref: string;                      // r2://evidence-private/...
  sha256: Sha256;
  target: { method: 'PUT'; url: string; headers: Record<string, string> } | null;
  expires_at?: ISODateTime;
}

export interface HeartbeatV1 {
  instance_id: string; at: ISODateTime;
  run?: { started_at: ISODateTime; outcome: 'succeeded' | 'failed'; error?: { code: string; retryable: boolean }; captures_submitted: number };
}

// EST: ResolverClient.submit / heartbeat
export interface ResolverClient {
  submit(capture: CaptureV1, opts: { idempotencyKey: string }): Promise<Receipt>;
  heartbeat(beat: HeartbeatV1): Promise<{ lease_expires_at: ISODateTime | null; consecutive_failures: number }>;
}

// EST: accepted-change event fields; PROP: tenant_id, catalog_id, sequence
export interface AcceptedChangeEventV1<P = unknown> {
  event_id: string;                 // stable across redelivery
  tenant_id: string;                // PROP · stamped from the outbox row
  catalog_id: string;               // PROP · tenant-private catalog, independent identity from tenant
  history_epoch: string;             // external restore fence; never auto-adopt from a delivery
  sequence: number;                 // transactional cursor row; contiguous committed positions per tenant/catalog/epoch; rollback consumes none
  type: string;                     // '*.upserted' | '*.created' | '*.corrected' | '*.retracted' | 'shipping_policy.revised' | 'inventory_observation.recorded'
  schema_version: 'accepted-change/1';
  aggregate: { type: string; id: string; revision: number };
  dependencies: { type: string; id: string; revision: number }[];
  decision_id: string;
  evidence: string[];               // capture ids
  observed_at: ISODateTime;         // when the source said it
  accepted_at: ISODateTime;         // resolver commit
  published_at: ISODateTime;        // outbox relay
  payload: P;
}

export type InboxOutcome = 'ignored_discarded_history' | 'applied' | 'ignored_duplicate' | 'ignored_stale_revision' | 'parked';

// PROP: queue messages carry refs + sha256 only — never raw artifacts.
// At-least-once; no ordering assumed; consumers dedupe on job_id / event_id; DLQ after N attempts.
export interface ArtifactRefOnly { artifact_id: string; ref: string; sha256: Sha256; media_type: string; bytes: number }

// PROP: queue resolution.job.v1 (intake → extraction / normalization / identity consumers)
export interface ResolutionJobV1 {
  schema_version: 'resolution.job/1';
  job_id: string;                   // capture_id:stage:attempt_group
  stage: 'extract' | 'normalize' | 'identity' | 'llm_extract' | 'llm_review';
  tenant_id: string; catalog_id: string; history_epoch: string;
  capture_id: string; receipt_id: string; source_id: string;
  artifacts: ArtifactRefOnly[];
  pins: { adapter?: string; profile?: string } | null;
  enqueued_at: ISODateTime;
  attempt_group: number;
  trace_id?: string;
}

// PROP: queue resolution.replay.v1 (one message per capture; preserves observed_at)
export interface ReplayJobV1 {
  schema_version: 'resolution.replay/1';
  job_id: string;                   // replay_id:capture_id
  replay_id: string; improvement_id: string;
  tenant_id: string; catalog_id: string; history_epoch: string;
  capture_id: string;
  artifacts: ArtifactRefOnly[];
  pins: { adapter: string; profile: string; identity_generation: number; definition_versions: Record<string, number> };
  mode: 'dry_run' | 'apply';
  preserve_observed_at: true;
  requested_by: string;             // principal id
  enqueued_at: ISODateTime;
}

// PROP: projection bootstrap — POST /v1/exports/snapshots, GET /v1/exports/snapshots/{id}
export interface ProjectionSnapshotManifestV1 {
  schema_version: 'projection-snapshot/1';
  export_id: string;
  tenant_id: string; catalog_id: string; 
  status: 'pending' | 'ready' | 'expired' | 'failed';
  format: 'ndjson+gzip';
  history_epoch: string; lease_id: string;
  watermark: { outbox_seq: number }; // same repeatable-read snapshot as exported state; retained tail > W
  created_at: ISODateTime; expires_at: ISODateTime;
  objects: { aggregate_type: string; ref: string; sha256: Sha256; records: number; bytes: number; download?: { url: string; expires_at: ISODateTime } }[];
}

// PROP: shipping evaluation (product service). One seller on one source per basket.
export type Tri = 'eligible' | 'ineligible' | 'unknown';
export interface ShippingEvaluationRequest {
  source_id: string; merchant_id: string;
  lines: { listing_id: string; quantity: number }[];
  destination: { country: string | null; subdivision: string | null; postcode: string | null };
  service: 'standard';
  currency: string;
  subtotal_basis: 'after_discounts_before_tax';
  customer_conditions?: Record<string, 'yes' | 'no' | 'unknown'>;
  evaluation?: { mode: 'current' | 'historical'; as_of?: ISODateTime | null };
}
export interface ShippingEvaluation {
  evaluation: { mode: 'current' | 'historical'; as_of: ISODateTime };
  scope: { source_id: string; merchant_id: string; service: string; currency: string };
  can_ship: { answer: Tri; clauses: string[]; reasons: string[]; missing_inputs: string[] };
  free_shipping: { answer: Tri; clauses: string[]; reasons: string[]; eligible_subtotal: { amount: number; currency: string } | null };
  cost: { kind: 'known'; amount: number; currency: string; clauses: string[] } | { kind: 'estimated' | 'unknown' | 'not_applicable'; clauses: string[] };
  delivery: { kind: 'known_estimate'; text: string; source: string; as_of: ISODateTime; caveats: string[] } | { kind: 'unknown' };
  policy: { id: string; revision: number; applied_revision: number; accepted: boolean; effective_from: ISODateTime; effective_to: ISODateTime | null; captured_at: ISODateTime; stale: boolean } | null;
  conflicts: string[];
}
export interface ShippingEvaluationError {
  error: 'BASKET_SPANS_MERCHANTS' | 'BASKET_SPANS_SOURCES' | 'BASKET_SCOPE_MISMATCH' | 'INVALID_DESTINATION' | 'INVALID_EVALUATION_TIME' | 'UNKNOWN_LISTING';
  detail: string;
  issues: { field: string; message: string }[];
  groups: { source: string; merchant: string; lines: string[] }[];
}
// Public schemas use allowlists, not omission from an operator object.
export interface PublicMerchant { published_revision: number; publication_revision: number; id: string; name: string; catalog_id: string; sources: string[]; listings: {id: string; original_url: string; observed_at: ISODateTime | null}[]; explanations: string[]; published_at: ISODateTime; }
export interface OperatorMerchant { merchant: PublicMerchant | null; private_evidence: unknown[]; review_cases: string[]; decisions: string[]; accepted_revision: number; publication_status: 'private' | 'published'; }
export interface PublicationCommand { tenant_id: string; catalog_id: string; aggregate: {type: string; id: string; revision: number}; explanations: string[]; allow_observation_metadata: boolean; }
export interface RetainedEventPage { history_epoch: string; lease_id: string; after: number; next_after: number; target: number; events: AcceptedChangeEventV1[]; }
export interface IdentityActivation { candidate_generation: number; expected_analysis_revision: number; approved_by: string; }

export type Permission = 'captures:write' | 'artifacts:write' | 'receipts:read' | 'workers:heartbeat' | 'workers:register' | 'reviews:read' | 'decisions:write' | 'attributes:publish' | 'replays:write' | 'events:publish' | 'events:consume' | 'projections:read' | 'projections:write' | 'evidence:read' | 'models:invoke' | 'secrets:read' | 'exports:read' | 'publications:write' | 'identity-generations:activate';

// Public release snapshots; never serialize a live internal projection directly.
export interface PublicItem { id: string; name: string; class: string; family: string; catalog_id: string; attributes: Record<string, unknown>; tags: string[]; published_revision: number; publication_revision: number; projection_revision: number; explanations: string[]; published_at: ISODateTime; last_observed_at: ISODateTime | null; }
export interface PublicItemCollection { items: PublicItem[]; note?: string; }
export interface PublicListing { listing_id: string; item_id: string; item_revision: number; mapping_revision: number; source_id: string; merchant_id: string; original_url: string; native_id: string; market: string; price: {amount: number; currency: string; observed_at: ISODateTime}; }
export interface PublicListings { item: string; catalog_id: string; published_revision: number; publication_revision: number; listings: PublicListing[]; }
export interface PublicInventory { item: string; catalog_id: string; published_revision: number; publication_revision: number; inventory: {listing_id: string; item_id: string; item_revision: number; mapping_revision: number; original_url: string; pool?: string | null; scope?: string; state: string; quantity?: number | null; observed_at?: ISODateTime; stale?: boolean; overlaps?: string[] | null}[]; note: string; }
export interface WorkerRegistrationRequest { worker_id: string; instance_id: string; version: string; runtime: string; runtime_kind: string; targets: string[]; capabilities?: string[]; execution_region?: {country: string; label?: string; declared_by: string}; egress?: {country: string; verified: boolean}; }
export interface WorkerRegistrationResponse { instance_id: string; lease_seconds: number; heartbeat_url: string; granted: {tenant_id: string; sources: string[]}; }

