/**
 * This is the directory structure assumed for the following code
 
linkedRepoRoot
  remoteCode <---- (remoteRoot)
    metrobundles
      1
        appconfig.json
        assets
        main.jsbundle
        assetmap.json
    generated
      kflasdl12412kl239212387bfasjo128i3890230331bkjdsfasldfjaos28
        dist
        index.js
    plugins   <---- (pluginsRoot)
      plugin1 <---- (pluginFolder)
        dist
          bundle.js
        source
          widget.jsx
          component.jsx
      plugin2
      plugin3
    navigators
      navigator1
        dist
          bundle.js
        source
          index.js
      navigator2
 */

import path from "path";
import webpack from "webpack";
import archiver from "archiver";
import {writeFile, mkdir, readdir, readFile} from 'node:fs/promises';
import {createWriteStream} from 'fs';
import {exec} from 'child_process';
import {sendLog} from '../websocket';
import {toCamelCase} from '../utils'

import webpackConfig from "./webpack.config";

export async function compilePlugin(pluginFolder: string) {
  const config = {...webpackConfig}
  config.entry = path.resolve(pluginFolder, `source/widget.jsx`);
  config.output!.path = path.resolve(pluginFolder, 'dist');
  config.resolve!.modules = [path.resolve(pluginFolder, '../../../node_modules')];
  console.log("Compiling with webpack config: ", JSON.stringify(config, null, 2));

  const compiler = webpack(config);
  return new Promise((resolve, reject) => {
    compiler.run((err, stats) => {
      if (err || stats?.hasErrors()) {
        console.error(err, stats);
        if (stats) {
          const compilationStats = stats.toJson({
            errors: true,
            errorsCount: true
          });
          const result = {
            message: `compilation failed with ${compilationStats.errorsCount} errors`,
            errors: compilationStats.errors?.map(it => {
              return {
                error: `${it.message}`,
                location: `${it.moduleId} at ${it.loc}`
              }
            })
          }
          reject(result);
          sendLog(JSON.stringify(result, null, 2));
        } else {
          reject(err);
          sendLog("Could not compile for web: " + err?.message + "\n" + err?.stack);
        }
      } else {
        sendLog("compilation finished!");
        if (stats) {
          const serializedStats = stats.toJson();
          const result = `Compilation finished with ${serializedStats.errorsCount} errors and ${serializedStats.warningsCount} warnings`;
          resolve(result);
          sendLog(result);
        }
      }
    })
  });
}

export async function codegenIntegrations(appLocation: string, appIntegrations: any[]) {
  // move package.json updates here
  const remoteCode = path.resolve(appLocation, 'remoteCode');
  const pkgJsonLocation = path.resolve(appLocation, 'package.json');
  const pkgJsonRaw = await readFile(pkgJsonLocation, {encoding: "utf8"});
  const pkgJson = JSON.parse(pkgJsonRaw);
  for (let integration of appIntegrations) {
    if (pkgJson.dependencies[integration.integrationCode] !== integration.packageLocation) {
      pkgJson.dependencies[integration.integrationCode] = integration.packageLocation;
    }

    const packageName = integration.integrationCode;
    const camelCasePackageName = toCamelCase(packageName);
    try {
      await mkdir(
        path.resolve(remoteCode, 'plugins', packageName, 'source'), 
        {recursive: true}
      );
    } catch(err: any) {
      if (err?.code !== 'EEXIST') {
        console.error("Could not create directory for integration", err)
      }
    }

    const pluginsLinkingFile = path.resolve(
      remoteCode, 'plugins', packageName, 'source/widget.jsx'
    );

    await writeFile(pluginsLinkingFile,
  `import ${camelCasePackageName} from "${packageName}";
export default ${camelCasePackageName};\n`)
    // write the file
    // have a function in projectSetup to re-run the codegen for linking and run it
  }
  await writeFile(pkgJsonLocation, JSON.stringify(pkgJson, null, 2));
}

async function metroCodegenPlugins(remoteCode: string, pluginNames: string[]) {
  let contents = `export function initPlugins() {
return [
`; 
  for (let name of pluginNames) {
    const camelCasePackageName = toCamelCase(name);
    contents = `import ${camelCasePackageName} from "./plugins/${name}/source/widget";\n` + contents;
    contents += `    ${camelCasePackageName},\n`
  }
  contents = "// This file is generated. Do not edit.\n" + contents + "  ];\n}";
  const iosRemoteEntryPath = path.resolve(remoteCode, 'index.js');
  await writeFile(iosRemoteEntryPath, contents)
  sendLog(`Code generation finished for ios. Output written to ${iosRemoteEntryPath}`);
}

type CompileResult = {message: string; errors?: Array<{error: string[]; location: string;}>};
export async function compileMultiplePlugins(remoteCode: string, pluginNames: string[], compiledDirName: string): Promise<CompileResult> {
  try {
    const generatedPath = path.resolve(remoteCode, 'generated', compiledDirName);
    const generatedEntryPath = path.resolve(generatedPath, 'index.js');
    const distPath = path.resolve(generatedPath, 'dist');
    const pluginsRoot = path.resolve(remoteCode, 'plugins');
    await mkdir(distPath, { recursive: true });

    // Generate for web
    sendLog("Generating import file for plugins " + pluginNames.join(','));
    let contents = `\n\nexport default [\n`;

    for (let name of pluginNames) {
      contents = `import ${toCamelCase(name)} from "${path.resolve(pluginsRoot, name, 'source/widget')}";\n` + contents;    
      contents += `  ${toCamelCase(name)},\n`;
    }
    contents += "]";

    await writeFile(generatedEntryPath, contents)
    sendLog(`Code generation finished. Output written to ${generatedEntryPath}`);

    await metroCodegenPlugins(remoteCode, pluginNames);

    const config = {...webpackConfig}
    config.entry = generatedEntryPath;
    config.output!.path = distPath;
    config.resolve!.modules = [path.resolve(remoteCode, '../node_modules')];
    console.log("Compiling with webpack config: ", JSON.stringify(config, null, 2));

    const compiler = webpack(config);
    return new Promise((resolve, reject) => {
      compiler.run((err, stats) => {
        if (err || stats?.hasErrors()) {
          sendLog(`compilation failed: ${err?.message}`);
          if (stats) {
            const compilationStats = stats.toJson({
              errors: true,
              errorsCount: true
            });
            sendLog(`compilation failed with ${compilationStats.errorsCount} errors`);
            const result = {
              message: `compilation failed with ${compilationStats.errorsCount} errors`,
              errors: compilationStats.errors?.map(it => {
                sendLog('------------------------------------------------------------------')
                sendLog(it.message.replace(/\x1B\[[0-9;]*m/g, ''));
                sendLog(`location: \n${it.moduleId} at ${it.loc}`)
                return {
                  error: it.message.replace(/\x1B\[[0-9;]*m/g, ''),
                  location: `${it.moduleId} at ${it.loc}`
                }
              })
            };
            reject(result);
          } else {
            const result = { message: err?.toString() || "error is null" };
            reject(result);
            sendLog(JSON.stringify(result, null, 2));
          }
        } else {
          console.log("compilation finished! ", stats?.compilation.assets);
          if (stats) {
            const serializedStats = stats.toJson();
            const result = {
              message: `Compilation finished with ${serializedStats.errorsCount} errors and ${serializedStats.warningsCount} warnings`
            };
            resolve(result);
            sendLog(JSON.stringify(result, null, 2));
          } else {
            resolve({
              message: `Compilation completed but no stats were generated`
            });
            sendLog(`Compilation completed but no stats were generated`);
          }
        }
      })
    });
  } catch (err: any) {
    sendLog(`compilation failed due to ${err.toString()}`);
    return Promise.reject({
      message: `compilation failed due to ${err.toString()}`
    });
  }
}

export async function compileNavCreator(appid: string, name: string) {
  const config = {...webpackConfig}
  config.entry = path.resolve(__dirname, 
                              `../workspace/${appid}/navigators/${name}/index.jsx`);
  config.output!.path = path.resolve(__dirname,
                            `../workspace/${appid}/navigators/${name}/dist`);

  const compiler = webpack(config);
  return new Promise((resolve, reject) => {
    compiler.run((err, stats) => {
      if (err || stats?.hasErrors()) {
        console.error(err, stats);
        if (stats) {
          const compilationStats = stats.toJson({
            errors: true,
            errorsCount: true
          });
          reject({
            message: `compilation failed with ${compilationStats.errorsCount} errors`,
            errors: compilationStats.errors?.map(it => {
              return {
                error: `${it.message}`,
                location: `${it.moduleId} at ${it.loc}`
              }
            })
          });
        } else {
          reject(err);
        }
      } else {
        console.log("compilation finished!");
        if (stats) {
          const serializedStats = stats.toJson();
          resolve(`Compilation finished with ${serializedStats.errorsCount} errors and ${serializedStats.warningsCount} warnings`);
        }
      }
    })
  });

}

async function metroCodegenNavs(remoteCode: string, navNames: string[]) {
  const navsRoot = path.resolve(remoteCode, 'navigators');
  // Generate for phone
  let contents = `import {registerCreator} from 'apptile-core';
export const navs = [
`;
  for (let name of navNames) {
    const camelCasePackageName = toCamelCase(name);
    contents = `import ${camelCasePackageName} from "${path.resolve(navsRoot, name, `source/index`)}";\n` + contents;
    contents += `  {creator: ${camelCasePackageName}, name: "${name}"},\n`;
  }
  contents += `];\n
export function initNavs() {
  for (let nav of navs) {
    registerCreator(nav.name, nav.creator);
  }
}
  `;
  contents = `// This file is generated. Do not edit.\n` + contents;
  const iosRemoteNavEntry = path.resolve(remoteCode, 'indexNav.js');
  await writeFile(iosRemoteNavEntry, contents);
  sendLog(`Codegeneration finished for mobile navigators\n`);
}

export async function compileMultipleNavCreators(repoPath: string, navNames: string[], compiledDirName: string): Promise<CompileResult> {
  const remoteCode = path.resolve(repoPath, 'remoteCode');
  try {
    const generatedPath = path.resolve(remoteCode, 'generated', compiledDirName)
    const generatedEntryPath = path.resolve(generatedPath, 'index.js');
    const distPath = path.resolve(generatedPath, 'dist');
    const navsRoot = path.resolve(remoteCode, 'navigators');
    await mkdir(distPath, { recursive: true });

    // Generate for web
    sendLog("Generating imports file for navigators" + navNames.join(','));
    let contents = `\n\nexport default [\n`;

    for (let name of navNames) {
      contents = `import ${name} from "${path.resolve(navsRoot, name, 'source')}";\n` + contents;    
      contents += `  { creator: ${name}, name: "${name}" },\n`;
    }
    contents += "]";

    await writeFile(generatedEntryPath, contents)
    sendLog(`Code generation finished. Output written to ${generatedEntryPath}`);

    metroCodegenNavs(remoteCode, navNames);

    const config = {...webpackConfig}
    config.entry = generatedEntryPath;
    config.output!.path = distPath;
    config.resolve!.modules = [path.resolve(remoteCode, '../node_modules')];
    console.log("Compiling with webpack config: ", JSON.stringify(config, null, 2));

    const compiler = webpack(config);
    return new Promise((resolve, reject) => {
      compiler.run((err, stats) => {
        if (err || stats?.hasErrors()) {
          console.error(err, stats);
          if (stats) {
            const compilationStats = stats.toJson({
              errors: true,
              errorsCount: true
            });
            reject({
              message: `compilation failed with ${compilationStats.errorsCount} errors`,
              errors: compilationStats.errors?.map(it => {
                return {
                  error: `${it.message}`,
                  location: `${it.moduleId} at ${it.loc}`
                }
              })
            });
          } else {
            reject({ message: err?.toString() || "error is null" });
          }
        } else {
          console.log("compilation finished!");
          if (stats) {
            const serializedStats = stats.toJson();
            resolve({
              message: `Compilation finished with ${serializedStats.errorsCount} errors and ${serializedStats.warningsCount} warnings`
            });
          } else {
            resolve({
              message: `Compilation completed but no stats were generated`
            });
          }
        }
      })
    });
  } catch (err: any) {
    return Promise.reject({
      message: `compilation failed due to ${err.toString()}`
    })    
  }
}

export async function generatAndroidBundle(appRoot: string) {
  const remoteCode = path.resolve(appRoot, 'remoteCode');
  // get list of plugins and navs
  const pluginsDir = path.resolve(remoteCode, 'plugins');
  const pluginEntries = await readdir(pluginsDir, {withFileTypes: true});
  const plugins = pluginEntries.filter(it => it.isDirectory()).map(it => it.name);
  const navDir = path.resolve(remoteCode, 'navigators');
  const navEntries = await readdir(navDir, {withFileTypes: true});
  const navs = navEntries.filter(it => it.isDirectory()).map(it => it.name);

  // do metro codegen for plugins and navs
  await metroCodegenPlugins(remoteCode, plugins);
  await metroCodegenNavs(remoteCode, navs);

  // execute metro bundler and send logs
  const command = "node_modules/.bin/react-native bundle --entry-file ./index.js --platform android --dev false --minify true --bundle-output ./android/index.android.bundle --assets-dest ./android/bundleassets";
  sendLog("Start bundling for phone");
  await new Promise((resolve, reject) => {
    exec(command, 
      {
        cwd: path.resolve(appRoot)
      }, 
      (err, stdout, stderr) => {
        if (err) {
          sendLog("Failed to generate build! " + err.message);
          console.error(err);
          reject(err);
        } else {
          sendLog(stdout);
          sendLog(stderr);
          resolve({});
        }
      }
    )
  });

  // zip assets
  const timestamp = Date.now();
  const generatedPath = path.resolve(remoteCode, `generated/bundles/android/${timestamp}`);
  await mkdir(generatedPath, {recursive: true});

  // put built files into generated folder so that it can be served through cli
  // await cp(
  //   path.resolve(appRoot, 'ios/main.jsbundle'), 
  //   path.resolve(generatedPath, 'main.jsbundle')
  // );

  await new Promise((resolve, reject) => {
    const bundleDestination = path.resolve(generatedPath, 'bundle.zip')  
    const writeStream = createWriteStream(bundleDestination);
    writeStream.on('close', () => {
      sendLog(`zipping finished. Bundle is at ${bundleDestination}`);
      resolve({})
    });
    writeStream.on('error', (err) => {
      sendLog('Zipping failed ' + err.message)
      reject(err)
    });
    const archive = archiver('zip', {zlib: {level: 9}});
    archive.on('warning', wrn => {
      sendLog('archiver warning: ' + wrn.message);
      console.warn('archiver warning: ', wrn);
    });

    archive.on('error', err => {
      if (err) {
        sendLog('archiver error: ' + err.message);
        console.error("Failure in archiver ", err);
      }
    });
    archive.pipe(writeStream);
      
    archive.file(path.resolve(appRoot, 'android/index.android.bundle'), {name: 'index.android.bundle'});
    archive.directory(path.resolve(appRoot, 'android/bundleassets'), false);
    archive.finalize();
  })
}

export async function generatIOSBundle(appRoot: string) {
  const remoteCode = path.resolve(appRoot, 'remoteCode');
  // get list of plugins and navs
  const pluginsDir = path.resolve(remoteCode, 'plugins');
  const pluginEntries = await readdir(pluginsDir, {withFileTypes: true});
  const plugins = pluginEntries.filter(it => it.isDirectory()).map(it => it.name);
  const navDir = path.resolve(remoteCode, 'navigators');
  const navEntries = await readdir(navDir, {withFileTypes: true});
  const navs = navEntries.filter(it => it.isDirectory()).map(it => it.name);

  // do metro codegen for plugins and navs
  await metroCodegenPlugins(remoteCode, plugins);
  await metroCodegenNavs(remoteCode, navs);

  // execute metro bundler and send logs
  const command = "node_modules/.bin/react-native bundle --entry-file ./index.js --platform ios --dev false --minify true --bundle-output ./ios/main.jsbundle --assets-dest ./ios/bundleassets";
  sendLog("Start bundling for phone");
  await new Promise((resolve, reject) => {
    exec(command, 
      {
        cwd: path.resolve(appRoot)
      }, 
      (err, stdout, stderr) => {
        if (err) {
          sendLog("Failed to generate build! " + err.message);
          console.error(err);
          reject(err);
        } else {
          sendLog(stdout);
          sendLog(stderr);
          resolve({});
        }
      }
    )
  });

  // zip assets
  const timestamp = Date.now();
  const generatedPath = path.resolve(remoteCode, `generated/bundles/ios/${timestamp}`);
  await mkdir(generatedPath, {recursive: true});

  // put built files into generated folder so that it can be served through cli
  // await cp(
  //   path.resolve(appRoot, 'ios/main.jsbundle'), 
  //   path.resolve(generatedPath, 'main.jsbundle')
  // );

  await new Promise((resolve, reject) => {
    const bundleDestination = path.resolve(generatedPath, 'bundle.zip')  
    const writeStream = createWriteStream(bundleDestination);
    writeStream.on('close', () => {
      sendLog(`zipping finished. Bundle is at ${bundleDestination}`);
      resolve({})
    });
    writeStream.on('error', (err) => {
      sendLog('Zipping failed ' + err.message)
      reject(err)
    });
    const archive = archiver('zip', {zlib: {level: 9}});
    archive.on('warning', wrn => {
      sendLog('archiver warning: ' + wrn.message);
      console.warn('archiver warning: ', wrn);
    });

    archive.on('error', err => {
      if (err) {
        sendLog('archiver error: ' + err.message);
        console.error("Failure in archiver ", err);
      }
    });
    archive.pipe(writeStream);
      
    archive.file(path.resolve(appRoot, 'ios/main.jsbundle'), {name: 'main.jsbundle'});
    archive.directory(path.resolve(appRoot, 'ios/bundleassets'), false);
    archive.finalize();
  })
}

export async function generateAndroidBundle(appRoot: string) {
  const remoteCode = path.resolve(appRoot, 'remoteCode');
  // get list of plugins and navs
  const pluginsDir = path.resolve(remoteCode, 'plugins');
  const pluginEntries = await readdir(pluginsDir, {withFileTypes: true});
  const plugins = pluginEntries.filter(it => it.isDirectory()).map(it => it.name);
  const navDir = path.resolve(remoteCode, 'navigators');
  const navEntries = await readdir(navDir, {withFileTypes: true});
  const navs = navEntries.filter(it => it.isDirectory()).map(it => it.name);

  // do metro codegen for plugins and navs
  await metroCodegenPlugins(remoteCode, plugins);
  await metroCodegenNavs(remoteCode, navs);

  // execute metro bundler and send logs
  const command = "node_modules/.bin/react-native bundle --entry-file ./index.js --platform android --dev false --minify true --bundle-output ./android/app/src/main/assets/index.android.bundle --assets-dest ./android/app/src/main/assets/assets";
  sendLog("Start bundling for phone");
  await new Promise((resolve, reject) => {
    exec(command, 
      {
        cwd: path.resolve(appRoot)
      }, 
      (err, stdout, stderr) => {
        if (err) {
          sendLog("Failed to generate build! " + err.message);
          console.error(err);
          reject(err);
        } else {
          sendLog(stdout);
          sendLog(stderr);
          resolve({});
        }
      }
    )
  });

  // zip assets
  const timestamp = Date.now();
  const generatedPath = path.resolve(remoteCode, `generated/bundles/android/${timestamp}`);
  await mkdir(generatedPath, {recursive: true});

  // put built files into generated folder so that it can be served through cli
  // await cp(
  //   path.resolve(appRoot, 'ios/main.jsbundle'), 
  //   path.resolve(generatedPath, 'main.jsbundle')
  // );

  await new Promise((resolve, reject) => {
    const bundleDestination = path.resolve(generatedPath, 'bundle.zip')  
    const writeStream = createWriteStream(bundleDestination);
    writeStream.on('close', () => {
      sendLog(`zipping finished. Bundle is at ${bundleDestination}`);
      resolve({})
    });
    writeStream.on('error', (err) => {
      sendLog('Zipping failed ' + err.message)
      reject(err)
    });
    const archive = archiver('zip', {zlib: {level: 9}});
    archive.on('warning', wrn => {
      sendLog('archiver warning: ' + wrn.message);
      console.warn('archiver warning: ', wrn);
    });

    archive.on('error', err => {
      if (err) {
        sendLog('archiver error: ' + err.message);
        console.error("Failure in archiver ", err);
      }
    });
    archive.pipe(writeStream);
      
    archive.file(path.resolve(appRoot, 'android/app/src/main/assets/index.android.bundle'), {name: 'index.android.bundle'});
    archive.directory(path.resolve(appRoot, 'android/app/src/main/assets/assets'), false);
    archive.finalize();
  })
}
