import * as fs from "fs";
import * as path from "path";
import type { ViewArgs } from "../types/clientPayloads";

// Type representing a parsed Move view function
type ViewFunction = {
  name: string;
  args: Array<{ name: string; type: string }>;
  file: string;
};

/**
 * Recursively find all .move files in a directory
 */
function findMoveFiles(dir: string): string[] {
  let results: string[] = [];
  const list = fs.readdirSync(dir);
  list.forEach((file) => {
    const filePath = path.join(dir, file);
    const stat = fs.statSync(filePath);
    if (stat && stat.isDirectory()) {
      results = results.concat(findMoveFiles(filePath));
    } else if (
      file.endsWith(".move") &&
      !file.endsWith(".test.move") &&
      !file.endsWith(".spec.move")
    ) {
      results.push(filePath);
    }
  });

  return results;
}

/**
 * Parse Move source code and extract #[view] functions
 */
function parseViewFunctionsFromSource(
  source: string,
  file: string,
): ViewFunction[] {
  const lines = source.split(/\r?\n/);
  const viewFunctions: ViewFunction[] = [];
  for (let i = 0; i < lines.length; i++) {
    if (lines[i].trim().startsWith("#[view]")) {
      // Look ahead for the function signature
      let j = i + 1;
      while (j < lines.length && !lines[j].includes("fun ")) j++;
      if (j >= lines.length) continue;
      const funLine = lines[j].trim();
      // Match function signature: public fun name(args...): returnType {
      const funMatch = funLine.match(/fun\s+([a-zA-Z0-9_]+)\s*\(([^)]*)\)/);
      if (!funMatch) continue;
      const [, name, argsStr] = funMatch;
      // Parse arguments
      const args = argsStr
        .split(",")
        .map((arg) => arg.trim())
        .filter(Boolean)
        .map((arg) => {
          // arg: name: type
          const [argName, ...typeParts] = arg.split(":").map((s) => s.trim());
          return argName && typeParts.length > 0
            ? { name: argName, type: typeParts.join(": ") }
            : null;
        })
        .filter(Boolean) as { name: string; type: string }[];
      viewFunctions.push({ name, args, file });
    }
  }
  return viewFunctions;
}

/**
 * Main function: Given a directory, return all view functions as TypeScript types
 */
export function extractViewFunctions(dir: string): ViewFunction[] {
  const moveFiles = findMoveFiles(dir);
  let allViews: ViewFunction[] = [];
  for (const file of moveFiles) {
    const source = fs.readFileSync(file, "utf8");
    const views = parseViewFunctionsFromSource(source, file);
    allViews = allViews.concat(views);
  }
  return allViews;
}

// Helper: Generate TypeScript type definitions for view functions
export function generateViewTypes(viewFns: ViewFunction[]): string {
  return viewFns
    .map((fn) => {
      const typeName =
        fn.name.charAt(0).toUpperCase() + fn.name.slice(1) + "View";
      return `export type ${typeName} = {\n  name: string;\n  args: Array<{ name: string; type: string }>;\n  file: string;\n}`;
    })
    .join("\n\n");
}

/**
 * Convert a ViewFunction to a ViewArgs payload (Aptos SDK compatible)
 * @param fn ViewFunction
 * @param params Array of argument values to use as functionArguments
 * @param moduleAddress Optional module address (defaults to 0x1)
 * @returns ViewArgs
 */
export function viewFunctionToViewArgs(
  fn: ViewFunction,
  params: unknown[] = [],
  moduleAddress = "0x1",
): ViewArgs {
  // Try to infer module name from file path, fallback to 'module'
  const fileName = fn.file.split(path.sep).pop() || "module.move";
  const moduleMatch = fileName.match(/([^.]+)\.move$/);
  const moduleName = moduleMatch ? moduleMatch[1] : "module";
  const functionName =
    `${moduleAddress}::${moduleName}::${fn.name}` as `${string}::${string}::${string}`;
  return {
    payload: {
      function: functionName,
      functionArguments: params,
    },
  };
}

/**
 * Generate a TypeScript file with constants for each discovered ViewFunction.
 * @param moveDir Directory to search for .move files
 * @param outFile Output .ts file path
 */
export function generate(
  moveDir: string,
  outFile: string,
  importPath = "../types/clientPayloads",
) {
  const views = extractViewFunctions(moveDir);
  // Deduplicate by fully qualified function name
  const seen = new Set<string>();
  const dedupedViews = views.filter((fn) => {
    const fileName = fn.file.split(path.sep).pop() || "module.move";
    const moduleMatch = fileName.match(/([^.]+)\.move$/);
    const moduleName = moduleMatch ? moduleMatch[1] : "module";
    const functionName = `0x1::${moduleName}::${fn.name}`;
    if (seen.has(functionName)) return false;
    seen.add(functionName);
    return true;
  });
  const lines: string[] = [];
  lines.push(
    "// This file is auto-generated by parseViewFunctions.ts. Do not edit manually.",
  );
  lines.push(`import type { ViewArgs } from "${importPath}";`);
  lines.push("");
  for (const fn of dedupedViews) {
    // Create a constant name in camelCase
    const constName = fn.name.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
    // Compose the function string
    const fileName = fn.file.split(path.sep).pop() || "module.move";
    const moduleMatch = fileName.match(/([^.]+)\.move$/);
    const moduleName = moduleMatch ? moduleMatch[1] : "module";
    const functionName = `0x1::${moduleName}::${fn.name}`;
    // Arguments as a comment
    const argComment = fn.args.length
      ? `// args: ${fn.args.map((a) => `${a.name}: ${a.type}`).join(", ")}`
      : "// no args";
    // FunctionArguments placeholder
    const argsArray = fn.args.length
      ? `[${fn.args.map((a) => `/* ${a.name}: ${a.type} */`).join(", ")}]`
      : "[]";
    lines.push(`${argComment}`);
    lines.push(`export const ${constName}View: ViewArgs = {`);
    lines.push(`  payload: {`);
    lines.push(`    function: "${functionName}",`);
    lines.push(`    functionArguments: ${argsArray},`);
    lines.push(`  },`);
    lines.push(`};\n`);
  }
  fs.writeFileSync(outFile, lines.join("\n"));
}

/**
 * Generate a TypeScript file with sugar functions for each discovered ViewFunction.
 * Each function mirrors the Move view function signature and returns a ViewArgs payload.
 * @param moveDir Directory to search for .move files
 * @param outFile Output .ts file path
 */
export function generateSugar(
  moveDir: string,
  outFile: string,
  importPath = "../types/clientPayloads",
) {
  const views = extractViewFunctions(moveDir);
  // Deduplicate by fully qualified function name
  const seen = new Set<string>();
  const dedupedViews = views.filter((fn) => {
    const fileName = fn.file.split(path.sep).pop() || "module.move";
    const moduleMatch = fileName.match(/([^.]+)\.move$/);
    const moduleName = moduleMatch ? moduleMatch[1] : "module";
    const functionName = `0x1::${moduleName}::${fn.name}`;
    if (seen.has(functionName)) return false;
    seen.add(functionName);
    return true;
  });
  const lines: string[] = [];
  lines.push(
    "// This file is auto-generated by parseViewFunctions.ts. Do not edit manually.",
  );
  lines.push(`import type { ViewArgs } from "${importPath}";`);
  lines.push("");

  for (const fn of dedupedViews) {
    // Compose the Move module and function names
    const fileName = fn.file.split(path.sep).pop() || "module.move";
    const moduleMatch = fileName.match(/([^.]+)\.move$/);
    const moduleNameRaw = moduleMatch ? moduleMatch[1] : "module";
    // Helper to camelCase the function name (first part lower, then capitalize after underscores)
    function toCamelCase(str: string) {
      return str.replace(/_([a-zA-Z])/g, (_, c) => c.toUpperCase());
    }
    // Helper to lower-case only the first letter
    function lowerFirst(str: string) {
      return str.charAt(0).toLowerCase() + str.slice(1);
    }
    const moduleNameCamel = lowerFirst(toCamelCase(moduleNameRaw));
    const functionNameCamel = toCamelCase(fn.name);
    // TS function name: moduleNameCamel_functionNameCamel
    const tsFuncName = `${moduleNameCamel}_${functionNameCamel}`;
    // Args signature
    function mapMoveTypeToTsType(moveType: string): string {
      const t = moveType.trim().toLowerCase();
      if (
        t === "u8" ||
        t === "u16" ||
        t === "u32" ||
        t === "u64" ||
        t === "u128" ||
        t === "u256"
      )
        return "number";
      if (t === "bool") return "boolean";
      if (t.startsWith("vector<u8>")) return "string"; // treat bytes as string
      if (t === "address" || t.startsWith("vector<")) return "string";
      return "any";
    }
    const argList = fn.args
      .map((a, i) => `${a.name || "arg" + i}: ${mapMoveTypeToTsType(a.type)}`)
      .join(", ");
    const argNames = fn.args.map((a, i) => a.name || "arg" + i).join(", ");
    // Compose the Move function name
    const functionName = `0x1::${moduleNameRaw}::${fn.name}`;
    // JSDoc
    lines.push("/**");
    lines.push(` * Sugar for ${tsFuncName}`);
    if (fn.args.length) {
      fn.args.forEach((a, i) =>
        lines.push(` * @param ${a.name || "arg" + i} ${a.type}`),
      );
    }
    lines.push(" * @returns ViewArgs");
    lines.push(" */");
    lines.push(`export function ${tsFuncName}(${argList}): ViewArgs {`);
    lines.push("  return {");
    lines.push("    payload: {");
    lines.push(`      function: "${functionName}",`);
    lines.push(`      functionArguments: [${argNames}],`);
    lines.push("    },");
    lines.push("  };");
    lines.push("}");
    lines.push("");
  }
  fs.writeFileSync(outFile, lines.join("\n"));
}
