interface Token {
  field: string;
  value: string;
  isNegative: boolean;
}

const SEARCH_CONDITIONS = [
  "url",
  "name",
  "description",
  "type",
  "collection",
  "pinned",
  "public",
  "before",
  "after",
  "tag",
];

export function parseSearchTokens(searchQueryString: string): Token[] {
  // Split on whitespace
  const rawTokens =
    searchQueryString.match(/\\?.|^$/g)?.reduce(
      (p, c) => {
        if (c === '"' || c === "'") {
          p.quote ^= 1;
        } else if (!p.quote && c === " ") {
          p.str.push("");
        } else {
          p.str[p.str.length - 1] += c.replace(/\\(.)/, "$1");
        }
        return p;
      },
      { str: [""], quote: 0 }
    ).str || [];
  const tokens: Token[] = [];

  for (let token of rawTokens) {
    let isNegative = false;

    if (token.startsWith("!") && token.length > 1) {
      const valueAfterNegation = token.substring(1);

      if (
        SEARCH_CONDITIONS.some((field) =>
          valueAfterNegation.startsWith(`${field}:`)
        )
      ) {
        isNegative = true;
        token = valueAfterNegation;
      }
    }

    let match = false;

    for (const field of SEARCH_CONDITIONS) {
      if (token.startsWith(`${field}:`) && token.length > `${field}:`.length) {
        tokens.push({
          field,
          value: token.substring(`${field}:`.length),
          isNegative,
        });
        match = true;
        break;
      }
    }

    if (!match) {
      // everything else -> 'general' text
      tokens.push({ field: "general", value: token, isNegative });
    }
  }

  const SEARCH_FILTER_LIMIT = Number(process.env.SEARCH_FILTER_LIMIT);

  if (SEARCH_FILTER_LIMIT) {
    const generalTokens = tokens.filter((t) => t.field === "general");
    const otherTokens = tokens.filter((t) => t.field !== "general");
    return [...generalTokens, ...otherTokens.slice(0, SEARCH_FILTER_LIMIT)];
  }

  return tokens;
}

export function buildMeiliQuery(tokens: Token[]): string {
  const generalValues = tokens
    .filter((t) => t.field === "general")
    .map((t) => t.value);

  return generalValues.join(" ");
}

export function buildMeiliFilters({
  tokens,
  userId,
  publicOnly,
}: {
  tokens: Token[];
  userId?: number;
  publicOnly?: boolean;
}): string[] {
  const filters: string[] = publicOnly
    ? ["collectionIsPublic = true"]
    : [`(collectionOwnerId = ${userId}) OR (collectionMemberIds = ${userId})`];

  for (const { field, value, isNegative } of tokens) {
    switch (field) {
      case "url":
        filters.push(
          isNegative
            ? `NOT url = "${escapeForMeilisearch(value)}"`
            : `url = "${escapeForMeilisearch(value)}"`
        );
        break;

      case "name":
        filters.push(
          isNegative
            ? `NOT name = "${escapeForMeilisearch(value)}"`
            : `name = "${escapeForMeilisearch(value)}"`
        );
        break;

      case "description":
        filters.push(
          isNegative
            ? `NOT description = "${escapeForMeilisearch(value)}"`
            : `description = "${escapeForMeilisearch(value)}"`
        );
        break;

      case "type":
        filters.push(
          isNegative
            ? `NOT type = "${escapeForMeilisearch(value)}"`
            : `type = "${escapeForMeilisearch(value)}"`
        );
        break;

      case "collection":
        filters.push(
          isNegative
            ? `NOT collectionName = "${escapeForMeilisearch(value)}"`
            : `collectionName = "${escapeForMeilisearch(value)}"`
        );
        break;

      case "pinned":
        if (value === "true") {
          filters.push(
            isNegative ? `NOT pinnedBy = ${userId}` : `pinnedBy = ${userId}`
          );
        } else if (value === "false") {
          filters.push(
            isNegative ? `pinnedBy = ${userId}` : `NOT pinnedBy = ${userId}`
          );
        }
        break;

      case "public":
        if (value === "true") {
          filters.push(
            isNegative
              ? `NOT collectionIsPublic = true`
              : `collectionIsPublic = true`
          );
        }
        break;

      case "before":
        if (!isNaN(Date.parse(value))) {
          const creationTimestamp = Date.parse(value) / 1000;
          filters.push(
            isNegative
              ? `creationTimestamp >= ${creationTimestamp}`
              : `creationTimestamp < ${creationTimestamp}`
          );
        }
        break;

      case "after":
        if (!isNaN(Date.parse(value))) {
          const creationTimestamp = Date.parse(value) / 1000;
          filters.push(
            isNegative
              ? `creationTimestamp <= ${creationTimestamp}`
              : `creationTimestamp > ${creationTimestamp}`
          );
        }
        break;

      case "tag":
        filters.push(
          isNegative
            ? `NOT tags = "${escapeForMeilisearch(value)}"`
            : `tags = "${escapeForMeilisearch(value)}"`
        );
        break;
      // "general" text is handled by the main query, not by filters
      case "general":
      default:
        break;
    }
  }

  return filters;
}

export function escapeForMeilisearch(value: string): string {
  return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
}
