import { EXTERNAL_PROVIDER_CONTRACT_VERSION } from "./contract-version.mjs";
import {
  SECRET_LIKE_KEY,
  SECRET_LIKE_REFERENCE,
  SECRET_LIKE_VALUE,
} from "./sensitive-field-policy.mjs";
import { validateGeoJsonGeometry } from "./geometry.mjs";

const IDENTIFIER = /^[a-z][a-z0-9._:-]{2,127}$/;
const CONTRACT_VERSION = /^\d+\.\d+\.\d+(?:[-+][a-z0-9.-]+)?$/i;
const MAX_BATCH_SEQUENCE = 2_147_483_647;
const MAX_FACT_ATTRIBUTES_BYTES = 64 * 1024;

/** Provider-neutral batch written by an L2 connector to External Data Plane. */
export function validateIntakeBatch(value) {
  const errors = baseErrors(value, "intakeBatch");
  rejectUnknownKeys(value, new Set(["schemaVersion", "source", "contract", "batch", "raw", "facts"]), "intakeBatch", errors);
  rejectUnknownKeys(value?.source, new Set(["providerId", "tenantId", "connectionId"]), "source", errors);
  rejectUnknownKeys(value?.contract, new Set(["dataProductId", "ontologyRevision", "version"]), "contract", errors);
  rejectUnknownKeys(value?.batch, new Set(["runId", "sequence", "idempotencyKey", "receivedAt", "mode", "generationAt"]), "batch", errors);
  requiredIdentifier(value?.source?.providerId, "source.providerId", errors);
  requiredIdentifier(value?.source?.tenantId, "source.tenantId", errors);
  requiredIdentifier(value?.source?.connectionId, "source.connectionId", errors);
  requiredIdentifier(value?.contract?.dataProductId, "contract.dataProductId", errors);
  requiredIdentifier(value?.contract?.ontologyRevision, "contract.ontologyRevision", errors);
  requiredString(value?.contract?.version, "contract.version", errors);
  if (value?.contract?.version && !CONTRACT_VERSION.test(value.contract.version)) {
    errors.push("contract.version_must_be_semver");
  }
  requiredIdentifier(value?.batch?.runId, "batch.runId", errors);
  requiredIdentifier(value?.batch?.idempotencyKey, "batch.idempotencyKey", errors);
  if (!Number.isInteger(value?.batch?.sequence) || value.batch.sequence < 0 || value.batch.sequence > MAX_BATCH_SEQUENCE) {
    errors.push("batch.sequence_must_be_integer_0_to_2147483647");
  }
  requiredIsoTimestamp(value?.batch?.receivedAt, "batch.receivedAt", errors);
  const mode = value?.batch?.mode === undefined ? "upsert" : value.batch.mode;
  if (!new Set(["upsert", "replace"]).has(mode)) errors.push("batch.mode_invalid");
  if (mode === "replace") requiredIsoTimestamp(value?.batch?.generationAt, "batch.generationAt", errors);
  if (mode === "upsert" && value?.batch?.generationAt !== undefined) errors.push("batch.generationAt_forbidden_for_upsert");

  if (!Array.isArray(value?.facts)) {
    errors.push("facts_must_be_array");
  } else if (value.facts.length === 0 && mode !== "replace") {
    errors.push("facts_must_be_nonempty_array_for_upsert");
  } else {
    value.facts.forEach((fact, index) => {
      validateFact(fact, `facts[${index}]`, errors);
      if (mode === "replace"
        && !Number.isNaN(Date.parse(fact?.observedAt))
        && Date.parse(fact.observedAt) !== Date.parse(value.batch.generationAt)) {
        errors.push(`facts[${index}].observedAt_must_equal_generationAt`);
      }
    });
  }

  if (value?.raw !== undefined) validateRawEnvelope(value.raw, errors);
  if (containsSecretLikeMaterial(value)) errors.push("intake_must_not_contain_secret_material");

  return result(errors);
}

function baseErrors(value, label) {
  const errors = [];
  if (!isPlainObject(value)) return [`${label}_must_be_object`];
  if (value.schemaVersion !== EXTERNAL_PROVIDER_CONTRACT_VERSION) errors.push("schemaVersion_mismatch");
  return errors;
}

function requiredString(value, path, errors) {
  if (typeof value !== "string" || !value.trim()) errors.push(`${path}_required`);
}

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

function requiredIsoTimestamp(value, path, errors) {
  if (typeof value !== "string" || Number.isNaN(Date.parse(value))) errors.push(`${path}_invalid_timestamp`);
}

function validateFact(value, path, errors) {
  if (!isPlainObject(value)) {
    errors.push(`${path}_must_be_object`);
    return;
  }
  rejectUnknownKeys(value, new Set(["sourceId", "semanticType", "observedAt", "attributes", "geometry"]), path, errors);
  requiredIdentifier(value.sourceId, `${path}.sourceId`, errors);
  requiredIdentifier(value.semanticType, `${path}.semanticType`, errors);
  requiredIsoTimestamp(value.observedAt, `${path}.observedAt`, errors);
  if (value.attributes !== undefined) {
    if (!isPlainObject(value.attributes)) {
      errors.push(`${path}.attributes_must_be_object`);
    } else if (serializedByteLength(value.attributes) > MAX_FACT_ATTRIBUTES_BYTES) {
      errors.push(`${path}.attributes_size_exceeded`);
    }
  }
  if (value.geometry !== undefined) validateGeoJsonGeometry(value.geometry, `${path}.geometry`, errors);
}

function validateRawEnvelope(value, errors) {
  if (!isPlainObject(value)) {
    errors.push("raw_must_be_object");
    return;
  }
  rejectUnknownKeys(value, new Set(["contentType", "payload", "hash", "ref", "retentionDays"]), "raw", errors);
  requiredString(value.contentType, "raw.contentType", errors);
  if (value.payload === undefined && value.ref === undefined) errors.push("raw_requires_payload_or_ref");
  if (value.payload !== undefined) errors.push("raw.inline_payload_not_supported");
  if (value.payload === undefined) requiredString(value.hash, "raw.hash", errors);
  if (value.hash !== undefined) requiredString(value.hash, "raw.hash", errors);
  if (value.ref !== undefined) {
    requiredString(value.ref, "raw.ref", errors);
    if (typeof value.ref === "string" && SECRET_LIKE_REFERENCE.test(value.ref)) {
      errors.push("raw.ref_must_not_contain_secret_material");
    }
  }
  if (value.retentionDays !== undefined && (!Number.isInteger(value.retentionDays) || value.retentionDays < 1)) {
    errors.push("raw.retentionDays_must_be_positive_integer");
  }
}

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

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

function serializedByteLength(value) {
  try {
    return Buffer.byteLength(JSON.stringify(value));
  } catch {
    return Number.POSITIVE_INFINITY;
  }
}

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

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