/**
 * @module build-context
 * @description Functions for managing build context and module files for Rspack plugin
 */
import { RSPACK_DOCTOR_CONTEXT } from "./constants";

const fs = require('fs');
const path = require('path');

const { getCustomConfigFilePath } = require('./processes');

const { logError } = require('meteor/tools-core/lib/log');

const { capitalizeFirstLetter } = require('meteor/tools-core/lib/string');

const {
  getMeteorAppDir,
  getMeteorInitialAppEntrypoints,
  isMeteorAppDevelopment,
  isMeteorAppRun,
  isMeteorAppBuild,
  isMeteorBlazeProject,
  isMeteorAppNative,
} = require('meteor/tools-core/lib/meteor');

const {
  getGlobalState,
  setGlobalState
} = require('meteor/tools-core/lib/global-state');

const {
  addGitignoreEntries
} = require('meteor/tools-core/lib/git');

const {
  RSPACK_BUILD_CONTEXT,
  RSPACK_CHUNKS_CONTEXT,
  RSPACK_ASSETS_CONTEXT,
  GLOBAL_STATE_KEYS,
  FILE_ROLE,
} = require('./constants');

// Common warning message for autogenerated files
const AUTO_GENERATED_WARNING = `* ⚠️ Note: This file is autogenerated. It is not meant to be modified manually.
* These files also act as a cache: they can be safely removed and will be
* regenerated on the next build. They should be ignored in IDE suggestions
* and version control.`;

/**
 * Gets entry points from Meteor configuration
 * Retrieves from global state if already stored, otherwise gets from Meteor
 * @returns {Object} Object containing entry points for client and server
 */
export function getInitialEntrypoints() {
  const existingEntrypoint = getGlobalState(GLOBAL_STATE_KEYS.INITIAL_ENTRYPONTS);
  if (existingEntrypoint) return existingEntrypoint;
  const initialEntrypoints = getMeteorInitialAppEntrypoints();
  const hasInitialEntrypoints = initialEntrypoints && Object.values(initialEntrypoints).length > 0 && Object.values(initialEntrypoints).every((value) => value != null);
  if (hasInitialEntrypoints) {
    setGlobalState(GLOBAL_STATE_KEYS.INITIAL_ENTRYPONTS, initialEntrypoints);
  }
  return initialEntrypoints;
}

/**
 * Ensures the Rspack build context directory exists
 * Creates the directory if it doesn't exist and adds it to .gitignore
 * @returns {string} Path to the build context directory
 * @throws {Error} If directory creation fails
 */
export function ensureRspackBuildContextExists() {
  const appDir = getMeteorAppDir();
  const buildContextPath = path.join(appDir, RSPACK_BUILD_CONTEXT);

  if (!fs.existsSync(buildContextPath)) {
    try {
      fs.mkdirSync(buildContextPath, { recursive: true });
    } catch (error) {
      logError(`Failed to create Rspack build context directory: ${error.message}`);
      throw error;
    }
  }

  addGitignoreEntries(
    appDir,
    [
      RSPACK_BUILD_CONTEXT,
      `*/${RSPACK_ASSETS_CONTEXT}`,
      `*/${RSPACK_CHUNKS_CONTEXT}`,
      RSPACK_DOCTOR_CONTEXT,
    ],
    'Meteor Modern-Tools build context directories',
  );

  return buildContextPath;
}

/**
 * Ensures module files exist in the build context directory
 * Creates default module files if they don't exist
 * @returns {void}
 */
export function ensureModuleFilesExist() {
  const appDir = getMeteorAppDir();

  const env = {
    ...(isMeteorAppDevelopment() ? { isDevelopment: true } : { isProduction: true }),
    isNative: isMeteorAppNative(),
  };
  const commandRole = isMeteorAppRun()
    ? { role: FILE_ROLE.run }
    : isMeteorAppBuild()
    ? { role: FILE_ROLE.build }
    : { role: FILE_ROLE.run };
  const initialEntrypoints = getInitialEntrypoints();
  const mainClientFiles = {
    entryFile: initialEntrypoints.mainClient || '',
    outputFile: getBuildFilePath({ isMain: true, isClient: true, ...env, role: FILE_ROLE.output, onlyFilename: true }),
  };
  const mainServerFiles = {
    entryFile: initialEntrypoints.mainServer || '',
    outputFile: getBuildFilePath({ isMain: true, isServer: true, ...env, role: FILE_ROLE.output, onlyFilename: true }),
  };
  const isTestEager =
    initialEntrypoints.testModule == null &&
    initialEntrypoints.testClient == null &&
    initialEntrypoints.testServer == null;
  const isTestModule = initialEntrypoints.testModule != null || isTestEager;
  const testClientFiles = {
    entryFile: initialEntrypoints.testClient || '',
    outputFile: getBuildFilePath({ isTest: true, isTestModule, isClient: true, role: FILE_ROLE.output, onlyFilename: true }),
  };
  const testServerFiles = {
    entryFile: initialEntrypoints.testServer || '',
    outputFile: getBuildFilePath({ isTest: true, isTestModule, isServer: true, role: FILE_ROLE.output, onlyFilename: true }),
  };

  const moduleFiles = {
    /* Main module files for client and server */
    [getBuildFilePath({ isMain: true, isClient: true, ...env, ...commandRole })]:
      getBuildFileContent({ isMain: true, isClient: true, ...env, ...commandRole, ...mainClientFiles }),
    [getBuildFilePath({ isMain: true, isClient: true, ...env, role: FILE_ROLE.entry })]:
      getBuildFileContent({ isMain: true, isClient: true, ...env, role: FILE_ROLE.entry, ...mainClientFiles }),
    [getBuildFilePath({ isMain: true, isClient: true, ...env, role: FILE_ROLE.output })]:
      getBuildFileContent({ isMain: true, isClient: true, ...env, role: FILE_ROLE.output, ...mainClientFiles }),
    [getBuildFilePath({ isMain: true, isServer: true, ...env, ...commandRole })]:
      getBuildFileContent({ isMain: true, isServer: true, ...env, ...commandRole, ...mainServerFiles }),
    [getBuildFilePath({ isMain: true, isServer: true, ...env, role: FILE_ROLE.entry })]:
      getBuildFileContent({ isMain: true, isServer: true, ...env, role: FILE_ROLE.entry, ...mainServerFiles }),
    [getBuildFilePath({ isMain: true, isServer: true, ...env, role: FILE_ROLE.output })]:
      getBuildFileContent({ isMain: true, isServer: true, ...env, role: FILE_ROLE.output, ...mainServerFiles }),
    /* Test module files when test module, test module files for client and server are present or eager discovery */
    [getBuildFilePath({ isTest: true, isTestModule, isClient: true, ...commandRole })]:
      getBuildFileContent({ isTest: true, isTestModule, isClient: true, ...commandRole, ...testClientFiles }),
    [getBuildFilePath({ isTest: true, isTestModule, isClient: true, role: FILE_ROLE.entry })]:
      getBuildFileContent({ isTest: true, isTestModule, isClient: true, role: FILE_ROLE.entry, ...testClientFiles }),
    [getBuildFilePath({ isTest: true, isTestModule, isClient: true, role: FILE_ROLE.output })]:
      getBuildFileContent({ isTest: true, isTestModule, isClient: true, role: FILE_ROLE.output, ...testClientFiles }),
    [getBuildFilePath({ isTest: true, isTestModule, isServer: true, ...commandRole })]:
      getBuildFileContent({ isTest: true, isTestModule, isServer: true, ...commandRole, ...testServerFiles }),
    [getBuildFilePath({ isTest: true, isTestModule, isServer: true, role: FILE_ROLE.entry })]:
      getBuildFileContent({ isTest: true, isTestModule, isServer: true, role: FILE_ROLE.entry, ...testServerFiles }),
    [getBuildFilePath({ isTest: true, isTestModule, isServer: true, role: FILE_ROLE.output })]:
      getBuildFileContent({ isTest: true, isTestModule, isServer: true, role: FILE_ROLE.output, ...testServerFiles }),
  };

  Object.entries(moduleFiles).forEach(([filename, defaultContent]) => {
    // 1. Build full path and ensure directory exists
    const filePath = path.join(appDir, RSPACK_BUILD_CONTEXT, filename);
    const dir = path.dirname(filePath);
    if (!fs.existsSync(dir)) {
      try {
        fs.mkdirSync(dir, { recursive: true });
      } catch (err) {
        logError(`Failed to create directory ${dir}: ${err.message}`);
        return; // stop here if we can’t make the folder
      }
    }

    // 2. If the file exists, check its contents
    if (fs.existsSync(filePath)) {
      let existing;
      try {
        existing = fs.readFileSync(filePath, 'utf8');
      } catch (err) {
        logError(`Failed to read existing file ${filename}: ${err.message}`);
        return;
      }

      // 3. If it doesn't already start with the new defaultContent, overwrite it
      if (!existing.includes(defaultContent)) {
        try {
          fs.writeFileSync(filePath, defaultContent, 'utf8');
        } catch (err) {
          logError(`Failed to rewrite module file ${filename}: ${err.message}`);
        }
      }

      // 4. If the file doesn't exist at all, write it for the first time
    } else {
      try {
        fs.writeFileSync(filePath, defaultContent, 'utf8');
      } catch (err) {
        logError(`Failed to create module file ${filename}: ${err.message}`);
      }
    }
  });
}

/**
 * Generates a build file path based on configuration parameters
 * @param {Object} config - Configuration object containing build settings
 * @returns {string} The build file path or filename
 */
export function getBuildFilePath(config) {
  // Determine the module part (directory name)
  let module = '';
  if (config?.isTest) {
    module = 'test';
  } else if (config?.isMain) {
    module = 'main';
  }

  // Determine the side part (first part of filename)
  let side = '';
  if (config?.isServer) {
    side = 'server';
  } else if (config?.isClient) {
    side = 'client';
  }

  // Determine the environment part (only for non-test files)
  let env = '';
  if (!config?.isTest) {
    if (config?.isDevelopment) {
      env = 'dev';
    } else if (config?.isProduction) {
      env = 'prod';
    }
  }

  // Determine the role part
  let role = config?.role;
  if ([FILE_ROLE.run, FILE_ROLE.build].includes(role)) {
    role = 'meteor';
  } else if ([FILE_ROLE.output].includes(role)) {
    role = 'rspack';
  }

  // 5. Get file extension (default to js)
  const extension = config?.extension || 'js';

  // 6. Construct the filename: {side}-{role}.{extension}
  const filename = `${side}-${role}.${extension}`;

  // Return either just the filename or the full path
  if (config?.onlyFilename) {
    return filename;
  } else {
    // Full path format: {module}[-{env}]/{filename}
    const envSuffix = env ? `-${env}` : '';
    return `${module}${envSuffix}/${filename}`;
  }
}

/**
 * Gets the appropriate banner based on file configuration
 * @param {Object} config - Configuration object
 * @param {string} side - The side (client, server, test)
 * @param {string} env - The environment (development, production)
 * @param {string} module - The module (main, test)
 * @param {string} role - The role (build, entry, run, output)
 * @returns {string} The banner content
 */
function getBanner(config, side, env, module, role) {
  const envDisplay = capitalizeFirstLetter(env || module);
  const sideDisplay = capitalizeFirstLetter(side);

  // For test mode, use the existing banners
  if (module === 'test') {
    // Test file banners
    if (role === FILE_ROLE.entry) {
      // For test mode, if side is client or server, include it in the title
      const testType = side === 'test' ? 'Test' : `Test ${sideDisplay}`;
      return `/**
* @file ${side}-entry.js
* @description Entry point for Rspack test build process
* --------------------------------------------------------------------------
* ⚡ Rspack ${testType} Entry (${envDisplay})
* --------------------------------------------------------------------------
* • [■ ${side}-entry.js ] ──▶ [   ${side}-rspack.js ] ──▶ [   ${side}-meteor.js ]
*
* This file is the starting point for the Rspack test build. It imports your
* Meteor app's test modules so Rspack can resolve every dependency and
* generate the bundled output: \`${side}-rspack.js\`.
*
${AUTO_GENERATED_WARNING}
*/`;
    }

    if (role === FILE_ROLE.output) {
      // For test mode, if side is client or server, include it in the title
      const testType = side === 'test' ? 'Test' : `Test ${sideDisplay}`;
      return `/**
* @file ${side}-rspack.js
* @description Bundled output generated by Rspack for tests
* --------------------------------------------------------------------------
* ⚡ Rspack ${testType} App (${envDisplay})
* --------------------------------------------------------------------------
* • [   ${side}-entry.js ] ──▶ [■ ${side}-rspack.js ] ──▶ [   ${side}-meteor.js ]
*
* This file is the bundle that Rspack outputs for tests. It contains all of
* your test code in one optimized file. Next step is loading this bundle via
* \`${side}-meteor.js\`.
*
${AUTO_GENERATED_WARNING}
*/`;
    }

    if (role === FILE_ROLE.run || role === FILE_ROLE.build) {
      // For test mode, if side is client or server, include it in the title
      const testType = side === 'test' ? 'Test' : `Test ${sideDisplay}`;
      return `/**
* @file ${side}-meteor.js
* @description Meteor runtime file that imports the Rspack test bundle
* --------------------------------------------------------------------------
* ☄️ Meteor ${testType} App (${envDisplay})
* --------------------------------------------------------------------------
* • [   ${side}-entry.js ] ──▶ [   ${side}-rspack.js ] ──▶ [■ ${side}-meteor.js ]
*
* Defined under \`meteor.testModule${side === 'test' ? '' : `.${side}`}\` in package.json. Meteor loads this
* file at runtime to import the Rspack test bundle (\`${side}-rspack.js\`) and
* run your tests.
*
${AUTO_GENERATED_WARNING}
*/`;
    }
    return '';
  }

  // For main modules (not test mode), use the new templates
  // Entry files
  if (role === FILE_ROLE.entry) {
    return `/**
* @file ${side}-entry.js
* @description Entry point for Rspack build process
* --------------------------------------------------------------------------
* 🔌 Rspack ${sideDisplay} Entry (${envDisplay})
* --------------------------------------------------------------------------
* • [■ ${side}-entry.js ] ──▶ [   ${side}-rspack.js ] ──▶ [   ${side}-meteor.js ]
*
* This file is the entry point that Rspack uses to start the build process.
* It imports the module defined in \`meteor.mainModule.${side}\` inside package.json.
* From here, Rspack can trace the entire dependency graph of your application
* and generate the bundled output (\`${side}-rspack.js\`).
*
${AUTO_GENERATED_WARNING}
*/`;
  }

  // Rspack output files
  if (role === FILE_ROLE.output) {
    return `/**
* @file ${side}-rspack.js
* @description Bundled output generated by Rspack
* --------------------------------------------------------------------------
* ⚡ Rspack ${sideDisplay} App (${envDisplay})
* --------------------------------------------------------------------------
* • [   ${side}-entry.js ] ──▶ [■ ${side}-rspack.js ] ──▶ [   ${side}-meteor.js ]
*
* This file is the bundled output generated by Rspack.
* It contains all application code and assets combined into one build.
* It is not used directly, but will be imported by the Meteor main module
* file (\`${side}-meteor.js\`) so that Meteor runs the Rspack bundle.
*
${AUTO_GENERATED_WARNING}
*/`;
  }

  // Meteor files (run or build role)
  if (role === FILE_ROLE.run || role === FILE_ROLE.build) {
    return `/**
* @file ${side}-meteor.js
* @description Meteor runtime file that imports the Rspack bundle
* --------------------------------------------------------------------------
* ☄️ Meteor ${sideDisplay} App (${envDisplay})
* --------------------------------------------------------------------------
* • [   ${side}-entry.js ] ──▶ [   ${side}-rspack.js ] ──▶ [■ ${side}-meteor.js ]
*
* This file overrides the corresponding \`meteor.mainModule.${side}\` entry in
* package.json. Meteor loads it at runtime, and it imports the Rspack
* bundle (\`${side}-rspack.js\`) so the application executes using the build
* produced by Rspack.
*
${AUTO_GENERATED_WARNING}
*/`;
  }

  return '';
}

/**
 * Gets the HMR code if applicable
 * @returns {string} The HMR code or empty string
 */
function getHmrCode(config, role) {
  if (role === FILE_ROLE.entry && config?.isClient && !config?.isTest) {
    return `/* Enables HMR */
if (module.hot) {
  module.hot.accept();
}`;
  }
  return '';
}

/**
 * Gets the import content based on configuration
 * @returns {string} The import content
 */
function getImportContent(config, side, role) {
  if (config?.entryFile && role === FILE_ROLE.entry) {
    return `/* Link to 🔌 Meteor ${capitalizeFirstLetter(side)} Entry */
import '../../${config?.entryFile}';`;
  }

  if (config?.outputFile &&
    (role === FILE_ROLE.build || config?.isProduction ||
      (role === FILE_ROLE.run &&
        (config?.isServer || config?.isTest || config?.isNative)))
  ) {
    return `/* Link to ⚡ Rspack ${capitalizeFirstLetter(side)} App */
${
  (isMeteorBlazeProject() && config?.isClient && '// In Blaze, import happens last so HTML files preload first') ||
  `import './${config?.outputFile || ''}';`
}`;
  }

  if (role === FILE_ROLE.run && config?.isServer && !config?.isTest) {
    return '/* No link to ☄️ Meteor Server App as served by HMR server */';
  }

  if (role === FILE_ROLE.run && config?.isClient && !config?.isTest) {
    return '/* No link to ⚡ Rspack Client App as served by HMR server */';
  }

  if (role === FILE_ROLE.output && config?.isClient && !config?.isTest) {
    return '/* No code generated as served by HMR server */';
  }

  if (role === FILE_ROLE.output && (config?.isServer || config?.isTest)) {
    return '/* Code generated */';
  }

  if (role === FILE_ROLE.entry && config?.isTest) {
    return '/* Tests automatically imported */';
  }

  return '';
}

/**
 * Generates build file content based on configuration parameters
 * @param {Object} config - Configuration object
 * @returns {string} The build file content
 */
export function getBuildFileContent(config) {
  // Extract configuration values
  const module = config?.isTest ? 'test' : config?.isMain ? 'main' : '';
  const side = config?.isTestModule ? 'test' : config?.isServer ? 'server' : config?.isClient ? 'client' : '';
  const env = config?.isDevelopment ? 'development' : config?.isProduction ? 'production' : '';
  const role = config?.role;

  // Get banner based on configuration
  const banner = getBanner(config, side, env, module, role);

  // Get HMR code if applicable
  const hmr = getHmrCode(config, role);

  // Get import content based on configuration
  const importContent = getImportContent(config, side, role);

  // Combine all parts to create the file content
  return `${banner}
${hmr && `
${hmr}
` || ''}
${importContent}
`;
}

/**
 * Cleans the build context files of the current environment
 * Removes all build files and directories for the current environment
 * Also cleans _build-* files from public and private folders
 * @returns {void}
 */
export function cleanBuildContextFiles() {
  const appDir = getMeteorAppDir();
  const buildContextPath = path.join(appDir, RSPACK_BUILD_CONTEXT);

  // Only proceed if the build context directory exists
  if (!fs.existsSync(buildContextPath)) {
    return;
  }

  // Get current environment
  const env = {
    ...(isMeteorAppDevelopment() ? { isDevelopment: true } : { isProduction: true }),
    isNative: isMeteorAppNative(),
  };

  try {
    // Clean main module directories
    const mainClientPath = path.dirname(path.join(buildContextPath, getBuildFilePath({ isMain: true, isClient: true, ...env })));
    const mainServerPath = path.dirname(path.join(buildContextPath, getBuildFilePath({ isMain: true, isServer: true, ...env })));

    // Clean test module directories if they exist
    const testModulePath = path.dirname(path.join(buildContextPath, getBuildFilePath({ isTest: true, isTestModule: true })));
    const testClientPath = path.dirname(path.join(buildContextPath, getBuildFilePath({ isTest: true, isClient: true })));
    const testServerPath = path.dirname(path.join(buildContextPath, getBuildFilePath({ isTest: true, isServer: true })));

    // Create a Set to ensure unique directory paths
    const uniqueDirPaths = new Set([mainClientPath, mainServerPath, testModulePath, testClientPath, testServerPath]);

    // Remove directories if they exist
    [...uniqueDirPaths].forEach(dirPath => {
      if (fs.existsSync(dirPath)) {
        fs.rmSync(dirPath, { recursive: true, force: true });
      }
    });

    // Clean _build-* files from public and private folders
    const publicDir = path.join(appDir, 'public');
    const privateDir = path.join(appDir, 'private');

    [publicDir, privateDir].forEach(dir => {
      if (fs.existsSync(dir)) {
        try {
          const files = fs.readdirSync(dir);
          files.forEach(file => {
            if ([RSPACK_ASSETS_CONTEXT, RSPACK_CHUNKS_CONTEXT, RSPACK_DOCTOR_CONTEXT].includes(file)) {
              const filePath = path.join(dir, file);
              fs.rmSync(filePath, { recursive: true, force: true });
            }
          });

          // Also remove client-rspack.js from public directory if it exists
          if (dir === publicDir) {
            const clientRspackPath = path.join(dir, 'client-rspack.js');
            if (fs.existsSync(clientRspackPath)) {
              fs.rmSync(clientRspackPath, { force: true });
            }
          }
        } catch (err) {
          logError(`Failed to clean _build-* files from ${dir}: ${err.message}`);
        }
      }
    });
  } catch (error) {
    logError(`Failed to clean build context files: ${error.message}`);
  }
}

/**
 * Ensures the rspack.config.js file exists at the project level
 * Creates the file if it doesn't exist with the required template
 * Will not create a new file if rspack.config.mjs or rspack.config.cjs exists
 * @returns {string} Path to the rspack.config file (.js, .mjs, or .cjs)
 */
export function ensureRspackConfigExists() {
  const appDir = getMeteorAppDir();

  // Check if any config file already exists using the helper function
  const existingConfigPath = getCustomConfigFilePath(appDir);
  if (existingConfigPath) {
    return existingConfigPath;
  }

  // If no config file exists, we'll create a .js one
  const jsConfigPath = path.join(appDir, 'rspack.config.js');

  const configTemplate = `const { defineConfig } = require('@meteorjs/rspack');

/**
 * Rspack configuration for Meteor projects.
 *
 * Provides typed flags on the \`Meteor\` object, such as:
 * - \`Meteor.isClient\` / \`Meteor.isServer\`
 * - \`Meteor.isDevelopment\` / \`Meteor.isProduction\`
 * - …and other flags available
 *
 * Use these flags to adjust your build settings based on environment.
 */
module.exports = defineConfig(Meteor => {
  return {};
});
`;

  if (!fs.existsSync(jsConfigPath)) {
    try {
      fs.writeFileSync(jsConfigPath, configTemplate, 'utf8');
    } catch (error) {
      logError(`Failed to create rspack.config.js file: ${error.message}`);
      throw error;
    }
  }

  return jsConfigPath;
}
