'use strict';

const RECORD_TYPES = Object.freeze({
  SOURCE_ITEM: 'source_item',
  MEMORY_RECORD: 'memory_record',
  SOURCE_SUMMARY: 'source_summary',
});

const INGEST_MODES = Object.freeze({
  CHUNKED_CONTENT: 'chunked_content',
  WHOLE_RECORD: 'whole_record',
  METADATA_ONLY: 'metadata_only',
});

const PRIVACY_CLASSES = Object.freeze({
  PUBLIC: 'public',
  INTERNAL: 'internal',
  PII_POTENTIAL: 'pii_potential',
  SENSITIVE: 'sensitive',
  SECRETS_POSSIBLE: 'secrets_possible',
});

const PRIVACY_CLASS_ORDER = Object.freeze([
  PRIVACY_CLASSES.PUBLIC,
  PRIVACY_CLASSES.INTERNAL,
  PRIVACY_CLASSES.PII_POTENTIAL,
  PRIVACY_CLASSES.SENSITIVE,
  PRIVACY_CLASSES.SECRETS_POSSIBLE,
]);

class SourceValidationError extends Error {
  constructor(message, { errors = [], record = null } = {}) {
    super(message);
    this.name = 'SourceValidationError';
    this.errors = errors;
    this.record = record;
  }
}

function validateSourceRef(ref = {}) {
  const errors = [];
  if (!ref || typeof ref !== 'object') errors.push('source ref must be an object');
  if (!nonEmpty(ref.adapterId)) errors.push('adapterId is required');
  if (!nonEmpty(ref.uri) && !nonEmpty(ref.sourceFile)) errors.push('uri or sourceFile is required');
  if (ref.sourceId != null && !nonEmpty(ref.sourceId)) errors.push('sourceId must be a non-empty string when provided');
  if (ref.privacyClass != null && !isPrivacyClass(ref.privacyClass)) {
    errors.push(`privacyClass must be one of: ${PRIVACY_CLASS_ORDER.join(', ')}`);
  }
  if (ref.metadata != null && !plainObject(ref.metadata)) errors.push('metadata must be an object when provided');
  return result(errors, normalizeSourceRef(ref));
}

function normalizeSourceRef(ref = {}) {
  const uri = ref.uri || ref.sourceFile || '';
  return {
    adapterId: String(ref.adapterId || ''),
    uri: String(uri || ''),
    sourceFile: ref.sourceFile || uri || '',
    sourceId: ref.sourceId || `${ref.adapterId || 'source'}:${uri || 'unknown'}`,
    cwd: ref.cwd || '',
    privacyClass: ref.privacyClass || PRIVACY_CLASSES.PII_POTENTIAL,
    metadata: plainObject(ref.metadata) ? { ...ref.metadata } : {},
  };
}

function validateAdapterSchema(schema = {}) {
  const errors = [];
  if (!schema || typeof schema !== 'object') errors.push('adapter schema must be an object');
  if (!nonEmpty(schema.adapterId)) errors.push('adapterId is required');
  if (!nonEmpty(schema.version)) errors.push('version is required');
  if (schema.defaultPrivacyClass != null && !isPrivacyClass(schema.defaultPrivacyClass)) {
    errors.push(`defaultPrivacyClass must be one of: ${PRIVACY_CLASS_ORDER.join(', ')}`);
  }
  if (schema.declaredTransformations != null && !Array.isArray(schema.declaredTransformations)) {
    errors.push('declaredTransformations must be an array');
  }
  if (schema.fields != null && !plainObject(schema.fields)) errors.push('fields must be an object when provided');
  return result(errors, {
    adapterId: schema.adapterId || '',
    version: schema.version || '',
    supportsIncremental: Boolean(schema.supportsIncremental),
    defaultPrivacyClass: schema.defaultPrivacyClass || PRIVACY_CLASSES.PII_POTENTIAL,
    declaredTransformations: normalizeTransformList(schema.declaredTransformations || []),
    fields: plainObject(schema.fields) ? { ...schema.fields } : {},
    capabilities: plainObject(schema.capabilities) ? { ...schema.capabilities } : {},
  });
}

function validateSourceItemMetadata(record = {}) {
  const errors = [];
  if (!plainObject(record)) errors.push('source item must be an object');
  if (record.type !== RECORD_TYPES.SOURCE_ITEM) errors.push(`type must be ${RECORD_TYPES.SOURCE_ITEM}`);
  validateCommonSourceRecord(record, errors);
  if (!nonEmpty(record.version)) errors.push('version is required');
  if (record.ingestMode != null && !isIngestMode(record.ingestMode)) {
    errors.push(`ingestMode must be one of: ${Object.values(INGEST_MODES).join(', ')}`);
  }
  return result(errors, {
    ...record,
    ingestMode: record.ingestMode || INGEST_MODES.WHOLE_RECORD,
    privacyClass: record.privacyClass || PRIVACY_CLASSES.PII_POTENTIAL,
    metadata: normalizeMetadata(record.metadata),
  });
}

function validateMemoryRecord(record = {}, schema = null) {
  const errors = [];
  if (!plainObject(record)) errors.push('memory record must be an object');
  if (record.type !== RECORD_TYPES.MEMORY_RECORD) errors.push(`type must be ${RECORD_TYPES.MEMORY_RECORD}`);
  validateCommonSourceRecord(record, errors);
  if (!nonEmpty(record.memoryType)) errors.push('memoryType is required');
  if (!nonEmpty(record.content) && !nonEmpty(record.contentRaw)) errors.push('content or contentRaw is required');
  if (record.content != null && typeof record.content !== 'string') errors.push('content must be a string');
  if (record.contentRaw != null && typeof record.contentRaw !== 'string') errors.push('contentRaw must be a string');
  if (record.transforms != null && !Array.isArray(record.transforms)) errors.push('transforms must be an array');
  if (record.ingestMode != null && !isIngestMode(record.ingestMode)) {
    errors.push(`ingestMode must be one of: ${Object.values(INGEST_MODES).join(', ')}`);
  }
  if (schema) errors.push(...undeclaredTransformErrors(record, schema));
  return result(errors, {
    ...record,
    content: record.content == null ? record.contentRaw : record.content,
    contentRaw: record.contentRaw == null ? record.content : record.contentRaw,
    transforms: normalizeTransformList(record.transforms || []),
    ingestMode: record.ingestMode || INGEST_MODES.WHOLE_RECORD,
    privacyClass: record.privacyClass || PRIVACY_CLASSES.PII_POTENTIAL,
    metadata: normalizeMetadata(record.metadata),
  });
}

function validateSourceSummary(record = {}) {
  const errors = [];
  if (!plainObject(record)) errors.push('source summary must be an object');
  if (record.type !== RECORD_TYPES.SOURCE_SUMMARY) errors.push(`type must be ${RECORD_TYPES.SOURCE_SUMMARY}`);
  validateCommonSourceRecord(record, errors, { itemIdRequired: false });
  if (!nonEmpty(record.summary)) errors.push('summary is required');
  return result(errors, {
    ...record,
    privacyClass: record.privacyClass || PRIVACY_CLASSES.PII_POTENTIAL,
    metadata: normalizeMetadata(record.metadata),
  });
}

function validateIngestRecord(record = {}, schema = null) {
  if (record?.type === RECORD_TYPES.SOURCE_ITEM) return validateSourceItemMetadata(record);
  if (record?.type === RECORD_TYPES.MEMORY_RECORD) return validateMemoryRecord(record, schema);
  if (record?.type === RECORD_TYPES.SOURCE_SUMMARY) return validateSourceSummary(record);
  return result([`unknown source record type: ${record?.type || '(missing)'}`], record);
}

function assertValid(validation, message = 'invalid source record') {
  if (!validation.ok) {
    throw new SourceValidationError(`${message}: ${validation.errors.join('; ')}`, {
      errors: validation.errors,
      record: validation.value,
    });
  }
  return validation.value;
}

function assertIngestRecord(record, schema = null) {
  return assertValid(validateIngestRecord(record, schema), 'invalid ingest record');
}

function assertAdapterSchema(schema) {
  return assertValid(validateAdapterSchema(schema), 'invalid adapter schema');
}

function assertSourceRef(ref) {
  return assertValid(validateSourceRef(ref), 'invalid source ref');
}

function undeclaredTransformErrors(record = {}, schema = {}) {
  const declared = new Set(normalizeTransformList(schema.declaredTransformations || []));
  const transforms = normalizeTransformList(record.transforms || []);
  return transforms
    .filter((name) => !declared.has(name))
    .map((name) => `transform is not declared by adapter ${schema.adapterId || '(unknown)'}: ${name}`);
}

function validateCommonSourceRecord(record, errors, { itemIdRequired = true } = {}) {
  if (!nonEmpty(record.sourceId)) errors.push('sourceId is required');
  if (!nonEmpty(record.sourceFile)) errors.push('sourceFile is required');
  if (itemIdRequired && !nonEmpty(record.itemId)) errors.push('itemId is required');
  if (record.timestamp != null && !validDate(record.timestamp)) errors.push('timestamp must be parseable when provided');
  if (record.privacyClass != null && !isPrivacyClass(record.privacyClass)) {
    errors.push(`privacyClass must be one of: ${PRIVACY_CLASS_ORDER.join(', ')}`);
  }
  if (record.metadata != null && !plainObject(record.metadata)) errors.push('metadata must be an object when provided');
}

function normalizeTransformList(list) {
  return [...new Set((list || [])
    .filter((name) => typeof name === 'string')
    .map((name) => name.trim())
    .filter(Boolean))];
}

function normalizeMetadata(metadata) {
  return plainObject(metadata) ? { ...metadata } : {};
}

function privacyClassAllows({ recordClass, floor }) {
  if (!floor) return true;
  if (!isPrivacyClass(recordClass) || !isPrivacyClass(floor)) return false;
  return PRIVACY_CLASS_ORDER.indexOf(recordClass) <= PRIVACY_CLASS_ORDER.indexOf(floor);
}

function result(errors, value) {
  return { ok: errors.length === 0, errors, value };
}

function isIngestMode(value) {
  return Object.values(INGEST_MODES).includes(value);
}

function isPrivacyClass(value) {
  return PRIVACY_CLASS_ORDER.includes(value);
}

function nonEmpty(value) {
  return typeof value === 'string' && value.trim().length > 0;
}

function plainObject(value) {
  return !!value && typeof value === 'object' && !Array.isArray(value);
}

function validDate(value) {
  return typeof value === 'string' && !Number.isNaN(new Date(value).getTime());
}

module.exports = {
  INGEST_MODES,
  PRIVACY_CLASSES,
  PRIVACY_CLASS_ORDER,
  RECORD_TYPES,
  SourceValidationError,
  assertAdapterSchema,
  assertIngestRecord,
  assertSourceRef,
  assertValid,
  normalizeSourceRef,
  normalizeTransformList,
  privacyClassAllows,
  validateAdapterSchema,
  validateIngestRecord,
  validateMemoryRecord,
  validateSourceItemMetadata,
  validateSourceRef,
  validateSourceSummary,
};
