/**
 * Phase 4 Pipeline -- Stage 6: ERP Client Generator
 *
 * Generates typed ERP surface client code for the resolved implementation
 * language. Produces a proper Anti-Corruption Layer
 * with domain-to-wire type mapping, governance enforcement, circuit breaker
 * protection, sync queue for offline resilience, and audit logging.
 *
 * TypeScript target: ERP client with ACL mapper, governance guard,
 *   circuit breaker, and retry utilities.
 * Rust target: reqwest-based async client with equivalent retry logic.
 *
 * All generated files carry a "do not edit manually" header.
 *
 * Exports:
 *   - generateErpClient(context) -- pipeline entry point
 *
 * Error codes:
 *   ECLI-P4-060  Missing prerequisite stage output (erp-target-resolution)
 *   ECLI-P4-061  ERP client file write failure
 */
import { mkdirSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { hasTemplateSupport } from '../types.js';
import { createSpan, endSpan, emitSpan } from '../../phase2/telemetry.js';
import { loadTechStack, getERPOperationPaths } from '../../phase2/phases/tech-stack-detector.js';
// ============================================================================
// Constants
// ============================================================================
const DIR_MODE = 0o700;
const FILE_MODE = 0o600;
const GENERATED_HEADER_TS = '// Generated by Phase 4 pipeline — do not edit manually\n';
const GENERATED_HEADER_RS = '// Generated by Phase 4 pipeline — do not edit manually\n';
function resolveErpClientNames(erpSystem) {
    const n = erpSystem.toLowerCase();
    if (n.includes('netsuite'))
        return { className: 'NetSuiteClient', mapperName: 'NetSuiteMapper', wirePrefix: 'NetSuite', displayName: 'NetSuite' };
    if (n.includes('sap'))
        return { className: 'SAPClient', mapperName: 'SAPMapper', wirePrefix: 'SAP', displayName: 'SAP' };
    if (n.includes('dynamics'))
        return { className: 'DynamicsClient', mapperName: 'DynamicsMapper', wirePrefix: 'Dynamics', displayName: 'Dynamics 365' };
    if (n.includes('deltek') || n.includes('costpoint'))
        return { className: 'DeltekClient', mapperName: 'DeltekMapper', wirePrefix: 'Deltek', displayName: 'Deltek Costpoint' };
    if (n.includes('guidewire'))
        return { className: 'GuidewireClient', mapperName: 'GuidewireMapper', wirePrefix: 'Guidewire', displayName: 'Guidewire' };
    if (n.includes('epicor'))
        return { className: 'EpicorClient', mapperName: 'EpicorMapper', wirePrefix: 'Epicor', displayName: 'Epicor' };
    if (n.includes('infor'))
        return { className: 'InforClient', mapperName: 'InforMapper', wirePrefix: 'Infor', displayName: 'Infor' };
    if (n.includes('workday'))
        return { className: 'WorkdayClient', mapperName: 'WorkdayMapper', wirePrefix: 'Workday', displayName: 'Workday' };
    if (n.includes('ifs'))
        return { className: 'IFSClient', mapperName: 'IFSMapper', wirePrefix: 'IFS', displayName: 'IFS' };
    if (n.includes('sage') || n.includes('intacct'))
        return { className: 'SageClient', mapperName: 'SageMapper', wirePrefix: 'Sage', displayName: 'Sage Intacct' };
    if (n.includes('oracle'))
        return { className: 'OracleERPClient', mapperName: 'OracleERPMapper', wirePrefix: 'OracleERP', displayName: 'Oracle ERP' };
    return { className: 'ERPClient', mapperName: 'ERPMapper', wirePrefix: 'ERP', displayName: 'ERP' };
}
// ============================================================================
// TypeScript Generation — ERP ACL
// ============================================================================
/**
 * Generate TypeScript types.ts with domain and ERP wire types.
 */
function tsTypes(erpSystem) {
    const names = resolveErpClientNames(erpSystem);
    const wp = names.wirePrefix;
    return [
        GENERATED_HEADER_TS,
        '/**',
        ` * ERP surface types for ${erpSystem}.`,
        ` * Domain types and ${names.displayName} wire format types for the ACL boundary.`,
        ' */',
        '',
        `export type ErpSystemType = '${erpSystem}';`,
        '',
        '// ============================================================================',
        '// Request/Response Envelope',
        '// ============================================================================',
        '',
        'export interface ErpSurfaceRequest {',
        '  readonly erp_type: ErpSystemType;',
        '  readonly operation: string;',
        '  readonly entity_type: string;',
        '  readonly query?: Record<string, unknown>;',
        '  readonly correlation_id: string;',
        '}',
        '',
        'export interface ErpSurfaceResponse {',
        '  readonly id: string;',
        '  readonly status: number;',
        '  readonly message: string;',
        '  readonly data: unknown;',
        '  readonly correlation_id: string;',
        '  readonly timestamp: string;',
        '}',
        '',
        '// ============================================================================',
        '// Domain Types (internal model)',
        '// ============================================================================',
        '',
        'export interface DomainTransferOrder {',
        '  readonly id: string;',
        '  readonly from_facility: string;',
        '  readonly to_facility: string;',
        '  readonly sku: string;',
        '  readonly quantity: number;',
        '  readonly status: string;',
        '  readonly priority: string;',
        '}',
        '',
        'export interface DomainPurchaseOrder {',
        '  readonly id: string;',
        '  readonly facility_id: string;',
        '  readonly sku: string;',
        '  readonly quantity: number;',
        '  readonly vendor_id: string;',
        '  readonly status: string;',
        '}',
        '',
        'export interface DomainInventoryBalance {',
        '  readonly facility_id: string;',
        '  readonly sku: string;',
        '  readonly quantity: number;',
        '  readonly unit_of_measure: string;',
        '  readonly last_synced_at: string;',
        '}',
        '',
        'export interface DomainItemReceipt {',
        '  readonly transfer_id: string;',
        '  readonly facility_id: string;',
        '  readonly sku: string;',
        '  readonly quantity_received: number;',
        '  readonly received_at: string;',
        '}',
        '',
        '// ============================================================================',
        `// ${names.displayName} Wire Types (ERP REST API)`,
        '// ============================================================================',
        '',
        `export interface ${wp}TransferOrder {`,
        '  readonly subsidiary: { readonly id: string };',
        '  readonly location: { readonly id: string };',
        '  readonly transferLocation: { readonly id: string };',
        '  readonly memo?: string;',
        '  readonly item: {',
        '    readonly items: ReadonlyArray<{',
        '      readonly item: { readonly id: string };',
        '      readonly quantity: number;',
        '      readonly units?: { readonly id: string };',
        '    }>;',
        '  };',
        '}',
        '',
        `export interface ${wp}PurchaseOrder {`,
        '  readonly entity: { readonly id: string };',
        '  readonly subsidiary: { readonly id: string };',
        '  readonly location: { readonly id: string };',
        '  readonly memo?: string;',
        '  readonly item: {',
        '    readonly items: ReadonlyArray<{',
        '      readonly item: { readonly id: string };',
        '      readonly quantity: number;',
        '      readonly rate?: number;',
        '    }>;',
        '  };',
        '}',
        '',
        `export interface ${wp}InventoryBalance {`,
        '  readonly subsidiary: { readonly id: string; readonly refName: string };',
        '  readonly location: { readonly id: string; readonly refName: string };',
        '  readonly item: { readonly id: string; readonly refName: string };',
        '  readonly quantityOnHand: number;',
        '  readonly quantityAvailable: number;',
        '  readonly lastModifiedDate: string;',
        '}',
        '',
        `export interface ${wp}ItemReceipt {`,
        '  readonly createdFrom: { readonly id: string };',
        '  readonly subsidiary: { readonly id: string };',
        '  readonly item: {',
        '    readonly items: ReadonlyArray<{',
        '      readonly item: { readonly id: string };',
        '      readonly quantity: number;',
        '      readonly location: { readonly id: string };',
        '    }>;',
        '  };',
        '}',
        '',
        '// ============================================================================',
        '// Governance Config',
        '// ============================================================================',
        '',
        'export interface GovernanceConfig {',
        '  readonly read_only: boolean;',
        '  readonly audit_enabled: boolean;',
        '  readonly pii_redaction: boolean;',
        '  readonly max_tokens_per_request: number;',
        '}',
        '',
        '// ============================================================================',
        '// Sync Queue Types',
        '// ============================================================================',
        '',
        'export interface SyncQueueEntry {',
        '  readonly id: string;',
        '  readonly operation: string;',
        '  readonly payload: unknown;',
        '  readonly created_at: string;',
        '  readonly retry_count: number;',
        '  readonly last_error: string | null;',
        '}',
        '',
        'export interface SyncStatus {',
        '  readonly last_successful_sync: string | null;',
        '  readonly pending_queue_depth: number;',
        '  readonly circuit_breaker_state: \'closed\' | \'open\' | \'half-open\';',
        '  readonly recent_errors: ReadonlyArray<{ timestamp: string; operation: string; error: string }>;',
        '}',
        '',
    ].join('\n');
}
/**
 * Generate the ERP mapper class — translates between domain and wire types.
 */
function tsMapper(erpSystem) {
    const names = resolveErpClientNames(erpSystem);
    const wp = names.wirePrefix;
    return [
        GENERATED_HEADER_TS,
        `import type {`,
        `  DomainTransferOrder,`,
        `  DomainPurchaseOrder,`,
        `  DomainInventoryBalance,`,
        `  DomainItemReceipt,`,
        `  ${wp}TransferOrder,`,
        `  ${wp}PurchaseOrder,`,
        `  ${wp}InventoryBalance,`,
        `  ${wp}ItemReceipt,`,
        `  GovernanceConfig,`,
        `} from './types.js';`,
        '',
        '// ============================================================================',
        '// PII Redaction',
        '// ============================================================================',
        '',
        'const PII_PATTERNS = [',
        '  /\\b\\d{3}-\\d{2}-\\d{4}\\b/g,                    // SSN',
        '  /\\b[A-Z]{2}\\d{6,9}\\b/g,                       // Passport',
        '  /\\b\\d{4}[\\s-]?\\d{4}[\\s-]?\\d{4}[\\s-]?\\d{4}\\b/g, // Credit card',
        '  /patient[_\\s]?(?:id|name|mrn)/gi,              // Patient identifiers',
        '];',
        '',
        'function redactPii(value: string): string {',
        '  let result = value;',
        '  for (const pattern of PII_PATTERNS) {',
        '    result = result.replace(pattern, \'[REDACTED]\');',
        '  }',
        '  return result;',
        '}',
        '',
        'function redactObject(obj: Record<string, unknown>, governance: GovernanceConfig): Record<string, unknown> {',
        '  if (!governance.pii_redaction) return obj;',
        '  const result: Record<string, unknown> = {};',
        '  for (const [key, value] of Object.entries(obj)) {',
        '    if (typeof value === \'string\') {',
        '      result[key] = redactPii(value);',
        '    } else if (typeof value === \'object\' && value !== null && !Array.isArray(value)) {',
        '      result[key] = redactObject(value as Record<string, unknown>, governance);',
        '    } else {',
        '      result[key] = value;',
        '    }',
        '  }',
        '  return result;',
        '}',
        '',
        '// ============================================================================',
        `// ${names.mapperName} — Domain ↔ Wire Type Translation`,
        '// ============================================================================',
        '',
        `export class ${names.mapperName} {`,
        '  constructor(private readonly governance: GovernanceConfig) {}',
        '',
        '  // --------------------------------------------------------------------------',
        `  // Domain → ${names.displayName} (outbound)`,
        '  // --------------------------------------------------------------------------',
        '',
        `  toTransferOrder(domain: DomainTransferOrder): ${wp}TransferOrder {`,
        `    const wire: ${wp}TransferOrder = {`,
        '      subsidiary: { id: \'1\' },',
        '      location: { id: domain.from_facility },',
        '      transferLocation: { id: domain.to_facility },',
        '      memo: `Transfer ${domain.sku} — priority: ${domain.priority}`,',
        '      item: {',
        '        items: [{',
        '          item: { id: domain.sku },',
        '          quantity: domain.quantity,',
        '          units: { id: \'1\' },',
        '        }],',
        '      },',
        '    };',
        '    if (this.governance.pii_redaction) {',
        '      return redactObject(wire as unknown as Record<string, unknown>, this.governance) as unknown as ${wp}TransferOrder;',
        '    }',
        '    return wire;',
        '  }',
        '',
        `  toPurchaseOrder(domain: DomainPurchaseOrder): ${wp}PurchaseOrder {`,
        `    const wire: ${wp}PurchaseOrder = {`,
        '      entity: { id: domain.vendor_id },',
        '      subsidiary: { id: \'1\' },',
        '      location: { id: domain.facility_id },',
        '      memo: `PO for ${domain.sku} — qty: ${domain.quantity}`,',
        '      item: {',
        '        items: [{',
        '          item: { id: domain.sku },',
        '          quantity: domain.quantity,',
        '        }],',
        '      },',
        '    };',
        '    if (this.governance.pii_redaction) {',
        '      return redactObject(wire as unknown as Record<string, unknown>, this.governance) as unknown as ${wp}PurchaseOrder;',
        '    }',
        '    return wire;',
        '  }',
        '',
        `  toItemReceipt(domain: DomainItemReceipt, transferNsId: string): ${wp}ItemReceipt {`,
        '    return {',
        '      createdFrom: { id: transferNsId },',
        '      subsidiary: { id: \'1\' },',
        '      item: {',
        '        items: [{',
        '          item: { id: domain.sku },',
        '          quantity: domain.quantity_received,',
        '          location: { id: domain.facility_id },',
        '        }],',
        '      },',
        '    };',
        '  }',
        '',
        '  // --------------------------------------------------------------------------',
        `  // ${names.displayName} → Domain (inbound)`,
        '  // --------------------------------------------------------------------------',
        '',
        `  fromInventoryBalance(ns: ${wp}InventoryBalance): DomainInventoryBalance {`,
        '    return {',
        '      facility_id: ns.location.id,',
        '      sku: ns.item.id,',
        '      quantity: ns.quantityAvailable,',
        '      unit_of_measure: \'EA\',',
        '      last_synced_at: ns.lastModifiedDate,',
        '    };',
        '  }',
        '}',
        '',
    ].join('\n');
}
/**
 * Generate TypeScript client.ts with ERP client ACL, governance guard,
 * circuit breaker, sync queue, and audit logging.
 */
function tsClient(erpSystem, paths) {
    const names = resolveErpClientNames(erpSystem);
    const op = paths ?? getERPOperationPaths(undefined);
    return [
        GENERATED_HEADER_TS,
        `import type {`,
        `  ErpSurfaceResponse,`,
        `  DomainTransferOrder,`,
        `  DomainPurchaseOrder,`,
        `  DomainItemReceipt,`,
        `  DomainInventoryBalance,`,
        `  GovernanceConfig,`,
        `  SyncQueueEntry,`,
        `  SyncStatus,`,
        `} from './types.js';`,
        `import { ${names.mapperName} } from './mapper.js';`,
        `import { withRetry, CircuitBreaker } from './retry.js';`,
        '',
        'const GCP_METADATA_TOKEN_URL =',
        '  \'http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity\';',
        '// ADR-050: Configurable timeout — bulk ERP operations can exceed 30s',
        'const REQUEST_TIMEOUT_MS = parseInt(process.env.ERP_REQUEST_TIMEOUT_MS ?? \'30000\', 10);',
        'const BULK_TIMEOUT_MS = parseInt(process.env.ERP_BULK_TIMEOUT_MS ?? \'120000\', 10);',
        '',
        '// ============================================================================',
        '// Audit Logger Interface',
        '// ============================================================================',
        '',
        'export interface AuditLogger {',
        '  log(entityType: string, entityId: string, action: string, actor: string, payload: unknown): Promise<void>;',
        '}',
        '',
        '// ============================================================================',
        `// ${names.className} — Anti-Corruption Layer`,
        '// ============================================================================',
        '',
        `export class ${names.className} {`,
        '  private readonly baseUrl: string;',
        '  private readonly serviceAccountEmail: string;',
        `  private readonly mapper: ${names.mapperName};`,
        '  private readonly breaker: CircuitBreaker;',
        '  private readonly governance: GovernanceConfig;',
        '  private readonly audit: AuditLogger | null;',
        '  private readonly syncQueue: SyncQueueEntry[] = [];',
        '  private readonly recentErrors: Array<{ timestamp: string; operation: string; error: string }> = [];',
        '  private lastSuccessfulSync: string | null = null;',
        '',
        '  constructor(config: {',
        '    baseUrl: string;',
        '    serviceAccountEmail: string;',
        '    governance: GovernanceConfig;',
        '    audit?: AuditLogger;',
        '    circuitBreakerThreshold?: number;',
        '    circuitBreakerResetMs?: number;',
        '  }) {',
        '    this.baseUrl = config.baseUrl.replace(/\\/$/, \'\');',
        '    this.serviceAccountEmail = config.serviceAccountEmail;',
        '    this.governance = config.governance;',
        `    this.mapper = new ${names.mapperName}(config.governance);`,
        '    this.breaker = new CircuitBreaker(',
        '      config.circuitBreakerThreshold ?? 5,',
        '      config.circuitBreakerResetMs ?? 60_000,',
        '    );',
        '    this.audit = config.audit ?? null;',
        '  }',
        '',
        '  // --------------------------------------------------------------------------',
        '  // Governance Guard',
        '  // --------------------------------------------------------------------------',
        '',
        '  private assertWriteAllowed(operation: string): void {',
        '    if (this.governance.read_only) {',
        '      throw new Error(`Governance: write operation \'${operation}\' blocked — read_only mode is enabled`);',
        '    }',
        '  }',
        '',
        '  // --------------------------------------------------------------------------',
        '  // Domain Operations (ACL boundary)',
        '  // --------------------------------------------------------------------------',
        '',
        '  async createTransferOrder(domain: DomainTransferOrder, correlationId: string): Promise<ErpSurfaceResponse> {',
        '    this.assertWriteAllowed(\'createTransferOrder\');',
        '    const wire = this.mapper.toTransferOrder(domain);',
        '    return this.executeWithFallback(\'createTransferOrder\', domain.id, correlationId, async () => {',
        `      return this.post('${op.transferOrder}', wire, correlationId);`,
        '    }, wire);',
        '  }',
        '',
        '  async createPurchaseOrder(domain: DomainPurchaseOrder, correlationId: string): Promise<ErpSurfaceResponse> {',
        '    this.assertWriteAllowed(\'createPurchaseOrder\');',
        '    const wire = this.mapper.toPurchaseOrder(domain);',
        '    return this.executeWithFallback(\'createPurchaseOrder\', domain.id, correlationId, async () => {',
        `      return this.post('${op.purchaseOrder}', wire, correlationId);`,
        '    }, wire);',
        '  }',
        '',
        '  async getInventoryBalance(subsidiary: string, location: string, item: string, correlationId: string): Promise<DomainInventoryBalance | null> {',
        '    await this.auditBefore(\'getInventoryBalance\', \'inventory\', correlationId, { subsidiary, location, item });',
        '    try {',
        '      const response = await this.breaker.execute(() =>',
        '        withRetry(() =>',
        `          this.get(\`${op.inventoryBalance}?subsidiary=\${encodeURIComponent(subsidiary)}&location=\${encodeURIComponent(location)}&item=\${encodeURIComponent(item)}\`, correlationId),`,
        '        ),',
        '      );',
        '      this.lastSuccessfulSync = new Date().toISOString();',
        '      await this.auditAfter(\'getInventoryBalance\', \'inventory\', correlationId, response);',
        '      if (response.status === 200 && response.data) {',
        '        return this.mapper.fromInventoryBalance(response.data as never);',
        '      }',
        '      return null;',
        '    } catch (err) {',
        '      this.recordError(\'getInventoryBalance\', err);',
        '      return null;',
        '    }',
        '  }',
        '',
        '  async updateItemReceipt(domain: DomainItemReceipt, transferNsId: string, correlationId: string): Promise<ErpSurfaceResponse> {',
        '    this.assertWriteAllowed(\'updateItemReceipt\');',
        '    const wire = this.mapper.toItemReceipt(domain, transferNsId);',
        '    return this.executeWithFallback(\'updateItemReceipt\', domain.transfer_id, correlationId, async () => {',
        `      return this.post('${op.itemReceipt}', wire, correlationId);`,
        '    }, wire);',
        '  }',
        '',
        '  // --------------------------------------------------------------------------',
        '  // Sync Queue & Status',
        '  // --------------------------------------------------------------------------',
        '',
        '  getSyncStatus(): SyncStatus {',
        '    return {',
        '      last_successful_sync: this.lastSuccessfulSync,',
        '      pending_queue_depth: this.syncQueue.length,',
        '      circuit_breaker_state: this.breaker.state,',
        '      recent_errors: this.recentErrors.slice(-20),',
        '    };',
        '  }',
        '',
        '  getPendingQueue(): ReadonlyArray<SyncQueueEntry> {',
        '    return this.syncQueue;',
        '  }',
        '',
        '  async drainSyncQueue(): Promise<{ processed: number; failed: number }> {',
        '    // ADR-049: Governance check — drainSyncQueue must respect read_only mode',
        '    this.assertWriteAllowed(\'drainSyncQueue\');',
        '',
        '    let processed = 0;',
        '    let failed = 0;',
        '    const remaining: SyncQueueEntry[] = [];',
        '',
        '    for (const entry of this.syncQueue) {',
        '      try {',
        '        await this.breaker.execute(() =>',
        '          withRetry(() =>',
        '            this.post(this.operationToPath(entry.operation), entry.payload, entry.id),',
        '          ),',
        '        );',
        '        processed++;',
        '        this.lastSuccessfulSync = new Date().toISOString();',
        '        // ADR-049: Audit successful drain',
        '        await this.auditAfter(entry.operation, entry.id, entry.id, { status: \'drained\', retry_count: entry.retry_count });',
        '      } catch (err) {',
        '        failed++;',
        '        // ADR-049: Audit failed drain attempt',
        '        await this.auditAfter(entry.operation, entry.id, entry.id, { status: \'drain_failed\', error: err instanceof Error ? err.message : String(err), retry_count: entry.retry_count + 1 });',
        '        remaining.push({',
        '          ...entry,',
        '          retry_count: entry.retry_count + 1,',
        '          last_error: err instanceof Error ? err.message : String(err),',
        '        });',
        '      }',
        '    }',
        '',
        '    this.syncQueue.length = 0;',
        '    this.syncQueue.push(...remaining);',
        '    return { processed, failed };',
        '  }',
        '',
        '  // --------------------------------------------------------------------------',
        '  // Internal: HTTP Transport',
        '  // --------------------------------------------------------------------------',
        '',
        '  private async post(path: string, body: unknown, correlationId: string): Promise<ErpSurfaceResponse> {',
        '    const token = await this.getIdentityToken();',
        '    const controller = new AbortController();',
        '    const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);',
        '',
        '    try {',
        '      const response = await fetch(`${this.baseUrl}${path}`, {',
        '        method: \'POST\',',
        '        headers: {',
        '          \'Content-Type\': \'application/json\',',
        '          \'Authorization\': `Bearer ${token}`,',
        '          \'X-Correlation-ID\': correlationId,',
        '        },',
        '        body: JSON.stringify(body),',
        '        signal: controller.signal,',
        '      });',
        '',
        '      if (!response.ok) {',
        `        throw new Error(\`${names.displayName} returned HTTP \${response.status}: \${await response.text()}\`);`,
        '      }',
        '      return response.json() as Promise<ErpSurfaceResponse>;',
        '    } finally {',
        '      clearTimeout(timeoutId);',
        '    }',
        '  }',
        '',
        '  private async get(path: string, correlationId: string): Promise<ErpSurfaceResponse> {',
        '    const token = await this.getIdentityToken();',
        '    const controller = new AbortController();',
        '    const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);',
        '',
        '    try {',
        '      const response = await fetch(`${this.baseUrl}${path}`, {',
        '        method: \'GET\',',
        '        headers: {',
        '          \'Authorization\': `Bearer ${token}`,',
        '          \'X-Correlation-ID\': correlationId,',
        '        },',
        '        signal: controller.signal,',
        '      });',
        '',
        '      if (!response.ok) {',
        `        throw new Error(\`${names.displayName} returned HTTP \${response.status}: \${await response.text()}\`);`,
        '      }',
        '      return response.json() as Promise<ErpSurfaceResponse>;',
        '    } finally {',
        '      clearTimeout(timeoutId);',
        '    }',
        '  }',
        '',
        '  // ADR-050: Token caching — GCP identity tokens have 1h TTL, cache for 55min',
        '  private cachedToken: { token: string; expiresAt: number } | null = null;',
        '',
        '  private async getIdentityToken(): Promise<string> {',
        '    // Return cached token if still valid (55min TTL)',
        '    if (this.cachedToken && Date.now() < this.cachedToken.expiresAt) {',
        '      return this.cachedToken.token;',
        '    }',
        '',
        '    const url = new URL(GCP_METADATA_TOKEN_URL);',
        '    url.searchParams.set(\'audience\', this.serviceAccountEmail);',
        '    const response = await fetch(url.toString(), {',
        '      headers: { \'Metadata-Flavor\': \'Google\' },',
        '      signal: AbortSignal.timeout(5_000),',
        '    });',
        '    if (!response.ok) {',
        '      throw new Error(`Failed to obtain GCP identity token: HTTP ${response.status}`);',
        '    }',
        '    const token = await response.text();',
        '    this.cachedToken = { token, expiresAt: Date.now() + 55 * 60 * 1000 };',
        '    return token;',
        '  }',
        '',
        '  // --------------------------------------------------------------------------',
        '  // Internal: Fallback & Queue',
        '  // --------------------------------------------------------------------------',
        '',
        '  private async executeWithFallback(',
        '    operation: string,',
        '    entityId: string,',
        '    correlationId: string,',
        '    fn: () => Promise<ErpSurfaceResponse>,',
        '    payload: unknown,',
        '  ): Promise<ErpSurfaceResponse> {',
        '    await this.auditBefore(operation, entityId, correlationId, payload);',
        '    try {',
        '      const result = await this.breaker.execute(() => withRetry(fn));',
        '      this.lastSuccessfulSync = new Date().toISOString();',
        '      await this.auditAfter(operation, entityId, correlationId, result);',
        '      return result;',
        '    } catch (err) {',
        '      this.recordError(operation, err);',
        '      // ADR-049: Audit the failure before queuing',
        '      await this.auditAfter(operation, entityId, correlationId, { status: \'queued\', error: err instanceof Error ? err.message : String(err) });',
        '      // Queue for retry when ERP is unreachable',
        '      this.syncQueue.push({',
        '        id: correlationId,',
        '        operation,',
        '        payload,',
        '        created_at: new Date().toISOString(),',
        '        retry_count: 0,',
        '        last_error: err instanceof Error ? err.message : String(err),',
        '      });',
        '      // Return a queued response so domain services can continue',
        '      return {',
        '        id: correlationId,',
        '        status: 202,',
        `        message: \`Queued for sync — ${names.displayName} temporarily unreachable\`,`,
        '        data: { queued: true, operation, entity_id: entityId },',
        '        correlation_id: correlationId,',
        '        timestamp: new Date().toISOString(),',
        '      };',
        '    }',
        '  }',
        '',
        '  private recordError(operation: string, err: unknown): void {',
        '    this.recentErrors.push({',
        '      timestamp: new Date().toISOString(),',
        '      operation,',
        '      error: err instanceof Error ? err.message : String(err),',
        '    });',
        '    if (this.recentErrors.length > 100) this.recentErrors.splice(0, this.recentErrors.length - 100);',
        '  }',
        '',
        '  private operationToPath(operation: string): string {',
        '    const paths: Record<string, string> = {',
        `      createTransferOrder: '${op.transferOrder}',`,
        `      createPurchaseOrder: '${op.purchaseOrder}',`,
        `      updateItemReceipt: '${op.itemReceipt}',`,
        '    };',
        `    return paths[operation] ?? '${op.defaultEntity}';`,
        '  }',
        '',
        '  // --------------------------------------------------------------------------',
        '  // Internal: Audit Logging',
        '  // --------------------------------------------------------------------------',
        '',
        '  private async auditBefore(operation: string, entityId: string, correlationId: string, payload: unknown): Promise<void> {',
        '    if (!this.governance.audit_enabled || !this.audit) return;',
        `    await this.audit.log('${op.auditLogLabel}', entityId, \`\${operation}:before\`, correlationId, payload);`,
        '  }',
        '',
        '  private async auditAfter(operation: string, entityId: string, correlationId: string, result: unknown): Promise<void> {',
        '    if (!this.governance.audit_enabled || !this.audit) return;',
        `    await this.audit.log('${op.auditLogLabel}', entityId, \`\${operation}:after\`, correlationId, result);`,
        '  }',
        '}',
        '',
    ].join('\n');
}
/**
 * Generate TypeScript retry.ts with exponential back-off and circuit breaker.
 */
function tsRetry() {
    return [
        GENERATED_HEADER_TS,
        'export interface RetryConfig {',
        '  readonly maxAttempts: number;',
        '  readonly initialDelayMs: number;',
        '  readonly multiplier: number;',
        '}',
        '',
        'const DEFAULT_RETRY_CONFIG: RetryConfig = {',
        '  maxAttempts: 3,',
        '  initialDelayMs: 1_000,',
        '  multiplier: 2,',
        '};',
        '',
        'export async function withRetry<T>(',
        '  fn: () => Promise<T>,',
        '  config: RetryConfig = DEFAULT_RETRY_CONFIG,',
        '): Promise<T> {',
        '  let lastError: unknown;',
        '  let delayMs = config.initialDelayMs;',
        '',
        '  for (let attempt = 1; attempt <= config.maxAttempts; attempt++) {',
        '    try {',
        '      return await fn();',
        '    } catch (err) {',
        '      lastError = err;',
        '      if (attempt < config.maxAttempts) {',
        '        // ADR-050: Jittered exponential backoff — prevents thundering herd on ERP recovery',
        '        const jitter = delayMs * (0.5 + Math.random() * 0.5);',
        '        await sleep(Math.min(Math.floor(jitter), 30_000));',
        '        delayMs *= config.multiplier;',
        '      }',
        '    }',
        '  }',
        '',
        '  throw lastError;',
        '}',
        '',
        'function sleep(ms: number): Promise<void> {',
        '  return new Promise((resolve) => setTimeout(resolve, ms));',
        '}',
        '',
        'export class CircuitBreaker {',
        '  private failures = 0;',
        '  private openedAt: number | null = null;',
        '',
        '  constructor(',
        '    private readonly threshold: number = 5,',
        '    private readonly resetTimeoutMs: number = 60_000,',
        '  ) {}',
        '',
        '  get state(): \'closed\' | \'open\' | \'half-open\' {',
        '    if (this.openedAt === null) return \'closed\';',
        '    if (Date.now() - this.openedAt >= this.resetTimeoutMs) return \'half-open\';',
        '    return \'open\';',
        '  }',
        '',
        '  get isOpen(): boolean {',
        '    if (this.openedAt === null) return false;',
        '    if (Date.now() - this.openedAt >= this.resetTimeoutMs) {',
        '      this.openedAt = null;',
        '      this.failures = 0;',
        '      return false;',
        '    }',
        '    return true;',
        '  }',
        '',
        '  async execute<T>(fn: () => Promise<T>): Promise<T> {',
        '    if (this.isOpen) {',
        '      throw new Error(\'Circuit breaker is open — ERP service unavailable\');',
        '    }',
        '    try {',
        '      const result = await fn();',
        '      this.failures = 0;',
        '      return result;',
        '    } catch (err) {',
        '      this.failures++;',
        '      if (this.failures >= this.threshold) {',
        '        this.openedAt = Date.now();',
        '      }',
        '      throw err;',
        '    }',
        '  }',
        '',
        '  reset(): void {',
        '    this.failures = 0;',
        '    this.openedAt = null;',
        '  }',
        '}',
        '',
    ].join('\n');
}
// ============================================================================
// Rust Generation (unchanged)
// ============================================================================
function rustTypes(erpSystem) {
    return [
        GENERATED_HEADER_RS,
        `//! ERP surface types for ${erpSystem}.`,
        '',
        'use serde::{Deserialize, Serialize};',
        'use std::collections::HashMap;',
        '',
        '#[derive(Debug, Clone, Serialize, Deserialize)]',
        'pub struct ErpSurfaceRequest {',
        `    pub erp_type: String,`,
        '    pub operation: String,',
        '    pub entity_type: String,',
        '    #[serde(skip_serializing_if = "Option::is_none")]',
        '    pub query: Option<HashMap<String, serde_json::Value>>,',
        '    pub correlation_id: String,',
        '}',
        '',
        '#[derive(Debug, Clone, Serialize, Deserialize)]',
        'pub struct ErpSurfaceResponse {',
        '    pub id: String,',
        '    pub status: u16,',
        '    pub message: String,',
        '    pub data: serde_json::Value,',
        '    pub correlation_id: String,',
        '    pub timestamp: String,',
        '}',
        '',
    ].join('\n');
}
function rustClient() {
    return [
        GENERATED_HEADER_RS,
        'use crate::erp_client::types::{ErpSurfaceRequest, ErpSurfaceResponse};',
        'use crate::erp_client::retry::with_retry;',
        '',
        'const GCP_METADATA_TOKEN_URL: &str =',
        '    "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity";',
        'const REQUEST_TIMEOUT_SECS: u64 = 30;',
        '',
        'pub struct ErpSurfaceClient {',
        '    base_url: String,',
        '    service_account_email: String,',
        '    http: reqwest::Client,',
        '}',
        '',
        'impl ErpSurfaceClient {',
        '    pub fn new(base_url: String, service_account_email: String) -> Self {',
        '        let http = reqwest::Client::builder()',
        '            .timeout(std::time::Duration::from_secs(REQUEST_TIMEOUT_SECS))',
        '            .build()',
        '            .expect("Failed to build HTTP client");',
        '        let base_url = base_url.trim_end_matches(\'/\').to_string();',
        '        Self { base_url, service_account_email, http }',
        '    }',
        '',
        '    pub async fn push_entity(',
        '        &self,',
        '        request: ErpSurfaceRequest,',
        '    ) -> Result<ErpSurfaceResponse, Box<dyn std::error::Error>> {',
        '        let client = self;',
        '        with_retry(|| async {',
        '            let token = client.get_identity_token().await?;',
        '            let url = format!("{}/v1/erpsurface", client.base_url);',
        '            let response = client.http',
        '                .post(&url)',
        '                .header("Authorization", format!("Bearer {}", token))',
        '                .header("Content-Type", "application/json")',
        '                .json(&request)',
        '                .send()',
        '                .await?;',
        '',
        '            if !response.status().is_success() {',
        '                let status = response.status().as_u16();',
        '                let body = response.text().await.unwrap_or_default();',
        '                return Err(format!("ERP surface returned HTTP {}: {}", status, body).into());',
        '            }',
        '',
        '            Ok(response.json::<ErpSurfaceResponse>().await?)',
        '        }).await',
        '    }',
        '',
        '    async fn get_identity_token(&self) -> Result<String, Box<dyn std::error::Error>> {',
        '        let url = format!("{}?audience={}", GCP_METADATA_TOKEN_URL, self.service_account_email);',
        '        let token = self.http',
        '            .get(&url)',
        '            .header("Metadata-Flavor", "Google")',
        '            .send()',
        '            .await?',
        '            .error_for_status()?',
        '            .text()',
        '            .await?;',
        '        Ok(token)',
        '    }',
        '}',
        '',
    ].join('\n');
}
function rustRetry() {
    return [
        GENERATED_HEADER_RS,
        'use std::future::Future;',
        'use std::sync::{Arc, Mutex};',
        'use std::time::{Duration, Instant};',
        '',
        '#[derive(Debug, Clone)]',
        'pub struct RetryConfig {',
        '    pub max_attempts: u32,',
        '    pub initial_delay: Duration,',
        '    pub multiplier: u32,',
        '}',
        '',
        'impl Default for RetryConfig {',
        '    fn default() -> Self {',
        '        Self { max_attempts: 3, initial_delay: Duration::from_secs(1), multiplier: 2 }',
        '    }',
        '}',
        '',
        'pub async fn with_retry<F, Fut, T, E>(mut f: F) -> Result<T, E>',
        'where',
        '    F: FnMut() -> Fut,',
        '    Fut: Future<Output = Result<T, E>>,',
        '{',
        '    let config = RetryConfig::default();',
        '    let mut delay = config.initial_delay;',
        '    let mut last_err: Option<E> = None;',
        '',
        '    for attempt in 1..=config.max_attempts {',
        '        match f().await {',
        '            Ok(value) => return Ok(value),',
        '            Err(err) => {',
        '                last_err = Some(err);',
        '                if attempt < config.max_attempts {',
        '                    tokio::time::sleep(delay).await;',
        '                    delay *= config.multiplier;',
        '                }',
        '            }',
        '        }',
        '    }',
        '    Err(last_err.unwrap())',
        '}',
        '',
        '#[derive(Debug)]',
        'struct CircuitBreakerState {',
        '    failures: u32,',
        '    opened_at: Option<Instant>,',
        '}',
        '',
        '#[derive(Debug, Clone)]',
        'pub struct CircuitBreaker {',
        '    threshold: u32,',
        '    reset_timeout: Duration,',
        '    state: Arc<Mutex<CircuitBreakerState>>,',
        '}',
        '',
        'impl CircuitBreaker {',
        '    pub fn new(threshold: u32, reset_timeout_ms: u64) -> Self {',
        '        Self {',
        '            threshold,',
        '            reset_timeout: Duration::from_millis(reset_timeout_ms),',
        '            state: Arc::new(Mutex::new(CircuitBreakerState { failures: 0, opened_at: None })),',
        '        }',
        '    }',
        '',
        '    pub async fn execute<F, Fut, T>(&self, f: F) -> Result<T, Box<dyn std::error::Error>>',
        '    where',
        '        F: FnOnce() -> Fut,',
        '        Fut: Future<Output = Result<T, Box<dyn std::error::Error>>>,',
        '    {',
        '        {',
        '            let mut state = self.state.lock().unwrap();',
        '            if let Some(opened_at) = state.opened_at {',
        '                if opened_at.elapsed() < self.reset_timeout {',
        '                    return Err("Circuit breaker is open — downstream service unavailable".into());',
        '                }',
        '                state.failures = 0;',
        '                state.opened_at = None;',
        '            }',
        '        }',
        '        match f().await {',
        '            Ok(value) => {',
        '                self.state.lock().unwrap().failures = 0;',
        '                Ok(value)',
        '            }',
        '            Err(err) => {',
        '                let mut state = self.state.lock().unwrap();',
        '                state.failures += 1;',
        '                if state.failures >= self.threshold {',
        '                    state.opened_at = Some(Instant::now());',
        '                }',
        '                Err(err)',
        '            }',
        '        }',
        '    }',
        '}',
        '',
        'impl Default for CircuitBreaker {',
        '    fn default() -> Self { Self::new(5, 60_000) }',
        '}',
        '',
    ].join('\n');
}
// ============================================================================
// File Writing
// ============================================================================
function writeClientFile(absPath, content) {
    const dir = absPath.substring(0, absPath.lastIndexOf('/'));
    mkdirSync(dir, { recursive: true, mode: DIR_MODE });
    try {
        writeFileSync(absPath, content, { encoding: 'utf-8', mode: FILE_MODE });
    }
    catch (err) {
        const message = err instanceof Error ? err.message : String(err);
        throw new Error(`ECLI-P4-061: Failed to write ERP client file ${absPath}: ${message}`);
    }
}
// ============================================================================
// Public: Pipeline Entry Point
// ============================================================================
export function generateErpClient(context) {
    const startTime = Date.now();
    const rootSpan = createSpan('erp-client-generation', 'generate-erp-client', {
        phase4Dir: context.phase4Dir,
        traceId: context.traceId,
    });
    try {
        const erpStage = context.stageOutputs.get('erp-target-resolution');
        if (!erpStage || erpStage.status !== 'completed') {
            throw new Error('ECLI-P4-060: ERP target resolution not available on context. ' +
                'Ensure the erp-target-resolution stage completed successfully before running erp-client-generation.');
        }
        const erpResolution = erpStage.data;
        const erpSystem = erpResolution.erpSystem;
        const language = context.phase3Artifacts.implementationLanguage;
        // Non-TS/Rust languages handled by Ruflo swarm
        if (!hasTemplateSupport(language)) {
            const span = endSpan(rootSpan, 'ok', { delegated: true, language });
            emitSpan(span);
            return {
                stage: 'erp-client-generation',
                status: 'completed',
                data: { files: [], endpointCount: 0 },
                artifacts: [],
                timing: Date.now() - startTime,
                summary: `ERP client for ${language} delegated to Ruflo swarm.`,
            };
        }
        const isTs = language === 'typescript';
        const clientDir = join(context.phase4Dir, 'project', 'src', 'erp-client');
        mkdirSync(clientDir, { recursive: true, mode: DIR_MODE });
        const clientFiles = [];
        const writtenPaths = [];
        const genSpan = createSpan('erp-client-generation', 'generate-files', { language, erpSystem }, rootSpan.spanId);
        // ADR-029 Fix 1: Load tech stack from Phase 2 for ERP-specific API paths
        const techStack = loadTechStack(context.phase2Dir);
        const erpPaths = techStack?.erp
            ? getERPOperationPaths(techStack.erp.slug)
            : getERPOperationPaths(erpResolution.erpSystem.toLowerCase().includes('sap') ? 'sap-s4hana'
                : erpResolution.erpSystem.toLowerCase().includes('oracle') ? 'oracle-fusion'
                    : erpResolution.erpSystem.toLowerCase().includes('workday') ? 'workday'
                        : erpResolution.erpSystem.toLowerCase().includes('dynamics') ? 'dynamics-365'
                            : erpResolution.erpSystem.toLowerCase().includes('infor') ? 'infor-cloudsuite'
                                : 'netsuite');
        if (isTs) {
            const files = [
                { name: 'types.ts', content: tsTypes(erpSystem) },
                { name: 'mapper.ts', content: tsMapper(erpSystem) },
                { name: 'client.ts', content: tsClient(erpSystem, erpPaths) },
                { name: 'retry.ts', content: tsRetry() },
            ];
            for (const file of files) {
                const absPath = join(clientDir, file.name);
                writeClientFile(absPath, file.content);
                const relPath = `project/src/erp-client/${file.name}`;
                clientFiles.push({ relativePath: relPath, content: file.content, kind: 'erp-client' });
                writtenPaths.push(absPath);
            }
        }
        else {
            const rustClientDir = join(context.phase4Dir, 'project', 'src', 'erp_client');
            mkdirSync(rustClientDir, { recursive: true, mode: DIR_MODE });
            const files = [
                { name: 'types.rs', content: rustTypes(erpSystem) },
                { name: 'client.rs', content: rustClient() },
                { name: 'retry.rs', content: rustRetry() },
            ];
            for (const file of files) {
                const absPath = join(rustClientDir, file.name);
                writeClientFile(absPath, file.content);
                const relPath = `project/src/erp_client/${file.name}`;
                clientFiles.push({ relativePath: relPath, content: file.content, kind: 'erp-client' });
                writtenPaths.push(absPath);
            }
            const modContent = [GENERATED_HEADER_RS, 'pub mod client;', 'pub mod retry;', 'pub mod types;', ''].join('\n');
            const modPath = join(rustClientDir, 'mod.rs');
            writeClientFile(modPath, modContent);
            clientFiles.push({ relativePath: 'project/src/erp_client/mod.rs', content: modContent, kind: 'erp-client' });
            writtenPaths.push(modPath);
        }
        // 4 domain operations: createTransferOrder, createPurchaseOrder, getInventoryBalance, updateItemReceipt
        const endpointCount = 4;
        const finalGenSpan = endSpan(genSpan, 'ok', { filesGenerated: writtenPaths.length, endpointCount });
        emitSpan(finalGenSpan);
        const finalRootSpan = endSpan(rootSpan, 'ok', {
            language, erpSystem, filesGenerated: writtenPaths.length, endpointCount,
        });
        emitSpan(finalRootSpan);
        const timing = Date.now() - startTime;
        const output = {
            stage: 'erp-client-generation',
            status: 'completed',
            data: { files: clientFiles, endpointCount },
            artifacts: writtenPaths,
            timing,
            summary: `Generated ERP client (${language}) for ${erpSystem}: ${writtenPaths.length} files, ${endpointCount} endpoint types (${timing}ms)`,
        };
        context.stageOutputs.set('erp-client-generation', output);
        return output;
    }
    catch (err) {
        const errorMessage = err instanceof Error ? err.message : String(err);
        const finalRootSpan = endSpan(rootSpan, 'error', { error: errorMessage });
        emitSpan(finalRootSpan);
        const timing = Date.now() - startTime;
        const output = {
            stage: 'erp-client-generation',
            status: 'failed',
            data: { error: errorMessage },
            artifacts: [],
            timing,
            summary: `ERP client generation failed: ${errorMessage}`,
        };
        context.stageOutputs.set('erp-client-generation', output);
        return output;
    }
}
//# sourceMappingURL=erp-client-generator.js.map