/**
 * BaseProviderAdapter -- abstract base class for all LLM provider adapters.
 *
 * Provides:
 *   - Common retry logic with exponential back-off and jitter
 *   - Common LlmProviderError hierarchy instantiation
 *   - Common streaming-chunk accumulation
 *   - Common token / cost accounting normalisation
 *   - Common governance hook dispatch (PreTool / PostTool)
 *   - Audit event emission via configurable emitter
 *
 * Concrete adapters extend this class and override provider-specific
 * methods only. No provider-specific logic lives in this base.
 *
 * Spec: P3-SUB-01 AC 3 (BaseProviderAdapter)
 *
 * @connexum/ai-governance
 */
import { LlmProviderError, LlmAuthError, } from './index.js';
import { exponentialBackoffRetry, accumulateStream } from './shared.js';
// ---------------------------------------------------------------------------
// Cross-module-record compliance registry (process-singleton via globalThis)
// ---------------------------------------------------------------------------
//
// F-NEW-PROVIDER-COMPLIANCE-REGISTRY-CROSS-MODULE: hoist the COMPLIANCE_REGISTRY
// + UNVERIFIED_BAA_PROVIDERS Maps to globalThis slots keyed by Symbol.for(...)
// so all module records share one Map instance regardless of CJS/ESM split.
//
// Background: the root SDK package has no `"type"` field (CJS-default for tsx)
// while `packages/governance-server` declares `"type": "module"` (ESM). When
// a gov-server test imports an adapter module from the root SDK
// (e.g. `import { DeepSeekAdapter } from '../../src/llm-adapters/deepseek.js'`),
// tsx may load `base.ts` as a separate module record from the test's own
// `import { getProviderCompliance } from '../../src/llm-adapters/base.js'`.
// Module-private state is duplicated; registrations land in one Map and
// reads return undefined from the other.
//
// Same class of issue solved for ChainTamperDetectedError via Symbol.for
// branding (F-NEW-CHAIN-TAMPER-ERROR-DIVERGENCE PR #484). `Symbol.for(<key>)`
// returns the same symbol across all module records (it's keyed in the
// runtime's symbol registry, not module-local), so `host[REGISTRY_SYMBOL]`
// resolves to the same Map regardless of which copy of `base.ts` does
// the read.
const REGISTRY_SYMBOL = Symbol.for('@connexum/ai-governance/COMPLIANCE_REGISTRY');
const UNVERIFIED_SYMBOL = Symbol.for('@connexum/ai-governance/UNVERIFIED_BAA_PROVIDERS');
const _registryHost = globalThis;
/** Registered provider compliance metadata, keyed by providerId. Shared
 *  across all module records via globalThis + Symbol.for. */
const COMPLIANCE_REGISTRY = _registryHost[REGISTRY_SYMBOL] ??
    (_registryHost[REGISTRY_SYMBOL] = new Map());
/**
 * Providers whose original registration declared baa: true but was missing
 * attestationUrl and/or attestationDate. Stored as the ORIGINAL (pre-
 * normalization) registration so the boot-time audit hook can re-emit events
 * for them after the emitter has been wired. Shared across module records.
 *
 * Key: providerId. Value: original registration snapshot at time of call.
 */
const UNVERIFIED_BAA_PROVIDERS = _registryHost[UNVERIFIED_SYMBOL] ??
    (_registryHost[UNVERIFIED_SYMBOL] = new Map());
/**
 * Register provider compliance metadata.
 *
 * If `baa: true` is declared without `attestationUrl` or `attestationDate`,
 * this function emits a CRITICAL audit event and stores the registration
 * with `baa` treated as false for pack evaluation (AC spec Section 3.2).
 *
 * The audit emitter parameter is optional here because register() is
 * typically called at module load time, before an emitter is wired.
 * Boot hooks must call `auditUnverifiedBaaClaims(emit)` after wiring.
 */
export function registerProviderCompliance(registration, emit) {
    const effective = { ...registration };
    if (effective.baa && (!effective.attestationUrl || !effective.attestationDate)) {
        // Record the original (unverified) registration so the boot-time
        // auditUnverifiedBaaClaims hook can re-emit after the emitter is wired.
        UNVERIFIED_BAA_PROVIDERS.set(registration.providerId, { ...registration });
        if (emit) {
            emit({
                type: 'provider-unverified-baa-claim',
                severity: 'CRITICAL',
                adapterId: registration.providerId,
                providerId: registration.providerId,
                detail: `Provider '${registration.providerId}' declares baa: true but is missing ` +
                    'attestationUrl and/or attestationDate. Treating as baa: false for pack ' +
                    'evaluation until registration is corrected.',
                timestamp: new Date().toISOString(),
            });
        }
        // Treat as baa: false for evaluation
        effective.baa = false;
    }
    else {
        // If a previously-unverified provider is re-registered with full
        // attestation, remove it from the unverified set.
        UNVERIFIED_BAA_PROVIDERS.delete(registration.providerId);
    }
    COMPLIANCE_REGISTRY.set(registration.providerId, effective);
}
/**
 * Retrieve effective compliance registration for a provider.
 * Returns undefined if the provider has not registered.
 */
export function getProviderCompliance(providerId) {
    return COMPLIANCE_REGISTRY.get(providerId);
}
/**
 * Emit CRITICAL audit events for all registered providers with
 * unverified BAA claims. Call this at server boot after wiring the emitter.
 */
export function auditUnverifiedBaaClaims(emit) {
    for (const [providerId] of UNVERIFIED_BAA_PROVIDERS.entries()) {
        emit({
            type: 'provider-unverified-baa-claim',
            severity: 'CRITICAL',
            adapterId: providerId,
            providerId,
            detail: `Provider '${providerId}' has baa: true but no attestationUrl/attestationDate. ` +
                'Treated as baa: false. Fix registration before enabling HIPAA pack.',
            timestamp: new Date().toISOString(),
        });
    }
}
// ---------------------------------------------------------------------------
// BaseProviderAdapter
// ---------------------------------------------------------------------------
export class BaseProviderAdapter {
    config = null;
    initialized = false;
    auditEmit = null;
    // ---------------------------------------------------------------------------
    // Lifecycle
    // ---------------------------------------------------------------------------
    /**
     * Initialize the adapter. Subclasses call super.initialize(config) then
     * perform provider-specific setup.
     *
     * Throws LlmAuthError synchronously if credentials are structurally
     * missing (e.g. no apiKey when the provider requires one).
     *
     * F-NEW-CRIT-32: when config.activePacks is non-empty, consults
     * ProviderCompliance.check() and refuses to initialize if this provider
     * is blocked by any active pack (e.g., DeepSeek under DORA, HIPAA, etc.).
     * Closes the gap where pack.providerPolicy.blockedProviders was dead data.
     */
    initialize(config) {
        if (config.activePacks && config.activePacks.length > 0) {
            // Lazy-import to avoid a hard cycle with provider-compliance which
            // may register adapters via the same module graph.
            // eslint-disable-next-line @typescript-eslint/no-require-imports
            const { ProviderCompliance } = require('../security/provider-compliance.js');
            const enforcer = new ProviderCompliance({
                activePacks: config.activePacks,
                strictMode: config.providerComplianceStrictMode ?? true,
            });
            const result = enforcer.check(this.providerId);
            if (!result.allowed) {
                throw new LlmAuthError({
                    message: `[PROVIDER_BLOCKED_BY_PACK] Adapter '${this.adapterId}' targets provider ` +
                        `'${this.providerId}' which is blocked by active pack(s) ` +
                        `[${result.blockedByPacks.join(', ')}]. Reasons: ${result.reasons.join('; ')}`,
                    providerId: this.providerId,
                    adapterId: this.adapterId,
                });
            }
        }
        this.config = config;
        this.initialized = true;
    }
    /**
     * Wire an audit emitter. Called by the governance runtime after
     * initialization. Adapters emit events through this channel.
     */
    setAuditEmitter(emit) {
        this.auditEmit = emit;
    }
    ensureInitialized() {
        if (!this.initialized || !this.config) {
            throw new LlmAuthError({
                message: `Adapter '${this.adapterId}' must be initialized before use. Call initialize(config) first.`,
                providerId: this.providerId,
                adapterId: this.adapterId,
            });
        }
    }
    // ---------------------------------------------------------------------------
    // Common implementations (AC 3)
    // ---------------------------------------------------------------------------
    /**
     * Normalise token accounting from a response.
     * Subclasses may override if the provider returns additional fields.
     */
    accountTokens(response) {
        return response.usage ?? {
            inputTokens: 0,
            outputTokens: 0,
            totalTokens: 0,
        };
    }
    /**
     * Returns true if the registered compliance metadata for this provider
     * declares a BAA with valid attestation. Falls back to false if
     * unregistered or attestation is missing.
     */
    claimsBaa() {
        const reg = getProviderCompliance(this.providerId);
        if (!reg)
            return false;
        return reg.baa && !!reg.attestationUrl && !!reg.attestationDate;
    }
    // ---------------------------------------------------------------------------
    // Retry helper
    // ---------------------------------------------------------------------------
    /**
     * Execute `fn` with the common retry policy.
     * Auth failures and model-not-found errors are NOT retried.
     */
    async withRetry(fn, opts) {
        const maxRetries = this.config?.maxRetries ?? 3;
        return exponentialBackoffRetry(fn, {
            maxAttempts: maxRetries,
            retryIf: (err) => {
                // Do not retry auth failures or model-not-found errors
                if (err instanceof LlmAuthError)
                    return false;
                if (err instanceof LlmProviderError && err.code === 'MODEL_NOT_FOUND')
                    return false;
                // Retry on transient errors
                return true;
            },
            ...opts,
        });
    }
    // ---------------------------------------------------------------------------
    // Stream accumulation helper
    // ---------------------------------------------------------------------------
    /**
     * Accumulate a streaming response into a single LlmChatResponse.
     */
    async collectStream(request) {
        const stream = this.chatStream(request);
        return accumulateStream(stream, request.modelId, this.config?.timeoutMs ?? 60000);
    }
    // ---------------------------------------------------------------------------
    // Audit emission helper
    // ---------------------------------------------------------------------------
    emit(event) {
        if (!this.auditEmit)
            return;
        try {
            this.auditEmit(event);
        }
        catch {
            // Broken audit sink must not crash the governance path
        }
    }
}
//# sourceMappingURL=base.js.map