/**
 * HuggingFace Governance Adapter
 *
 * Maps HuggingFace Inference API and Inference Endpoints function calling to
 * the GovernanceAdapter interface.
 *
 * TWO API variants are supported, selected by the REQUIRED `apiVariant` field:
 *
 * 1. 'inference-api' (shared/free):
 *    POST https://api-inference.huggingface.co/models/<modelId>
 *    Auth: Authorization: Bearer <HF_TOKEN>
 *    Body: { inputs: '<prompt>', parameters: { ... } }
 *    Response: [{ generated_text: '...' }]
 *    Rate-limited on the free tier. `modelId` is required.
 *
 * 2. 'inference-endpoints' (managed/paid):
 *    POST <endpointUrl>  (custom HTTPS URL per endpoint)
 *    Auth: same Bearer token
 *    Body shape depends on endpoint task; text-generation and chat-completion
 *    endpoints mirror the OpenAI chat-completions shape (messages, tools, etc.)
 *    `endpointUrl` is required. `modelId` is optional (used for audit metadata).
 *
 * Variant discriminator: `apiVariant: 'inference-api' | 'inference-endpoints'`
 *   - Cross-contamination (e.g. providing endpointUrl in 'inference-api' variant)
 *     is rejected at construction.
 *   - Missing required fields per variant are rejected at construction.
 *
 * Tool calling:
 *   Mistral, Llama 3.1+, Qwen, and Zephyr models support tool calls via their
 *   respective prompt templates. This adapter passes tool definitions through
 *   to the model and parses tool_call output from the response, consistent with
 *   the OpenAI/DeepSeek tool_calls format.
 *
 * Commonly supported models on HuggingFace:
 *   mistralai/Mistral-7B-Instruct-v0.3
 *   mistralai/Mixtral-8x7B-Instruct-v0.1
 *   meta-llama/Meta-Llama-3-8B-Instruct
 *   meta-llama/Meta-Llama-3.1-70B-Instruct
 *   Qwen/Qwen2.5-72B-Instruct
 *   tiiuae/falcon-7b-instruct
 *   HuggingFaceH4/zephyr-7b-beta
 *
 * Security constraints:
 *   - HTTPS only. HTTP endpointUrl values are rejected at construction.
 *   - HF token (hf_*) is NEVER written to audit entries, logs, or errors.
 *   - endpointUrl is recorded in audit entries as-is (HF never puts tokens in URLs).
 *
 * Pack-binding primacy (Thomas Directive, 2026-04-22):
 *   Every record governed by this adapter MUST carry a non-empty
 *   packBinding[]. Callers passing an empty array will receive an
 *   UnboundDataError before any action is dispatched.
 *
 * No HuggingFace Hub SDK dependency -- pure wire format mapping.
 *
 * @connexum/ai-governance
 */
import { BaseGovernanceAdapter, ActionDecisionType, } from '../adapters/index.js';
import { UnboundDataError } from '../errors/index.js';
import { PromptInjectionDetector, buildInjectionAuditEvent, } from '../security/prompt-injection-detector.js';
// ---------------------------------------------------------------------------
// HuggingFace-specific constants
// ---------------------------------------------------------------------------
/** Base URL for the shared HuggingFace Inference API (HTTPS only). */
export const HUGGINGFACE_INFERENCE_API_BASE = 'https://api-inference.huggingface.co/models';
/**
 * Common HuggingFace model identifiers as of 2026-04-30.
 * Non-exhaustive; any HF model path is valid for 'inference-api'.
 */
export const HUGGINGFACE_KNOWN_MODELS = [
    'mistralai/Mistral-7B-Instruct-v0.3',
    'mistralai/Mixtral-8x7B-Instruct-v0.1',
    'meta-llama/Meta-Llama-3-8B-Instruct',
    'meta-llama/Meta-Llama-3.1-70B-Instruct',
    'Qwen/Qwen2.5-72B-Instruct',
    'tiiuae/falcon-7b-instruct',
    'HuggingFaceH4/zephyr-7b-beta',
];
// ---------------------------------------------------------------------------
// HuggingFaceAdapter
// ---------------------------------------------------------------------------
export class HuggingFaceAdapter extends BaseGovernanceAdapter {
    blockedFunctions = [];
    approvalRequiredFunctions = [];
    agentIdentity = { name: 'unknown' };
    preActionLog = [];
    postActionLog = [];
    injectionDetector = new PromptInjectionDetector();
    injectionConfig = {};
    /** Effective API variant. Set during initialize(). */
    configuredVariant = 'inference-api';
    /** Effective model ID (for 'inference-api' required; for 'inference-endpoints' optional). */
    configuredModelId;
    /** Effective endpoint URL (for 'inference-endpoints' required). */
    configuredEndpointUrl;
    // ---------------------------------------------------------------------------
    // Lifecycle
    // ---------------------------------------------------------------------------
    /**
     * Initialize with HuggingFace-specific configuration.
     *
     * Throws synchronously if:
     *   - apiVariant is absent or not one of the two recognised values
     *   - apiVariant is 'inference-api' and modelId is absent, empty, or contains whitespace
     *   - apiVariant is 'inference-api' and endpointUrl is provided (cross-contamination)
     *   - apiVariant is 'inference-endpoints' and endpointUrl is absent
     *   - apiVariant is 'inference-endpoints' and endpointUrl does not start with 'https://'
     */
    initialize(config) {
        super.initialize(config);
        const cfg = config;
        // Validate apiVariant -- REQUIRED, two accepted values only
        if (cfg.apiVariant !== 'inference-api' && cfg.apiVariant !== 'inference-endpoints') {
            throw new Error(`HuggingFaceAdapter: apiVariant must be 'inference-api' or 'inference-endpoints'. ` +
                `Received: ${JSON.stringify(cfg.apiVariant)}. ` +
                'Set apiVariant in your HuggingFaceAdapterConfig.');
        }
        this.configuredVariant = cfg.apiVariant;
        if (cfg.apiVariant === 'inference-api') {
            // Cross-contamination guard: endpointUrl must NOT be provided
            if (cfg.endpointUrl !== undefined) {
                throw new Error("HuggingFaceAdapter: endpointUrl must not be provided when apiVariant is 'inference-api'. " +
                    "Remove endpointUrl from config or switch apiVariant to 'inference-endpoints'.");
            }
            // modelId is REQUIRED for 'inference-api'
            if (!cfg.modelId) {
                throw new Error("HuggingFaceAdapter: modelId is required when apiVariant is 'inference-api'. " +
                    "Example: { apiVariant: 'inference-api', modelId: 'mistralai/Mistral-7B-Instruct-v0.3' }.");
            }
            // Reject modelId values with leading/trailing whitespace
            if (cfg.modelId !== cfg.modelId.trim()) {
                throw new Error(`HuggingFaceAdapter: modelId must not contain leading or trailing whitespace. ` +
                    `Received: ${JSON.stringify(cfg.modelId)}.`);
            }
            this.configuredModelId = cfg.modelId;
            this.configuredEndpointUrl = undefined;
        }
        else {
            // apiVariant === 'inference-endpoints'
            // endpointUrl is REQUIRED for 'inference-endpoints'
            if (!cfg.endpointUrl) {
                throw new Error("HuggingFaceAdapter: endpointUrl is required when apiVariant is 'inference-endpoints'. " +
                    "Example: { apiVariant: 'inference-endpoints', endpointUrl: 'https://my-endpoint.endpoints.huggingface.cloud' }.");
            }
            // HTTPS only
            if (!cfg.endpointUrl.startsWith('https://')) {
                throw new Error(`HuggingFaceAdapter: endpointUrl must use HTTPS. Received: "${cfg.endpointUrl}". ` +
                    'Only HTTPS endpoints are permitted per web-interaction-security rules.');
            }
            this.configuredEndpointUrl = cfg.endpointUrl;
            // modelId is optional for 'inference-endpoints' (audit metadata only)
            this.configuredModelId = cfg.modelId;
        }
        this.blockedFunctions = cfg.blockedFunctions ?? [];
        this.approvalRequiredFunctions = cfg.approvalRequiredFunctions ?? [];
        this.agentIdentity = cfg.agentIdentity ?? { name: cfg.adapterName };
        this.injectionConfig = cfg.promptInjection ?? {};
        this.injectionDetector = new PromptInjectionDetector(this.injectionConfig.detectorConfig);
        this.preActionLog = [];
        this.postActionLog = [];
    }
    // ---------------------------------------------------------------------------
    // Pack-binding primacy enforcement (Thomas Directive, 2026-04-22)
    // ---------------------------------------------------------------------------
    /**
     * Assert that packBinding is non-empty.
     * Throws UnboundDataError immediately if the binding is empty or absent.
     * Every caller that writes a governed record must pass their pack bindings.
     *
     * @param packBinding - Array of compliance pack IDs governing this record.
     * @param writeSite   - Identifier of the call site (for error reporting).
     * @param tenantId    - Tenant/org identifier (for error reporting).
     */
    assertPackBinding(packBinding, writeSite, tenantId) {
        if (!packBinding || packBinding.length === 0) {
            throw new UnboundDataError(writeSite, tenantId);
        }
    }
    // ---------------------------------------------------------------------------
    // Prompt injection scanning
    // ---------------------------------------------------------------------------
    /**
     * Scan an array of LLM messages for prompt injection BEFORE sending to HuggingFace.
     *
     * Returns ActionDecision DENY if injection detected at denyThreshold severity,
     * ALLOW otherwise. Audit event written to stderr (privacy-safe, no full content).
     */
    scanMessagesForInjection(messages) {
        if (this.injectionConfig.enabled === false) {
            return { type: ActionDecisionType.ALLOW, reason: 'Prompt injection detection disabled.' };
        }
        const detection = this.injectionDetector.detectInjection(messages);
        if (detection.detected) {
            const denyThreshold = this.injectionConfig.denyThreshold ?? 'high';
            const shouldDeny = detection.severity === 'high' ||
                (denyThreshold === 'medium' && detection.severity === 'medium');
            const auditPayload = buildInjectionAuditEvent(messages, detection, this.agentIdentity.name, this.agentIdentity.orgId ?? 'unknown');
            process.stderr.write(`[HuggingFaceAdapter] prompt-injection:${detection.severity} detected. ` +
                `patterns=${detection.patterns.join(',')} confidence=${detection.confidence.toFixed(2)} ` +
                `contentHash=${auditPayload['contentHash']}\n`);
            if (shouldDeny) {
                const decision = {
                    type: ActionDecisionType.DENY,
                    reason: `Prompt injection detected (severity=${detection.severity}, ` +
                        `patterns=${detection.patterns.join(', ')}). ` +
                        'Request blocked by @connexum/ai-governance (Onyx #5 / OWASP LLM01).',
                    metadata: {
                        injectionPatterns: detection.patterns,
                        injectionSeverity: detection.severity,
                        injectionConfidence: detection.confidence,
                    },
                };
                this.preActionLog.push({
                    context: {
                        toolName: 'llm.messages.create',
                        input: { injectionDetected: true },
                        agent: { ...this.agentIdentity },
                        timestamp: new Date().toISOString(),
                    },
                    decision,
                });
                return decision;
            }
        }
        return { type: ActionDecisionType.ALLOW, reason: 'No injection pattern detected.' };
    }
    // ---------------------------------------------------------------------------
    // GovernanceAdapter overrides
    // ---------------------------------------------------------------------------
    /**
     * Pre-action governance check.
     * Evaluates blocked and approval-required rules against the tool name.
     */
    preAction(context) {
        this.ensureInitialized();
        for (const rule of this.blockedFunctions) {
            if (this.matchesRule(context.toolName, rule)) {
                const decision = {
                    type: ActionDecisionType.DENY,
                    reason: rule.reason,
                    metadata: {
                        rule: 'blocked_function',
                        pattern: rule.pattern instanceof RegExp ? rule.pattern.source : rule.pattern,
                        toolName: context.toolName,
                    },
                };
                this.preActionLog.push({ context, decision });
                return decision;
            }
        }
        for (const rule of this.approvalRequiredFunctions) {
            if (this.matchesRule(context.toolName, rule)) {
                const decision = {
                    type: ActionDecisionType.REQUIRE_APPROVAL,
                    reason: rule.reason,
                    metadata: {
                        rule: 'approval_required',
                        pattern: rule.pattern instanceof RegExp ? rule.pattern.source : rule.pattern,
                        toolName: context.toolName,
                    },
                };
                this.preActionLog.push({ context, decision });
                return decision;
            }
        }
        const decision = {
            type: ActionDecisionType.ALLOW,
            reason: 'No governance rule matched. Action permitted.',
        };
        this.preActionLog.push({ context, decision });
        return decision;
    }
    /**
     * Post-action audit logging.
     * HuggingFace has no native post-action hook; callers invoke this after execution.
     */
    postAction(context, result) {
        this.ensureInitialized();
        this.postActionLog.push({ context, result });
    }
    /**
     * Report adapter capabilities.
     * HuggingFace lacks native hooks -- this adapter is a middleware wrapper.
     */
    getCapabilities() {
        return {
            preAction: false,
            postAction: false,
            auditLogging: false,
            circuitBreaker: false,
            approvalQueue: false,
            tamperDetection: false,
        };
    }
    // ---------------------------------------------------------------------------
    // HuggingFace format parsing
    // ---------------------------------------------------------------------------
    /**
     * Parse a HuggingFace function_call (legacy format) into ActionContext.
     */
    parseFunctionCall(call) {
        this.ensureInitialized();
        let parsedArgs;
        try {
            parsedArgs = JSON.parse(call.arguments);
        }
        catch {
            parsedArgs = { _raw: call.arguments };
        }
        return {
            toolName: call.name,
            input: parsedArgs,
            agent: { ...this.agentIdentity },
            timestamp: new Date().toISOString(),
            sessionInfo: {
                format: 'function_call',
                provider: 'huggingface',
                apiVariant: this.configuredVariant,
                rawArguments: call.arguments,
            },
        };
    }
    /**
     * Parse a HuggingFace tool call (tool_calls array element) into ActionContext.
     */
    parseToolCall(call) {
        this.ensureInitialized();
        let parsedArgs;
        try {
            parsedArgs = JSON.parse(call.function.arguments);
        }
        catch {
            parsedArgs = { _raw: call.function.arguments };
        }
        return {
            toolName: call.function.name,
            input: parsedArgs,
            agent: { ...this.agentIdentity },
            timestamp: new Date().toISOString(),
            sessionInfo: {
                format: 'tool_calls',
                provider: 'huggingface',
                apiVariant: this.configuredVariant,
                toolCallId: call.id,
                rawArguments: call.function.arguments,
            },
        };
    }
    /**
     * Parse either format into ActionContext (auto-detects).
     */
    parseCall(call) {
        if (this.isToolCall(call)) {
            return this.parseToolCall(call);
        }
        return this.parseFunctionCall(call);
    }
    // ---------------------------------------------------------------------------
    // Governance-wrapped execution
    // ---------------------------------------------------------------------------
    /**
     * Wrap a HuggingFace function/tool call with governance enforcement.
     *
     * Primary integration point. Callers pass the HuggingFace tool call extracted
     * from the inference response. Governance is applied before execution.
     *
     * Audit entry includes:
     *   - provider: 'huggingface'
     *   - apiVariant: which API surface was used
     *   - modelId: present for 'inference-api'; optional for 'inference-endpoints'
     *   - endpointUrl: present for 'inference-endpoints' (sanitised; HF never puts
     *     tokens in URLs but we confirm the field is clean)
     *
     * The HF token (hf_*) is NEVER written to the audit entry or any log output.
     *
     * @param call      - HuggingFace function_call or tool_calls element
     * @param executor  - Optional function that executes the call.
     *                    Only invoked if governance allows.
     * @param modelId   - Optional model ID override for audit entry.
     * @returns HuggingFaceWrappedCallResult
     */
    async wrapFunctionCall(call, executor, modelId) {
        const context = this.parseCall(call);
        const format = this.isToolCall(call) ? 'tool_calls' : 'function_call';
        const decision = this.preAction(context);
        const effectiveModelId = modelId ?? this.configuredModelId;
        const baseAuditEntry = {
            provider: 'huggingface',
            apiVariant: this.configuredVariant,
            modelId: effectiveModelId,
            endpointUrl: this.configuredEndpointUrl,
            toolName: context.toolName,
            format,
            decisionType: decision.type,
            timestamp: context.timestamp,
            // HF token (hf_*) is intentionally absent from the audit entry
        };
        if (decision.type !== ActionDecisionType.ALLOW) {
            return { decision, originalCall: call, auditEntry: baseAuditEntry };
        }
        if (!executor) {
            return { decision, originalCall: call, auditEntry: baseAuditEntry };
        }
        const startMs = Date.now();
        let actionResult;
        try {
            const output = await executor(context);
            actionResult = {
                success: true,
                outputSummary: typeof output === 'string' ? output : JSON.stringify(output ?? 'ok'),
                durationMs: Date.now() - startMs,
            };
        }
        catch (err) {
            actionResult = {
                success: false,
                outputSummary: '',
                error: err instanceof Error ? err.message : String(err),
                durationMs: Date.now() - startMs,
            };
        }
        this.postAction(context, actionResult);
        return {
            decision,
            result: actionResult,
            originalCall: call,
            auditEntry: {
                ...baseAuditEntry,
                durationMs: actionResult.durationMs,
            },
        };
    }
    /**
     * Synchronous version of wrapFunctionCall for non-async executors.
     */
    wrapFunctionCallSync(call, executor, modelId) {
        const context = this.parseCall(call);
        const format = this.isToolCall(call) ? 'tool_calls' : 'function_call';
        const decision = this.preAction(context);
        const effectiveModelId = modelId ?? this.configuredModelId;
        const baseAuditEntry = {
            provider: 'huggingface',
            apiVariant: this.configuredVariant,
            modelId: effectiveModelId,
            endpointUrl: this.configuredEndpointUrl,
            toolName: context.toolName,
            format,
            decisionType: decision.type,
            timestamp: context.timestamp,
        };
        if (decision.type !== ActionDecisionType.ALLOW) {
            return { decision, originalCall: call, auditEntry: baseAuditEntry };
        }
        if (!executor) {
            return { decision, originalCall: call, auditEntry: baseAuditEntry };
        }
        const startMs = Date.now();
        let actionResult;
        try {
            const output = executor(context);
            actionResult = {
                success: true,
                outputSummary: typeof output === 'string' ? output : JSON.stringify(output ?? 'ok'),
                durationMs: Date.now() - startMs,
            };
        }
        catch (err) {
            actionResult = {
                success: false,
                outputSummary: '',
                error: err instanceof Error ? err.message : String(err),
                durationMs: Date.now() - startMs,
            };
        }
        this.postAction(context, actionResult);
        return {
            decision,
            result: actionResult,
            originalCall: call,
            auditEntry: {
                ...baseAuditEntry,
                durationMs: actionResult.durationMs,
            },
        };
    }
    // ---------------------------------------------------------------------------
    // Tool call extraction helper
    // ---------------------------------------------------------------------------
    /**
     * Extract a HuggingFaceToolCall from an Inference API response string.
     *
     * For models returning tool_calls in the generated_text field (Mistral, Llama 3.1+),
     * the output is a JSON object with a `tool_calls` array or a `tool_call` field.
     *
     * Returns the first extracted tool call, or null if none found.
     * Callers should check for null before passing to wrapFunctionCall.
     */
    extractToolCallFromOutput(output) {
        if (!output)
            return null;
        try {
            const parsed = JSON.parse(output);
            // OpenAI-compatible tool_calls array (Mistral/Qwen/Llama 3.1+)
            const toolCallsArr = parsed['tool_calls'];
            if (Array.isArray(toolCallsArr) && toolCallsArr.length > 0) {
                const first = toolCallsArr[0];
                const fn = first['function'];
                if (fn && typeof fn['name'] === 'string') {
                    return {
                        id: typeof first['id'] === 'string' ? first['id'] : `hf-${Date.now()}`,
                        type: 'function',
                        function: {
                            name: fn['name'],
                            arguments: typeof fn['arguments'] === 'string'
                                ? fn['arguments']
                                : JSON.stringify(fn['arguments'] ?? {}),
                        },
                    };
                }
            }
            // Single tool_call object (some Llama fine-tunes)
            const singleToolCall = parsed['tool_call'];
            if (singleToolCall &&
                typeof singleToolCall === 'object' &&
                singleToolCall['type'] === 'function') {
                const tc = singleToolCall;
                const fn = tc['function'];
                if (fn && typeof fn['name'] === 'string') {
                    return {
                        id: typeof tc['id'] === 'string' ? tc['id'] : `hf-${Date.now()}`,
                        type: 'function',
                        function: {
                            name: fn['name'],
                            arguments: typeof fn['arguments'] === 'string'
                                ? fn['arguments']
                                : JSON.stringify(fn['arguments'] ?? {}),
                        },
                    };
                }
            }
        }
        catch {
            // Output is not JSON (plain text response, no tool call)
        }
        return null;
    }
    // ---------------------------------------------------------------------------
    // Response builders
    // ---------------------------------------------------------------------------
    /**
     * Build a HuggingFace/OpenAI-compatible tool response message from an ActionResult.
     */
    buildToolResponse(callId, result) {
        return {
            role: 'tool',
            tool_call_id: callId,
            content: result.success
                ? result.outputSummary
                : JSON.stringify({ error: result.error ?? 'Unknown error' }),
        };
    }
    /**
     * Build a governance-denied response in HuggingFace/OpenAI tool response format.
     */
    buildDeniedResponse(callId, decision) {
        return {
            role: 'tool',
            tool_call_id: callId,
            content: JSON.stringify({
                error: 'governance_denied',
                type: decision.type,
                reason: decision.reason,
            }),
        };
    }
    // ---------------------------------------------------------------------------
    // Accessors (for testing; do not expose the API token)
    // ---------------------------------------------------------------------------
    /** Returns the configured API variant. */
    getApiVariant() {
        return this.configuredVariant;
    }
    /**
     * Returns the effective endpoint URL.
     * For 'inference-api': the constructed URL for the configured model.
     * For 'inference-endpoints': the configured endpointUrl.
     */
    getEffectiveEndpointUrl() {
        if (this.configuredVariant === 'inference-endpoints') {
            return this.configuredEndpointUrl ?? '';
        }
        return `${HUGGINGFACE_INFERENCE_API_BASE}/${this.configuredModelId ?? ''}`;
    }
    /** Returns the configured model ID, if any. */
    getModelId() {
        return this.configuredModelId;
    }
    /** Get the pre-action decision log. */
    getPreActionLog() {
        return [...this.preActionLog];
    }
    /** Get the post-action result log. */
    getPostActionLog() {
        return [...this.postActionLog];
    }
    // ---------------------------------------------------------------------------
    // Private helpers
    // ---------------------------------------------------------------------------
    isToolCall(call) {
        return 'id' in call && 'type' in call && call.type === 'function';
    }
    matchesRule(toolName, rule) {
        if (rule.pattern instanceof RegExp) {
            return rule.pattern.test(toolName);
        }
        return toolName === rule.pattern;
    }
}
//# sourceMappingURL=huggingface.js.map