import { TOOL_CATEGORIES, TOOL_TO_CATEGORY, describeRule, describeScope, periodToMs } from "./policy-types.mjs";
import { getPolicyStore } from "./policy-store.mjs";
import { randomUUID } from "node:crypto";
//#region extensions/crypto/src/services/policy-evaluator.ts
/**
* Policy Evaluator — Runtime enforcement of user-defined policies.
*
* Checks an ActionContext against all active policies for a user.
* Returns the most restrictive decision: block > confirm > allow.
*
* Also handles:
* - Usage recording after successful tool execution
* - Extracting ActionContext from tool args (best-effort)
* - Rendering policy displays for user verification
*/
/** Priority: block > confirm > allow. */
const ACTION_PRIORITY = {
	block: 3,
	confirm: 2,
	allow: 1
};
/**
* Evaluate all active policies for a user against a proposed action.
* Returns the most restrictive decision across all matching policies.
*/
function evaluatePolicies(ctx) {
	const store = getPolicyStore();
	if (store.isCorrupted(ctx.userId)) return {
		action: "block",
		reason: "Policy store is corrupted — all write operations are blocked for safety. Check ~/.openclawnch/policies/ for .corrupt files and restore or delete them."
	};
	const policies = store.getActivePolicies(ctx.userId);
	if (policies.length === 0) return { action: "allow" };
	let worst = { action: "allow" };
	for (const policy of policies) {
		if (!policyApplies(policy, ctx.toolName)) continue;
		for (const rule of policy.rules) {
			const decision = evaluateRule(rule, ctx, policy);
			if (ACTION_PRIORITY[decision.action] > ACTION_PRIORITY[worst.action]) worst = decision;
			if (worst.action === "block") return worst;
		}
	}
	return worst;
}
/** Check if a policy's scope covers a given tool. */
function policyApplies(policy, toolName) {
	const { scope } = policy;
	switch (scope.type) {
		case "all_write": return true;
		case "tools": return (scope.tools ?? []).includes(toolName);
		case "categories": {
			const toolCategory = TOOL_TO_CATEGORY[toolName];
			if (!toolCategory) return false;
			return (scope.categories ?? []).includes(toolCategory);
		}
	}
}
/** Evaluate a single rule against an action context. */
function evaluateRule(rule, ctx, policy) {
	const base = {
		policyId: policy.id,
		policyName: policy.name
	};
	switch (rule.type) {
		case "spending_limit": {
			if (ctx.amountUsd == null) return {
				...base,
				action: "confirm",
				reason: `Policy "${policy.name}" has a spending limit but the USD value of this action is unknown. Confirm to proceed.`,
				ruleSummary: describeRule(rule)
			};
			const windowMs = periodToMs(rule.period);
			const spent = getPolicyStore().getSpendInWindow(ctx.userId, policy.id, windowMs);
			const remaining = rule.maxAmountUsd - spent;
			if (ctx.amountUsd > remaining) return {
				...base,
				action: "block",
				reason: `Policy "${policy.name}": spending limit $${rule.maxAmountUsd}/${rule.period}. Already spent $${spent.toFixed(2)}, remaining $${remaining.toFixed(2)}. This action ($${ctx.amountUsd.toFixed(2)}) would exceed the limit.`,
				ruleSummary: describeRule(rule)
			};
			return { action: "allow" };
		}
		case "rate_limit":
			if (getPolicyStore().getCallsInWindow(ctx.userId, policy.id, rule.periodMs) >= rule.maxCalls) return {
				...base,
				action: "block",
				reason: `Policy "${policy.name}": rate limit ${rule.maxCalls} calls reached. Wait for the window to reset.`,
				ruleSummary: describeRule(rule)
			};
			return { action: "allow" };
		case "allowlist": {
			const value = getFieldValue(rule.field, ctx);
			if (value == null) return {
				...base,
				action: "confirm",
				reason: `Policy "${policy.name}": cannot determine ${rule.field} for allowlist check. Confirm to proceed.`,
				ruleSummary: describeRule(rule)
			};
			if (!rule.values.map((v) => v.toLowerCase()).includes(value.toLowerCase())) return {
				...base,
				action: "block",
				reason: `Policy "${policy.name}": ${rule.field} "${value}" is not in the allowlist [${rule.values.join(", ")}].`,
				ruleSummary: describeRule(rule)
			};
			return { action: "allow" };
		}
		case "blocklist": {
			const value = getFieldValue(rule.field, ctx);
			if (value == null) return {
				...base,
				action: "confirm",
				reason: `Policy "${policy.name}": cannot determine ${rule.field} for blocklist check. Confirm to proceed.`,
				ruleSummary: describeRule(rule)
			};
			if (rule.values.map((v) => v.toLowerCase()).includes(value.toLowerCase())) return {
				...base,
				action: "block",
				reason: `Policy "${policy.name}": ${rule.field} "${value}" is blocked [${rule.values.join(", ")}].`,
				ruleSummary: describeRule(rule)
			};
			return { action: "allow" };
		}
		case "time_window": {
			const tz = rule.timezone ?? "UTC";
			const now = /* @__PURE__ */ new Date();
			let hour;
			let day;
			try {
				const parts = new Intl.DateTimeFormat("en-US", {
					timeZone: tz,
					hour: "numeric",
					hour12: false,
					weekday: "short"
				}).formatToParts(now);
				hour = parseInt(parts.find((p) => p.type === "hour")?.value ?? "0", 10);
				day = {
					Sun: 0,
					Mon: 1,
					Tue: 2,
					Wed: 3,
					Thu: 4,
					Fri: 5,
					Sat: 6
				}[parts.find((p) => p.type === "weekday")?.value ?? "Mon"] ?? 1;
			} catch {
				return { action: "allow" };
			}
			if (rule.allowedHours) {
				const { start, end } = rule.allowedHours;
				if (!(start <= end ? hour >= start && hour < end : hour >= start || hour < end)) return {
					...base,
					action: "block",
					reason: `Policy "${policy.name}": outside allowed hours (${start}:00–${end}:00 ${tz}). Current hour: ${hour}:00.`,
					ruleSummary: describeRule(rule)
				};
			}
			if (rule.allowedDays && rule.allowedDays.length > 0) {
				if (!rule.allowedDays.includes(day)) {
					const dayNames = [
						"Sun",
						"Mon",
						"Tue",
						"Wed",
						"Thu",
						"Fri",
						"Sat"
					];
					return {
						...base,
						action: "block",
						reason: `Policy "${policy.name}": not an allowed day. Today: ${dayNames[day]}. Allowed: ${rule.allowedDays.map((d) => dayNames[d]).join(", ")}.`,
						ruleSummary: describeRule(rule)
					};
				}
			}
			return { action: "allow" };
		}
		case "approval_threshold":
			if (ctx.amountUsd == null) return {
				...base,
				action: "confirm",
				reason: `Policy "${policy.name}" requires confirmation above $${rule.amountUsd}, but USD value is unknown. Confirm to proceed.`,
				ruleSummary: describeRule(rule)
			};
			if (ctx.amountUsd > rule.amountUsd) return {
				...base,
				action: "confirm",
				reason: `Policy "${policy.name}": this action ($${ctx.amountUsd.toFixed(2)}) exceeds the $${rule.amountUsd} confirmation threshold.`,
				ruleSummary: describeRule(rule)
			};
			return { action: "allow" };
		case "max_amount":
			if (ctx.amountUsd == null) return {
				...base,
				action: "confirm",
				reason: `Policy "${policy.name}" blocks actions above $${rule.maxAmountUsd}, but USD value is unknown. Confirm to proceed.`,
				ruleSummary: describeRule(rule)
			};
			if (ctx.amountUsd > rule.maxAmountUsd) return {
				...base,
				action: "block",
				reason: `Policy "${policy.name}": this action ($${ctx.amountUsd.toFixed(2)}) exceeds the hard limit of $${rule.maxAmountUsd}.`,
				ruleSummary: describeRule(rule)
			};
			return { action: "allow" };
		case "erc20_limit": return { action: "allow" };
	}
}
/** Extract a field value from ActionContext for allowlist/blocklist checks. */
function getFieldValue(field, ctx) {
	switch (field) {
		case "tokens": return ctx.token;
		case "chains": return ctx.chain?.toString();
		case "addresses": return ctx.toAddress;
		case "contracts": return ctx.toAddress;
	}
}
/**
* Record a tool execution against all matching active policies.
* Called AFTER successful execution (not on block/confirm).
*/
function recordToolExecution(ctx) {
	const store = getPolicyStore();
	const policies = store.getActivePolicies(ctx.userId);
	const entry = {
		timestamp: Date.now(),
		toolName: ctx.toolName,
		action: ctx.action,
		amountUsd: ctx.amountUsd
	};
	for (const policy of policies) {
		if (!policyApplies(policy, ctx.toolName)) continue;
		store.recordUsage(ctx.userId, policy.id, entry);
	}
}
/**
* Shared helper to extract userId from tool execution context.
* Used by both the enforcement gate (index.ts) and the policy_manage tool.
* Ensures consistent fallback chain everywhere.
*/
function extractPolicyUserId(ctx) {
	return ctx?.senderId ?? ctx?.from ?? ctx?.metadata?.senderId ?? "owner";
}
/**
* Best-effort extraction of ActionContext from tool args.
* Different tools encode amounts, tokens, addresses differently.
* Returns what we can determine; undefined fields are left out.
*/
function extractActionContext(toolName, args, userId) {
	const ctx = {
		toolName,
		userId
	};
	if (typeof args.action === "string") ctx.action = args.action;
	if (typeof args.token === "string") ctx.token = args.token;
	else if (typeof args.tokenIn === "string") ctx.token = args.tokenIn;
	else if (typeof args.asset === "string") ctx.token = args.asset;
	if (typeof args.to === "string") ctx.toAddress = args.to;
	else if (typeof args.recipient === "string") ctx.toAddress = args.recipient;
	else if (typeof args.address === "string") ctx.toAddress = args.address;
	if (typeof args.chain === "number") ctx.chain = args.chain;
	else if (typeof args.chainId === "number") ctx.chain = args.chainId;
	else if (typeof args.toChain === "number") ctx.chain = args.toChain;
	if (typeof args.amount === "string") {
		const num = parseFloat(args.amount);
		if (!isNaN(num)) {
			if (toolName === "fiat_payment") ctx.amountUsd = num;
		}
	}
	if (typeof args.amountUsd === "number") ctx.amountUsd = args.amountUsd;
	return ctx;
}
/**
* Build a full display object for user verification.
* Shows both the original NL and the exact structured interpretation.
*/
function buildPolicyDisplay(policy, userId) {
	const store = getPolicyStore();
	const ruleDescriptions = policy.rules.map(describeRule);
	const scopeDescription = describeScope(policy.scope);
	const usageParts = [];
	for (const rule of policy.rules) {
		if (rule.type === "spending_limit") {
			const windowMs = periodToMs(rule.period);
			const spent = store.getSpendInWindow(userId, policy.id, windowMs);
			usageParts.push(`Spent $${spent.toFixed(2)} of $${rule.maxAmountUsd} (${rule.period})`);
		}
		if (rule.type === "rate_limit") {
			const calls = store.getCallsInWindow(userId, policy.id, rule.periodMs);
			usageParts.push(`${calls} of ${rule.maxCalls} calls used`);
		}
	}
	return {
		name: policy.name,
		status: policy.status,
		description: policy.description,
		ruleDescriptions,
		scopeDescription,
		usageSummary: usageParts.length > 0 ? usageParts.join("; ") : void 0
	};
}
/**
* Render a PolicyDisplay as a formatted text block for chat output.
* Both the user's original words and the structured interpretation
* are shown so there's no room for misunderstanding.
*/
function renderPolicyDisplay(display) {
	const lines = [];
	const statusEmoji = display.status === "active" ? "[ACTIVE]" : display.status === "draft" ? "[DRAFT]" : "[DISABLED]";
	lines.push(`**${display.name}** ${statusEmoji}`);
	lines.push("");
	lines.push(`You said: "${display.description}"`);
	lines.push("");
	lines.push("**What is enforced:**");
	for (const rd of display.ruleDescriptions) lines.push(`  - ${rd}`);
	lines.push(`  - Applies to: ${display.scopeDescription}`);
	if (display.usageSummary) {
		lines.push("");
		lines.push(`**Current usage:** ${display.usageSummary}`);
	}
	return lines.join("\n");
}
/** Default TTL for confirmation nonces: 5 minutes. */
const NONCE_TTL_MS = 300 * 1e3;
/** Max nonces stored (prevent unbounded growth). Old entries are evicted. */
const MAX_NONCES = 500;
var PolicyConfirmationStore = class {
	nonces = /* @__PURE__ */ new Map();
	/** Generate a nonce for a confirm decision. */
	create(userId, toolName) {
		this.prune();
		const nonce = randomUUID();
		this.nonces.set(nonce, {
			nonce,
			userId,
			toolName,
			createdAt: Date.now()
		});
		return nonce;
	}
	/** Validate and consume a nonce. Returns true if valid. */
	consume(nonce, userId, toolName) {
		const entry = this.nonces.get(nonce);
		if (!entry) return false;
		this.nonces.delete(nonce);
		if (Date.now() - entry.createdAt > NONCE_TTL_MS) return false;
		if (entry.userId !== userId) return false;
		if (entry.toolName !== toolName) return false;
		return true;
	}
	/** Prune expired entries and enforce max size. */
	prune() {
		const now = Date.now();
		for (const [key, entry] of this.nonces) if (now - entry.createdAt > NONCE_TTL_MS) this.nonces.delete(key);
		if (this.nonces.size > MAX_NONCES) {
			const sorted = [...this.nonces.entries()].sort((a, b) => a[1].createdAt - b[1].createdAt);
			const toRemove = sorted.slice(0, sorted.length - MAX_NONCES);
			for (const [key] of toRemove) this.nonces.delete(key);
		}
	}
	/** Clear all nonces (for testing). */
	reset() {
		this.nonces.clear();
	}
};
let _confirmStore = null;
function getPolicyConfirmationStore() {
	if (!_confirmStore) _confirmStore = new PolicyConfirmationStore();
	return _confirmStore;
}
function resetPolicyConfirmationStore() {
	_confirmStore?.reset();
	_confirmStore = null;
}
/** Expand category names into tool lists for scope rendering. */
function expandScope(scope) {
	switch (scope.type) {
		case "all_write": {
			const all = [];
			for (const tools of Object.values(TOOL_CATEGORIES)) all.push(...tools);
			return all;
		}
		case "tools": return scope.tools ?? [];
		case "categories": {
			const result = [];
			for (const cat of scope.categories ?? []) result.push(...TOOL_CATEGORIES[cat] ?? []);
			return result;
		}
		default: return [];
	}
}
//#endregion
export { buildPolicyDisplay, evaluatePolicies, expandScope, extractActionContext, extractPolicyUserId, getPolicyConfirmationStore, recordToolExecution, renderPolicyDisplay, resetPolicyConfirmationStore };

//# sourceMappingURL=policy-evaluator.mjs.map