import { createHash, randomBytes } from "node:crypto";

const IDENTIFIER = /^[a-z][a-z0-9._:-]{2,127}$/;
const TOKEN_PREFIX = "ndc_edpwb_";
const SECRET_LIKE_KEY = /(token|secret|password|access[_-]?token|refresh[_-]?token|api[_-]?key)/i;
const BINDING_REQUEST_KEYS = new Set(["source", "allowedDataProductIds", "expiresAt"]);
const MANAGED_BINDING_REQUEST_KEYS = new Set([
  "source",
  "allowedDataProductIds",
  "expiresAt",
  "generation",
  "capabilityDigest",
]);
const BINDING_SOURCE_KEYS = new Set(["tenantId", "connectionId", "providerId"]);
const SHA256_DIGEST = /^[a-f0-9]{64}$/;

/**
 * Produces an opaque, high-entropy capability. The plaintext is returned only
 * to the trusted provisioning caller; persistence uses its SHA-256 digest.
 */
export function createWriterToken() {
  return `${TOKEN_PREFIX}${randomBytes(32).toString("base64url")}`;
}

export function hashWriterToken(token) {
  return createHash("sha256").update(String(token), "utf8").digest("hex");
}

export function normalizeWriterBindingRequest(value, { now = new Date(), maxTtlDays = 90 } = {}) {
  if (!isPlainObject(value) || !isPlainObject(value.source)) {
    throw writerBindingError("writer_binding_request_invalid");
  }
  if (containsSecretLikeKey(value)) {
    throw writerBindingError("writer_binding_request_secret_material_forbidden");
  }
  if (value.endpoint !== undefined || value.url !== undefined || value.host !== undefined) {
    throw writerBindingError("writer_binding_request_transport_forbidden");
  }
  if (!hasOnlyKeys(value, BINDING_REQUEST_KEYS) || !hasOnlyKeys(value.source, BINDING_SOURCE_KEYS)) {
    throw writerBindingError("writer_binding_request_fields_invalid");
  }

  const tenantId = normalizeIdentifier(value.source.tenantId);
  const connectionId = normalizeIdentifier(value.source.connectionId);
  const providerId = normalizeIdentifier(value.source.providerId);
  if (!tenantId || !connectionId || !providerId) {
    throw writerBindingError("writer_binding_scope_invalid");
  }

  const allowedDataProductIds = uniqueIdentifiers(value.allowedDataProductIds);
  if (!allowedDataProductIds.length) {
    throw writerBindingError("writer_binding_data_products_invalid");
  }

  const expiresAt = new Date(String(value.expiresAt || ""));
  const maxExpiresAt = new Date(now.getTime() + maxTtlDays * 24 * 60 * 60 * 1000);
  if (Number.isNaN(expiresAt.getTime()) || expiresAt <= now || expiresAt > maxExpiresAt) {
    throw writerBindingError("writer_binding_expiry_invalid");
  }

  return Object.freeze({
    tenantId,
    connectionId,
    providerId,
    allowedDataProductIds,
    expiresAt: expiresAt.toISOString(),
  });
}

/**
 * Validates the zero-touch control-plane form. The native credential secret is
 * generated and stored inside Engine; EDP receives only its SHA-256 digest.
 * `generation` makes a retry address the same immutable binding generation.
 */
export function normalizeManagedWriterBindingRequest(value) {
  if (!isPlainObject(value) || !isPlainObject(value.source)) {
    throw writerBindingError("managed_writer_binding_request_invalid");
  }
  if (containsSecretLikeKey(value)) {
    throw writerBindingError("managed_writer_binding_secret_material_forbidden");
  }
  if (!hasOnlyKeys(value, MANAGED_BINDING_REQUEST_KEYS) || !hasOnlyKeys(value.source, BINDING_SOURCE_KEYS)) {
    throw writerBindingError("managed_writer_binding_request_fields_invalid");
  }

  const tenantId = normalizeIdentifier(value.source.tenantId);
  const connectionId = normalizeIdentifier(value.source.connectionId);
  const providerId = normalizeIdentifier(value.source.providerId);
  const allowedDataProductIds = uniqueIdentifiers(value.allowedDataProductIds);
  if (!tenantId || !connectionId || !providerId || !allowedDataProductIds.length) {
    throw writerBindingError("managed_writer_binding_scope_invalid");
  }
  if (value.expiresAt !== null) {
    throw writerBindingError("managed_writer_binding_must_be_durable");
  }
  const generation = Number(value.generation);
  if (!Number.isSafeInteger(generation) || generation < 1 || generation > 2_147_483_647) {
    throw writerBindingError("managed_writer_binding_generation_invalid");
  }
  const capabilityDigest = String(value.capabilityDigest || "").toLowerCase();
  if (!SHA256_DIGEST.test(capabilityDigest)) {
    throw writerBindingError("managed_writer_binding_capability_digest_invalid");
  }

  return Object.freeze({
    tenantId,
    connectionId,
    providerId,
    allowedDataProductIds: Object.freeze([...allowedDataProductIds].sort()),
    expiresAt: null,
    generation,
    capabilityDigest,
  });
}

export function writerBindingRequestHash(policy) {
  const canonical = JSON.stringify({
    tenantId: policy.tenantId,
    connectionId: policy.connectionId,
    providerId: policy.providerId,
    allowedDataProductIds: [...policy.allowedDataProductIds].sort(),
    expiresAt: null,
    generation: policy.generation,
    capabilityDigest: policy.capabilityDigest,
  });
  return createHash("sha256").update(canonical, "utf8").digest("hex");
}

export function canMigrateManagedWriterBindingToDurable(binding, policy, now = new Date()) {
  if (!isPlainObject(binding) || !isPlainObject(policy) || binding.active !== true) return false;
  const expiresAt = new Date(binding.expiresAt);
  if (binding.expiresAt === null || Number.isNaN(expiresAt.getTime()) || expiresAt <= now) return false;
  return binding.tenantId === policy.tenantId
    && binding.connectionId === policy.connectionId
    && binding.providerId === policy.providerId
    && binding.capabilityDigest === policy.capabilityDigest
    && JSON.stringify(uniqueIdentifiers(binding.allowedDataProductIds).sort())
      === JSON.stringify(uniqueIdentifiers(policy.allowedDataProductIds).sort())
    && policy.expiresAt === null;
}

/**
 * Converts a caller-provided, deliberately unscoped intake envelope into the
 * canonical scoped form. Caller scope is rejected, never trusted or merged.
 */
export function materializeWriterBoundBatch(value, binding, { hasScopeHeaders = false, now = new Date() } = {}) {
  if (!isPlainObject(value) || !isPlainObject(value.source) || !isPlainObject(binding)) {
    throw writerBindingError("writer_bound_intake_invalid");
  }
  if (hasScopeHeaders || value.source.tenantId !== undefined || value.source.connectionId !== undefined) {
    throw writerBindingError("writer_bound_scope_forbidden");
  }
  if (binding.active !== true || !bindingIsCurrent(binding, now)) {
    throw writerBindingError("writer_binding_inactive");
  }

  const providerId = normalizeIdentifier(value.source.providerId);
  const tenantId = normalizeIdentifier(binding.tenantId);
  const connectionId = normalizeIdentifier(binding.connectionId);
  if (!providerId || !tenantId || !connectionId || providerId !== binding.providerId) {
    throw writerBindingError("writer_binding_provider_forbidden");
  }

  const dataProductId = normalizeIdentifier(value.contract?.dataProductId);
  const allowedDataProductIds = uniqueIdentifiers(binding.allowedDataProductIds);
  if (!dataProductId || !allowedDataProductIds.includes(dataProductId)) {
    throw writerBindingError("writer_binding_data_product_forbidden");
  }

  return {
    ...value,
    source: { providerId, tenantId, connectionId },
  };
}

/**
 * Materializes the provider-neutral Data Product publish wire form. Unlike the
 * legacy writer-bound intake, the caller cannot send provider identity,
 * contract metadata, receivedAt or any scope at all.
 */
export function materializeDataProductPublish(value, binding, definition, dataProductId, { now = new Date() } = {}) {
  if (!isPlainObject(value) || !isPlainObject(binding) || !isPlainObject(definition)) {
    throw writerBindingError("data_product_publish_invalid");
  }
  if (binding.active !== true || !bindingIsCurrent(binding, now)) {
    throw writerBindingError("writer_binding_inactive");
  }
  const normalizedProductId = normalizeIdentifier(dataProductId);
  const allowedDataProductIds = uniqueIdentifiers(binding.allowedDataProductIds);
  if (!normalizedProductId || normalizedProductId !== definition.id || !allowedDataProductIds.includes(normalizedProductId)) {
    throw writerBindingError("writer_binding_data_product_forbidden");
  }
  const tenantId = normalizeIdentifier(binding.tenantId);
  const connectionId = normalizeIdentifier(binding.connectionId);
  const providerId = normalizeIdentifier(binding.providerId);
  if (!tenantId || !connectionId || !providerId) throw writerBindingError("writer_binding_scope_invalid");

  return {
    schemaVersion: "nodedc.external-provider-contract/v1",
    source: { tenantId, connectionId, providerId },
    contract: {
      dataProductId: normalizedProductId,
      ontologyRevision: definition.ontologyRevision,
      version: definition.version,
    },
    batch: {
      runId: value.batch?.runId,
      sequence: value.batch?.sequence,
      idempotencyKey: value.batch?.idempotencyKey,
      receivedAt: now.toISOString(),
      ...(value.batch?.mode === "replace" ? {
        mode: "replace",
        generationAt: value.batch?.generationAt,
      } : {}),
    },
    facts: value.facts,
  };
}

export function safeWriterBinding(binding) {
  return {
    id: binding.id,
    ...(binding.bindingKey ? { bindingKey: binding.bindingKey } : {}),
    ...(Number.isInteger(Number(binding.generation)) ? { generation: Number(binding.generation) } : {}),
    tenantId: binding.tenantId,
    connectionId: binding.connectionId,
    providerId: binding.providerId,
    allowedDataProductIds: uniqueIdentifiers(binding.allowedDataProductIds),
    active: binding.active === true,
    expiresAt: binding.expiresAt === null ? null : new Date(binding.expiresAt).toISOString(),
    createdAt: binding.createdAt ? new Date(binding.createdAt).toISOString() : undefined,
    rotatedAt: binding.rotatedAt ? new Date(binding.rotatedAt).toISOString() : undefined,
    revokedAt: binding.revokedAt ? new Date(binding.revokedAt).toISOString() : undefined,
  };
}

export function writerBindingError(code) {
  return Object.assign(new Error(code), { status: 400, code });
}

function bindingIsCurrent(binding, now) {
  if (binding.expiresAt === null) return true;
  const expiresAt = new Date(binding.expiresAt);
  return !Number.isNaN(expiresAt.getTime()) && expiresAt > now;
}

function normalizeIdentifier(value) {
  const normalized = typeof value === "string" ? value.trim() : "";
  return IDENTIFIER.test(normalized) ? normalized : "";
}

function uniqueIdentifiers(value) {
  if (!Array.isArray(value)) return [];
  return [...new Set(value.map(normalizeIdentifier).filter(Boolean))];
}

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

function containsSecretLikeKey(value) {
  if (Array.isArray(value)) return value.some(containsSecretLikeKey);
  if (!isPlainObject(value)) return false;
  return Object.entries(value).some(([key, child]) => SECRET_LIKE_KEY.test(key) || containsSecretLikeKey(child));
}

function hasOnlyKeys(value, allowedKeys) {
  return Object.keys(value).every((key) => allowedKeys.has(key));
}
