/**
 *  Copyright (C) 2018 Basalt
    This file is part of Knapsack.
    Knapsack is free software; you can redistribute it and/or modify it
    under the terms of the GNU General Public License as published by the Free
    Software Foundation; either version 2 of the License, or (at your option)
    any later version.

    Knapsack is distributed in the hope that it will be useful, but WITHOUT
    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
    more details.

    You should have received a copy of the GNU General Public License along
    with Knapsack; if not, see <https://www.gnu.org/licenses>.
 */
import express from 'express';
import urlJoin from 'url-join';
import md from 'marked';
import fs from 'fs-extra';
import { join, relative, parse as parsePath } from 'path';
import { exec } from 'child_process';
import highlight from 'highlight.js';
import { paramCase } from 'change-case';
import shortid from 'shortid';
import multer from 'multer';
import * as Files from '../schemas/api/files';
import { endpoint as pluginsEndpoint } from '../schemas/api/plugins';
import { handlePluginsEndpoint } from './api/plugins';
import { MemDb } from './dbs/mem-db';
import * as log from '../cli/log';
import { BASE_PATHS, HTTP_STATUS } from '../lib/constants';
import { getUserInfo } from './auth';
import { KnapsackBrain, KnapsackConfig } from '../schemas/main-types';
import { KnapsackMeta, FileResponse } from '../schemas/misc';
import { KnapsackTemplateDemo } from '../schemas/patterns';
import { KsRenderResults } from '../schemas/knapsack-config';
import { fileExists, resolvePath } from './server-utils';
import { getFeaturesForUser } from '../lib/features';
import { PatternRenderData } from '../schemas/api/render';

const router = express.Router();
const memDb = new MemDb<KnapsackTemplateDemo>();

// https://marked.js.org/#/USING_ADVANCED.md
md.setOptions({
  highlight: code => highlight.highlightAuto(code).value,
});

export function getApiRoutes({
  registerEndpoint,
  patternManifest,
  pageBuilder,
  settingsStore,
  meta,
  baseUrl,
  tokens,
  config,
}: {
  patternManifest: KnapsackBrain['patterns'];
  config: KnapsackBrain['config'];
  webroot: string;
  public: string;
  baseUrl: string;
  meta: KnapsackMeta;
  tokens: KnapsackBrain['tokens'];
  pageBuilder: KnapsackBrain['pageBuilderPages'];
  settingsStore: KnapsackBrain['settings'];
  registerEndpoint: (pathname: string, method?: 'GET' | 'POST') => void;
  templateRenderers: KnapsackConfig['templateRenderers'];
}): typeof router {
  router.get(baseUrl, (req, res) => {
    res.json({
      ok: true,
      message: 'Welcome to the API!',
    });
  });

  router.post(pluginsEndpoint, handlePluginsEndpoint);

  {
    const url = urlJoin(baseUrl, 'data');
    registerEndpoint(url, 'POST');
    router.post(url, async (req, res) => {
      const { body, headers } = req;
      if (headers['content-type'] !== 'application/json') {
        res.send({
          ok: false,
          message:
            "Must send with a Header of 'Content-Type: application/json'",
        });
        return;
      }

      const hash = memDb.addData(body);
      res.send({
        ok: true,
        message: `Data has been made and can be viewed with hash "${hash}"`,
        data: {
          hash,
        },
      });
    });
  }

  if (patternManifest) {
    async function render({
      patternId,
      templateId,
      demoId,
      assetSetId,
      isInIframe,
      dataId,
    }: {
      patternId: string;
      templateId: string;
      assetSetId?: string;
      /**
       * Data id from `saveData()`
       * Cannot use with `demoId`
       */
      dataId?: string;
      /**
       * Cannot use with `dataId`
       */
      demoId?: string;
      /**
       * Will this be in an iFrame?
       */
      isInIframe?: boolean;
    }): Promise<KsRenderResults> {
      if (demoId && dataId) {
        const message = `Cannot send query params "demoId" and "dataId" to render endpont for pattern "${patternId}", template "${templateId}"`;
        log.error(message, {
          patternId,
          templateId,
          demoId,
          assetSetId,
          isInIframe,
          dataId,
        });
        return {
          ok: false,
          html: `<p>${message}</p>`,
          wrappedHtml: `<p>${message}</p>`,
          message,
          dataId: '',
        };
      }

      const demo = dataId ? memDb.getData(dataId) : demoId;
      // log.inspect({ demoId, demo });
      try {
        const results = patternManifest.render({
          patternId,
          templateId,
          demo,
          isInIframe,
          websocketsPort: meta.websocketsPort,
          assetSetId,
          // demoDataId,
        });
        return results;
      } catch (e) {
        return {
          ok: false,
          html: `<p>${e.message}</p>`,
          wrappedHtml: `<p>${e.message}</p>`,
          message: e.message,
          dataId: '',
        };
      }
    }

    const url = urlJoin(baseUrl, '/render');
    registerEndpoint(url, 'GET');
    registerEndpoint(url, 'POST');
    router.post(url, async (req, res) => {
      const { body } = req;
      const {
        patternId,
        templateId,
        isInIframe,
        assetSetId,
        demoId,
        dataId,
      }: PatternRenderData = body;
      if (!(patternId && templateId)) {
        res.send({
          ok: false,
          message: `Missing patternId or templateId in body`,
          data: body,
        });
      } else {
        const results = await render({
          patternId,
          templateId,
          isInIframe,
          assetSetId,
          demoId,
          dataId,
        });
        res.send(results);
      }
    });
    router.get(url, async (req, res) => {
      const { query } = req;
      query.wrapHtml = query.wrapHtml === 'true';
      query.isInIframe = query.isInIframe === 'true';
      const {
        patternId,
        templateId,
        isInIframe,
        wrapHtml,
        assetSetId,
        demoId,
        dataId,
      }: PatternRenderData = query;

      const results = await render({
        patternId,
        templateId,
        assetSetId,
        dataId,
        demoId,
        isInIframe,
      });

      if (results.ok) {
        res.send(wrapHtml ? results.wrappedHtml : results.html);
      } else {
        let demo;
        if (dataId) {
          demo = memDb.getData(dataId);
        }
        log.error(`Error rendering template`, {
          patternId,
          templateId,
          dataId,
          wrapHtml,
          isInIframe,
          assetSetId,
          message: results.message,
          demo,
        });
        console.log(); // empty line
        res.send(results.message);
      }
    });
  }

  if (patternManifest) {
    const url1 = urlJoin(baseUrl, 'pattern/:id');
    registerEndpoint(url1);
    router.get(url1, async (req, res) => {
      const results = await patternManifest.getPattern(req.params.id);
      res.send(results);
    });

    const url2 = urlJoin(baseUrl, 'patterns');
    registerEndpoint(url2);
    router.get(url2, async (req, res) => {
      const results = await patternManifest.getPatterns();
      res.send(results);
    });

    {
      const url = Files.endpoint;
      registerEndpoint(url);
      router.post(url, async (req, res) => {
        const userInfo = getUserInfo(req);
        const { isLocalDev } = getFeaturesForUser(userInfo);
        const localOnly = () =>
          res.status(HTTP_STATUS.BAD.BAD_REQUEST).send({
            ok: false,
            message: 'This endpoint only available to local developers',
          });

        let response: Files.ActionResponses;
        const reqBody: Files.Actions = req.body;
        const { data: dataDir } = config;

        switch (reqBody.type) {
          case Files.ACTIONS.verify: {
            if (!isLocalDev) {
              return localOnly();
            }
            const { path } = reqBody.payload;
            const { exists, absolutePath, type } = resolvePath({
              path,
              resolveFromDirs: [dataDir],
            });

            response = {
              type: Files.ACTIONS.verify,
              payload: {
                exists,
                relativePath: exists ? relative(dataDir, absolutePath) : '',
                absolutePath,
                type,
              },
            };
            break;
          }
          case Files.ACTIONS.saveTemplateDemo: {
            if (!isLocalDev) {
              return localOnly();
            }
            const { patternId, templateId, demoId, code } = reqBody.payload;
            const fullPath = patternManifest.getTemplateDemoAbsolutePath({
              patternId,
              templateId,
              demoId,
            });

            try {
              await fs.writeFile(fullPath, code, 'utf8');
              response = {
                type: Files.ACTIONS.saveTemplateDemo,
                payload: {
                  ok: true,
                },
              };
            } catch (e) {
              response = {
                type: Files.ACTIONS.saveTemplateDemo,
                payload: {
                  ok: false,
                  message: e.message,
                },
              };
            }
            break;
          }
          case Files.ACTIONS.deleteTemplateDemo: {
            if (!isLocalDev) {
              return localOnly();
            }
            const { path } = reqBody.payload;
            const { exists, absolutePath } = resolvePath({
              path,
              resolveFromDirs: [dataDir],
            });
            if (!exists) {
              response = {
                type: Files.ACTIONS.deleteTemplateDemo,
                payload: {
                  ok: false,
                  message: 'File already did not exist',
                },
              };
            } else {
              try {
                await fs.remove(absolutePath);
                response = {
                  type: Files.ACTIONS.deleteTemplateDemo,
                  payload: {
                    ok: true,
                  },
                };
              } catch (e) {
                log.error('Files.ACTIONS.deleteTemplateDemo', e, '/api/files');
                response = {
                  type: Files.ACTIONS.deleteTemplateDemo,
                  payload: {
                    ok: false,
                    message: e.message,
                  },
                };
              }
            }

            break;
          }

          case Files.ACTIONS.openFile: {
            if (!isLocalDev) {
              return localOnly();
            }
            const { filePath } = reqBody.payload;
            const { exists, absolutePath } = resolvePath({
              path: filePath,
              resolveFromDirs: [dataDir],
            });
            if (exists) {
              exec(`open ${absolutePath}`, err => {
                if (err)
                  log.error(`error opening file: ${err.message}`, {
                    filePath,
                    err,
                  });
                response = {
                  type: Files.ACTIONS.openFile,
                  payload: {
                    ok: !err,
                    message: err ? err.message : '',
                  },
                };
              });
            }
          }
        }

        res.send(response);
      });
    }
  }

  if (pageBuilder) {
    const url1 = urlJoin(baseUrl, `${BASE_PATHS.PAGE_BUILDER}/:id`);
    registerEndpoint(url1);
    router.get(url1, async (req, res) => {
      try {
        const page = await pageBuilder.getPageBuilderPage(req.params.id);
        res.send({
          ok: true,
          page,
        });
      } catch (error) {
        if (error.code === 'ENOENT') {
          res.send({
            ok: false,
            message: `Example "${req.params.id}" not found.`,
          });
        } else {
          res.send({
            ok: false,
            message: error.toString(),
          });
        }
      }
    });

    const url2 = urlJoin(baseUrl, `${BASE_PATHS.PAGE_BUILDER}/:id`);
    registerEndpoint(url2, 'POST');
    router.post(url2, async (req, res) => {
      const results = await pageBuilder.setPageBuilderPage(
        req.params.id,
        req.body,
      );
      res.send(results);
    });

    const url3 = urlJoin(baseUrl, BASE_PATHS.PAGE_BUILDER);
    registerEndpoint(url3);
    router.get(url3, async (req, res) => {
      const results = await pageBuilder.getPageBuilderPages();
      res.send(results);
    });
  } else {
    router.get(urlJoin(baseUrl, BASE_PATHS.PAGE_BUILDER), async (req, res) => {
      res.send([]);
    });
  }

  const url3 = urlJoin(baseUrl, 'settings');
  registerEndpoint(url3);
  router.get(url3, async (req, res) => {
    const settings = settingsStore.getData();
    res.send(settings);
  });

  const url4 = urlJoin(baseUrl, 'design-tokens');
  registerEndpoint(url4);
  router.get(url4, (req, res) => {
    res.send(tokens.getTokens());
  });

  const url5 = urlJoin(baseUrl, 'meta');
  registerEndpoint(url5);
  router.get(url5, (req, res) => {
    res.send(meta);
  });

  {
    const url = urlJoin(baseUrl, 'loading');
    registerEndpoint(url);
    router.get(url, (req, res) => {
      res.send(`<p>Loading...</p>`);
    });
  }

  const url6 = urlJoin(baseUrl, 'permissions');
  registerEndpoint(url6);
  router.get(url6, (req, res) => {
    const { role } = getUserInfo(req);
    res.send(role.permissions);
  });

  const destination = join(config.public, 'uploads');

  const uploaderLocal = multer({
    dest: config.public,
    storage: multer.diskStorage({
      destination,
      filename(req, file, cb) {
        const { originalname } = file;
        const { name, ext } = parsePath(originalname);
        let filename = `${paramCase(name)}${ext}`;
        if (fs.existsSync(join(destination, filename))) {
          filename = `${paramCase(name)}-${shortid.generate()}${ext}`;
        }
        cb(null, filename);
      },
    }),
  });
  const uploadLocalSingle = uploaderLocal.single('file');

  const url7 = urlJoin(baseUrl, 'upload');
  registerEndpoint(url7);

  router.post(url7, (req, res) => {
    let resData: FileResponse;

    uploadLocalSingle(req, res, err => {
      const { file } = req as Express.Request;
      if (err instanceof multer.MulterError) {
        resData = {
          ok: false,
          message: err.message,
        };
        return res.send(resData);
      }
      if (err) {
        resData = {
          ok: false,
          message: err.message,
        };
        return res.send(resData);
      }
      if (!file) {
        resData = {
          ok: false,
          message: 'Did not receive a file',
        };
        return res.send(resData);
      }

      const {
        path,
        size,
        mimetype,
        originalname,
        filename,
      } = file as Express.Multer.File;
      const publicPath = `/${relative(config.public, path)}`;
      resData = {
        ok: true,
        data: {
          publicPath,
          size,
          mimetype,
          originalName: originalname,
          filename,
        },
      };
      return res.status(200).send(resData);
    });
  });

  return router;
}
