import { parseTorontoDate, termForMonth } from "./dateUtils";
import { EventInput, PublishRequest, TERMS } from "./types";

export class ValidationError extends Error {
  constructor(message: string) {
    super(message);
    this.name = "ValidationError";
  }
}

const REQUEST_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
const SLUG_PATTERN = /^[A-Za-z0-9][A-Za-z0-9-]*$/;
const EMAIL_SUFFIX = "@uwaterloo.ca";
const NO_NUL = "\0";

const TOP_LEVEL_FIELDS = ["requestId", "submittedByEmail", "event"];
const EVENT_FIELDS = [
  "name", "short", "startDate", "endDate", "online", "location",
  "descriptionMarkdown", "registerLink", "year", "term", "slug", "poster",
];
const POSTER_FIELDS = ["sourceUrl", "filename", "contentType"];

function fail(message: string): never {
  throw new ValidationError(message);
}

function checkNoUnknownFields(obj: unknown, allowed: string[], where: string) {
  if (typeof obj !== "object" || obj === null || Array.isArray(obj)) {
    fail(`${where} must be an object`);
  }
  for (const key of Object.keys(obj as Record<string, unknown>)) {
    if (!allowed.includes(key)) {
      fail(`${where} has unknown field "${key}"`);
    }
  }
}

function isSingleLine(value: string): boolean {
  return !value.includes("\n") && !value.includes("\r");
}

function validateString(
  value: unknown,
  field: string,
  { min, max, singleLine = false }: { min: number; max: number; singleLine?: boolean }
): string {
  if (typeof value !== "string") {
    fail(`${field} must be a string`);
  }
  if (value.includes(NO_NUL)) {
    fail(`${field} must not contain a NUL character`);
  }
  if (value.length < min || value.length > max) {
    fail(`${field} must be between ${min} and ${max} characters`);
  }
  if (singleLine && !isSingleLine(value)) {
    fail(`${field} must be a single line`);
  }
  return value;
}

function validatePoster(poster: unknown): EventInput["poster"] {
  if (poster === null || poster === undefined) {
    return null;
  }
  checkNoUnknownFields(poster, POSTER_FIELDS, "event.poster");
  const p = poster as Record<string, unknown>;

  const sourceUrl = validateString(p.sourceUrl, "event.poster.sourceUrl", {
    min: 1,
    max: 2048,
  });
  if (!sourceUrl.startsWith("https://")) {
    fail("event.poster.sourceUrl must start with https://");
  }
  const filename = validateString(p.filename, "event.poster.filename", {
    min: 1,
    max: 255,
  });
  if (filename.includes("/") || filename.includes("\\")) {
    fail("event.poster.filename must not contain path separators");
  }
  const contentType = validateString(p.contentType, "event.poster.contentType", {
    min: 1,
    max: 100,
  });
  if (!["image/png", "image/jpeg"].includes(contentType)) {
    fail("event.poster.contentType must be image/png or image/jpeg");
  }

  return { sourceUrl, filename, contentType };
}

function validateEvent(event: unknown): EventInput {
  checkNoUnknownFields(event, EVENT_FIELDS, "event");
  const e = event as Record<string, unknown>;

  const name = validateString(e.name, "event.name", { min: 1, max: 200, singleLine: true });
  const short = validateString(e.short, "event.short", { min: 1, max: 300, singleLine: true });

  const startDate = validateString(e.startDate, "event.startDate", { min: 1, max: 64 });
  const parsedStart = parseTorontoDate(startDate);
  if (!parsedStart) {
    fail("event.startDate must match format 'MMMM dd yyyy HH:mm'");
  }

  let endDate: string | null = null;
  if (e.endDate !== undefined && e.endDate !== null) {
    endDate = validateString(e.endDate, "event.endDate", { min: 1, max: 64 });
    const parsedEnd = parseTorontoDate(endDate);
    if (!parsedEnd) {
      fail("event.endDate must match format 'MMMM dd yyyy HH:mm'");
    }
    if (parsedEnd!.utcDate.getTime() <= parsedStart!.utcDate.getTime()) {
      fail("event.endDate must be strictly later than event.startDate");
    }
  }

  if (typeof e.online !== "boolean") {
    fail("event.online must be a boolean");
  }

  const location = validateString(e.location, "event.location", {
    min: 1,
    max: 200,
    singleLine: true,
  });

  const descriptionMarkdown = validateString(e.descriptionMarkdown, "event.descriptionMarkdown", {
    min: 1,
    max: 20000,
  });

  let registerLink: string | null = null;
  if (e.registerLink !== undefined && e.registerLink !== null) {
    registerLink = validateString(e.registerLink, "event.registerLink", { min: 1, max: 2048 });
    if (!registerLink.startsWith("https://")) {
      fail("event.registerLink must start with https://");
    }
  }

  if (typeof e.year !== "number" || !Number.isInteger(e.year)) {
    fail("event.year must be an integer");
  }
  const year = e.year as number;
  if (year < 2000 || year > 2100) {
    fail("event.year must be between 2000 and 2100");
  }
  if (year !== parsedStart!.year) {
    fail("event.year must equal the year in event.startDate");
  }

  if (typeof e.term !== "string" || !TERMS.includes(e.term as (typeof TERMS)[number])) {
    fail(`event.term must be one of ${TERMS.join(", ")}`);
  }
  const term = e.term as (typeof TERMS)[number];
  if (term !== termForMonth(parsedStart!.monthIndex)) {
    fail("event.term must match the month in event.startDate");
  }

  const slug = validateString(e.slug, "event.slug", { min: 1, max: 120 });
  if (!SLUG_PATTERN.test(slug)) {
    fail("event.slug must match pattern ^[A-Za-z0-9][A-Za-z0-9-]*$");
  }

  const poster = validatePoster(e.poster);

  return {
    name,
    short,
    startDate,
    endDate,
    online: e.online as boolean,
    location,
    descriptionMarkdown,
    registerLink,
    year,
    term,
    slug,
    poster,
  };
}

export function validatePublishRequest(body: unknown): PublishRequest {
  checkNoUnknownFields(body, TOP_LEVEL_FIELDS, "request body");
  const b = body as Record<string, unknown>;

  const requestId = validateString(b.requestId, "requestId", { min: 1, max: 128 });
  if (!REQUEST_ID_PATTERN.test(requestId)) {
    fail("requestId must match pattern ^[A-Za-z0-9][A-Za-z0-9._-]*$");
  }

  const submittedByEmail = validateString(b.submittedByEmail, "submittedByEmail", {
    min: EMAIL_SUFFIX.length + 1,
    max: 254,
  });
  if (!submittedByEmail.toLowerCase().endsWith(EMAIL_SUFFIX)) {
    fail(`submittedByEmail must end in ${EMAIL_SUFFIX}`);
  }

  if (b.event === undefined || b.event === null) {
    fail("event is required");
  }
  const event = validateEvent(b.event);

  return { requestId, submittedByEmail, event };
}
