import { searchJMESPath } from "./jmespath-utils.js";
import { createHmac, randomBytes, scrypt, timingSafeEqual } from "node:crypto";
import { promisify } from "node:util";

//#region src/utils/trigger-auth.ts
const scryptAsync = promisify(scrypt);
const SALT_LENGTH = 32;
const KEY_LENGTH = 64;
const VALUE_PREFIX_LENGTH = 8;
/**
* Hash a header value using scrypt for secure storage.
* Returns the hash and a prefix for display purposes.
*
* @param value - The plaintext header value to hash
* @returns Object containing valueHash (for storage) and valuePrefix (for display)
*/
async function hashTriggerHeaderValue(value) {
	const salt = randomBytes(SALT_LENGTH);
	const hashedBuffer = await scryptAsync(value, salt, KEY_LENGTH);
	return {
		valueHash: Buffer.concat([salt, hashedBuffer]).toString("base64"),
		valuePrefix: value.substring(0, VALUE_PREFIX_LENGTH)
	};
}
/**
* Validate a header value against its stored hash using timing-safe comparison.
*
* @param value - The plaintext header value from the incoming request
* @param storedHash - The hash stored in the database
* @returns True if the value matches the hash
*/
async function validateTriggerHeaderValue(value, storedHash) {
	try {
		const combined = Buffer.from(storedHash, "base64");
		const salt = combined.subarray(0, SALT_LENGTH);
		return timingSafeEqual(combined.subarray(SALT_LENGTH), await scryptAsync(value, salt, KEY_LENGTH));
	} catch {
		return false;
	}
}
/**
* Transform authentication input (plaintext values) to stored format (hashed values).
* Used when creating or updating triggers.
*
* @param headers - Array of header inputs with plaintext values
* @returns Array of headers with hashed values for storage
*/
async function hashAuthenticationHeaders(headers) {
	return Promise.all(headers.map(async (header) => {
		const { valueHash, valuePrefix } = await hashTriggerHeaderValue(header.value);
		return {
			name: header.name,
			valueHash,
			valuePrefix
		};
	}));
}
/**
* Verifies incoming webhook requests using the configured authentication headers.
* Each configured header must be present and match its expected value (via hash comparison).
*
* @param c - Hono context containing the request headers
* @param authentication - Trigger authentication configuration with hashed header values
* @returns TriggerAuthResult indicating success/failure with appropriate HTTP status
*/
async function verifyTriggerAuth(c, authentication) {
	if (!authentication || !authentication.headers || authentication.headers.length === 0) return { success: true };
	for (const header of authentication.headers) {
		const headerName = header.name.toLowerCase();
		const actualValue = c.req.header(headerName);
		if (!actualValue) return {
			success: false,
			status: 401,
			message: `Missing authentication header: ${header.name}`
		};
		if (!await validateTriggerHeaderValue(actualValue, header.valueHash)) return {
			success: false,
			status: 403,
			message: `Invalid value for header: ${header.name}`
		};
	}
	return { success: true };
}
/**
* Extracts the signature from the request based on configuration.
* Supports extraction from headers, query parameters, or body via JMESPath.
* Optionally strips prefix and applies regex extraction.
*
* @param c - Hono context
* @param config - Signature verification configuration
* @param body - Raw request body as string
* @returns Extracted signature string or null if not found
*/
function extractSignature(c, config, body) {
	const { signature } = config;
	const caseSensitive = config.validation?.headerCaseSensitive ?? false;
	let value;
	if (signature.source === "header") {
		const headerKey = caseSensitive ? signature.key : signature.key.toLowerCase();
		value = c.req.header(headerKey);
	} else if (signature.source === "query") value = c.req.query(signature.key);
	else if (signature.source === "body") try {
		value = searchJMESPath(JSON.parse(body), signature.key);
	} catch {
		return null;
	}
	if (!value) return null;
	if (signature.prefix && value.startsWith(signature.prefix)) value = value.slice(signature.prefix.length);
	if (signature.regex) try {
		const match = value.match(new RegExp(signature.regex));
		if (match?.[1]) value = match[1];
		else return null;
	} catch {
		return null;
	}
	return value;
}
/**
* Extracts a single signed component from the request.
*
* @param c - Hono context
* @param component - Component configuration
* @param body - Raw request body as string
* @param caseSensitive - Whether header names should be case-sensitive
* @returns Extracted component value or null if required and not found
*/
function extractComponent(c, component, body, caseSensitive) {
	let value;
	if (component.source === "literal") value = component.value ?? "";
	else if (component.source === "header") {
		if (!component.key) return component.required ? null : "";
		const headerKey = caseSensitive ? component.key : component.key.toLowerCase();
		value = c.req.header(headerKey);
	} else if (component.source === "body") if (!component.key) value = body;
	else try {
		value = searchJMESPath(JSON.parse(body), component.key);
	} catch {
		return component.required ? null : "";
	}
	if (value === void 0 || value === null) return component.required ? null : "";
	value = String(value);
	if (component.regex) try {
		const match = value.match(new RegExp(component.regex));
		if (match?.[1]) value = match[1];
		else return component.required ? null : "";
	} catch {
		return component.required ? null : "";
	}
	return value;
}
/**
* Verifies webhook signature using flexible, provider-agnostic configuration.
* Supports GitHub, Slack, Zendesk, Stripe and other webhook signature schemes.
*
* SECURITY: Uses crypto.timingSafeEqual() for all signature comparisons to prevent timing attacks.
*
* @param c - Hono context containing request headers, query, and body
* @param config - Signature verification configuration
* @param signingSecret - HMAC signing secret
* @param body - Raw request body as string
* @returns SignatureVerificationResult with success status and error details
*
* @example
* // GitHub webhook verification
* const result = verifySignatureWithConfig(c, {
*   algorithm: 'sha256',
*   encoding: 'hex',
*   signature: { source: 'header', key: 'x-hub-signature-256', prefix: 'sha256=' },
*   signedComponents: [{ source: 'body', key: '@' }],
*   componentJoin: { strategy: 'concatenate', separator: '' }
* }, secret, body);
*/
function verifySignatureWithConfig(c, config, signingSecret, body) {
	const caseSensitive = config.validation?.headerCaseSensitive ?? false;
	const allowEmptyBody = config.validation?.allowEmptyBody ?? true;
	const normalizeUnicode = config.validation?.normalizeUnicode ?? false;
	const signature = extractSignature(c, config, body);
	if (!signature) return {
		success: false,
		errorCode: "MISSING_SIGNATURE",
		status: 401,
		message: "Missing or invalid signature"
	};
	const components = [];
	for (const componentConfig of config.signedComponents) {
		const componentValue = extractComponent(c, componentConfig, body, caseSensitive);
		if (componentValue === null) return {
			success: false,
			errorCode: "MISSING_COMPONENT",
			status: 400,
			message: "Missing required signed component"
		};
		components.push(componentValue);
	}
	let signedData = components.join(config.componentJoin.separator);
	if (normalizeUnicode) signedData = signedData.normalize("NFC");
	if (!allowEmptyBody && signedData === "") return {
		success: false,
		errorCode: "MISSING_COMPONENT",
		status: 400,
		message: "Empty body not allowed"
	};
	const hmac = createHmac(config.algorithm, signingSecret);
	hmac.update(signedData);
	const expectedSignature = hmac.digest(config.encoding);
	try {
		const signatureBuffer = Buffer.from(signature, config.encoding);
		const expectedBuffer = Buffer.from(expectedSignature, config.encoding);
		if (signatureBuffer.length !== expectedBuffer.length) return {
			success: false,
			errorCode: "SIGNATURE_MISMATCH",
			status: 403,
			message: "Invalid signature"
		};
		if (!timingSafeEqual(signatureBuffer, expectedBuffer)) return {
			success: false,
			errorCode: "SIGNATURE_MISMATCH",
			status: 403,
			message: "Invalid signature"
		};
		return { success: true };
	} catch {
		return {
			success: false,
			errorCode: "INVALID_SIGNATURE_FORMAT",
			status: 403,
			message: "Invalid signature format"
		};
	}
}

//#endregion
export { hashAuthenticationHeaders, hashTriggerHeaderValue, validateTriggerHeaderValue, verifySignatureWithConfig, verifyTriggerAuth };