import fs from 'fs';
import path from 'path';

// Error code regex
const errorCodeRegex = /new ErrorWithCode\(\s*(.+),\s*(.+)\s*\)(;|,)/;

// Get the project directory
const pwd = path.join(process.env.PWD || process.env.CWD || __dirname, '../..');

/*------------------------------------------------------------------------*/
/* -------------------------------- Types ------------------------------- */
/*------------------------------------------------------------------------*/

// An error code and its implementation
type ErrorCode = {
  // Parent code
  parent: string,
  // The error code
  code: string,
  // The machine-readable name of the error code
  prop: string,
  // If true, found in any capacity
  found: boolean,
  // Appearances
  appearances: {
    // Error message code
    messageTemplate: string,
    // File path
    filePath: string,
  }[],
};

/*------------------------------------------------------------------------*/
/* ------------------------------- Helpers ------------------------------ */
/*------------------------------------------------------------------------*/

/**
 * Get all paths to the error code types within a folder
 * @author Gabe Abrams
 * @param parentFolderPath - the path to the parent folder
 * @returns an array of paths to the error code types
 */
const getErrorCodesPaths = (parentFolderPath: string): string[] => {
  // Get all files in the folder
  const files = fs.readdirSync(parentFolderPath);

  // For each item in the folder, recurse if folder, analyze if file
  const paths: string[] = [];
  files.forEach((file) => {
    // Get the full path
    const fullPath = path.join(parentFolderPath, file);

    // If folder, recurse
    const isFolder = (
      fs
        .lstatSync(fullPath)
        .isDirectory()
    );
    if (isFolder) {
      // Skip if the "from-server" folder
      if (file !== 'from-server') {
        // Recurse
        paths.push(...getErrorCodesPaths(fullPath));
      }
    } else {
      // Not folder. Analyze
      if (file.includes('ErrorCode') && (file.endsWith('.ts') || file.endsWith('.tsx'))) {
        // Found an error code file
        paths.push(fullPath);
      }
    }
  });

  // Return all accumulated paths
  return paths;
};

/**
 * Read an error code file and return the error codes within it
 * @author Gabe Abrams
 * @param filePath - the path to the error code file
 * @returns an array of error codes
 */
const readErrorCodes = (filePath: string): ErrorCode[] => {
  // Read the file
  const fileContents = fs.readFileSync(filePath, 'utf8');

  // Get the name of the error code file
  const parent = (
    (filePath.split('/').pop() || 'UnknownErrorCodeParent')
      .replace('.tsx', '')
      .replace('.ts', '')
  );

  // Split the file into lines
  const lines = fileContents.split('\n');

  // Find all error codes
  const errorCodes: ErrorCode[] = [];
  lines.forEach((line, lineNumber) => {
    // Format of an error code:
    // DescriptionOfErrorCode = 'CODE',

    // Check if this line is an error code
    const matches = line.trim().match(/(\w+) = '(\w+)',/);
    if (matches) {
      // Found an error code
      const name = matches[1];
      const code = matches[2];

      // Add to error codes
      errorCodes.push({
        parent,
        code,
        found: false,
        prop: name,
        appearances: [],
      });
    }
  });

  // Return all error codes
  return errorCodes;
};

/*------------------------------------------------------------------------*/
/* -------------------------------- Main -------------------------------- */
/*------------------------------------------------------------------------*/

/*----------------------------------------*/
/* ---------- Parse Error Codes --------- */
/*----------------------------------------*/

console.log('\nUpdating error docs...');

// Get error code file paths
const serverPaths = getErrorCodesPaths(path.join(pwd, '/server/src'));
const clientPaths = getErrorCodesPaths(path.join(pwd, '/client/src'));
const errorCodePaths = serverPaths.concat(clientPaths);

// Read each and process the enums
const errorCodes: ErrorCode[] = [];
errorCodePaths.forEach((filePath) => {
  errorCodes.push(...readErrorCodes(filePath));
});

/*----------------------------------------*/
/* -------- Validate Error Codes -------- */
/*----------------------------------------*/

// Display fatal error if duplicate error codes exist
const errorCodeSet = new Set<string>();
errorCodes.forEach((errorCode) => {
  if (errorCodeSet.has(errorCode.code)) {
    console.error(`Duplicate error code defined: ${errorCode.code} in ${errorCode.parent}. Fatal error. Exiting!`);
    process.exit(1);
  }
});

/*----------------------------------------*/
/* ---------- Find Appearances ---------- */
/*----------------------------------------*/

/**
 * Recursively look for appearances of error codes
 * @author Gabe Abrams
 * @param folderPath - the path to the folder to search
 */
const findAppearances = (folderPath: string) => {
  // Format of an error appearance:
  // new ErrorWithCode(
  //   `Message template with ${templateVariables} in it perhaps`,
  //   ErrorCodeParent.ErrorCodePropName,
  // );

  // Get all files in the folder
  const files = fs.readdirSync(folderPath);

  // For each item in the folder, recurse if folder, analyze if file
  files.forEach((file) => {
    // Get the full path
    const fullPath = path.join(folderPath, file);

    // If folder, recurse
    const isFolder = (
      fs
        .lstatSync(fullPath)
        .isDirectory()
    );
    if (isFolder) {
      // Recurse
      findAppearances(fullPath);
    } else {
      // Not folder. Analyze
      if (
        (file.endsWith('.ts') || file.endsWith('.tsx'))
        && !errorCodePaths.includes(fullPath)
      ) {
        // Read the file
        let fileContents = fs.readFileSync(fullPath, 'utf8');

        // Check if found
        errorCodes.find((errorCode) => {
          if (
            fileContents.includes(`${errorCode.parent}.${errorCode.prop},`)
            || fileContents.includes(`${errorCode.parent}.${errorCode.prop};`)
            || fileContents.includes(`${errorCode.parent}.${errorCode.prop} `)
            || fileContents.includes(`${errorCode.parent}.${errorCode.prop})`)
            || fileContents.includes(`${errorCode.parent}.${errorCode.prop}\n`)
          ) {
            errorCode.found = true;
          }
        });

        // Check if this line is an error appearance
        let matches = fileContents.trim().match(errorCodeRegex);
        while (matches) {
          // Found an error appearance
          const messageTemplate = matches[1];
          const parentAndProp = matches[2];

          // Find the error code
          const errorCode = errorCodes.find((errorCode) => {
            return (
              `${errorCode.parent}.${errorCode.prop},` === parentAndProp.trim()
            );
          });
          if (errorCode) {
            // Add the appearance
            errorCode.appearances.push({
              messageTemplate,
              filePath: fullPath,
            });
          }

          // Remove this match
          fileContents = fileContents.replace(matches[0], '');
          matches = fileContents.trim().match(errorCodeRegex);
        }
      }
    }
  });
};

// Add appearances in each folder
findAppearances(path.join(pwd, '/server/src'));
findAppearances(path.join(pwd, '/client/src'));

/*------------------------------------------------------------------------*/
/* ----------------------------- Build Docs ----------------------------- */
/*------------------------------------------------------------------------*/

let docsMD = '';

/*----------------------------------------*/
/* -------------- Metadata -------------- */
/*----------------------------------------*/

const pkg = require(path.join(pwd, 'package.json'));
if (!pkg) {
  console.log('package.json not found');
  process.exit(1);
}
const projectName = pkg.name || 'This Project';

/*----------------------------------------*/
/* ---------------- Intro --------------- */
/*----------------------------------------*/

docsMD = `# Error Cheatsheet for ${projectName}\n`;
docsMD += 'This is an auto-generated document that outlines the friendly error codes that users may encounter when using this app.\n\n';

/*----------------------------------------*/
/* ---------------- Table --------------- */
/*----------------------------------------*/

// Header
docsMD += '\n| Code | Internal Name | Associated Message |\n';
docsMD += '| -------- | -------- | -------- |\n';

// Error codes
errorCodes.forEach((err) => {
  // Skip ones that are not in use
  if (!err.found) {
    return;
  }

  let associatedMessage = (
    err
      .appearances
      .map((appearance) => {
        return `<div>${appearance.messageTemplate.replace('`', '\'')}<div style="font-size: 80%">${appearance.filePath.replace(pwd, '')}</div></div>`;
      })
      .join('<br>')
  );


  if (err.appearances.length === 0) {
    associatedMessage = 'No additional information';
  }

  docsMD += `| ${err.code} | ${err.prop} | ${associatedMessage} |\n`;
});

// Finish
fs.writeFileSync(
  path.join(pwd, 'docs/ErrorCodeDocs.md'),
  docsMD,
  'utf-8',
);

console.log('Success! Written to docs/ErrorCodeDocs.md');
