import path from 'path';

import { defaultSchema, joi } from '../joi';
import { pathTraversal } from '../joi/customValidators';
import { listRepoFiles, deleteFile, writeFile, move } from '../utils/fs';
import { entriesFromFiles, readMediaFile } from '../utils/entries';

import type {
  EntriesByFolderParams,
  EntriesByFilesParams,
  GetEntryParams,
  PersistEntryParams,
  GetMediaParams,
  GetMediaFileParams,
  PersistMediaParams,
  DeleteFileParams,
  DeleteFilesParams,
  DataFile,
} from '../types';
import type express from 'express';
import type winston from 'winston';

type FsOptions = {
  repoPath: string;
  logger: winston.Logger;
};

export function localFsMiddleware({ repoPath, logger }: FsOptions) {
  return async function (req: express.Request, res: express.Response) {
    try {
      const { body } = req;

      switch (body.action) {
        case 'info': {
          res.json({
            repo: path.basename(repoPath),
            publish_modes: ['simple'],
            type: 'local_fs',
          });
          break;
        }
        case 'entriesByFolder': {
          const payload = body.params as EntriesByFolderParams;
          const { folder, extension } = payload;
          const entries = await listRepoFiles(repoPath, folder, extension).then(items =>
            entriesFromFiles(
              repoPath,
              items.map(item => ({ path: item.file })),
            ),
          );
          res.json(entries);
          break;
        }
        case 'entriesByFiles': {
          const payload = body.params as EntriesByFilesParams;
          const entries = await entriesFromFiles(repoPath, payload.files);
          res.json(entries);
          break;
        }
        case 'getEntry': {
          const payload = body.params as GetEntryParams;
          const [entry] = await entriesFromFiles(repoPath, [{ path: payload.path }]);
          res.json(entry);
          break;
        }
        case 'persistEntry': {
          const {
            entry,
            dataFiles = [entry as DataFile],
            assets,
          } = body.params as PersistEntryParams;
          await Promise.all(
            dataFiles.map(dataFile => writeFile(path.join(repoPath, dataFile.path), dataFile.raw)),
          );
          // save assets
          await Promise.all(
            assets.map(a =>
              writeFile(path.join(repoPath, a.path), Buffer.from(a.content, a.encoding)),
            ),
          );
          if (dataFiles.every(dataFile => dataFile.newPath)) {
            dataFiles.forEach(async dataFile => {
              await move(
                path.join(repoPath, dataFile.path),
                path.join(repoPath, dataFile.newPath!),
              );
            });
          }
          res.json({ message: 'entry persisted' });
          break;
        }
        case 'getMedia': {
          const { mediaFolder, publicFolder = mediaFolder } = body.params as GetMediaParams;
          const fsItems = await listRepoFiles(repoPath, mediaFolder, '');
          res.json(
            fsItems.map(item => ({
              path: item.file.replace(/\\/g, '/'),
              url: item.file
                .replace(/\\/g, '/')
                .replace(mediaFolder.replace(/^\//g, ''), publicFolder),
              isDirectory: item.isDirectory,
            })),
          );
          break;
        }
        case 'getMediaFile': {
          const { path } = body.params as GetMediaFileParams;
          const mediaFile = await readMediaFile(repoPath, path);
          res.json(mediaFile);
          break;
        }
        case 'persistMedia': {
          const { asset } = body.params as PersistMediaParams;
          await writeFile(
            path.join(repoPath, asset.path),
            Buffer.from(asset.content, asset.encoding),
          );
          const file = await readMediaFile(repoPath, asset.path);
          res.json(file);
          break;
        }
        case 'deleteFile': {
          const { path: filePath } = body.params as DeleteFileParams;
          await deleteFile(repoPath, filePath);
          res.json({ message: `deleted file ${filePath}` });
          break;
        }
        case 'deleteFiles': {
          const { paths } = body.params as DeleteFilesParams;
          await Promise.all(paths.map(filePath => deleteFile(repoPath, filePath)));
          res.json({ message: `deleted files ${paths.join(', ')}` });
          break;
        }
        case 'getDeployPreview': {
          res.json(null);
          break;
        }
        default: {
          const message = `Unknown action ${body.action}`;
          res.status(422).json({ error: message });
          break;
        }
      }
    } catch (e: unknown) {
      if (e instanceof Error) {
        logger.error(`Error handling ${JSON.stringify(req.body)}: ${e.message}`);

        if (e.message.startsWith('ENOENT: no such file or directory')) {
          res.status(404).json({ error: 'Not found' });
          return;
        }
      }

      res.status(500).json({ error: 'Unknown error' });
    }
  };
}

export function getSchema({ repoPath }: { repoPath: string }) {
  const schema = defaultSchema({ path: pathTraversal(repoPath) });
  return schema;
}

type Options = {
  logger: winston.Logger;
};

export async function registerMiddleware(app: express.Express, options: Options) {
  const { logger } = options;
  const repoPath = path.resolve(process.env.GIT_REPO_DIRECTORY || process.cwd());
  app.post('/api/v1', joi(getSchema({ repoPath })));
  app.post('/api/v1', localFsMiddleware({ repoPath, logger }));
  logger.info(`Static CMS File System Proxy Server configured with ${repoPath}`);
}
