import { gql } from "@apollo/client";
import { Kind, parse, print, visit } from "graphql";
import client from "./GraphQLClient";

/**
 * The available operators for filtering (string)
 * @type {[{label: string, value: string},{label: string, value: string},{label: string, value: string},{label: string, value: string},{label: string, value: string},null,null,null]}
 */
const STRING_OPERATORS = [
  { value: "_eq", label: "equals" },
  { value: "_neq", label: "does not equal" },
  { value: "_like", label: "contains" },
  { value: "_nlike", label: "does not contain" },
  { value: "_ilike", label: "contains case-insensitive" },
  { value: "_nilike", label: "does not contain case-insensitive" },
  { value: "_in", label: "in", type: "array" },
  { value: "_nin", label: "not in", type: "array" }
];

/**
 * The available operators for filtering (dates)
 * @type {[{label: string, value: string},{label: string, value: string},{label: string, value: string},{label: string, value: string},{label: string, value: string},null,null,null]}
 */
const DATE_OPERATORS = [
  { value: "_eq", label: "equals" },
  { value: "_neq", label: "does not equal" },
  { value: "_gt", label: "greater than" },
  { value: "_lt", label: "less than" },
  { value: "_gte", label: "greater than or equal" },
  { value: "_lte", label: "less than or equal" },
  { value: "_in", label: "in", type: "array" },
  { value: "_nin", label: "not in", type: "array" }
];

/**
 * The available operators for filtering (booleans)
 * @type {[{label: string, value: string},{label: string, value: string}]}
 */
const BOOLEAN_OPERATORS = [
  { value: "_eq", label: "equals" },
  { value: "_neq", label: "does not equal" }
];

/**
 * The available operators for filtering (numbers)
 * @type {[{label: string, value: string},{label: string, value: string},{label: string, value: string},{label: string, value: string},{label: string, value: string},null,null,null]}
 */
const NUMBER_OPERATORS = [
  { value: "_eq", label: "equals" },
  { value: "_neq", label: "does not equal" },
  { value: "_gt", label: "greater than" },
  { value: "_lt", label: "less than" },
  { value: "_gte", label: "greater than or equal" },
  { value: "_lte", label: "less than or equal" },
  { value: "_in", label: "in", type: "array" },
  { value: "_nin", label: "not in", type: "array" }
];

/**
 * The available operators for sorting
 * @type {[{label: string, value: string},{label: string, value: string}]}
 */
const ORDER_BY_OPERATORS = [
  { value: "asc", label: "ascending" },
  { value: "desc", label: "descending" }
];

/**
 * Get the available operators for filtering
 * @returns {[{label: string, value: string},{label: string, value: string}]}
 */
export function getOrderOperatorsByType() {
  return ORDER_BY_OPERATORS;
}

/**
 * Get the available operators for filtering
 * @param type
 * @returns {[{label: string, value: string},{label: string, value: string},{label: string, value: string},{label: string, value: string},{label: string, value: string},null]}
 */
export function getWhereOperatorsByType(type = "string") {
  const operators = {
    string: STRING_OPERATORS,
    number: NUMBER_OPERATORS,
    boolean: BOOLEAN_OPERATORS,
    bool: BOOLEAN_OPERATORS,
    date: DATE_OPERATORS
  };
  return operators[type];
}

/**
 * Parse a GraphQL query into an AST
 * @param query
 * @returns {DocumentNode}
 */
export function parseQuery(query) {
  return parse(query);
}

/**
 * Print an AST back into a GraphQL query
 * @param query
 * @returns {string}
 */
export function printQuery(query) {
  return print(query);
}

/**
 * Generate a template based on the query and object
 * @param templateQueryToExecute
 * @param templateObject
 * @param useShopSpecificTemplate
 * @param shopSpecificTemplate
 * @returns {Promise<{contextData: {}, useShopSpecificTemplate, shopSpecificTemplate}>}
 */
export async function generateTemplate(
  templateQueryToExecute,
  templateObject,
  useShopSpecificTemplate,
  shopSpecificFolder
) {
  // Advanced Filtering and Sorting modifications start here

  // Parse the query and apply the filters and sorters
  const ast = parseQuery(templateQueryToExecute);

  if (templateObject?.filters && templateObject?.filters?.length) {
    applyFilters(ast, templateObject.filters);
  }

  if (templateObject?.sorters && templateObject?.sorters?.length) {
    applySorters(ast, templateObject.sorters);
  } else if (templateObject?.defaultSorters && templateObject?.defaultSorters?.length) {
    applySorters(ast, templateObject.defaultSorters);
  }

  const finalQuery = printQuery(ast);

  // commented out for future revision debugging
  // console.log('Modified Query');
  // console.log(finalQuery);

  let contextData = {};
  if (templateQueryToExecute) {
    const { data } = await client.query({
      query: gql(finalQuery),
      variables: { ...templateObject.variables },
      fetchPolicy: "no-cache"
    });
    contextData = data;
  }

  return { contextData, useShopSpecificTemplate, shopSpecificFolder };
}

/**
 * Apply sorters to the AST
 * @param ast
 * @param sorters
 */
export function applySorters(ast, sorters) {
  sorters.forEach((sorter) => {
    const fieldPath = sorter.field.split(".");
    visit(ast, {
      OperationDefinition: {
        enter(node) {
          // Loop through each sorter to apply it
          // noinspection DuplicatedCode

          let currentSelection = node; // Start with the root operation

          // Navigate down the field path to the correct location
          for (let i = 0; i < fieldPath.length - 1; i++) {
            let found = false;
            visit(currentSelection, {
              Field: {
                enter(node) {
                  if (node.name.value === fieldPath[i]) {
                    currentSelection = node; // Move down to the next level
                    found = true;
                  }
                }
              }
            });
            if (!found) break; // Stop if we can't find the next field in the path
          }

          // Apply the sorter at the correct level
          if (currentSelection) {
            const targetFieldName = fieldPath[fieldPath.length - 1];
            let orderByArg = currentSelection.arguments.find((arg) => arg.name.value === "order_by");
            if (!orderByArg) {
              orderByArg = {
                kind: Kind.ARGUMENT,
                name: { kind: Kind.NAME, value: "order_by" },
                value: { kind: Kind.OBJECT, fields: [] }
              };
              currentSelection.arguments.push(orderByArg);
            }

            const sorterField = {
              kind: Kind.OBJECT_FIELD,
              name: { kind: Kind.NAME, value: targetFieldName },
              value: { kind: Kind.ENUM, value: sorter.direction } // Adjust if your schema uses a different type for sorting directions
            };

            // Add the new sorter condition
            orderByArg.value.fields.push(sorterField);
          }
        }
      }
    });
  });
}

/**
 * Apply Top Level Sub to the AST
 * @param node
 * @param fieldPath
 * @param filterField
 */
function applyTopLevelSub(node, fieldPath, filterField) {
  // Find or create the where argument for the top-level subfield
  let whereArg = node.selectionSet.selections
    .find((selection) => selection.name.value === fieldPath[0])
    ?.arguments.find((arg) => arg.name.value === "where");

  if (!whereArg) {
    whereArg = {
      kind: Kind.ARGUMENT,
      name: { kind: Kind.NAME, value: "where" },
      value: { kind: Kind.OBJECT, fields: [] }
    };
    const topLevelSubSelection = node.selectionSet.selections.find(
      (selection) => selection.name.value === fieldPath[0]
    );
    if (topLevelSubSelection) {
      topLevelSubSelection.arguments = topLevelSubSelection.arguments || [];
      topLevelSubSelection.arguments.push(whereArg);
    }
  }

  // Correctly position the nested filter without an extra 'where'
  if (fieldPath.length > 2) {
    // More than one level deep
    let currentField = whereArg.value;
    fieldPath.slice(1, -1).forEach((path) => {
      let existingField = currentField.fields.find((f) => f.name.value === path);
      if (!existingField) {
        existingField = {
          kind: Kind.OBJECT_FIELD,
          name: { kind: Kind.NAME, value: path },
          value: { kind: Kind.OBJECT, fields: [] }
        };
        currentField.fields.push(existingField);
      }
      currentField = existingField.value;
    });
    currentField.fields.push(filterField);
  } else {
    // Directly under the top level
    whereArg.value.fields.push(filterField);
  }
}

/**
 * Apply filters to the AST
 * @param ast
 * @param filters
 * @returns {ASTNode}
 */
export function applyFilters(ast, filters) {
  return visit(ast, {
    OperationDefinition: {
      enter(node) {
        filters.forEach((filter) => {
          const fieldPath = filter.field.split(".");
          let topLevel = false;
          let topLevelSub = false;

          // Determine if the filter should be applied at the top level
          if (fieldPath.length === 2) {
            topLevel = true;
          }

          if (fieldPath.length > 2 && fieldPath[0].startsWith("[") && fieldPath[0].endsWith("]")) {
            fieldPath[0] = fieldPath[0].substring(1, fieldPath[0].length - 1); // Strip the brackets
            topLevelSub = true;
          }

          // Construct the filter for a top-level application
          const targetFieldName = fieldPath[fieldPath.length - 1];

          let filterValue = createFilterValue(filter);
          let filterField = createFilterField(targetFieldName, filter, filterValue);

          if (topLevel) {
            applyTopLevelFilter(node, fieldPath, filterField);
          } else if (topLevelSub) {
            applyTopLevelSub(node, fieldPath, filterField);
          } else {
            applyNestedFilter(node, fieldPath, filterField);
          }
        });
      }
    }
  });
}

/**
 * Create a filter value based on the filter
 * @param filter
 * @returns {{kind: (Kind|Kind.INT), value}|{kind: Kind.LIST, values: *}}
 */
function createFilterValue(filter) {
  if (Array.isArray(filter.value)) {
    // If it's an array, create a list value with the array items
    return {
      kind: Kind.LIST,
      values: filter.value.map((item) => ({
        kind: getGraphQLKind(item),
        value: item
      }))
    };
  } else {
    // If it's not an array, use the existing logic
    return {
      kind: getGraphQLKind(filter.value),
      value: filter.value
    };
  }
}

/**
 * Create a filter field based on the target field and filter
 * @param targetFieldName
 * @param filter
 * @param filterValue
 * @returns {{kind: Kind.OBJECT_FIELD, name: {kind: Kind.NAME, value}, value: {kind: Kind.OBJECT, fields: [{kind: Kind.OBJECT_FIELD, name: {kind: Kind.NAME, value}, value}]}}}
 */
function createFilterField(targetFieldName, filter, filterValue) {
  return {
    kind: Kind.OBJECT_FIELD,
    name: { kind: Kind.NAME, value: targetFieldName },
    value: {
      kind: Kind.OBJECT,
      fields: [
        {
          kind: Kind.OBJECT_FIELD,
          name: { kind: Kind.NAME, value: filter.operator },
          value: filterValue
        }
      ]
    }
  };
}

/**
 * Apply a top-level filter to the AST
 * @param node
 * @param fieldPath
 * @param filterField
 */
function applyTopLevelFilter(node, fieldPath, filterField) {
  // Find or create the where argument for the top-level field
  let whereArg = node.selectionSet.selections
    .find((selection) => selection.name.value === fieldPath[0])
    ?.arguments.find((arg) => arg.name.value === "where");

  if (!whereArg) {
    whereArg = {
      kind: Kind.ARGUMENT,
      name: { kind: Kind.NAME, value: "where" },
      value: { kind: Kind.OBJECT, fields: [] }
    };
    const topLevelSelection = node.selectionSet.selections.find((selection) => selection.name.value === fieldPath[0]);
    if (topLevelSelection) {
      topLevelSelection.arguments = topLevelSelection.arguments || [];
      topLevelSelection.arguments.push(whereArg);
    }
  }

  // Correctly position the nested filter without an extra 'where'
  if (fieldPath.length > 2) {
    // More than one level deep
    let currentField = whereArg.value;
    fieldPath.slice(1, -1).forEach((path) => {
      let existingField = currentField.fields.find((f) => f.name.value === path);
      if (!existingField) {
        existingField = {
          kind: Kind.OBJECT_FIELD,
          name: { kind: Kind.NAME, value: path },
          value: { kind: Kind.OBJECT, fields: [] }
        };
        currentField.fields.push(existingField);
      }
      currentField = existingField.value;
    });
    currentField.fields.push(filterField);
  } else {
    // Directly under the top level
    whereArg.value.fields.push(filterField);
  }
}

/**
 * Apply a nested filter to the AST
 * @param node
 * @param fieldPath
 * @param filterField
 */
function applyNestedFilter(node, fieldPath, filterField) {
  // Initialize a reference to the current selection to traverse down the AST
  let currentSelection = node;

  // Iterate over the fieldPath, except for the last entry, to navigate the structure
  for (let i = 0; i < fieldPath.length - 1; i++) {
    const fieldName = fieldPath[i];
    let fieldFound = false;

    // Check if the current selection has a selectionSet and selections
    if (currentSelection.selectionSet && currentSelection.selectionSet.selections) {
      // Look for the field in the current selection's selections
      const selection = currentSelection.selectionSet.selections.find((sel) => sel.name.value === fieldName);
      if (selection) {
        // Move down the AST to the found selection
        currentSelection = selection;
        fieldFound = true;
      }
    }

    // If the field was not found in the current path, it's an issue
    if (!fieldFound) {
      console.error(`Field ${fieldName} not found in the current selection.`);
      return; // Exit the loop and function due to error
    }
  }

  // At this point, currentSelection should be the parent field where the filter needs to be applied
  // Check if the 'where' argument already exists in the current selection
  const whereArg = currentSelection.arguments.find((arg) => arg.name.value === "where");
  if (!whereArg) {
    // If not found, create a new 'where' argument for the current selection
    currentSelection.arguments.push({
      kind: Kind.ARGUMENT,
      name: { kind: Kind.NAME, value: "where" },
      value: { kind: Kind.OBJECT, fields: [] } // Empty fields array to be populated with the filter
    });
  }

  // Add the filter field to the 'where' clause of the current selection
  currentSelection.arguments.find((arg) => arg.name.value === "where").value.fields.push(filterField);
}

/**
 * Get the GraphQL kind for a value
 * @param value
 * @returns {Kind|Kind.INT}
 */
function getGraphQLKind(value) {
  if (Array.isArray(value)) {
    return Kind.LIST;
  } else if (typeof value === "number") {
    return value % 1 === 0 ? Kind.INT : Kind.FLOAT;
  } else if (typeof value === "boolean") {
    return Kind.BOOLEAN;
  } else if (typeof value === "string") {
    return Kind.STRING;
  } else if (value instanceof Date) {
    return Kind.STRING; // GraphQL does not have a Date type, so we return it as a string
  }
}
