import { z } from "zod";
import {
  ApplicationError,
  applicationStatuses,
  type ApplicationRecord,
} from "./application-record";
import { interests } from "./contact-schema";
import { singaporeDate } from "./application-particulars";
import type { ApplicationRepository } from "./application-repository";
export const STAFF_PAGE_SIZE = 20;
export const STAFF_SCAN_LIMIT = 500;
const SCAN_BATCH = 100;
const positionSchema = z
  .object({ createdAt: z.iso.datetime(), id: z.uuid() })
  .strict();
type Position = z.infer<typeof positionSchema>;
export const staffSearchSchema = z
  .object({
    query: z.string().max(100),
    status: z.union([z.literal("all"), z.enum(applicationStatuses)]),
    interest: z.union([z.literal("all"), z.enum(interests)]),
    from: z.union([z.literal(""), z.iso.date()]),
    to: z.union([z.literal(""), z.iso.date()]),
    order: z.enum(["newest", "oldest"]),
    view: z.enum(["active", "archived", "all"]).default("active"),
    assignee: z
      .union([z.literal("all"), z.literal("unassigned"), z.email().max(254)])
      .default("all"),
    cursor: positionSchema.nullable(),
  })
  .strict();
export type StaffSearch = z.infer<typeof staffSearchSchema>;
export const emptyStaffSearch: Omit<StaffSearch, "cursor"> = {
  query: "",
  status: "all",
  interest: "all",
  from: "",
  to: "",
  order: "newest",
  view: "active",
  assignee: "all",
};
const listItemSchema = z.object({
  id: z.uuid(),
  reference: z.string(),
  name: z.string(),
  interest: z.string(),
  status: z.enum(applicationStatuses),
  createdAt: z.iso.datetime(),
  submittedAt: z.iso.datetime().nullable(),
  notificationState: z.string(),
  deleting: z.boolean(),
  archived: z.boolean(),
  assignee: z.string().nullable().default(null),
});
export const staffSearchResultSchema = z.object({
  records: z.array(listItemSchema).max(STAFF_PAGE_SIZE),
  next: positionSchema.nullable(),
  scanned: z.number().int().nonnegative(),
});
export type StaffListItem = z.infer<typeof listItemSchema>;

export type RecentQuery = {
  order: StaffSearch["order"];
  after: Position | null;
  limit: number;
  from?: string;
  before?: string;
};
const byCodeUnit = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0);
export function recentPage<T extends Position>(
  records: T[],
  { order, after, limit, from, before }: RecentQuery,
) {
  const sign = order === "newest" ? -1 : 1,
    compare = (a: Position, b: Position) =>
      sign * (byCodeUnit(a.createdAt, b.createdAt) || byCodeUnit(a.id, b.id));
  return records
    .filter(
      (record) =>
        (!from || record.createdAt >= from) &&
        (!before || record.createdAt < before) &&
        (!after || compare(record, after) > 0),
    )
    .sort(compare)
    .slice(0, limit);
}
export const singaporeDayStart = (date: string) =>
  new Date(`${date}T00:00:00.000+08:00`).toISOString();
const followingDate = (date: string) => {
  const day = new Date(`${date}T00:00:00.000Z`);
  day.setUTCDate(day.getUTCDate() + 1);
  return day.toISOString().slice(0, 10);
};

const normalized = (value: string) => value.normalize("NFKC").toLowerCase();
function searchTerms(query: string) {
  const terms: { text: string; phone: boolean }[] = [];
  let number: { text: string; phone: boolean } | null = null;
  for (const word of normalized(query).split(/\s+/).filter(Boolean)) {
    if (/^[+\d()-]+$/.test(word)) {
      if (!number) terms.push((number = { text: "", phone: true }));
      number.text += word.replace(/\D/g, "");
    } else {
      number = null;
      terms.push({ text: word, phone: false });
    }
  }
  return terms.filter((term) => term.text);
}
export const hasSearchWords = (query: string) => searchTerms(query).length > 0;
export function matchesStaffSearch(
  record: ApplicationRecord,
  search: Omit<StaffSearch, "cursor">,
) {
  if (
    search.view !== "all" &&
    Boolean(record.archivedAt) !== (search.view === "archived")
  )
    return false;
  if (
    search.assignee !== "all" &&
    (record.assignee ?? "unassigned") !== search.assignee.toLowerCase()
  )
    return false;
  if (search.status !== "all" && record.status !== search.status) return false;
  if (search.interest !== "all" && record.fields.interest !== search.interest)
    return false;
  const received = singaporeDate(Date.parse(record.createdAt));
  if (
    (search.from && received < search.from) ||
    (search.to && received > search.to)
  )
    return false;
  const text = normalized(
      [
        record.fields.name,
        record.fields.email,
        record.reference,
        record.particulars?.idNumber ?? "",
      ].join(" "),
    ),
    phone = record.fields.phone.replace(/\D/g, "");
  const phones = [phone, `65${phone}`];
  return searchTerms(search.query).every(
    (term) =>
      text.includes(term.text) ||
      (term.phone && phones.some((value) => value.includes(term.text))),
  );
}
const listItem = ({
  id,
  reference,
  fields,
  status,
  createdAt,
  submittedAt,
  notification,
  deleting,
  archivedAt,
  assignee,
}: ApplicationRecord): StaffListItem => ({
  id,
  reference,
  name: fields.name,
  interest: fields.interest,
  status,
  createdAt,
  submittedAt,
  notificationState: notification.state,
  deleting,
  archived: Boolean(archivedAt),
  assignee,
});

export async function searchStaffApplications(
  input: unknown,
  repo: Pick<ApplicationRepository, "recent">,
) {
  const parsed = staffSearchSchema.safeParse(input);
  if (!parsed.success)
    throw new ApplicationError(400, "Check the search and filters.");
  const search = parsed.data;
  if (search.from && search.to && search.from > search.to)
    throw new ApplicationError(
      400,
      "Choose a received-from date on or before the received-to date.",
    );
  const range = {
    from: search.from ? singaporeDayStart(search.from) : undefined,
    before: search.to ? singaporeDayStart(followingDate(search.to)) : undefined,
  };
  const records: StaffListItem[] = [];
  let cursor = search.cursor,
    scanned = 0;
  for (;;) {
    const want = Math.min(SCAN_BATCH, STAFF_SCAN_LIMIT - scanned),
      batch = await repo.recent({
        order: search.order,
        after: cursor,
        limit: want,
        ...range,
      });
    let index = 0;
    while (index < batch.length && records.length < STAFF_PAGE_SIZE) {
      const record = batch[index++];
      scanned++;
      cursor = { createdAt: record.createdAt, id: record.id };
      if (matchesStaffSearch(record, search)) records.push(listItem(record));
    }
    if (batch.length < want && index === batch.length)
      return { records, next: null, scanned };
    if (records.length === STAFF_PAGE_SIZE || scanned >= STAFF_SCAN_LIMIT)
      return { records, next: cursor, scanned };
  }
}
