import { createHash } from "node:crypto";
import { decryptWithAesGcm } from "@open-mercato/shared/lib/encryption/aes";
import { resolveTenantEncryptionService } from "@open-mercato/shared/lib/encryption/customFieldValues";
import { resolveEntityIdFromMetadata } from "@open-mercato/shared/lib/encryption/entityIds";
import { findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
import {
  resolveDateRange,
  getPreviousPeriod,
  calculatePercentageChange,
  determineChangeDirection,
  isValidDateRangePreset
} from "@open-mercato/ui/backend/date-range";
import {
  buildAggregationQuery
} from "../lib/aggregations.js";
const WIDGET_DATA_CACHE_TTL = 12e4;
const WIDGET_DATA_SEGMENT_TTL = 864e5;
const WIDGET_DATA_SEGMENT_KEY = "widget-data:__segment__";
const SAFE_IDENTIFIER_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
class WidgetDataValidationError extends Error {
  constructor(message) {
    super(message);
    this.name = "WidgetDataValidationError";
  }
}
function assertSafeIdentifier(value, name) {
  if (!SAFE_IDENTIFIER_PATTERN.test(value)) {
    throw new Error(`Invalid ${name}: ${value}`);
  }
}
class WidgetDataService {
  constructor(options) {
    this.em = options.em;
    this.scope = options.scope;
    this.registry = options.registry;
    this.cache = options.cache;
  }
  buildCacheKey(request) {
    const hash = createHash("sha256");
    hash.update(JSON.stringify({ request, scope: this.scope }));
    return `widget-data:${hash.digest("hex").slice(0, 16)}`;
  }
  getCacheTags(entityType) {
    return ["widget-data", `widget-data:${entityType}`];
  }
  async fetchWidgetData(request) {
    this.validateRequest(request);
    if (this.cache) {
      const cacheKey = this.buildCacheKey(request);
      try {
        const cached = await this.cache.get(cacheKey);
        if (cached && typeof cached === "object" && "value" in cached) {
          return cached;
        }
      } catch {
      }
    }
    const now = /* @__PURE__ */ new Date();
    let dateRangeResolved;
    let comparisonRange;
    if (request.dateRange) {
      dateRangeResolved = resolveDateRange(request.dateRange.preset, now);
      if (request.comparison) {
        comparisonRange = getPreviousPeriod(dateRangeResolved, request.dateRange.preset);
      }
    }
    const shouldFetchComparison = Boolean(comparisonRange && request.dateRange);
    const [mainResult, comparisonResult] = await Promise.all([
      this.executeQuery(request, dateRangeResolved),
      shouldFetchComparison && comparisonRange ? this.executeQuery(request, comparisonRange) : Promise.resolve(void 0)
    ]);
    const response = {
      value: mainResult.value,
      data: mainResult.data,
      metadata: {
        fetchedAt: now.toISOString(),
        recordCount: mainResult.data.length || (mainResult.value !== null ? 1 : 0)
      }
    };
    if (comparisonResult && mainResult.value !== null && comparisonResult.value !== null) {
      response.comparison = {
        value: comparisonResult.value,
        change: calculatePercentageChange(mainResult.value, comparisonResult.value),
        direction: determineChangeDirection(mainResult.value, comparisonResult.value)
      };
    }
    if (this.cache) {
      const cacheKey = this.buildCacheKey(request);
      const tags = this.getCacheTags(request.entityType);
      try {
        await this.cache.set(cacheKey, response, { ttl: WIDGET_DATA_CACHE_TTL, tags });
        await this.cache.set(
          WIDGET_DATA_SEGMENT_KEY,
          { updatedAt: response.metadata.fetchedAt },
          { ttl: WIDGET_DATA_SEGMENT_TTL, tags: ["widget-data"] }
        );
      } catch {
      }
    }
    return response;
  }
  validateRequest(request) {
    if (!this.registry.isValidEntityType(request.entityType)) {
      throw new WidgetDataValidationError(`Invalid entity type: ${request.entityType}`);
    }
    if (!request.metric?.field || !request.metric?.aggregate) {
      throw new WidgetDataValidationError("Metric field and aggregate are required");
    }
    const metricMapping = this.registry.getFieldMapping(request.entityType, request.metric.field);
    if (!metricMapping) {
      throw new WidgetDataValidationError(
        `Invalid metric field: ${request.metric.field} for entity type: ${request.entityType}`
      );
    }
    const validAggregates = ["count", "sum", "avg", "min", "max"];
    if (!validAggregates.includes(request.metric.aggregate)) {
      throw new WidgetDataValidationError(`Invalid aggregate function: ${request.metric.aggregate}`);
    }
    if (request.dateRange && !isValidDateRangePreset(request.dateRange.preset)) {
      throw new WidgetDataValidationError(`Invalid date range preset: ${request.dateRange.preset}`);
    }
    if (request.groupBy) {
      const groupMapping = this.registry.getFieldMapping(request.entityType, request.groupBy.field);
      if (!groupMapping) {
        const [baseField] = request.groupBy.field.split(".");
        const baseMapping = this.registry.getFieldMapping(request.entityType, baseField);
        if (!baseMapping || baseMapping.type !== "jsonb") {
          throw new WidgetDataValidationError(`Invalid groupBy field: ${request.groupBy.field}`);
        }
      }
    }
  }
  async executeQuery(request, dateRange) {
    const query = buildAggregationQuery({
      entityType: request.entityType,
      metric: request.metric,
      groupBy: request.groupBy,
      dateRange: dateRange && request.dateRange ? { field: request.dateRange.field, ...dateRange } : void 0,
      filters: request.filters,
      scope: this.scope,
      registry: this.registry
    });
    if (!query) {
      throw new Error("Failed to build aggregation query");
    }
    const rows = await this.em.getConnection().execute(query.sql, query.params);
    const results = Array.isArray(rows) ? rows : [];
    if (request.groupBy) {
      let data = results.map((row) => ({
        groupKey: row.group_key,
        value: row.value !== null ? Number(row.value) : null
      }));
      if (request.groupBy.resolveLabels) {
        data = await this.resolveGroupLabels(data, request.entityType, request.groupBy.field);
      }
      const totalValue = data.reduce((sum, item) => sum + (item.value ?? 0), 0);
      return { value: totalValue, data };
    }
    const singleValue = results[0]?.value !== void 0 ? Number(results[0].value) : null;
    return { value: singleValue, data: [] };
  }
  async resolveGroupLabels(data, entityType, groupByField) {
    const config = this.registry.getLabelResolverConfig(entityType, groupByField);
    if (!config) {
      return data.map((item) => ({
        ...item,
        groupLabel: item.groupKey != null && item.groupKey !== "" ? String(item.groupKey) : void 0
      }));
    }
    const ids = data.map((item) => item.groupKey).filter((id) => {
      if (typeof id !== "string" || id.length === 0) return false;
      return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(id);
    });
    if (ids.length === 0) {
      return data.map((item) => ({ ...item, groupLabel: void 0 }));
    }
    const uniqueIds = [...new Set(ids)];
    assertSafeIdentifier(config.table, "table name");
    assertSafeIdentifier(config.idColumn, "id column");
    assertSafeIdentifier(config.labelColumn, "label column");
    const meta = this.resolveEntityMetadata(config.table);
    const idProp = meta ? this.resolveEntityPropertyName(meta, config.idColumn) : null;
    const labelProp = meta ? this.resolveEntityPropertyName(meta, config.labelColumn) : null;
    const tenantProp = meta ? this.resolveEntityPropertyName(meta, "tenant_id") ?? this.resolveEntityPropertyName(meta, "tenantId") : null;
    const organizationProp = meta ? this.resolveEntityPropertyName(meta, "organization_id") ?? this.resolveEntityPropertyName(meta, "organizationId") : null;
    const entityName = meta ? meta.class ?? meta.className ?? meta.name : null;
    if (meta && idProp && labelProp && tenantProp && entityName) {
      const where = {
        [idProp]: { $in: uniqueIds },
        [tenantProp]: this.scope.tenantId
      };
      if (organizationProp && this.scope.organizationIds && this.scope.organizationIds.length > 0) {
        where[organizationProp] = { $in: this.scope.organizationIds };
      }
      try {
        const records = await findWithDecryption(
          this.em,
          entityName,
          where,
          { fields: [idProp, labelProp, tenantProp, organizationProp].filter(Boolean) },
          { tenantId: this.scope.tenantId, organizationId: this.resolveOrganizationId() }
        );
        const encryptionService = resolveTenantEncryptionService(this.em);
        const dek = encryptionService?.isEnabled() ? await encryptionService.getDek(this.scope.tenantId) : null;
        let hasEncryptedLabels = false;
        let hasDecryptedLabels = false;
        const labelMap = /* @__PURE__ */ new Map();
        for (const record of records) {
          const id = record[idProp];
          let labelValue = record[labelProp];
          if (typeof labelValue === "string" && this.isEncryptedPayload(labelValue)) {
            hasEncryptedLabels = true;
            if (dek?.key) {
              const decrypted = this.decryptWithDek(labelValue, dek.key);
              if (decrypted !== null) {
                labelValue = decrypted;
                hasDecryptedLabels = true;
              }
            }
          } else if (labelValue != null && labelValue !== "") {
            hasDecryptedLabels = true;
          }
          if (typeof id === "string" && labelValue != null && labelValue !== "") {
            labelMap.set(id, String(labelValue));
          }
        }
        if (labelMap.size > 0 && (!hasEncryptedLabels || hasDecryptedLabels)) {
          return data.map((item) => ({
            ...item,
            groupLabel: typeof item.groupKey === "string" && labelMap.has(item.groupKey) ? labelMap.get(item.groupKey) : void 0
          }));
        }
      } catch {
      }
    }
    const clauses = [`"${config.idColumn}" = ANY(?::uuid[])`, "tenant_id = ?"];
    const params = [`{${uniqueIds.join(",")}}`, this.scope.tenantId];
    if (this.scope.organizationIds && this.scope.organizationIds.length > 0) {
      clauses.push("organization_id = ANY(?::uuid[])");
      params.push(`{${this.scope.organizationIds.join(",")}}`);
    }
    const sql = `SELECT "${config.idColumn}" as id, "${config.labelColumn}" as label, tenant_id, organization_id FROM "${config.table}" WHERE ${clauses.join(
      " AND "
    )}`;
    try {
      const labelRows = await this.em.getConnection().execute(sql, params);
      const entityId = this.resolveEntityId(meta);
      const encryptionService = resolveTenantEncryptionService(this.em);
      const organizationId = this.resolveOrganizationId();
      const dek = encryptionService?.isEnabled() ? await encryptionService.getDek(this.scope.tenantId) : null;
      const labelMap = /* @__PURE__ */ new Map();
      for (const row of labelRows) {
        let labelValue = row.label;
        if (entityId && encryptionService?.isEnabled() && labelValue != null) {
          const rowOrgId = row.organization_id ?? organizationId ?? null;
          const decrypted = await encryptionService.decryptEntityPayload(
            entityId,
            { [config.labelColumn]: labelValue },
            this.scope.tenantId,
            rowOrgId
          );
          const resolved = decrypted[config.labelColumn];
          if (typeof resolved === "string" || typeof resolved === "number") {
            labelValue = String(resolved);
          }
        }
        if (labelValue && dek?.key && this.isEncryptedPayload(labelValue)) {
          const decrypted = this.decryptWithDek(labelValue, dek.key);
          if (decrypted !== null) {
            labelValue = decrypted;
          }
        }
        if (row.id && labelValue != null && labelValue !== "") {
          labelMap.set(row.id, labelValue);
        }
      }
      return data.map((item) => ({
        ...item,
        groupLabel: typeof item.groupKey === "string" && labelMap.has(item.groupKey) ? labelMap.get(item.groupKey) : void 0
      }));
    } catch {
      return data.map((item) => ({
        ...item,
        groupLabel: void 0
      }));
    }
  }
  resolveOrganizationId() {
    if (!this.scope.organizationIds || this.scope.organizationIds.length !== 1) return null;
    return this.scope.organizationIds[0] ?? null;
  }
  resolveEntityMetadata(tableName) {
    const registry = this.em?.getMetadata?.();
    if (!registry) return null;
    const entries = typeof registry.getAll === "function" && registry.getAll() || (Array.isArray(registry.metadata) ? registry.metadata : Object.values(registry.metadata ?? {}));
    const metas = Array.isArray(entries) ? entries : Object.values(entries ?? {});
    const match = metas.find((meta) => {
      const table = meta?.tableName ?? meta?.collection;
      if (typeof table !== "string") return false;
      if (table === tableName) return true;
      return table.split(".").pop() === tableName;
    });
    return match ?? null;
  }
  resolveEntityPropertyName(meta, columnName) {
    const properties = meta?.properties ? Object.values(meta.properties) : [];
    for (const prop of properties) {
      const fieldName = prop?.fieldName;
      const fieldNames = prop?.fieldNames;
      if (typeof fieldName === "string" && fieldName === columnName) return prop?.name ?? null;
      if (Array.isArray(fieldNames) && fieldNames.includes(columnName)) return prop?.name ?? null;
      if (prop?.name === columnName) return prop?.name ?? null;
    }
    return null;
  }
  resolveEntityId(meta) {
    if (!meta) return null;
    try {
      return resolveEntityIdFromMetadata(meta);
    } catch {
      return null;
    }
  }
  isEncryptedPayload(value) {
    const parts = value.split(":");
    return parts.length === 4 && parts[3] === "v1";
  }
  decryptWithDek(value, dek) {
    const first = decryptWithAesGcm(value, dek);
    if (first === null) return null;
    if (!this.isEncryptedPayload(first)) return first;
    return decryptWithAesGcm(first, dek) ?? first;
  }
}
function createWidgetDataService(em, scope, registry, cache) {
  return new WidgetDataService({ em, scope, registry, cache });
}
export {
  WidgetDataService,
  WidgetDataValidationError,
  createWidgetDataService
};
//# sourceMappingURL=widgetDataService.js.map
