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 {uploadMobileBundle} from '../controllers/bundleUpload'
import {toCamelCase, makeHeadersWithCookie} from '../utils';
import { getImagesFromPlanner, getOpenApp } from "../database/cliconfig";
import { getAppConfigsFromFS } from "../controllers/workspace";
import { AppPlannerAgent } from "../controllers/agents/appPlannerAgent";
import { LLMProviders, narrowAmazonModel, narrowAnthropicModel, narrowGoogleModel, narrowLLMProvider, narrowOpenAIModel } from "../controllers/agents/baseAgent";
import { tools } from "../controllers/agents/mcp-servers/tools";
import { logger } from "../utils/logger";
import chalk from "chalk";
import { recordTokenUsage } from "../services/apptileServerTokenManagement";
import { getMidUrlAppValue } from "../utils";
import { supabaseConfigManager } from "../controllers/agents/mcp-servers/supabase";
import { ToolSpec } from "../controllers/agents/mcp-servers/utils";
// export const getMidUrlAppValue = () => {
//   return process.env.NODE_ENV === 'production'? 'admin/api/v2/apps' : 'api/v2/app';
// }

// const plannerAgent = new AppPlannerAgent();

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

router.get('/test', async (req, res) => {
  const appId = "07a6b5be-5f8a-4273-ada1-20dceb9fa21a";
  let creds: any = "";
  // const creds = await supabaseConfigManager.getCreds(appId);
  // let tool = tools.projectCodeWrite[1];
  // let tool = tools.projectCodeReadForPlanner[0];
  let tool: ToolSpec<any> | null = null;
  $outer: for (let key of Object.keys(tools)) {
    // @ts-ignore
    const toolGroup = tools[key] as Array<ToolSpec<any>>;
    for (let t of toolGroup)
    if (t?.name === "run_curl") {
      tool = t;
      break $outer;
    }
  }

  if (!tool?.name) {
    res.status(200);
    res.end("No tool found!");
  } else {
    if (tool.name.startsWith("supabase_")) {
      creds = await supabaseConfigManager.getCreds(appId);
    }

    try {
      const result = await tool.cb({
        // root_path: "source",
        // sql_query: "UPDATE todos SET title = 'Updated title', description = 'Updated description', is_completed = true, updated_at = NOW() WHERE id = '00000000-0000-0000-0000-000000000000';",
        curl_string: "curl --location 'https://api.themoviedb.org/3/discover/tv?include_adult=false&include_null_first_air_dates=false&language=en-US&page=1&sort_by=popularity.desc' --header 'accept: application/json' --header 'authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiI1ODg0YzAxZDdhYjVhZWQ0MzhlMzllMWUzMWJiOGMxNCIsIm5iZiI6MTc0ODM1OTMwMS4wMTUsInN1YiI6IjY4MzVkODg1ZWI0ZDZhOGNmMzAzN2IzNSIsInNjb3BlcyI6WyJhcGlfcmVhZCJdLCJ2ZXJzaW9uIjoxfQ.1xyec4MZjgRtI_uG6DmqVH2JQdOntZZz1j7oxTVk1jU'",
        url: "https://api.themoviedb.org/3/discover/tv?include_adult=true&include_video=true&language=en-US&page=1&sort_by=popularity.desc",
        method: "GET",
        headers: {
          "accept": "application/json",
          "origin": "https://developer.themoviedb.org",
          "referer": "https://developer.themoviedb.org/",
          "Authorization": "Bearer eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiI1ODg0YzAxZDdhYjVhZWQ0MzhlMzllMWUzMWJiOGMxNCIsIm5iZiI6MTc0ODM1OTMwMS4wMTUsInN1YiI6IjY4MzVkODg1ZWI0ZDZhOGNmMzAzN2IzNSIsInNjb3BlcyI6WyJhcGlfcmVhZCJdLCJ2ZXJzaW9uIjoxfQ.1xyec4MZjgRtI_uG6DmqVH2JQdOntZZz1j7oxTVk1jU"
        },
        body: "",
        file_path: "source/component.jsx",
        line_ranges: [{first_line: 1, last_line: 20}],
        apptile_context: {
          app_id: appId,
          plugin_root: "/Users/gauravgautam/apptile-cli-home/singlepromptapp-testbed/remoteCode/plugins/todolist",
          plugins_dir: "/Users/gauravgautam/apptile-cli-home/singlepromptapp-testbed/remoteCode/plugins",
          supabase_creds: creds
        }
      });
      
      res.json(result);
    } catch (err) {
      res.status(500);
      res.end();
      console.error("Error when running the tool", err)
    }
  }
});

router.get(
  "/:appid/plannerimages",
  async (req, res, next) => {
    try {
      const appId = req.params.appid;
      const imageContents = getImagesFromPlanner(appId);
      res.json(imageContents);
    } catch (err) {
      next(err);
    }
  }
)

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;
  const app = await getOpenApp(appId);
  
  if (app) {
    try {
      const header = makeHeadersWithCookie(app.apptilecookie, {});
      const apptileConfig = await getAppConfigsFromFS(appId);
      await axios.put(
        `${apptileConfig.APPTILE_BACKEND_URL}/${getMidUrlAppValue()}/${appId}/deploy/${bundleId}`, 
        {},
        header
      );
      res.status(200);
      res.end();
    } catch (err) {
      console.error("Failed to publish", err);
      next(err);
    }
  } else {
    console.error("Opened app doesn't exist or doesn't have apptilecookie. Update database!");
    next(new Error("No app or cookie found during publishing bundle"));
  }
});

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, next) => {
  const appId = req.params.appid;
  const openedApp = await getOpenApp(appId);
  let headers = {};
  if (openedApp) {
    headers = {headers: makeHeadersWithCookie(openedApp.apptilecookie, {})};
  } else {
    console.error("No apptilecookie found 1");
    next(new Error("No apptile cookie"));
    return;
  }
  try {
    const apptileConfig = await getAppConfigsFromFS(appId);
    const {data: manifest} = await axios.get(`${apptileConfig.APPTILE_BACKEND_URL}/${getMidUrlAppValue()}/${appId}/manifest`, headers);
    const {data: bundles} = await axios.get(`${apptileConfig.APPTILE_BACKEND_URL}/${getMidUrlAppValue()}/${appId}/bundles`, headers);
    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, next) => {
  const appId = req.params.appid;
  const os = req.params.os;
  const mode = req.params.mode;
  try {
    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();
    }
  } catch (err) {
    console.error("Build failed");
    next(err);
  }
});

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 openedApp = await getOpenApp(appId);
    
    if (!openedApp) {
      console.error("No apptilecookie found");
      res.status(404);
      res.end();
      return;
    }
    
    if (narrowOSName(os)) {
      try {
        await uploadMobileBundle(appId, bundleName, os, openedApp.apptilecookie);
        res.status(200);
        res.end();
      } catch (error: any) {
        // Check if this was a bundle retrieval error, in which case we want to match original behavior
        if (error && error.message && typeof error.message === 'string' && error.message.includes('Failed to retrieve bundle')) {
          console.error("Failed to retrieve bundle:", error);
          res.status(500);
          res.end();
          return;
        }
        console.error("Error uploading mobile bundle:", error);
        next(error);
      }
    } else {
      next(new Error("Cannot upload bundle for os name: " + os))
    }
  } catch (err) {
    console.error("in uploadMobileBundle", err);
    next(err);
  }
});

router.get("/:appid/commits", async (req, res, next) => {
  const appId = req.params.appid;
  const openedApp = await getOpenApp(appId);
  let headers = {};
  if (openedApp) {
    headers = makeHeadersWithCookie(openedApp.apptilecookie, {});
  } else {
    console.error("openedApp: ", openedApp);
    next(new Error("No apptilecookie found 3"));
    return;
  }
  try {
    const apptileConfig = await getAppConfigsFromFS(appId);
    const commits = await axios.get(
      `${apptileConfig.APPTILE_BACKEND_URL}/${getMidUrlAppValue()}/${appId}/commits`,
      headers);
    res.json(commits.data);
  } catch (err) {
    next(err);
  }
});

router.post("/:appid/pushLogs", jsonParser, async (req, res, next) => {
  const appId = req.params.appid;
  const payload = req.body;
  const openedApp = await getOpenApp(appId);
  let headers = {};
  if (openedApp) {
    headers = makeHeadersWithCookie(openedApp.apptilecookie, {});
  } else {
    next(new Error("No apptilecookie found 4"));
    return;
  }
  const apptileConfig = await getAppConfigsFromFS(appId);
  try {
    // ["appId", "androidBundleId", "iosBundleId", "pluginsBundleId", "navigatorsBundleId", "publishedCommitId", "comment"]
    await axios.post(
      `${apptileConfig.APPTILE_BACKEND_URL}/${getMidUrlAppValue()}/${appId}/pushLogs`, 
        {
        appId,
        androidBundleId: parseInt(payload.androidBundleId),
        iosBundleId: parseInt(payload.iosBundleId),
        pluginsBundleId: parseInt(payload.pluginsBundleId),
        navigatorsBundleId: parseInt(payload.navigatorsBundleId),
        publishedCommitId: parseInt(payload.publishedCommitId),
        comment: payload.comment
      },
      headers
    );
    res.json({});
  } catch (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 = await 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 openedApp = await getOpenApp(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 headers = {};
        if (openedApp) {
          headers = makeHeadersWithCookie(openedApp.apptilecookie, formData.getHeaders());
        } else {
          next(new Error("Failed to get apptilecookie"));
          return;
        }

        let res = await axios.post(
          `${apptileConfig.APPTILE_BACKEND_URL}/${getMidUrlAppValue()}/${appid}/upload`,
          formData,
          headers
        );

        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');
        headers = makeHeadersWithCookie(openedApp.apptilecookie, formData.getHeaders());
        res = await axios.post(
          `${apptileConfig.APPTILE_BACKEND_URL}/${getMidUrlAppValue()}/${appid}/upload`,
          formData,
          headers);
        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 = await getOpenApp(appId);
  if (openedApp) {
    try {
      await generatIOSBundle(openedApp.repoPath);
      res.status(200).json({
        message: "iOS Bundle generation complete!",
      });
    } 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 = await getOpenApp(appId);
  if (openedApp) {
    try {
      await generatAndroidBundle(openedApp.repoPath);
      res.send(200).json({
        message: "Android Bundle generation complete!",
      });
    } catch(err) {
      next(err);
    }
  } else {
    res.status(404);
    res.end();
  }
});

router.post(
  "/:appid/generatePreviewDraft",
  jsonParser,
  async (req, res, next) => {
    console.log("in generatePreviewDraft", req.body);
    const appId = req.params.appid;
    const forkId = req.body.forkId;
    const branchName = req.body.branchName;
    const publishedCommitId = req.body.publishedCommitId;

    console.log("in generatePreviewDraft", appId, forkId, branchName, publishedCommitId);
    const openedApp = await getOpenApp(appId);
    if (openedApp) {
      try {
        await npmInstall(openedApp.repoPath);
        
        // Generate bundles for both platforms
        const [iosBundle, androidBundle] = await Promise.all([
          generatIOSBundle(openedApp.repoPath),
          generatAndroidBundle(openedApp.repoPath),
        ]);

        const iosTimestamp = iosBundle.bundleDestination.split("/").at(-2);
        const androidTimestamp = androidBundle.bundleDestination
          .split("/")
          .at(-2);
        console.log("iosTimestamp", iosTimestamp);
        console.log("androidTimestamp", androidTimestamp);

        // Upload both bundles to the server
        const [iosResult, androidResult] = await Promise.all([
          uploadMobileBundle(
            appId,
            iosTimestamp!,
            "ios",
            openedApp.apptilecookie
          ),
          uploadMobileBundle(
            appId,
            androidTimestamp!,
            "android",
            openedApp.apptilecookie
          ),
        ]);

        const apptileConfig = await getAppConfigsFromFS(appId);

        const draftResult = await axios.put(
          `${apptileConfig.APPTILE_BACKEND_URL}/${getMidUrlAppValue()}/${appId}/fork/${forkId}/branch/${branchName}/PreviewAppDraft`,
          {
            androidBundleUrlStatus: "done",
            iosBundleUrlStatus: "done",
            iosBundleUrl: iosResult.data.cdnlink,
            androidBundleUrl: androidResult.data.cdnlink,
            publishedCommitId,
          },
          makeHeadersWithCookie(openedApp.apptilecookie, {})
        );

        if (draftResult.status >= 400) {
          try {
            const errorMessage = draftResult.data;
            throw new Error(`Failed to create draft: ${errorMessage}`);
          } catch (err) {
            console.error("Unprocessable error:", err);
            throw err;
          }
        }

        res.status(200).json({
          message:
            "Preview draft generated and uploaded successfully for both platforms",
          ios: iosResult.data,
          android: androidResult.data,
        });
      } catch (err) {
        console.error("Error generating preview draft:", 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;