import { Prisma } from "@calcom/prisma/client";

import type { FilterValue, SortingState } from "./types";
import {
  isSingleSelectFilterValue,
  isMultiSelectFilterValue,
  isTextFilterValue,
  isNumberFilterValue,
  isDateRangeFilterValue,
} from "./utils";

type MakeWhereClauseProps = {
  columnName: string;
  filterValue: FilterValue;
  json?: true | { path: string[] };
};

export function makeOrderBy(sorting: SortingState) {
  if (!sorting || !sorting.length) return undefined;

  return sorting.map((sort) => ({
    [sort.id]: sort.desc ? ("desc" as const) : ("asc" as const),
  }));
}

/**
 * Builds a Prisma where clause for use with Prisma queries
 */
export function makeWhereClause(props: MakeWhereClauseProps) {
  const { columnName, filterValue } = props;
  const isJson = props.json === true || (typeof props.json === "object" && props.json.path?.length > 0);
  const jsonPath = isJson && typeof props.json === "object" ? props.json.path : undefined;

  const jsonPathObj = isJson && jsonPath ? { path: jsonPath } : {};
  if (isMultiSelectFilterValue(filterValue)) {
    return {
      [columnName]: {
        ...jsonPathObj,
        ...(isJson ? { array_contains: filterValue.data } : { in: filterValue.data }),
      },
    };
  } else if (isSingleSelectFilterValue(filterValue)) {
    return {
      [columnName]: {
        ...jsonPathObj,
        equals: filterValue.data,
      },
    };
  } else if (isTextFilterValue(filterValue)) {
    const { operator, operand } = filterValue.data;
    switch (operator) {
      case "equals":
        return {
          [columnName]: {
            ...jsonPathObj,
            equals: operand,
          },
        };
      case "notEquals":
        return {
          [columnName]: {
            ...jsonPathObj,
            not: operand,
          },
        };
      case "contains":
        return {
          [columnName]: {
            ...jsonPathObj,
            ...(isJson ? { string_contains: operand } : { contains: operand, mode: "insensitive" }),
          },
        };
      case "notContains":
        return {
          NOT: {
            [columnName]: {
              ...jsonPathObj,
              ...(isJson ? { string_contains: operand } : { contains: operand, mode: "insensitive" }),
            },
          },
        };
      case "startsWith":
        return {
          [columnName]: {
            ...jsonPathObj,
            ...(isJson ? { string_starts_with: operand } : { startsWith: operand, mode: "insensitive" }),
          },
        };
      case "endsWith":
        return {
          [columnName]: {
            ...jsonPathObj,
            ...(isJson ? { string_ends_with: operand } : { endsWith: operand, mode: "insensitive" }),
          },
        };
      case "isEmpty":
        return {
          [columnName]: {
            ...jsonPathObj,
            equals: "",
          },
        };
      case "isNotEmpty":
        return {
          NOT: {
            [columnName]: {
              ...jsonPathObj,
              equals: "",
            },
          },
        };
      default:
        throw new Error(`Invalid operator for text filter: ${operator}`);
    }
  } else if (isNumberFilterValue(filterValue)) {
    const { operator, operand } = filterValue.data;
    switch (operator) {
      case "eq":
        return {
          [columnName]: {
            ...jsonPathObj,
            equals: operand,
          },
        };
      case "neq":
        return {
          [columnName]: {
            ...jsonPathObj,
            not: operand,
          },
        };
      case "gt":
        return {
          [columnName]: {
            ...jsonPathObj,
            gt: operand,
          },
        };
      case "gte":
        return {
          [columnName]: {
            ...jsonPathObj,
            gte: operand,
          },
        };
      case "lt":
        return {
          [columnName]: {
            ...jsonPathObj,
            lt: operand,
          },
        };
      case "lte":
        return {
          [columnName]: {
            ...jsonPathObj,
            lte: operand,
          },
        };
      default:
        throw new Error(`Invalid operator for number filter: ${operator}`);
    }
  } else if (isDateRangeFilterValue(filterValue)) {
    const { startDate, endDate } = filterValue.data;
    if (!startDate || !endDate) {
      throw new Error(`Invalid date range filter: ${JSON.stringify({ columnName, startDate, endDate })}`);
    }

    return {
      [columnName]: {
        ...jsonPathObj,
        gte: startDate,
        lte: endDate,
      },
    };
  }
  throw new Error(`Invalid filter type: ${JSON.stringify({ columnName, filterValue })}`);
}

/**
 * Builds a SQL where clause for use with raw SQL queries
 */
export function makeSqlCondition(filterValue: FilterValue): Prisma.Sql | null {
  if (isMultiSelectFilterValue(filterValue)) {
    return Prisma.sql`= ANY(${filterValue.data})`;
  } else if (isSingleSelectFilterValue(filterValue)) {
    return Prisma.sql`= ${filterValue.data}`;
  } else if (isTextFilterValue(filterValue)) {
    const { operator, operand } = filterValue.data;
    switch (operator) {
      case "equals":
        return Prisma.sql`= ${operand}`;
      case "notEquals":
        return Prisma.sql`!= ${operand}`;
      case "contains":
        return Prisma.sql`ILIKE ${`%${operand}%`}`;
      case "notContains":
        return Prisma.sql`NOT ILIKE ${`%${operand}%`}`;
      case "startsWith":
        return Prisma.sql`ILIKE ${`${operand}%`}`;
      case "endsWith":
        return Prisma.sql`ILIKE ${`%${operand}`}`;
      case "isEmpty":
        return Prisma.sql`= ''`;
      case "isNotEmpty":
        return Prisma.sql`!= ''`;
      default:
        return null;
    }
  } else if (isNumberFilterValue(filterValue)) {
    const { operator, operand } = filterValue.data;
    switch (operator) {
      case "eq":
        return Prisma.sql`= ${operand}`;
      case "neq":
        return Prisma.sql`!= ${operand}`;
      case "gt":
        return Prisma.sql`> ${operand}`;
      case "gte":
        return Prisma.sql`>= ${operand}`;
      case "lt":
        return Prisma.sql`< ${operand}`;
      case "lte":
        return Prisma.sql`<= ${operand}`;
      default:
        return null;
    }
  }

  return null;
}
