import { createHash } from "node:crypto";

export const TELEMETRY_FIELD_REGISTRY_SCHEMA_VERSION = "nodedc.telemetry-field-registry/v1";
export const TELEMETRY_FIELD_PROJECTION_SCHEMA_VERSION = "nodedc.telemetry-field-projection/v1";

const IDENTIFIER = /^[a-z][a-z0-9._:-]{2,127}$/;
const SEMVER = /^\d+\.\d+\.\d+(?:[-+][a-z0-9.-]+)?$/i;
const SOURCE_KEY = /^[A-Za-z_][A-Za-z0-9_.:-]{0,127}$/;
const VALUE_TYPES = new Set(["unknown", "number", "boolean", "string"]);
const STATUSES = new Set(["observed", "classified", "approved", "restricted", "deprecated"]);
const SENSITIVITIES = new Set(["operational", "unclassified", "identifier", "personal", "command", "restricted"]);
const SURFACES = new Set(["audit", "data_product", "analytics", "foundry"]);
const REGISTRY_KEYS = new Set([
  "schemaVersion",
  "id",
  "version",
  "providerPackage",
  "dataProductId",
  "sourceNamespace",
  "entries",
]);
const PACKAGE_KEYS = new Set(["id", "version"]);
const ENTRY_KEYS = new Set([
  "id",
  "sourceKey",
  "status",
  "sensitivity",
  "valueType",
  "semanticReading",
  "allowedSurfaces",
]);
const READING_KEYS = new Set(["id", "labelSource", "label", "unitSource", "unit"]);
const LABEL_SOURCES = new Set(["registry", "provider_configured"]);
const UNIT_SOURCES = new Set(["none", "registry", "provider_configured"]);
const PROJECTION_OPTION_KEYS = new Set([
  "providerPackageId",
  "providerPackageVersion",
  "dataProductId",
  "surface",
]);

/**
 * Validate a values-free, provider-scoped registry that classifies dynamic
 * telemetry parameters before any Data Product or interface can expose them.
 */
export function validateTelemetryFieldRegistry(value) {
  const errors = [];
  if (!isPlainObject(value)) return result(["registry_must_be_object"]);
  rejectUnknownKeys(value, REGISTRY_KEYS, "registry", errors);
  if (value.schemaVersion !== TELEMETRY_FIELD_REGISTRY_SCHEMA_VERSION) {
    errors.push("registry.schemaVersion_mismatch");
  }
  requiredIdentifier(value.id, "registry.id", errors);
  requiredSemver(value.version, "registry.version", errors);
  requiredIdentifier(value.dataProductId, "registry.dataProductId", errors);
  requiredIdentifier(value.sourceNamespace, "registry.sourceNamespace", errors);

  if (!isPlainObject(value.providerPackage)) {
    errors.push("registry.providerPackage_must_be_object");
  } else {
    rejectUnknownKeys(value.providerPackage, PACKAGE_KEYS, "registry.providerPackage", errors);
    requiredIdentifier(value.providerPackage.id, "registry.providerPackage.id", errors);
    requiredSemver(value.providerPackage.version, "registry.providerPackage.version", errors);
  }

  if (!Array.isArray(value.entries) || value.entries.length === 0 || value.entries.length > 512) {
    errors.push("registry.entries_must_be_nonempty_bounded_array");
  } else {
    const entryIds = new Set();
    const sourceKeys = new Set();
    const approvedReadingIds = new Set();
    value.entries.forEach((entry, index) => {
      const path = `registry.entries[${index}]`;
      if (!isPlainObject(entry)) {
        errors.push(`${path}_must_be_object`);
        return;
      }
      rejectUnknownKeys(entry, ENTRY_KEYS, path, errors);
      requiredIdentifier(entry.id, `${path}.id`, errors);
      if (entryIds.has(entry.id)) errors.push(`${path}.id_must_be_unique`);
      entryIds.add(entry.id);
      if (typeof entry.sourceKey !== "string" || !SOURCE_KEY.test(entry.sourceKey)) {
        errors.push(`${path}.sourceKey_invalid`);
      }
      if (sourceKeys.has(entry.sourceKey)) errors.push(`${path}.sourceKey_must_be_unique`);
      sourceKeys.add(entry.sourceKey);
      if (!STATUSES.has(entry.status)) errors.push(`${path}.status_invalid`);
      if (!SENSITIVITIES.has(entry.sensitivity)) errors.push(`${path}.sensitivity_invalid`);
      if (!VALUE_TYPES.has(entry.valueType)) errors.push(`${path}.valueType_invalid`);
      validateSurfaces(entry.allowedSurfaces, path, errors);
      validateSemanticReading(entry.semanticReading, path, errors);

      const surfaces = new Set(Array.isArray(entry.allowedSurfaces) ? entry.allowedSurfaces : []);
      const externallyExposed = [...surfaces].some((surface) => surface !== "audit");
      if (entry.status === "approved") {
        if (!isPlainObject(entry.semanticReading)) errors.push(`${path}.approved_requires_semantic_reading`);
        if (entry.sensitivity !== "operational") errors.push(`${path}.approved_must_be_operational`);
        if (entry.valueType === "unknown") errors.push(`${path}.approved_valueType_must_be_classified`);
        if (!surfaces.has("data_product")) errors.push(`${path}.approved_requires_data_product_surface`);
        const readingId = entry.semanticReading?.id;
        if (typeof readingId === "string") {
          if (approvedReadingIds.has(readingId)) errors.push(`${path}.semanticReading.id_must_be_unique`);
          approvedReadingIds.add(readingId);
        }
      } else if (externallyExposed) {
        errors.push(`${path}.nonapproved_surface_must_be_audit_only`);
      }
      if (new Set(["observed", "restricted"]).has(entry.status) && entry.semanticReading !== undefined) {
        errors.push(`${path}.${entry.status}_must_not_claim_semantic_reading`);
      }
      if (entry.status === "observed" && entry.sensitivity !== "unclassified") {
        errors.push(`${path}.observed_must_be_unclassified`);
      }
      if (entry.status === "restricted" && !new Set(["identifier", "personal", "command", "restricted"]).has(entry.sensitivity)) {
        errors.push(`${path}.restricted_sensitivity_invalid`);
      }
    });
  }

  return result(errors);
}

/**
 * Compile an exact, values-free allowlist for one consumer surface. Unknown,
 * observed, restricted and deprecated provider parameters are omitted.
 */
export function compileTelemetryFieldProjection(registry, options) {
  const validation = validateTelemetryFieldRegistry(registry);
  if (!validation.ok) {
    throw new Error(`telemetry_field_registry_invalid:${validation.errors.join(",")}`);
  }
  if (!isPlainObject(options)) throw new Error("telemetry_projection_options_must_be_object");
  const optionErrors = [];
  rejectUnknownKeys(options, PROJECTION_OPTION_KEYS, "options", optionErrors);
  requiredIdentifier(options.providerPackageId, "options.providerPackageId", optionErrors);
  requiredSemver(options.providerPackageVersion, "options.providerPackageVersion", optionErrors);
  requiredIdentifier(options.dataProductId, "options.dataProductId", optionErrors);
  if (!SURFACES.has(options.surface)) optionErrors.push("options.surface_invalid");
  if (options.providerPackageId !== registry.providerPackage.id) optionErrors.push("options.providerPackageId_mismatch");
  if (options.providerPackageVersion !== registry.providerPackage.version) optionErrors.push("options.providerPackageVersion_mismatch");
  if (options.dataProductId !== registry.dataProductId) optionErrors.push("options.dataProductId_mismatch");
  if (optionErrors.length) {
    throw new Error(`telemetry_projection_invalid:${[...new Set(optionErrors)].join(",")}`);
  }

  const entries = registry.entries
    .filter((entry) => entry.status === "approved" && entry.allowedSurfaces.includes(options.surface))
    .map((entry) => ({
      registryEntryId: entry.id,
      sourceNamespace: registry.sourceNamespace,
      sourceKey: entry.sourceKey,
      valueType: entry.valueType,
      reading: structuredClone(entry.semanticReading),
    }))
    .sort((left, right) => left.sourceKey.localeCompare(right.sourceKey));

  return deepFreeze({
    schemaVersion: TELEMETRY_FIELD_PROJECTION_SCHEMA_VERSION,
    registry: {
      id: registry.id,
      version: registry.version,
      digest: digestTelemetryFieldRegistry(registry),
    },
    providerPackage: structuredClone(registry.providerPackage),
    dataProductId: registry.dataProductId,
    surface: options.surface,
    entries,
  });
}

export function digestTelemetryFieldRegistry(registry) {
  const validation = validateTelemetryFieldRegistry(registry);
  if (!validation.ok) {
    throw new Error(`telemetry_field_registry_invalid:${validation.errors.join(",")}`);
  }
  return canonicalDigest(registry);
}

function validateSurfaces(value, path, errors) {
  if (!Array.isArray(value) || value.length === 0 || value.length > SURFACES.size) {
    errors.push(`${path}.allowedSurfaces_must_be_nonempty_bounded_array`);
    return;
  }
  if (new Set(value).size !== value.length) errors.push(`${path}.allowedSurfaces_must_be_unique`);
  value.forEach((surface) => {
    if (!SURFACES.has(surface)) errors.push(`${path}.allowedSurfaces_contains_invalid_surface`);
  });
}

function validateSemanticReading(value, path, errors) {
  if (value === undefined) return;
  if (!isPlainObject(value)) {
    errors.push(`${path}.semanticReading_must_be_object`);
    return;
  }
  rejectUnknownKeys(value, READING_KEYS, `${path}.semanticReading`, errors);
  requiredIdentifier(value.id, `${path}.semanticReading.id`, errors);
  if (!LABEL_SOURCES.has(value.labelSource)) {
    errors.push(`${path}.semanticReading.labelSource_invalid`);
  }
  if (value.labelSource === "registry"
    && (typeof value.label !== "string" || !value.label.trim() || value.label.length > 160)) {
    errors.push(`${path}.semanticReading.label_invalid`);
  }
  if (value.labelSource === "provider_configured"
    && value.label !== undefined
    && (typeof value.label !== "string" || !value.label.trim() || value.label.length > 160)) {
    errors.push(`${path}.semanticReading.label_invalid`);
  }
  if (!UNIT_SOURCES.has(value.unitSource)) {
    errors.push(`${path}.semanticReading.unitSource_invalid`);
  }
  if (value.unitSource === "registry"
    && (typeof value.unit !== "string" || !value.unit.trim() || value.unit.length > 32)) {
    errors.push(`${path}.semanticReading.unit_invalid`);
  }
  if (value.unitSource === "none" && value.unit !== undefined) {
    errors.push(`${path}.semanticReading.unit_not_allowed`);
  }
  if (value.unit !== undefined && (typeof value.unit !== "string" || value.unit.length > 32)) {
    errors.push(`${path}.semanticReading.unit_invalid`);
  }
}

function canonicalDigest(value) {
  return `sha256:${createHash("sha256").update(stableJson(value), "utf8").digest("hex")}`;
}

function stableJson(value) {
  return JSON.stringify(stableValue(value));
}

function stableValue(value) {
  if (Array.isArray(value)) return value.map(stableValue);
  if (!isPlainObject(value)) return value;
  return Object.fromEntries(
    Object.keys(value)
      .filter((key) => value[key] !== undefined)
      .sort()
      .map((key) => [key, stableValue(value[key])]),
  );
}

function rejectUnknownKeys(value, allowed, path, errors) {
  for (const key of Object.keys(value)) {
    if (!allowed.has(key)) errors.push(`${path}.${key}_not_allowed`);
  }
}

function requiredIdentifier(value, path, errors) {
  if (typeof value !== "string" || !IDENTIFIER.test(value)) errors.push(`${path}_invalid`);
}

function requiredSemver(value, path, errors) {
  if (typeof value !== "string" || !SEMVER.test(value)) errors.push(`${path}_invalid`);
}

function result(errors) {
  return { ok: errors.length === 0, errors: [...new Set(errors)] };
}

function isPlainObject(value) {
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}

function deepFreeze(input) {
  if (!input || typeof input !== "object" || Object.isFrozen(input)) return input;
  Object.freeze(input);
  for (const child of Object.values(input)) deepFreeze(child);
  return input;
}
