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

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

export function createReaderToken() {
  return `${TOKEN_PREFIX}${randomBytes(32).toString("base64url")}`;
}

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

export function normalizeReaderBindingRequest(value, { now = new Date(), maxTtlDays = 90 } = {}) {
  if (!isPlainObject(value) || !isPlainObject(value.source)) throw readerError("reader_binding_request_invalid");
  if (containsSecretLikeKey(value)) throw readerError("reader_binding_request_secret_material_forbidden");
  if (!hasOnlyKeys(value, REQUEST_KEYS) || !hasOnlyKeys(value.source, SOURCE_KEYS)) {
    throw readerError("reader_binding_request_fields_invalid");
  }
  const tenantId = identifier(value.source.tenantId);
  const connectionId = identifier(value.source.connectionId);
  const providerId = identifier(value.source.providerId);
  const allowedDataProductIds = uniqueIdentifiers(value.allowedDataProductIds);
  if (!tenantId || !connectionId || !providerId || !allowedDataProductIds.length) {
    throw readerError("reader_binding_scope_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 readerError("reader_binding_expiry_invalid");
  }
  return Object.freeze({ tenantId, connectionId, providerId, allowedDataProductIds, expiresAt: expiresAt.toISOString() });
}

export function normalizeManagedReaderBindingRequest(value) {
  if (!isPlainObject(value) || !isPlainObject(value.source)) {
    throw readerError("managed_reader_binding_request_invalid");
  }
  if (containsSecretLikeKey(value)) {
    throw readerError("managed_reader_binding_secret_material_forbidden");
  }
  if (!hasOnlyKeys(value, MANAGED_REQUEST_KEYS) || !hasOnlyKeys(value.source, SOURCE_KEYS)) {
    throw readerError("managed_reader_binding_request_fields_invalid");
  }
  const tenantId = identifier(value.source.tenantId);
  const connectionId = identifier(value.source.connectionId);
  const providerId = identifier(value.source.providerId);
  const allowedDataProductIds = uniqueIdentifiers(value.allowedDataProductIds);
  if (!tenantId || !connectionId || !providerId || !allowedDataProductIds.length) {
    throw readerError("managed_reader_binding_scope_invalid");
  }
  if (value.expiresAt !== null) {
    throw readerError("managed_reader_binding_must_be_durable");
  }
  const generation = Number(value.generation);
  if (!Number.isSafeInteger(generation) || generation < 1 || generation > 2_147_483_647) {
    throw readerError("managed_reader_binding_generation_invalid");
  }
  const capabilityDigest = String(value.capabilityDigest || "").toLowerCase();
  if (!SHA256_DIGEST.test(capabilityDigest)) {
    throw readerError("managed_reader_binding_capability_digest_invalid");
  }
  return Object.freeze({
    tenantId,
    connectionId,
    providerId,
    allowedDataProductIds: Object.freeze([...allowedDataProductIds].sort()),
    expiresAt: null,
    generation,
    capabilityDigest,
  });
}

export function normalizeManagedConsumerReaderPlanRequest(value) {
  if (!isPlainObject(value) || containsSecretLikeKey(value) || !hasOnlyKeys(value, MANAGED_CONSUMER_PLAN_KEYS)) {
    throw readerError("managed_consumer_reader_plan_request_invalid");
  }
  const allowedDataProductIds = uniqueIdentifiers(value.allowedDataProductIds);
  if (!allowedDataProductIds.length) throw readerError("managed_consumer_reader_scope_invalid");
  return Object.freeze({ allowedDataProductIds: Object.freeze([...allowedDataProductIds].sort()) });
}

export function normalizeManagedConsumerReaderBindingRequest(value) {
  if (!isPlainObject(value) || containsSecretLikeKey(value) || !hasOnlyKeys(value, MANAGED_CONSUMER_REQUEST_KEYS)) {
    throw readerError("managed_consumer_reader_binding_request_invalid");
  }
  const allowedDataProductIds = uniqueIdentifiers(value.allowedDataProductIds);
  if (!allowedDataProductIds.length) throw readerError("managed_consumer_reader_scope_invalid");
  if (value.expiresAt !== null) throw readerError("managed_consumer_reader_must_be_durable");
  const generation = Number(value.generation);
  if (!Number.isSafeInteger(generation) || generation < 1 || generation > 2_147_483_647) {
    throw readerError("managed_consumer_reader_generation_invalid");
  }
  const capabilityDigest = String(value.capabilityDigest || "").toLowerCase();
  if (!SHA256_DIGEST.test(capabilityDigest)) {
    throw readerError("managed_consumer_reader_capability_digest_invalid");
  }
  return Object.freeze({
    allowedDataProductIds: Object.freeze([...allowedDataProductIds].sort()),
    expiresAt: null,
    generation,
    capabilityDigest,
  });
}

export function readerBindingRequestHash(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 consumerReaderBindingRequestHash(policy) {
  return createHash("sha256").update(JSON.stringify({
    allowedDataProductIds: [...policy.allowedDataProductIds].sort(),
    expiresAt: null,
    generation: policy.generation,
    capabilityDigest: policy.capabilityDigest,
  }), "utf8").digest("hex");
}

export function assertReaderProduct(binding, dataProductId, now = new Date()) {
  const expired = binding?.expiresAt !== null && new Date(binding?.expiresAt) <= now;
  if (!isPlainObject(binding) || binding.active !== true || expired) {
    throw readerError("reader_binding_inactive", 401);
  }
  const normalized = identifier(dataProductId);
  if (!normalized || !uniqueIdentifiers(binding.allowedDataProductIds).includes(normalized)) {
    throw readerError("reader_binding_data_product_forbidden", 403);
  }
  return normalized;
}

export function safeReaderBinding(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 safeManagedConsumerReaderBinding(binding) {
  return {
    id: binding.id,
    bindingKey: binding.bindingKey,
    generation: Number(binding.generation),
    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,
    sourceScope: "resolved-server-side",
  };
}

function readerError(code, status = 400) {
  return Object.assign(new Error(code), { status, code });
}

function identifier(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(identifier).filter(Boolean))];
}

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 isPlainObject(value) {
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}

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