/**
 * AWS Route 53 DNS sync — server-side write validation + AWS error sanitizer.
 *
 * D-01: A closed allowlist of writable record types. NS and SOA are
 * zone-delegation records and must never be writable from Pulse — this check
 * runs in library code the CRUD routes call BEFORE any AWS command is
 * constructed, mirroring the credential-field blocklist pattern in
 * app/api/analyzer/itglue/applications/[id]/apply/route.ts (belt over the
 * UI's braces: never rely on the UI simply not offering the option).
 *
 * T-24-03: `sanitizeAwsError` is the only permitted source of a persisted or
 * client-returned AWS error string — it strips access key ids, ARNs, and AWS
 * account ids before the message is written to `route53_audit_log.error_message`
 * or returned in an API response body.
 *
 * No AWS SDK or Postgres dependency here by design — this module is plain,
 * synchronous, unit-testable validation logic.
 */

import type { Route53RecordValue, Route53WritableType } from '@/lib/types/route53';

/**
 * D-01: closed allowlist. NS and SOA are deliberately absent — an
 * unrecognized type is rejected, never passed through.
 */
export const WRITABLE_RECORD_TYPES: readonly Route53WritableType[] = Object.freeze([
  'A',
  'AAAA',
  'CNAME',
  'MX',
  'TXT',
  'SRV',
]);

const MIN_TTL = 0;
const MAX_TTL = 2147483647;
const MAX_RESOURCE_RECORDS = 100;
const ZONE_DELEGATION_TYPES = new Set(['NS', 'SOA']);

export interface ValidatedRecordWrite {
  name: string;
  type: Route53WritableType;
  ttl: number;
  resourceRecords: Route53RecordValue[];
}

export type ValidateRecordWriteResult =
  | { ok: true; value: ValidatedRecordWrite }
  | { ok: false; status: 400; reason: string };

interface ValidateRecordWriteInput {
  name?: unknown;
  type?: unknown;
  ttl?: unknown;
  resourceRecords?: unknown;
}

function fail(reason: string): ValidateRecordWriteResult {
  return { ok: false, status: 400, reason };
}

/**
 * Validate a record write payload against D-01's closed allowlist and basic
 * shape rules. Does not construct any AWS command and has no DB dependency —
 * callers invoke this first and only proceed to build a
 * ChangeResourceRecordSetsCommand when `ok` is true.
 */
export function validateRecordWrite(input: ValidateRecordWriteInput): ValidateRecordWriteResult {
  // 1. name: non-empty string after trimming, normalized to Route 53's
  // canonical lowercase + trailing-dot form.
  if (typeof input.name !== 'string' || input.name.trim().length === 0) {
    return fail('Record name is required and must be a non-empty string');
  }
  let name = input.name.trim().toLowerCase();
  if (!name.endsWith('.')) {
    name = `${name}.`;
  }

  // 2. type: string, uppercased, must be a member of the closed allowlist.
  if (typeof input.type !== 'string' || input.type.trim().length === 0) {
    return fail('Record type is required and must be a string');
  }
  const type = input.type.trim().toUpperCase();
  if (ZONE_DELEGATION_TYPES.has(type)) {
    return fail(
      `Record type ${type} is not writable from Pulse — NS and SOA are zone-delegation records (D-01)`
    );
  }
  if (!WRITABLE_RECORD_TYPES.includes(type as Route53WritableType)) {
    return fail(
      `Record type ${type} is not a supported writable type — must be one of ${WRITABLE_RECORD_TYPES.join(', ')}`
    );
  }

  // 3. ttl: integer 0..2147483647 inclusive, default 300 when omitted.
  let ttl: number;
  if (input.ttl === undefined || input.ttl === null) {
    ttl = 300;
  } else {
    if (typeof input.ttl !== 'number' || !Number.isInteger(input.ttl)) {
      return fail('ttl must be an integer');
    }
    if (input.ttl < MIN_TTL || input.ttl > MAX_TTL) {
      return fail(`ttl must be between ${MIN_TTL} and ${MAX_TTL}`);
    }
    ttl = input.ttl;
  }

  // 4. resourceRecords: non-empty array, each entry a non-empty string value,
  // capped at 100 entries (Route 53 rejects an empty value set; unbounded
  // arrays are a DoS surface — T-24-12).
  if (!Array.isArray(input.resourceRecords) || input.resourceRecords.length === 0) {
    return fail('resourceRecords must be a non-empty array');
  }
  if (input.resourceRecords.length > MAX_RESOURCE_RECORDS) {
    return fail(`resourceRecords cannot exceed ${MAX_RESOURCE_RECORDS} entries`);
  }
  const resourceRecords: Route53RecordValue[] = [];
  for (const entry of input.resourceRecords) {
    if (
      typeof entry !== 'object' ||
      entry === null ||
      typeof (entry as { value?: unknown }).value !== 'string' ||
      (entry as { value: string }).value.trim().length === 0
    ) {
      return fail('Each resourceRecords entry must have a non-empty string value');
    }
    resourceRecords.push({ value: (entry as { value: string }).value });
  }

  return {
    ok: true,
    value: {
      name,
      type: type as Route53WritableType,
      ttl,
      resourceRecords,
    },
  };
}

const AWS_ACCESS_KEY_ID_PATTERN = /AKIA[0-9A-Z]{16}/g;
const AWS_ARN_PATTERN = /arn:aws:[^\s"']+/g;
const AWS_ACCOUNT_ID_PATTERN = /\b[0-9]{12}\b/g;
const MAX_SANITIZED_LENGTH = 500;

/**
 * Redact anything that looks like an AWS access key id, ARN, or 12-digit
 * account id from an AWS SDK error, then truncate. This is the only string
 * that may be written to `route53_audit_log.error_message` or returned in an
 * API response body (T-24-03). Never throws.
 */
export function sanitizeAwsError(err: unknown): string {
  let message: string;
  try {
    message = err instanceof Error ? err.message : String(err);
  } catch {
    message = 'Unknown error';
  }

  let sanitized = message
    .replace(AWS_ACCESS_KEY_ID_PATTERN, '[redacted-key-id]')
    .replace(AWS_ARN_PATTERN, '[redacted-arn]')
    .replace(AWS_ACCOUNT_ID_PATTERN, '[redacted-account-id]');

  if (sanitized.length > MAX_SANITIZED_LENGTH) {
    sanitized = `${sanitized.slice(0, MAX_SANITIZED_LENGTH)}...`;
  }

  return sanitized;
}
