import { readFile } from "node:fs/promises";
import { buildSchema, getNamedType, isObjectType, GraphQLObjectType } from "graphql";
import path from "path";
import Fuse from "fuse.js"
import { ApolloClient, InMemoryCache, gql } from "@apollo/client";
import { Maybe } from "graphql/jsutils/Maybe";

interface FieldData {
  path: string;
  type: string;
  description: string;
  arguments: Array<{ name: string; type: string; description: string }> | null;
};

type SchemaSearchOptions = {
  startingPoint: string;
  maxDepth: number;
  includeArgs: boolean;
  includeDescriptions: boolean;
};

function getAllSubfields(type: GraphQLObjectType, level = 0, maxDepth: number, includeDescriptions: boolean, includeArguments: boolean, canGetSubfields: boolean): any {
  const subfields: any = {};
  const fields = type.getFields();

  Object.keys(fields).forEach(fieldName => {
    const field = fields[fieldName];
    const fieldType = getNamedType(field.type);

    subfields[fieldName] = {
      type: fieldType.toString(),
    };

    if (includeDescriptions) {
      subfields[fieldName].description = field.description || "No description available";
    }

    if (includeArguments) {
      subfields[fieldName].arguments = field.args.map(arg => ({
        name: arg.name,
        type: getNamedType(arg.type).toString(),
        description: arg.description || "No description available"
      }));
    }

    subfields[fieldName].isObjectType = isObjectType(fieldType);
    if (canGetSubfields && level < maxDepth && isObjectType(fieldType)) {
      subfields[fieldName].subfields = getAllSubfields(fieldType, level + 1, maxDepth, includeDescriptions, includeArguments, canGetSubfields);
    } else if (canGetSubfields && level >= maxDepth && isObjectType(fieldType)) {
      subfields[fieldName].subfields = "truncated because maxdepth has been reached";
    } 
  });

  return subfields;
}

function flattenSchema(type: GraphQLObjectType | null, parentPath = "", level = 0, options: SchemaSearchOptions): FieldData[] {
  if (!type || !isObjectType(type)) return [];

  const fields = type.getFields();
  let result: FieldData[] = [];
  Object.keys(fields).forEach(fieldName => {
    const field = fields[fieldName];
    const fieldType = getNamedType(field.type);
    const fullPath = parentPath ? `${parentPath}.${fieldName}` : fieldName;
    
    let args: any[] = [];
    if (options.includeArgs) {
      args = field.args.map(arg => ({
        name: arg.name,
        type: getNamedType(arg.type).toString(),
        description: options.includeDescriptions ? arg.description || "No description available" : "excluded because includeDescriptions was set to false"
      }));
    } else {
      args = ["excluded because includeArgs was set to false"];
    }

    result.push({
      path: fullPath,
      type: fieldType.toString(),
      description: options.includeDescriptions ? field.description || "No description available" : "excluded because includeDescriptions was set to false",
      arguments: args.length > 0 ? args : null
    });

    if (isObjectType(fieldType) && level !== options.maxDepth) {
      result = result.concat(flattenSchema(fieldType, fullPath, level + 1, options));
    }
  });
  return result;
}

export async function getObjectTypeForPath(path?: string): Promise<Maybe<GraphQLObjectType> | null> {
  const queryType = await loadQueryType();
  if (!path) {
    return queryType;
  }

  if (!queryType) return null;

  const pathSegments = path.split(".");
  let currentType: GraphQLObjectType | null = queryType;

  for (const segment of pathSegments) {
    if (!currentType || !isObjectType(currentType)) {
      return null;
    }

    const field: any = currentType.getFields()[segment];
    if (!field) return null;

    const fieldType = getNamedType(field.type);
    if (!isObjectType(fieldType)) return null;

    currentType = fieldType;
  }

  return currentType;
}

export async function loadQueryType() {
  const schemaFilePath = path.resolve(__dirname, "shopifyschema.graphqls");
  const schemaSDL = await readFile(schemaFilePath, "utf8");

  // const ast = parse(schemaSDL);
  const schema = buildSchema(schemaSDL);
  const queryType = schema.getQueryType();
  return queryType;
}

function extractQueryFields(type: GraphQLObjectType | null, depth = 0, maxLevel: number) {
  if (!type || !isObjectType(type) || depth > maxLevel) return {};

  const fields = type.getFields();
  let result: any = {};

  Object.keys(fields).forEach((fieldName) => {
    const field = fields[fieldName];
    const fieldType = getNamedType(field.type);

    /*
    const args = field.args.map(arg => {
      return {
        name: arg.name,
        type: getNamedType(arg.type).toString(),
        description: arg.description || "No description available"
      };
    });
    */

    result[fieldName] = {
      type: fieldType.toString(),
      description: field.description || "No description available",
      // arguments: args.length > 0 ? args : null,
      subfields: (isObjectType(fieldType) && depth < maxLevel) ? extractQueryFields(fieldType, depth + 1, maxLevel) : null,
    };
  });

  return result;
}

const client = new ApolloClient({
  uri: "https://<shopname>.myshopify.com/api/2024-10/graphql.json",
  headers: {
    "X-Shopify-Storefront-Access-Token": "<token>"
  },
  cache: new InMemoryCache()
});

export function getQueryTreeForPath(schema: GraphQLObjectType | null, targetPath: string, maxDepth: number, includeArgs: boolean, includeDescriptions: boolean, strictNoSubfields: boolean) {
  if (!schema || !isObjectType(schema)) return {};

  const pathParts = targetPath.split(".").filter(it => !!it);
  let currentType: GraphQLObjectType | null = schema;
  let tree: any = {};
  const treeRoot = tree;

  for (let i = 0; i < pathParts.length; i++) {
    const fieldName = pathParts[i];

    if (!currentType || !isObjectType(currentType)) {
      console.log(`Field "${fieldName}" not found in schema.`);
      return {};
    }

    const fields: any = currentType.getFields();
    if (!fields[fieldName]) {
      console.log(`Field "${fieldName}" does not exist.`);
      return {};
    }

    const field = fields[fieldName];
    const fieldType: any = getNamedType(field.type);

    tree[fieldName] = {
      name: field.name,
      type: fieldType.toString(),
    };

    if (includeArgs) {
      tree[fieldName].arguments = field.args.map((arg: any) => ({
        name: arg.name,
        type: (getNamedType(arg.type) as any).toString(),
        description: arg.description || "No description available"
      }));
    }

    if (includeDescriptions) {
      tree[fieldName].description = field.description || "No description available";
    }

    if (isObjectType(fieldType)) {
      currentType = fieldType;
      tree[fieldName].subfields = {};
      tree = tree[fieldName].subfields;
    } else {
      tree = null;
      break;
    }
  }

  if (isObjectType(currentType) && tree) {
    const subfields = getAllSubfields(currentType, 0, maxDepth, includeDescriptions, includeArgs, !strictNoSubfields);
    if (subfields) {
      Object.assign(tree, subfields);
    }
  }

  return treeRoot;
}

export async function searchSchema(query: string, options: SchemaSearchOptions) {
  const queryType: Maybe<GraphQLObjectType<any, any>> = await getObjectTypeForPath(options.startingPoint);

  if (!queryType) {
    console.log("Schema extraction failure!");
    return;
  }

  const fields = flattenSchema(queryType, options.startingPoint, 0, options);

  const fuse = new Fuse(fields, {
    keys: ["path", "description", "type"],
    threshold: 0.3
  });

  const results = fuse.search(query).map(res => res.item);
  // console.log("Search results: ", JSON.stringify(results, null, 2));
  return results;
}

export async function getQueriesUptoLevel(maxLevel: number) {
  const queryType = await loadQueryType();

  if (queryType) {
    const allQueries = extractQueryFields(queryType, 0, maxLevel);

    return allQueries;
  } else {
    console.log("Schema extraction failure!");
  }
}

export async function executeQuery(query: string, variables: any = {}) {
  try {
    await client.query({ query: gql`${query}`, variables });
    return "successful";
  } catch (err: any) {
    console.error("Graphql Query error: ", err);
    return `error: ${err?.toString()}`;
  }
}

export async function getSchemaForPath(path: string, maxDepth: number) {
  const queryType = await loadQueryType();
  if (queryType) {
    const tree = await getQueryTreeForPath(queryType, path, maxDepth, true, true, false);
    return tree;
  } else {
    console.log("Failed to extract schema");
  }
}

export async function enumerateFields(path: string, includeDescriptions: boolean, includeArgs: boolean) {
  const queryType = await loadQueryType();
  if (queryType) {
    const result = await getQueryTreeForPath(queryType, path, 0, includeArgs, includeDescriptions, true);
    return {isSchemaRoot: true, subfields: result};
  } else {
    console.log("Failed to extract schema");
  }
}
