import { runWithCacheTenant } from "@open-mercato/cache";
import { BusinessRule, RuleExecutionLog } from "../data/entities.js";
import * as ruleEvaluator from "./rule-evaluator.js";
import * as actionExecutor from "./action-executor.js";
import { ruleEngineContextSchema, ruleDiscoveryOptionsSchema, directRuleExecutionContextSchema, ruleIdExecutionContextSchema } from "../data/validators.js";
import { createLogger } from "@open-mercato/shared/lib/logger";
const logger = createLogger("business_rules").child({ component: "rule-engine" });
const DEFAULT_ENTITY_ID = "unknown";
const RULE_TYPE_GUARD = "GUARD";
const EXECUTION_RESULT_ERROR = "ERROR";
const EXECUTION_RESULT_SUCCESS = "SUCCESS";
const EXECUTION_RESULT_FAILURE = "FAILURE";
const MAX_RULES_PER_EXECUTION = 100;
const MAX_SINGLE_RULE_TIMEOUT_MS = 3e4;
const MAX_TOTAL_EXECUTION_TIMEOUT_MS = 6e4;
const RULE_DISCOVERY_CACHE_TTL = 5 * 60 * 1e3;
function normalizeCachePart(value) {
  return encodeURIComponent(value?.trim() || "*");
}
function getRuleDiscoveryCacheKey(options) {
  return [
    normalizeCachePart(options.tenantId),
    normalizeCachePart(options.organizationId),
    normalizeCachePart(options.entityType),
    normalizeCachePart(options.eventType),
    normalizeCachePart(options.ruleType)
  ].join(":");
}
function isCachedRuleDiscovery(value) {
  if (!value || typeof value !== "object") return false;
  const ruleIds = value.ruleIds;
  return Array.isArray(ruleIds) && ruleIds.every((entry) => typeof entry === "string");
}
function isRuleDiscoveryCache(value) {
  if (!value || typeof value !== "object") return false;
  const candidate = value;
  return typeof candidate.get === "function" && typeof candidate.set === "function" && typeof candidate.deleteByTags === "function";
}
function resolveBusinessRuleDiscoveryCache(resolve) {
  try {
    const cache = resolve("cache");
    return isRuleDiscoveryCache(cache) ? cache : null;
  } catch {
    return null;
  }
}
function getRuleDiscoveryCacheTags(options) {
  return [
    "business_rules:discovery",
    `business_rules:discovery:organization:${options.organizationId}`
  ];
}
async function getCachedRuleDiscovery(cache, options) {
  if (!cache) return null;
  let value;
  try {
    value = await runWithCacheTenant(
      options.tenantId,
      () => cache.get(getRuleDiscoveryCacheKey(options))
    );
  } catch (error) {
    logger.warn("Failed to read rule discovery cache", { err: error });
    return null;
  }
  return isCachedRuleDiscovery(value) ? value : null;
}
async function cacheRuleDiscovery(cache, options, rules) {
  if (!cache) return;
  const ruleIds = rules.map((rule) => rule.id).filter((id) => typeof id === "string" && id.length > 0);
  if (ruleIds.length !== rules.length) return;
  try {
    await runWithCacheTenant(
      options.tenantId,
      () => cache.set(
        getRuleDiscoveryCacheKey(options),
        { ruleIds },
        {
          ttl: RULE_DISCOVERY_CACHE_TTL,
          tags: getRuleDiscoveryCacheTags(options)
        }
      )
    );
  } catch (error) {
    logger.warn("Failed to write rule discovery cache", { err: error });
  }
}
async function invalidateBusinessRuleDiscoveryCache(cache, tenantId, organizationId) {
  if (!cache) return;
  const normalizedTenantId = tenantId?.trim();
  const normalizedOrganizationId = organizationId?.trim();
  if (!normalizedTenantId) {
    return;
  }
  const tags = normalizedOrganizationId ? [`business_rules:discovery:organization:${normalizedOrganizationId}`] : ["business_rules:discovery"];
  try {
    await runWithCacheTenant(normalizedTenantId, () => cache.deleteByTags(tags));
  } catch (error) {
    logger.warn("Failed to invalidate rule discovery cache", { err: error });
  }
}
async function withTimeout(promise, timeoutMs, errorMessage) {
  let timeoutId;
  const timeoutPromise = new Promise((_, reject) => {
    timeoutId = setTimeout(() => {
      reject(new Error(`${errorMessage} (timeout: ${timeoutMs}ms)`));
    }, timeoutMs);
  });
  try {
    return await Promise.race([promise, timeoutPromise]);
  } finally {
    clearTimeout(timeoutId);
  }
}
async function executeRules(em, context, options = {}) {
  const validation = ruleEngineContextSchema.safeParse(context);
  if (!validation.success) {
    const validationErrors = validation.error.issues.map((e) => `${e.path.join(".")}: ${e.message}`);
    return {
      allowed: false,
      executedRules: [],
      totalExecutionTime: 0,
      errors: validationErrors
    };
  }
  const startTime = Date.now();
  const executedRules = [];
  const errors = [];
  const logIds = [];
  try {
    const rules = await findApplicableRules(em, {
      entityType: context.entityType,
      eventType: context.eventType,
      tenantId: context.tenantId,
      organizationId: context.organizationId
    }, { cache: options.cache });
    if (rules.length > MAX_RULES_PER_EXECUTION) {
      errors.push(
        `Rule count limit exceeded: ${rules.length} rules found, maximum is ${MAX_RULES_PER_EXECUTION}`
      );
      return {
        allowed: false,
        executedRules: [],
        totalExecutionTime: Date.now() - startTime,
        errors
      };
    }
    const executionPromise = (async () => {
      for (const rule of rules) {
        try {
          const ruleResult = await executeSingleRule(em, rule, context, options);
          executedRules.push(ruleResult);
          if (ruleResult.logId) {
            logIds.push(ruleResult.logId);
          }
          if (ruleResult.error) {
            errors.push(
              `Rule execution failed [ruleId=${rule.ruleId}, type=${rule.ruleType}, entityType=${context.entityType}]: ${ruleResult.error}`
            );
          }
        } catch (error) {
          const errorMessage = error instanceof Error ? error.message : String(error);
          errors.push(
            `Unexpected error in rule execution [ruleId=${rule.ruleId}, type=${rule.ruleType}]: ${errorMessage}`
          );
          if (!context.dryRun) {
            await emitRuleExecutionFailed(options.eventBus, {
              ruleId: rule.ruleId,
              ruleName: rule.ruleName,
              entityType: context.entityType ?? null,
              errorMessage,
              tenantId: context.tenantId,
              organizationId: context.organizationId ?? null
            });
          }
          executedRules.push({
            rule,
            conditionResult: false,
            actionsExecuted: null,
            executionTime: 0,
            error: errorMessage
          });
        }
      }
    })();
    await withTimeout(
      executionPromise,
      MAX_TOTAL_EXECUTION_TIMEOUT_MS,
      `Total rule execution timeout [entityType=${context.entityType}]`
    );
    const guardRules = executedRules.filter((r) => r.rule.ruleType === RULE_TYPE_GUARD);
    const allowed = guardRules.length === 0 || guardRules.every((r) => r.conditionResult);
    const totalExecutionTime = Date.now() - startTime;
    return {
      allowed,
      executedRules,
      totalExecutionTime,
      errors: errors.length > 0 ? errors : void 0,
      logIds: logIds.length > 0 ? logIds : void 0
    };
  } catch (error) {
    const errorMessage = error instanceof Error ? error.message : String(error);
    const stack = error instanceof Error ? error.stack : void 0;
    errors.push(
      `Critical rule engine error [entityType=${context.entityType}, entityId=${context.entityId || "unknown"}]: ${errorMessage}${stack ? `
Stack: ${stack}` : ""}`
    );
    const totalExecutionTime = Date.now() - startTime;
    return {
      allowed: false,
      executedRules,
      totalExecutionTime,
      errors
    };
  }
}
async function executeSingleRule(em, rule, context, options = {}) {
  const startTime = Date.now();
  try {
    const executeWithTimeout = async () => {
      const evalContext = {
        entityType: context.entityType,
        entityId: context.entityId,
        eventType: context.eventType,
        user: context.user,
        tenant: context.tenant,
        organization: context.organization
      };
      const result = await ruleEvaluator.evaluateSingleRule(rule, context.data, evalContext);
      if (!result.evaluationCompleted) {
        const executionTime2 = Date.now() - startTime;
        let logId2;
        if (!context.dryRun) {
          logId2 = await logRuleExecution(em, {
            rule,
            context,
            conditionResult: false,
            actionsExecuted: null,
            executionTime: executionTime2,
            error: result.error
          });
          await emitRuleExecutionFailed(options.eventBus, {
            ruleId: rule.ruleId,
            ruleName: rule.ruleName,
            entityType: context.entityType ?? null,
            errorMessage: result.error ?? null,
            tenantId: context.tenantId,
            organizationId: context.organizationId ?? null
          });
        }
        return {
          rule,
          conditionResult: false,
          actionsExecuted: null,
          executionTime: executionTime2,
          error: result.error,
          logId: logId2
        };
      }
      const actions = result.conditionsPassed ? rule.successActions : rule.failureActions;
      let actionsExecuted = null;
      if (actions && Array.isArray(actions) && actions.length > 0) {
        const actionContext = {
          ...evalContext,
          data: context.data,
          ruleId: rule.ruleId,
          ruleName: rule.ruleName
        };
        actionsExecuted = await actionExecutor.executeActions(actions, actionContext);
      }
      const executionTime = Date.now() - startTime;
      let logId;
      if (!context.dryRun) {
        logId = await logRuleExecution(em, {
          rule,
          context,
          conditionResult: result.conditionsPassed,
          actionsExecuted,
          executionTime
        });
      }
      return {
        rule,
        conditionResult: result.conditionsPassed,
        actionsExecuted,
        executionTime,
        logId
      };
    };
    return await withTimeout(
      executeWithTimeout(),
      MAX_SINGLE_RULE_TIMEOUT_MS,
      `Single rule execution timeout [ruleId=${rule.ruleId}]`
    );
  } catch (error) {
    const executionTime = Date.now() - startTime;
    const errorMessage = error instanceof Error ? error.message : String(error);
    const enhancedError = `Failed to execute rule [ruleId=${rule.ruleId}, entityType=${context.entityType}]: ${errorMessage}`;
    let logId;
    if (!context.dryRun) {
      logId = await logRuleExecution(em, {
        rule,
        context,
        conditionResult: false,
        actionsExecuted: null,
        executionTime,
        error: enhancedError
      });
      await emitRuleExecutionFailed(options.eventBus, {
        ruleId: rule.ruleId,
        ruleName: rule.ruleName,
        entityType: context.entityType ?? null,
        errorMessage: enhancedError,
        tenantId: context.tenantId,
        organizationId: context.organizationId ?? null
      });
    }
    return {
      rule,
      conditionResult: false,
      actionsExecuted: null,
      executionTime,
      error: enhancedError,
      logId
    };
  }
}
async function findApplicableRules(em, options, cacheOptions = {}) {
  ruleDiscoveryOptionsSchema.parse(options);
  const { entityType, eventType, tenantId, organizationId, ruleType } = options;
  const baseWhere = {
    entityType,
    tenantId,
    organizationId,
    enabled: true,
    deletedAt: null
  };
  if (eventType) {
    baseWhere.eventType = eventType;
  }
  if (ruleType) {
    baseWhere.ruleType = ruleType;
  }
  const cached = await getCachedRuleDiscovery(cacheOptions.cache, options);
  let rules;
  if (cached) {
    if (cached.ruleIds.length === 0) {
      rules = [];
    } else {
      const cachedRules = await em.find(BusinessRule, {
        ...baseWhere,
        id: { $in: cached.ruleIds }
      }, {
        orderBy: { priority: "DESC", ruleId: "ASC" }
      });
      const byId = new Map(cachedRules.map((rule) => [rule.id, rule]));
      rules = cached.ruleIds.map((id) => byId.get(id)).filter((rule) => Boolean(rule));
    }
  } else {
    rules = await em.find(BusinessRule, baseWhere, {
      orderBy: { priority: "DESC", ruleId: "ASC" }
    });
    await cacheRuleDiscovery(cacheOptions.cache, options, rules);
  }
  const now = /* @__PURE__ */ new Date();
  return rules.filter((rule) => {
    if (rule.effectiveFrom && rule.effectiveFrom > now) {
      return false;
    }
    if (rule.effectiveTo && rule.effectiveTo < now) {
      return false;
    }
    return true;
  });
}
async function executeRuleById(em, context) {
  const startTime = Date.now();
  const validation = directRuleExecutionContextSchema.safeParse(context);
  if (!validation.success) {
    const validationErrors = validation.error.issues.map((e) => `${e.path.join(".")}: ${e.message}`);
    return {
      success: false,
      ruleId: context.ruleId,
      ruleName: "Unknown",
      conditionResult: false,
      actionsExecuted: null,
      executionTime: Date.now() - startTime,
      error: `Validation failed: ${validationErrors.join(", ")}`
    };
  }
  const rule = await em.findOne(BusinessRule, {
    id: context.ruleId,
    tenantId: context.tenantId,
    organizationId: context.organizationId,
    deletedAt: null
  });
  if (!rule) {
    return {
      success: false,
      ruleId: context.ruleId,
      ruleName: "Unknown",
      conditionResult: false,
      actionsExecuted: null,
      executionTime: Date.now() - startTime,
      error: "Rule not found"
    };
  }
  if (!rule.enabled) {
    return {
      success: false,
      ruleId: rule.ruleId,
      ruleName: rule.ruleName,
      conditionResult: false,
      actionsExecuted: null,
      executionTime: Date.now() - startTime,
      error: "Rule is disabled"
    };
  }
  const now = /* @__PURE__ */ new Date();
  if (rule.effectiveFrom && rule.effectiveFrom > now) {
    return {
      success: false,
      ruleId: rule.ruleId,
      ruleName: rule.ruleName,
      conditionResult: false,
      actionsExecuted: null,
      executionTime: Date.now() - startTime,
      error: `Rule is not yet effective (starts ${rule.effectiveFrom.toISOString()})`
    };
  }
  if (rule.effectiveTo && rule.effectiveTo < now) {
    return {
      success: false,
      ruleId: rule.ruleId,
      ruleName: rule.ruleName,
      conditionResult: false,
      actionsExecuted: null,
      executionTime: Date.now() - startTime,
      error: `Rule has expired (ended ${rule.effectiveTo.toISOString()})`
    };
  }
  const engineContext = {
    entityType: context.entityType || rule.entityType,
    entityId: context.entityId,
    eventType: context.eventType || rule.eventType || void 0,
    data: context.data,
    user: context.user,
    tenantId: context.tenantId,
    organizationId: context.organizationId,
    executedBy: context.executedBy,
    dryRun: context.dryRun
  };
  const result = await executeSingleRule(em, rule, engineContext);
  return {
    success: !result.error,
    ruleId: rule.ruleId,
    ruleName: rule.ruleName,
    conditionResult: result.conditionResult,
    actionsExecuted: result.actionsExecuted,
    executionTime: result.executionTime,
    error: result.error,
    logId: result.logId
  };
}
async function executeRuleByRuleId(em, context) {
  const startTime = Date.now();
  const validation = ruleIdExecutionContextSchema.safeParse(context);
  if (!validation.success) {
    const validationErrors = validation.error.issues.map((e) => `${e.path.join(".")}: ${e.message}`);
    return {
      success: false,
      ruleId: context.ruleId || "unknown",
      ruleName: "Unknown",
      conditionResult: false,
      actionsExecuted: null,
      executionTime: Date.now() - startTime,
      error: `Validation failed: ${validationErrors.join(", ")}`
    };
  }
  const rule = await em.findOne(BusinessRule, {
    ruleId: context.ruleId,
    // String identifier column
    tenantId: context.tenantId,
    organizationId: context.organizationId,
    deletedAt: null
  });
  if (!rule) {
    return {
      success: false,
      ruleId: context.ruleId,
      ruleName: "Unknown",
      conditionResult: false,
      actionsExecuted: null,
      executionTime: Date.now() - startTime,
      error: "Rule not found"
    };
  }
  if (!rule.enabled) {
    return {
      success: false,
      ruleId: rule.ruleId,
      ruleName: rule.ruleName,
      conditionResult: false,
      actionsExecuted: null,
      executionTime: Date.now() - startTime,
      error: "Rule is disabled"
    };
  }
  const now = /* @__PURE__ */ new Date();
  if (rule.effectiveFrom && rule.effectiveFrom > now) {
    return {
      success: false,
      ruleId: rule.ruleId,
      ruleName: rule.ruleName,
      conditionResult: false,
      actionsExecuted: null,
      executionTime: Date.now() - startTime,
      error: `Rule is not yet effective (starts ${rule.effectiveFrom.toISOString()})`
    };
  }
  if (rule.effectiveTo && rule.effectiveTo < now) {
    return {
      success: false,
      ruleId: rule.ruleId,
      ruleName: rule.ruleName,
      conditionResult: false,
      actionsExecuted: null,
      executionTime: Date.now() - startTime,
      error: `Rule has expired (ended ${rule.effectiveTo.toISOString()})`
    };
  }
  const engineContext = {
    entityType: context.entityType || rule.entityType,
    entityId: context.entityId,
    eventType: context.eventType || rule.eventType || void 0,
    data: context.data,
    user: context.user,
    tenantId: context.tenantId,
    organizationId: context.organizationId,
    executedBy: context.executedBy,
    dryRun: context.dryRun
  };
  const result = await executeSingleRule(em, rule, engineContext);
  return {
    success: !result.error,
    ruleId: rule.ruleId,
    ruleName: rule.ruleName,
    conditionResult: result.conditionResult,
    actionsExecuted: result.actionsExecuted,
    executionTime: result.executionTime,
    error: result.error,
    logId: result.logId
  };
}
const SENSITIVE_FIELD_PATTERNS = [
  /password/i,
  /passwd/i,
  /pwd/i,
  /secret/i,
  /token/i,
  /api[_-]?key/i,
  /auth/i,
  /credit[_-]?card/i,
  /card[_-]?number/i,
  /cvv/i,
  /ssn/i,
  /social[_-]?security/i,
  /tax[_-]?id/i,
  /driver[_-]?license/i,
  /passport/i
];
const MAX_SANITIZATION_DEPTH = 5;
function sanitizeForLogging(data, depth = 0) {
  if (depth > MAX_SANITIZATION_DEPTH) {
    return "[Max depth exceeded]";
  }
  if (data === null || data === void 0) {
    return data;
  }
  if (typeof data !== "object") {
    return data;
  }
  if (Array.isArray(data)) {
    return data.map((item) => sanitizeForLogging(item, depth + 1));
  }
  const sanitized = {};
  for (const [key, value] of Object.entries(data)) {
    const isSensitive = SENSITIVE_FIELD_PATTERNS.some((pattern) => pattern.test(key));
    if (isSensitive) {
      sanitized[key] = "[REDACTED]";
    } else if (typeof value === "object" && value !== null) {
      sanitized[key] = sanitizeForLogging(value, depth + 1);
    } else {
      sanitized[key] = value;
    }
  }
  return sanitized;
}
function sanitizeUser(user) {
  if (!user) {
    return void 0;
  }
  return {
    id: user.id,
    role: user.role
    // Don't log: email, name, phone, address, etc.
  };
}
async function logRuleExecution(em, options) {
  const { rule, context, conditionResult, actionsExecuted, executionTime, error } = options;
  const executionResult = error ? EXECUTION_RESULT_ERROR : conditionResult ? EXECUTION_RESULT_SUCCESS : EXECUTION_RESULT_FAILURE;
  const log = em.create(RuleExecutionLog, {
    rule,
    entityId: context.entityId || DEFAULT_ENTITY_ID,
    entityType: context.entityType,
    executionResult,
    inputContext: {
      data: sanitizeForLogging(context.data),
      eventType: context.eventType,
      user: sanitizeUser(context.user)
    },
    outputContext: actionsExecuted ? {
      conditionResult,
      actionsExecuted: actionsExecuted.results.map((r) => ({
        type: r.action.type,
        success: r.success,
        error: r.error
      }))
    } : null,
    errorMessage: error || null,
    executionTimeMs: executionTime,
    tenantId: context.tenantId,
    organizationId: context.organizationId,
    executedBy: context.executedBy || null
  });
  await em.persist(log).flush();
  return String(log.id);
}
async function emitRuleExecutionFailed(eventBus, payload) {
  if (!eventBus?.emitEvent) return;
  await eventBus.emitEvent("business_rules.rule.execution_failed", payload).catch(() => void 0);
}
export {
  executeRuleById,
  executeRuleByRuleId,
  executeRules,
  executeSingleRule,
  findApplicableRules,
  invalidateBusinessRuleDiscoveryCache,
  isRuleDiscoveryCache,
  logRuleExecution,
  resolveBusinessRuleDiscoveryCache
};
//# sourceMappingURL=rule-engine.js.map
