import { createHmac } from "node:crypto";

export const DEVICE_DISCOVERY_SIGNAL_SCHEMA =
  "nodedc.device.discovery-signal.v1";
export const DEVICE_DISCOVERY_VIEW_SCHEMA =
  "nodedc.device.discovery-view.v1";
export const DEVICE_PLANE_BINDING_SCHEMA =
  "nodedc.device-plane-control.binding.v1";

export const DEVICE_LIFECYCLE_STATES = Object.freeze([
  "quarantine",
  "claimed",
  "online",
  "offline",
  "retired",
]);

export const DEVICE_BINDING_CAPABILITIES = Object.freeze([
  "observe",
  "inspect",
  "configure",
  "command",
]);

const OPAQUE_REF_RE = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
const IMEI_RE = /^\d{15}$/;
const DIGEST_RE = /^hmac-sha256:[a-f0-9]{64}$/;
const forbiddenKeyFragments = Object.freeze([
  "password",
  "secret",
  "credential",
  "rawpayload",
  "rawpacket",
  "command",
  "authorization",
  "token",
]);
const safeStatusKeys = new Set([
  "commandtransport",
]);

export function normalizeDiscoverySignal(input) {
  assertPlainObject(input, "discovery_signal");
  rejectForbiddenKeys(input);

  if (input.schemaVersion !== DEVICE_DISCOVERY_SIGNAL_SCHEMA) {
    throw new TypeError("discovery_signal_schema_invalid");
  }

  const sessionRef = normalizeOpaqueRef(input.sessionRef, "session_ref");
  const modelProfileRef = normalizeOpaqueRef(
    input.modelProfileRef,
    "model_profile_ref",
  );
  const protocol = normalizeUpperToken(input.protocol, "protocol");
  const observedAt = normalizeTimestamp(input.observedAt, "observed_at");
  const identifier = normalizeRestrictedIdentifier(input.identifier);
  const evidence = normalizeDiscoveryEvidence(input.evidence);

  return Object.freeze({
    schemaVersion: DEVICE_DISCOVERY_SIGNAL_SCHEMA,
    sessionRef,
    modelProfileRef,
    protocol,
    observedAt,
    identifier,
    evidence,
    lifecycleState: "quarantine",
    commandTransport: "disabled",
  });
}

export function toSafeDiscoveryView(signal, options = {}) {
  const normalized = normalizeDiscoverySignal(signal);
  const discoveryRef = options.discoveryRef
    ? normalizeOpaqueRef(options.discoveryRef, "discovery_ref")
    : undefined;

  return Object.freeze({
    schemaVersion: DEVICE_DISCOVERY_VIEW_SCHEMA,
    ...(discoveryRef ? { discoveryRef } : {}),
    modelProfileRef: normalized.modelProfileRef,
    protocol: normalized.protocol,
    observedAt: normalized.observedAt,
    lifecycleState: normalized.lifecycleState,
    identifier: Object.freeze({
      kind: normalized.identifier.kind,
      masked: maskRestrictedIdentifier(normalized.identifier),
    }),
    evidence: normalized.evidence,
    commandTransport: "disabled",
  });
}

export function hashRestrictedIdentifier(identifier, pepper) {
  const normalized = normalizeRestrictedIdentifier(identifier);
  if (typeof pepper !== "string" || pepper.length < 32) {
    throw new TypeError("identifier_pepper_invalid");
  }

  const digest = createHmac("sha256", pepper)
    .update(`${normalized.kind}\0${normalized.value}`, "utf8")
    .digest("hex");
  return `hmac-sha256:${digest}`;
}

export function assertIdentifierDigest(value) {
  if (typeof value !== "string" || !DIGEST_RE.test(value)) {
    throw new TypeError("identifier_digest_invalid");
  }
  return value;
}

export function normalizeDevicePlaneBinding(input) {
  assertPlainObject(input, "device_plane_binding");
  rejectForbiddenKeys(input);
  if (input.schemaVersion !== DEVICE_PLANE_BINDING_SCHEMA) {
    throw new TypeError("device_plane_binding_schema_invalid");
  }

  const allowed = new Set(DEVICE_BINDING_CAPABILITIES);
  if (!Array.isArray(input.capabilities) || input.capabilities.length === 0) {
    throw new TypeError("device_plane_binding_capabilities_invalid");
  }
  const capabilities = [...new Set(input.capabilities.map((value) => {
    if (typeof value !== "string" || !allowed.has(value)) {
      throw new TypeError("device_plane_binding_capability_invalid");
    }
    return value;
  }))].sort();

  return Object.freeze({
    schemaVersion: DEVICE_PLANE_BINDING_SCHEMA,
    bindingRef: normalizeOpaqueRef(input.bindingRef, "binding_ref"),
    contourRef: normalizeOpaqueRef(input.contourRef, "contour_ref"),
    capabilities: Object.freeze(capabilities),
  });
}

export function assertSafeProjection(value) {
  assertPlainObject(value, "safe_projection");
  rejectForbiddenKeys(value);
  const serialized = JSON.stringify(value);
  if (/\b\d{15}\b/.test(serialized)) {
    throw new TypeError("safe_projection_contains_unmasked_imei");
  }
  return value;
}

function normalizeRestrictedIdentifier(input) {
  assertPlainObject(input, "restricted_identifier");
  if (input.kind !== "imei") {
    throw new TypeError("restricted_identifier_kind_unsupported");
  }
  if (typeof input.value !== "string" || !IMEI_RE.test(input.value)) {
    throw new TypeError("restricted_identifier_imei_invalid");
  }
  return Object.freeze({ kind: "imei", value: input.value });
}

function maskRestrictedIdentifier(identifier) {
  if (identifier.kind === "imei") {
    return `***********${identifier.value.slice(-4)}`;
  }
  throw new TypeError("restricted_identifier_kind_unsupported");
}

function normalizeDiscoveryEvidence(input) {
  assertPlainObject(input, "discovery_evidence");
  rejectForbiddenKeys(input);
  if (input.transport !== "tcp") {
    throw new TypeError("discovery_evidence_transport_invalid");
  }
  const bytesObserved = Number(input.bytesObserved);
  if (!Number.isSafeInteger(bytesObserved) || bytesObserved < 1 || bytesObserved > 4096) {
    throw new TypeError("discovery_evidence_bytes_invalid");
  }
  if (input.framingStatus !== "verified") {
    throw new TypeError("discovery_evidence_framing_unverified");
  }

  return Object.freeze({
    transport: "tcp",
    bytesObserved,
    framingStatus: "verified",
    specificationRef: normalizeOpaqueRef(
      input.specificationRef,
      "framing_specification_ref",
    ),
  });
}

function rejectForbiddenKeys(value, path = "$") {
  if (Array.isArray(value)) {
    value.forEach((item, index) => rejectForbiddenKeys(item, `${path}[${index}]`));
    return;
  }
  if (!value || typeof value !== "object") return;

  for (const [key, child] of Object.entries(value)) {
    const normalizedKey = key.toLowerCase().replace(/[^a-z0-9]/g, "");
    if (
      !safeStatusKeys.has(normalizedKey)
      && forbiddenKeyFragments.some((fragment) => normalizedKey.includes(fragment))
    ) {
      throw new TypeError(`forbidden_device_field:${path}.${key}`);
    }
    rejectForbiddenKeys(child, `${path}.${key}`);
  }
}

function normalizeOpaqueRef(value, label) {
  if (typeof value !== "string" || !OPAQUE_REF_RE.test(value)) {
    throw new TypeError(`${label}_invalid`);
  }
  return value;
}

function normalizeUpperToken(value, label) {
  if (typeof value !== "string" || !/^[A-Z][A-Z0-9_]{0,31}$/.test(value)) {
    throw new TypeError(`${label}_invalid`);
  }
  return value;
}

function normalizeTimestamp(value, label) {
  if (typeof value !== "string") throw new TypeError(`${label}_invalid`);
  const date = new Date(value);
  if (!Number.isFinite(date.getTime()) || date.toISOString() !== value) {
    throw new TypeError(`${label}_invalid`);
  }
  return value;
}

function assertPlainObject(value, label) {
  if (!value || typeof value !== "object" || Array.isArray(value)) {
    throw new TypeError(`${label}_invalid`);
  }
}
