import express, {Request} from "express";

import {
  killMetro, 
  buildApp,
  narrowOSName,
  narrowBuildMode,
  getGitShas,
  getMobileBundle,
  npmInstall,
  runAndroid,
  runIOS
} from '../controllers/projectSetup';
import bodyParser from 'body-parser'
import {
  currentPluginsBundle, 
  currentNavigatorsBundle,
  listDevices
} from '../controllers/projectSetup';
import {createReadStream} from 'fs';
import path from 'path';
import {readdir, writeFile, readFile, mkdir} from 'node:fs/promises';
import FormData from 'form-data';
import axios from 'axios';
import { sendLog } from '../websocket';
import {generatIOSBundle, generatAndroidBundle} from '../controllers/compiler'
import {toCamelCase} from '../utils';
import { getOpenApp } from "../database/cliconfig";
import { getAppConfigsFromFS } from "../controllers/workspace";

export const jsonParser = bodyParser.json({limit: '50mb'});
const router = express.Router();

router.get(`/devices`, async (req, res, next) => {
  try {
    const devices = await listDevices();
    res.json(devices);
  } catch (err) {
    console.error("Failed to get devices");
    next(err);
  }
});

router.post(`/:appid/bundles/:bundleid`, async (req, res, next) => {
  const appId = req.params.appid;
  const bundleId = req.params.bundleid;
  try {
    const apptileConfig = await getAppConfigsFromFS(appId);
    await axios.put(`${apptileConfig.APPTILE_BACKEND_URL}/api/v2/app/${appId}/deploy/${bundleId}`);
    res.status(200);
    res.end();
  } catch (err) {
    console.error("Failed to publish", err);
    next(err);
  }
});

router.post(`/:appId/runAndroid/:deviceId`, async (req, res, next) => {
  const appId = req.params.appId;
  const deviceId = req.params.deviceId;
  try {
    runAndroid(appId, deviceId);
    res.status(201);
    res.end();
  } catch (err) {
    next(err);
  }
});

router.post(`/:appId/runIOS/:deviceId`, async (req, res, next) => {
  const appId = req.params.appId;
  const deviceId = req.params.deviceId;
  try {
    runIOS(appId, deviceId);
    res.status(201);
    res.end();
  } catch (err) {
    next(err);
  }
});

router.get(`/:appid/bundles`, async (req, res) => {
  const appId = req.params.appid;
  const openedApp = getOpenApp(appId);
  try {
    const apptileConfig = await getAppConfigsFromFS(appId);
    const {data: manifest} = await axios.get(`${apptileConfig.APPTILE_BACKEND_URL}/api/v2/app/${appId}/manifest`);
    const {data: bundles} = await axios.get(`${apptileConfig.APPTILE_BACKEND_URL}/api/v2/app/${appId}/bundles`);
    let iosBundles: Array<{name: string; date: string; path: string;}> = [];
    try {
      if (openedApp) {
        const entries = await readdir(
          path.resolve(openedApp.repoPath, 'remoteCode/generated/bundles/ios'),
          {withFileTypes: true}
        );
        const formatter = Intl.DateTimeFormat('en-US', {timeStyle: 'short', dateStyle: 'full'});
        for (let entry of entries) {
          if (entry.isDirectory()) {
            try {
              const name = new Date(parseInt(entry.name));
              iosBundles.push({
                name: entry.name,
                date: formatter.format(name),
                path: path.resolve(openedApp.repoPath, 'remoteCode/generated/bundles/ios', entry.name)
              });
            } catch (err) {}
          }
        }
      }
    } catch(err: any) {
      sendLog("Error in trying to get local ios bundles: " + err?.message)
    }
    if (openedApp) {
      res.json({openedApp, manifest, bundles, iosBundles});
    } else {
      res.status(404);
      res.end();
    }
  } catch (err: any) {
    res.render('error', {message: err?.message});
  }
});

router.post('/:appid/build/:os/:mode', (req: Request<{appid: string; os: string; mode: string;}, any, any, {deviceId?: string}>, res) => {
  const appId = req.params.appid;
  const os = req.params.os;
  const mode = req.params.mode;
  if (narrowOSName(os) && narrowBuildMode(mode)) {
    const deviceId = req.query.deviceId;
    buildApp(appId, os, mode, deviceId);
    res.status(201);
    res.end();
  } else {
    console.error("Wrong os name or build mode");
    res.status(400);
    res.end();
  }
});

router.delete('/killmetro', (req, res) => {
  killMetro();
  res.status(200);
  res.end();
});

router.get('/close-install-popup', (req, res) => {
  res.render('installPopup.pug', {visible: false, valid: false});
});

router.post("/:appid/uploadMobileBundle/:bundleName/:os", async (req, res, next) => {
  try {
    const appId = req.params.appid;
    const bundleName = req.params.bundleName;
    const os = req.params.os;
    const apptileConfig = await getAppConfigsFromFS(appId);
    if (narrowOSName(os)) {
      const {err: bundleErr, assetZip} = await getMobileBundle(appId, bundleName, os);
      const {err, gitsha, sdksha} = await getGitShas(appId);
      if (err || bundleErr) {
        console.error("Failed to retrieve bundle", err, bundleErr);
        res.status(500);
        res.end();
      } else if (assetZip) {
        let formData = new FormData();
        formData.append('assetZipFile', createReadStream(assetZip));
        formData.append('uploadDestination', os === 'ios'? 'ios-jsbundle':'android-jsbundle');
        formData.append('gitsha', gitsha);
        formData.append('sdksha', sdksha);
        formData.append('tag', 'sometag');
        let res = await axios.post(`${apptileConfig.APPTILE_BACKEND_URL}/api/v2/app/${appId}/upload`,
                                      formData,
        {
          headers: {
            ...formData.getHeaders()
          }
        });
        if (res.status >= 400) {
          try {
            const erroMessage = res.data
            throw new Error("Failed to upload " + erroMessage);
          } catch(err) {
            console.error("Unprocessable error: ", err);
            throw err;
          }
        }
      } else {
        console.error("Link not found");
        next(new Error("Link not found when uploading mobile bundle"))
      }
    } else {
      next(new Error("Cannot upload bundle for os name: " + os))
    }
    res.status(200);
    res.end();
  } catch (err) {
    console.error("in uploadMobileBundle", err);
    next(err);
  }
});

router.post("/:appid/refreshIntegrations", jsonParser, async (req, res, next) => {
  // TODO(gaurav): this logic should be in a controller and the table 
  // should have new columns for all the columns that are being misused here
  const appId = req.params.appid;
  const openedApp = getOpenApp(appId);
  if (openedApp) {
    const integrations = req.body.integrations;
    sendLog("Refreshing integrations");
    try {
      // Check if a sdk integration has platformtype of apptilesdk
      let sdkIntegrations = [];
      for (let i = 0; i < integrations.length; ++i) {
        const integration = integrations[i];
        if (integration.packageLocation) {
          sdkIntegrations.push(integration);
        }
      }

      const packageJsonNewEntries: Record<string, string> = {};
      // Codegen the relevant files
      for (let i = 0; i < sdkIntegrations.length; ++i) {
        const packageName = sdkIntegrations[i].integrationCode;
        const camelCasePackageName = toCamelCase(packageName);
        const gitRepo = sdkIntegrations[i].packageLocation;
        sendLog(`Integration: ${packageName}: ${gitRepo}`);
        packageJsonNewEntries[packageName] = gitRepo;
        await mkdir(path.resolve(openedApp.repoPath, 'remoteCode/plugins', packageName, 'source'), {recursive: true});
        const pluginsLinkingFile = path.resolve(openedApp.repoPath, 'remoteCode/plugins', packageName, 'source/widget.jsx');
        await writeFile(pluginsLinkingFile, 
`import ${camelCasePackageName} from "${packageName}";
export default ${camelCasePackageName};\n`)
      }
      
      const existingPackageJSON = await readFile(
        path.resolve(openedApp.repoPath, 'package.json'),
        {encoding: 'utf8'}
      );

      const parsedPackageJSON = JSON.parse(existingPackageJSON);
      let pkgJSONUpdated = false;
      for (let pkg in packageJsonNewEntries) {
        if (parsedPackageJSON.dependencies[pkg] != packageJsonNewEntries[pkg]) {
          parsedPackageJSON.dependencies[pkg] = packageJsonNewEntries[pkg]; 
          pkgJSONUpdated = true;
        }
      }

      if (pkgJSONUpdated) {
        // write updated package.json
        const updatedPkgJson = JSON.stringify(parsedPackageJSON, null, 2);
        sendLog(`Updated package.json\n${updatedPkgJson}`);

        await writeFile(
          path.resolve(openedApp.repoPath, 'package.json'),
          updatedPkgJson
        );

        // run npm install
        await npmInstall(openedApp.repoPath)
      } else {
        sendLog("No updates required to package.json");
      }
      res.status(201);
      res.end();
    } catch (err: any) {
      sendLog("Failed while refreshing integrations " + err?.message);
      next(err);
    }
  } else {
    res.status(404);
    res.end();
  }
});

router.post("/:appid/uploadWebBundle", async (req, res, next) => {
  try {
    const appid = req.params.appid;
    const apptileConfig = await getAppConfigsFromFS(appid);
    const {err: pluginErr, res: pluginsBundle} = await currentPluginsBundle(appid);
    const {err: navErr, res: navigatorsBundle} = await currentNavigatorsBundle(appid);
    if (pluginErr || navErr) {
      console.error(pluginErr, navErr);
      res.render('error', {message: 'Failed to get plugin or navigator bundle'});
    } else {
      const {err, gitsha, sdksha} = await getGitShas(appid);
      if (err) {
        res.render('error', {message: 'Failed to get git shas'});
        return;
      } else if (gitsha && sdksha) {
        // Upload plugins bundle
        let formData = new FormData();
        formData.append('file', createReadStream(pluginsBundle));
        formData.append('uploadDestination', 'plugins');
        formData.append('gitsha', gitsha);
        formData.append('sdksha', sdksha);
        formData.append('tag', 'sometag');
        let res = await axios.post(
          `${apptileConfig.APPTILE_BACKEND_URL}/api/v2/app/${appid}/upload`,
          formData,
          {
            headers: {
              ...formData.getHeaders()
            }
          }
        );

        if (res.status >= 400) {
          try {
            const erroMessage = res.data
            throw new Error("Failed to upload " + erroMessage);
          } catch(err) {
            console.error("Unprocessable error: ", err);
            throw err;
          }
        }

        // Upload navigator bundle
        formData = new FormData();
        formData.append('file', createReadStream(navigatorsBundle));
        formData.append('uploadDestination', 'navigators');
        formData.append('gitsha', gitsha);
        formData.append('sdksha', sdksha);
        formData.append('tag', 'sometag');
        res = await axios.post(`${apptileConfig.APPTILE_BACKEND_URL}/api/v2/app/${appid}/upload`,
                                      formData,
        {
          headers: {
            ...formData.getHeaders()
          }
        });
        if (res.status >= 400) {
          try {
            const erroMessage = res.data;
            throw new Error("Failed to upload " + erroMessage);
          } catch(err) {
            console.error("Unprocessable error: ", err);
            throw err;
          }
        }
      } else {
        throw new Error("Invalid code path");
      }
      res.status(200);
      res.end();
      // render the bundles maybe
    }
  } catch (err) {
    console.error("in ubloadWebBundles", err);
    next(err);
  }
});

router.post("/:appid/bundleJsIOS", async (req, res, next) => {
  const appId = req.params.appid;
  const openedApp = getOpenApp(appId);
  if (openedApp) {
    try {
      await generatIOSBundle(openedApp.repoPath);
    } catch(err) {
      next(err);
    }
  } else {
    res.status(404);
    res.end();
  }
});

router.post("/:appid/bundleJsAndroid", async (req, res, next) => {
  const appId = req.params.appid;
  const openedApp = getOpenApp(appId);
  if (openedApp) {
    try {
      await generatAndroidBundle(openedApp.repoPath);
    } catch(err) {
      next(err);
    }
  } else {
    res.status(404);
    res.end();
  }
});


type FileTreeNode = {
  name: string;
  fullPath: string;
  type: 'directory'|'file'|'unknown';
  contents: FileTreeNode[];
};
async function buildFileTree(name: string, fullPath: string, depth: number): Promise<FileTreeNode> {
  try {
    const entries = await readdir(fullPath, {withFileTypes: true});
    const contents: FileTreeNode[] = [];
    for (let i = 0; i < entries.length; ++i) {
      const entry = entries[i];
      
      if (!['.DS_Store', 'idb-applications'].includes(entry.name)) {
        if (entry.isDirectory()) {
          if (depth > 1) {
            const subtree = await buildFileTree(entry.name, path.resolve(fullPath, entry.name ), depth - 1);
            contents.push(subtree);
          } else {
              contents.push({
                name: entry.name,
                fullPath: path.resolve(fullPath, entry.name),
                type: 'directory',
                contents: []
              });
          }
        } else {
          contents.push({
            name: entry.name,
            fullPath,
            type: 'file',
            contents: []
          });
        }
      }
    }

    return {
      name,
      fullPath,
      type: 'directory',
      contents
    };
  } catch (err: any) {
    if (err.code === 'ENOTDIR') {
      return {
        name,
        fullPath,
        type: 'file',
        contents: []
      };
    } else {
      console.error("Failed to build tree rooted at: ", err);
      return {
        name,
        fullPath,
        type: 'unknown',
        contents: []
      };
    }
  }
}

router.get("/explore/:path/:depth", async (req, res, next) => {
  try {
    const depth = parseInt(decodeURIComponent(req.params.depth));
    const pathToExplore = decodeURIComponent(req.params.path);
    const rootPath = path.resolve(pathToExplore);
    const tree = await buildFileTree(pathToExplore, rootPath, depth);
    res.json(tree)
  } catch (err) {
    console.error("Failed to explore: ", err);
    next(err);
  }
});

export default router;
