import { FeatureToggle, FeatureToggleOverride } from "../data/entities.js";
import { runWithCacheTenant } from "@open-mercato/cache";
import { createLogger } from "@open-mercato/shared/lib/logger";
const logger = createLogger("feature_toggles").child({ component: "flag-check" });
const toCachedResolution = (value) => {
  if (typeof value !== "object" || value === null) return null;
  const record = value;
  if (!record.valueType || typeof record.source !== "string" || !record.toggleId || !record.identifier || !record.tenantId)
    return null;
  return value;
};
const getIsEnabledCacheKey = (identifier, tenantId) => {
  return `feature_toggles:resolution:${identifier}:${tenantId}`;
};
const getIdentifierTag = (identifier) => `feature_toggles:identifier:${identifier}`;
const getTenantTag = (tenantId) => `feature_toggles:tenant:${tenantId}`;
const getCacheTags = (identifier, tenantId) => {
  return [getIdentifierTag(identifier), getTenantTag(tenantId)];
};
class FeatureTogglesService {
  constructor(cache, em) {
    this.cache = cache;
    this.em = em;
    this.cacheTtlMs = 1 * 60 * 1e3;
    // 1 minute
    // Resolution cache can be disabled via env (e.g. integration tests that flip
    // overrides rapidly between cases). The 1-minute TTL is a production
    // optimization; under fast flag churn it can serve a stale value across
    // override set/clear despite invalidation, so tests opt out for determinism.
    this.cacheDisabled = process.env.OM_FEATURE_TOGGLES_CACHE_DISABLED === "1" || process.env.OM_FEATURE_TOGGLES_CACHE_DISABLED === "true";
  }
  async saveCache(identifier, tenantId, result) {
    if (this.cacheDisabled) return;
    const key = getIsEnabledCacheKey(identifier, tenantId);
    await runWithCacheTenant(
      tenantId,
      () => this.cache.set(key, result, { ttl: this.cacheTtlMs, tags: getCacheTags(identifier, tenantId) })
    );
  }
  async resolveToggle(identifier, tenantId) {
    const key = getIsEnabledCacheKey(identifier, tenantId);
    if (!this.cacheDisabled) {
      const cached = await runWithCacheTenant(tenantId, () => this.cache.get(key));
      if (cached) {
        const parsed = toCachedResolution(cached);
        if (parsed) return parsed;
      }
    }
    let toggle = null;
    toggle = await this.em.findOne(FeatureToggle, { identifier, deletedAt: null });
    if (!toggle) {
      const result2 = {
        valueType: "boolean",
        value: null,
        source: "missing",
        toggleId: "",
        identifier,
        tenantId
      };
      return result2;
    }
    let override = null;
    override = await this.em.findOne(FeatureToggleOverride, { toggle: toggle.id, tenantId });
    const result = {
      valueType: toggle.type,
      value: override ? override.value : toggle.defaultValue,
      source: override ? "override" : "default",
      toggleId: toggle.id,
      identifier: toggle.identifier,
      tenantId
    };
    await this.saveCache(identifier, tenantId, result);
    return result;
  }
  async invalidateIsEnabledCacheByIdentifierTag(identifier) {
    await this.cache.deleteByTags([getIdentifierTag(identifier)]);
  }
  async invalidateIsEnabledCacheByKey(identifier, tenantId) {
    await runWithCacheTenant(tenantId, () => this.cache.delete(getIsEnabledCacheKey(identifier, tenantId)));
  }
  async getFeatureToggleValue(identifier, ctx) {
    const resolution = await this.resolveToggle(identifier, ctx.tenantId);
    if (resolution.source === "missing") {
      logger.warn("Toggle not found", { identifier });
      return {
        ok: false,
        error: {
          code: "MISSING_TOGGLE",
          message: `Toggle "${identifier}" not found (missing).`,
          identifier,
          expectedType: ctx.valueType,
          actualType: resolution.valueType,
          source: resolution.source
        },
        resolution
      };
    }
    if (resolution.valueType !== ctx.valueType) {
      logger.error("Toggle type mismatch", { identifier, expectedType: ctx.valueType, actualType: resolution.valueType, resolution });
      return {
        ok: false,
        error: {
          code: "TYPE_MISMATCH",
          message: `Toggle "${identifier}" has type "${resolution.valueType}" but "${ctx.valueType}" was requested.`,
          identifier,
          expectedType: ctx.valueType,
          actualType: resolution.valueType,
          source: resolution.source
        },
        resolution
      };
    }
    const isValueValid = ctx.valueType === "boolean" && typeof resolution.value === "boolean" || ctx.valueType === "string" && typeof resolution.value === "string" || ctx.valueType === "number" && typeof resolution.value === "number" || ctx.valueType === "json";
    if (!isValueValid) {
      logger.error("Toggle has invalid value for type", { identifier, valueType: resolution.valueType, resolution });
      return {
        ok: false,
        error: {
          code: "INVALID_VALUE",
          message: `Toggle "${identifier}" has invalid value for type "${resolution.valueType}".`,
          identifier,
          expectedType: ctx.valueType,
          actualType: resolution.valueType,
          source: resolution.source
        },
        resolution
      };
    }
    return {
      ok: true,
      value: resolution.value,
      resolution
    };
  }
  async getBoolConfig(identifier, tenantId) {
    return this.getFeatureToggleValue(identifier, { tenantId, valueType: "boolean" });
  }
  async getNumberConfig(identifier, tenantId) {
    return this.getFeatureToggleValue(identifier, { tenantId, valueType: "number" });
  }
  async getStringConfig(identifier, tenantId) {
    return this.getFeatureToggleValue(identifier, { tenantId, valueType: "string" });
  }
  async getJsonConfig(identifier, tenantId) {
    return this.getFeatureToggleValue(identifier, { tenantId, valueType: "json" });
  }
}
export {
  FeatureTogglesService,
  getIsEnabledCacheKey
};
//# sourceMappingURL=feature-flag-check.js.map
