// Copyright 2018 The Kubeflow Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import * as express from 'express';
import { Response } from 'express-serve-static-core';
import * as fs from 'fs';
import * as _path from 'path';
import { ApiExperiment, ApiListExperimentsResponse } from '../src/apis/experiment';
import { ApiFilter, PredicateOp } from '../src/apis/filter';
import { ApiJob, ApiListJobsResponse } from '../src/apis/job';
import {
  ApiListPipelinesResponse,
  ApiListPipelineVersionsResponse,
  ApiPipeline,
  ApiPipelineVersion,
} from '../src/apis/pipeline';
import { ApiListRunsResponse, ApiResourceType, ApiRun, ApiRunStorageState } from '../src/apis/run';
import { ExperimentSortKeys, PipelineSortKeys, RunSortKeys } from '../src/lib/Apis';
import RunUtils from '../src/lib/RunUtils';
import {
  data as fixedData,
  namedPipelines,
  PIPELINE_VERSIONS_LIST_FULL,
  PIPELINE_VERSIONS_LIST_MAP,
  v2PipelineSpecMap,
} from './fixed-data';
import helloWorldRuntime from './data/v1/runtime/integration-test-runtime';
import proxyMiddleware from './proxy-middleware';

const rocMetadataJsonPath = './eval-output/metadata.json';
const rocMetadataJsonPath2 = './eval-output/metadata2.json';
const rocDataPath = './eval-output/roc.csv';
const rocDataPath2 = './eval-output/roc2.csv';
const tableDataPath = './eval-output/table.csv';

const confusionMatrixMetadataJsonPath = './model-output/metadata.json';
const confusionMatrixPath = './model-output/confusion_matrix.csv';
const helloWorldHtmlPath = './model-output/hello-world.html';
const helloWorldBigHtmlPath = './model-output/hello-world-big.html';

const v1beta1Prefix = '/apis/v1beta1';

let tensorboardPod = '';

// This is a copy of the BaseResource defined within src/pages/ResourceSelector
interface BaseResource {
  id?: string;
  created_at?: Date;
  description?: string;
  name?: string;
  error?: string;
}

// tslint:disable-next-line:no-default-export
export default (app: express.Application) => {
  app.use((req, _, next) => {
    // tslint:disable-next-line:no-console
    console.info(req.method + ' ' + req.originalUrl);
    next();
  });

  proxyMiddleware(app as any, v1beta1Prefix);

  app.set('json spaces', 2);
  app.use(express.json());

  app.get(v1beta1Prefix + '/healthz', (_, res) => {
    res.header('Content-Type', 'application/json');
    res.send({
      apiServerCommitHash: 'd3c4add0a95e930c70a330466d0923827784eb9a',
      apiServerReady: true,
      buildDate: 'Wed Jan 9 19:40:24 UTC 2019',
      frontendCommitHash: '8efb2fcff9f666ba5b101647e909dc9c6889cecb',
    });
  });

  app.get('/hub/', (_, res) => {
    res.sendStatus(200);
  });

  function getSortKeyAndOrder(
    defaultSortKey: string,
    queryParam?: string,
  ): { desc: boolean; key: string } {
    let key = defaultSortKey;
    let desc = false;

    if (queryParam) {
      const keyParts = queryParam.split(' ');
      key = keyParts[0];

      // Check that the key is properly formatted.
      if (
        keyParts.length > 2 ||
        (keyParts.length === 2 && keyParts[1] !== 'asc' && keyParts[1] !== 'desc')
      ) {
        throw new Error(`Invalid sort string: ${queryParam}`);
      }

      desc = keyParts.length === 2 && keyParts[1] === 'desc';
    }
    return { desc, key };
  }

  app.get(v1beta1Prefix + '/jobs', (req, res) => {
    res.header('Content-Type', 'application/json');
    // Note: the way that we use the next_page_token here may not reflect the way the backend works.
    const response: ApiListJobsResponse = {
      jobs: [],
      next_page_token: '',
    };

    let jobs: ApiJob[] = fixedData.jobs;
    if (req.query.filter) {
      jobs = filterResources(fixedData.jobs, req.query.filter);
    }

    const { desc, key } = getSortKeyAndOrder(ExperimentSortKeys.CREATED_AT, req.query.sort_by);

    jobs.sort((a, b) => {
      let result = 1;
      if (a[key]! < b[key]!) {
        result = -1;
      }
      if (a[key]! === b[key]!) {
        result = 0;
      }
      return result * (desc ? -1 : 1);
    });

    const start = req.query.page_token ? +req.query.page_token : 0;
    const end = start + (+req.query.page_size || 20);
    response.jobs = jobs.slice(start, end);

    if (end < jobs.length) {
      response.next_page_token = end + '';
    }

    res.json(response);
  });

  app.get(v1beta1Prefix + '/experiments', (req, res) => {
    res.header('Content-Type', 'application/json');
    // Note: the way that we use the next_page_token here may not reflect the way the backend works.
    const response: ApiListExperimentsResponse = {
      experiments: [],
      next_page_token: '',
    };

    let experiments: ApiExperiment[] = fixedData.experiments;
    if (req.query.filter) {
      experiments = filterResources(fixedData.experiments, req.query.filter);
    }

    const { desc, key } = getSortKeyAndOrder(ExperimentSortKeys.NAME, req.query.sortBy);

    experiments.sort((a, b) => {
      let result = 1;
      if (a[key]! < b[key]!) {
        result = -1;
      }
      if (a[key]! === b[key]!) {
        result = 0;
      }
      return result * (desc ? -1 : 1);
    });

    const start = req.query.pageToken ? +req.query.pageToken : 0;
    const end = start + (+req.query.pageSize || 20);
    response.experiments = experiments.slice(start, end);

    if (end < experiments.length) {
      response.next_page_token = end + '';
    }

    res.json(response);
  });

  app.post(v1beta1Prefix + '/experiments', (req, res) => {
    const experiment: ApiExperiment = req.body;
    if (fixedData.experiments.find(e => e.name!.toLowerCase() === experiment.name!.toLowerCase())) {
      res.status(404).send('An experiment with the same name already exists');
      return;
    }
    experiment.id = 'new-experiment-' + (fixedData.experiments.length + 1);
    fixedData.experiments.push(experiment);

    setTimeout(() => {
      res.send(fixedData.experiments[fixedData.experiments.length - 1]);
    }, 1000);
  });

  app.get(v1beta1Prefix + '/experiments/:eid', (req, res) => {
    res.header('Content-Type', 'application/json');
    const experiment = fixedData.experiments.find(exp => exp.id === req.params.eid);
    if (!experiment) {
      res.status(404).send(`No experiment was found with ID: ${req.params.eid}`);
      return;
    }
    res.json(experiment);
  });

  app.post(v1beta1Prefix + '/jobs', (req, res) => {
    const job: ApiJob = req.body;
    job.id = 'new-job-' + (fixedData.jobs.length + 1);
    job.created_at = new Date();
    job.updated_at = new Date();
    job.enabled = !!job.trigger;
    fixedData.jobs.push(job);

    const date = new Date().toISOString();
    fixedData.runs.push({
      pipeline_runtime: {
        workflow_manifest: JSON.stringify(helloWorldRuntime),
      },
      run: {
        created_at: new Date(),
        id: 'job-at-' + date,
        name: 'job-' + job.name + date,
        scheduled_at: new Date(),
        status: 'Running',
      },
    });
    setTimeout(() => {
      res.send(fixedData.jobs[fixedData.jobs.length - 1]);
    }, 1000);
  });

  app.all(v1beta1Prefix + '/jobs/:jid', (req, res) => {
    res.header('Content-Type', 'application/json');
    switch (req.method) {
      case 'DELETE':
        const i = fixedData.jobs.findIndex(j => j.id === req.params.jid);
        if (fixedData.jobs[i].name!.startsWith('Cannot be deleted')) {
          res.status(502).send(`Deletion failed for job: '${fixedData.jobs[i].name}'`);
        } else {
          // Delete the job from fixedData.
          fixedData.jobs.splice(i, 1);
          res.json({});
        }
        break;
      case 'GET':
        const job = fixedData.jobs.find(j => j.id === req.params.jid);
        if (job) {
          res.json(job);
        } else {
          res.status(404).send(`No job was found with ID: ${req.params.jid}`);
        }
        break;
      default:
        res.status(405).send('Unsupported request type: ' + req.method);
    }
  });

  app.get(v1beta1Prefix + '/runs', (req, res) => {
    res.header('Content-Type', 'application/json');
    // Note: the way that we use the next_page_token here may not reflect the way the backend works.
    const response: ApiListRunsResponse = {
      next_page_token: '',
      runs: [],
    };

    let runs: ApiRun[] = fixedData.runs.map(r => r.run!);

    if (req.query.filter) {
      runs = filterResources(runs, req.query.filter);
    }

    if (req.query['resource_reference_key.type'] === ApiResourceType.EXPERIMENT) {
      runs = runs.filter(r =>
        RunUtils.getAllExperimentReferences(r).some(
          ref =>
            (ref.key && ref.key.id && ref.key.id === req.query['resource_reference_key.id']) ||
            false,
        ),
      );
    }

    const { desc, key } = getSortKeyAndOrder(RunSortKeys.CREATED_AT, req.query.sort_by);

    runs.sort((a, b) => {
      let result = 1;
      if (a[key]! < b[key]!) {
        result = -1;
      }
      if (a[key]! === b[key]!) {
        result = 0;
      }
      return result * (desc ? -1 : 1);
    });

    const start = req.query.page_token ? +req.query.page_token : 0;
    const end = start + (+req.query.page_size || 20);
    response.runs = runs.slice(start, end);

    if (end < runs.length) {
      response.next_page_token = end + '';
    }

    res.json(response);
  });

  app.get(v1beta1Prefix + '/runs/:rid', (req, res) => {
    const rid = req.params.rid;
    const run = fixedData.runs.find(r => r.run!.id === rid);
    if (!run) {
      res.status(404).send('Cannot find a run with id: ' + rid);
      return;
    }
    res.json(run);
  });

  app.post(v1beta1Prefix + '/runs', (req, res) => {
    const date = new Date();
    const run: ApiRun = req.body;
    run.id = 'new-run-' + (fixedData.runs.length + 1);
    run.created_at = date;
    run.scheduled_at = date;
    run.status = 'Running';

    fixedData.runs.push({
      pipeline_runtime: {
        workflow_manifest: JSON.stringify(helloWorldRuntime),
      },
      run,
    });
    setTimeout(() => {
      res.send(fixedData.jobs[fixedData.jobs.length - 1]);
    }, 1000);
  });

  app.post(v1beta1Prefix + '/runs/:rid::method', (req, res) => {
    if (req.params.method !== 'archive' && req.params.method !== 'unarchive') {
      res.status(500).send('Bad method');
    }
    const runDetail = fixedData.runs.find(r => r.run!.id === req.params.rid);
    if (runDetail) {
      runDetail.run!.storage_state =
        req.params.method === 'archive'
          ? ApiRunStorageState.ARCHIVED
          : ApiRunStorageState.AVAILABLE;
      res.json({});
    } else {
      res.status(500).send('Cannot find a run with id ' + req.params.rid);
    }
  });

  app.post(v1beta1Prefix + '/jobs/:jid/enable', (req, res) => {
    setTimeout(() => {
      const job = fixedData.jobs.find(j => j.id === req.params.jid);
      if (job) {
        job.enabled = true;
        res.json({});
      } else {
        res.status(500).send('Cannot find a job with id ' + req.params.jid);
      }
    }, 1000);
  });

  app.post(v1beta1Prefix + '/jobs/:jid/disable', (req, res) => {
    setTimeout(() => {
      const job = fixedData.jobs.find(j => j.id === req.params.jid);
      if (job) {
        job.enabled = false;
        res.json({});
      } else {
        res.status(500).send('Cannot find a job with id ' + req.params.jid);
      }
    }, 1000);
  });

  function filterResources(resources: BaseResource[], filterString?: string): BaseResource[] {
    if (!filterString) {
      return resources;
    }
    const filter: ApiFilter = JSON.parse(decodeURIComponent(filterString));
    ((filter && filter.predicates) || []).forEach(p => {
      resources = resources.filter(r => {
        switch (p.op) {
          case PredicateOp.EQUALS:
            if (p.key === 'name') {
              return (
                r.name && r.name.toLocaleLowerCase() === (p.string_value || '').toLocaleLowerCase()
              );
            } else if (p.key === 'storage_state') {
              return (
                (r as ApiRun).storage_state &&
                (r as ApiRun).storage_state!.toString() === p.string_value
              );
            } else {
              throw new Error(`Key: ${p.key} is not yet supported by the mock API server`);
            }
          case PredicateOp.NOTEQUALS:
            if (p.key === 'name') {
              return (
                r.name && r.name.toLocaleLowerCase() !== (p.string_value || '').toLocaleLowerCase()
              );
            } else if (p.key === 'storage_state') {
              return ((r as ApiRun).storage_state || {}).toString() !== p.string_value;
            } else {
              throw new Error(`Key: ${p.key} is not yet supported by the mock API server`);
            }
          case PredicateOp.ISSUBSTRING:
            if (p.key !== 'name') {
              throw new Error(`Key: ${p.key} is not yet supported by the mock API server`);
            }
            return (
              r.name &&
              r.name.toLocaleLowerCase().includes((p.string_value || '').toLocaleLowerCase())
            );
          case PredicateOp.NOTEQUALS:
          // Fall through
          case PredicateOp.GREATERTHAN:
          // Fall through
          case PredicateOp.GREATERTHANEQUALS:
          // Fall through
          case PredicateOp.LESSTHAN:
          // Fall through
          case PredicateOp.LESSTHANEQUALS:
            // Fall through
            throw new Error(`Op: ${p.op} is not yet supported by the mock API server`);
          default:
            throw new Error(`Unknown Predicate op: ${p.op}`);
        }
      });
    });
    return resources;
  }

  app.get(v1beta1Prefix + '/pipelines', (req, res) => {
    res.header('Content-Type', 'application/json');
    const response: ApiListPipelinesResponse = {
      next_page_token: '',
      pipelines: [],
    };

    let pipelines: ApiPipeline[] = fixedData.pipelines;
    if (req.query.filter) {
      pipelines = filterResources(fixedData.pipelines, req.query.filter);
    }

    const { desc, key } = getSortKeyAndOrder(PipelineSortKeys.CREATED_AT, req.query.sort_by);

    pipelines.sort((a, b) => {
      let result = 1;
      if (a[key]! < b[key]!) {
        result = -1;
      }
      if (a[key]! === b[key]!) {
        result = 0;
      }
      return result * (desc ? -1 : 1);
    });

    const start = req.query.page_token ? +req.query.page_token : 0;
    const end = start + (+req.query.page_size || 20);
    response.pipelines = pipelines.slice(start, end);

    if (end < pipelines.length) {
      response.next_page_token = end + '';
    }

    res.json(response);
  });

  app.delete(v1beta1Prefix + '/pipelines/:pid', (req, res) => {
    res.header('Content-Type', 'application/json');
    const i = fixedData.pipelines.findIndex(p => p.id === req.params.pid);

    if (i === -1) {
      res.status(404).send(`No pipelines was found with ID: ${req.params.pid}`);
      return;
    }

    if (fixedData.pipelines[i].name!.startsWith('Cannot be deleted')) {
      res.status(502).send(`Deletion failed for pipeline: '${fixedData.pipelines[i].name}'`);
      return;
    }
    // Delete the pipelines from fixedData.
    fixedData.pipelines.splice(i, 1);
    res.json({});
  });

  app.get(v1beta1Prefix + '/pipelines/:pid', (req, res) => {
    res.header('Content-Type', 'application/json');
    const pipeline = fixedData.pipelines.find(p => p.id === req.params.pid);
    if (!pipeline) {
      res.status(404).send(`No pipeline was found with ID: ${req.params.pid}`);
      return;
    }
    res.json(pipeline);
  });

  app.get(v1beta1Prefix + '/pipelines/:pid/templates', (req, res) => {
    res.header('Content-Type', 'text/x-yaml');
    const pipeline = fixedData.pipelines.find(p => p.id === req.params.pid);
    if (!pipeline) {
      res.status(404).send(`No pipeline was found with ID: ${req.params.pid}`);
      return;
    }
    let filePath = '';
    if (req.params.pid === namedPipelines.noParams.id) {
      filePath = './mock-backend/data/v1/template/mock-conditional-template.yaml';
    } else if (req.params.pid === namedPipelines.unstructuredText.id) {
      filePath = './mock-backend/data/v1/template/mock-recursive-template.yaml';
    } else {
      filePath = './mock-backend/data/v1/template/mock-template.yaml';
    }
    if (v2PipelineSpecMap.has(req.params.pid)) {
      const specPath = v2PipelineSpecMap.get(req.params.pid);
      if (specPath) {
        filePath = specPath;
      }
      console.log(filePath);
    }
    res.send(JSON.stringify({ template: fs.readFileSync(filePath, 'utf-8') }));
  });

  app.get(v1beta1Prefix + '/pipeline_versions/:pid/templates', (req, res) => {
    res.header('Content-Type', 'text/x-yaml');

    // Find v2 pipeline template
    const templatePath = v2PipelineSpecMap.get(req.params.pid);
    if (templatePath != null) {
      console.log(templatePath);
      res.send(JSON.stringify({ template: fs.readFileSync(templatePath, 'utf-8') }));
      return;
    }

    // Default and v1 version list. Return mock template consistently.
    const version = fixedData.versions.find(p => p.id === req.params.pid);
    if (!version) {
      res.status(404).send(`No pipeline was found with ID: ${req.params.pid}`);
      return;
    }
    const filePath = './mock-backend/mock-recursive-template.yaml';

    res.send(JSON.stringify({ template: fs.readFileSync(filePath, 'utf-8') }));
  });

  app.get(v1beta1Prefix + '/pipeline_versions/:pid', (req, res) => {
    res.header('Content-Type', 'application/json');
    const pipeline = PIPELINE_VERSIONS_LIST_FULL.find(p => p.id === req.params.pid);
    if (!pipeline) {
      res.status(404).send(`No pipeline was found with ID: ${req.params.pid}`);
      return;
    }
    res.json(pipeline);
  });

  app.get(v1beta1Prefix + '/pipeline_versions', (req, res) => {
    // Sample query format:
    // query: {
    //   'resource_key.type': 'PIPELINE',
    //   'resource_key.id': '8fbe3bd6-a01f-11e8-98d0-529269fb1459',
    //   page_size: '50',
    //   sort_by: 'created_at desc'
    // },
    if (
      req.query['resource_key.id'] &&
      req.query['resource_key.type'] === 'PIPELINE' &&
      req.query.page_size > 0
    ) {
      const response: ApiListPipelineVersionsResponse = {
        next_page_token: '',
        versions: [],
      };

      let versions: ApiPipelineVersion[] =
        PIPELINE_VERSIONS_LIST_MAP.get(req.query['resource_key.id']) || [];

      if (versions.length === 0) {
        const pipeline = fixedData.pipelines.find(p => p.id === req.query['resource_key.id']);

        if (pipeline == null || !pipeline.default_version) {
          return;
        }

        // Default version list is pipeline with single default version.
        const pipeline_versions_list_response: ApiListPipelineVersionsResponse = {
          total_size: 1,
          versions: [pipeline.default_version],
        };
        res.json(pipeline_versions_list_response);
        return;
      }

      const start = req.query.page_token ? +req.query.page_token : 0;
      const end = start + (+req.query.page_size || 20);
      response.versions = versions.slice(start, end);

      if (end < versions.length) {
        response.next_page_token = end + '';
      }

      res.json(response);

      return;
    }
    return;
  });

  app.get(v1beta1Prefix + '/pipeline_versions/:pid', (req, res) => {
    // TODO: Temporary returning default version only. It requires
    // keeping a record of all pipeline id in order to search non-default version.
    res.header('Content-Type', 'application/json');
    const pipeline = fixedData.pipelines.find(p => p.id === req.params.pid);
    if (!pipeline) {
      res
        .status(404)
        .send(
          `No pipeline found with ID: ${req.params.pid}, non-default version can't be found yet.`,
        );
      return;
    }
    if (pipeline.default_version) {
      res.json(pipeline.default_version);
    }
  });

  function mockCreatePipeline(res: Response, name: string, body?: any): void {
    res.header('Content-Type', 'application/json');
    // Don't allow uploading multiple pipelines with the same name
    if (fixedData.pipelines.find(p => p.name === name)) {
      res
        .status(502)
        .send(`A Pipeline named: "${name}" already exists. Please choose a different name.`);
    } else {
      const pipeline = body || {};
      pipeline.id = 'new-pipeline-' + (fixedData.pipelines.length + 1);
      pipeline.name = name;
      pipeline.created_at = new Date();
      pipeline.description =
        'TODO: the mock middleware does not actually use the uploaded pipeline';
      pipeline.parameters = [
        {
          name: 'output',
        },
        {
          name: 'param-1',
        },
        {
          name: 'param-2',
        },
      ];
      fixedData.pipelines.push(pipeline);
      setTimeout(() => {
        res.send(fixedData.pipelines[fixedData.pipelines.length - 1]);
      }, 1000);
    }
  }

  app.post(v1beta1Prefix + '/pipelines', (req, res) => {
    mockCreatePipeline(res, req.body.name);
  });

  app.post(v1beta1Prefix + '/pipelines/upload', (req, res) => {
    mockCreatePipeline(res, decodeURIComponent(req.query.name), req.body);
  });

  app.get('/artifacts/get', (req, res) => {
    const key = decodeURIComponent(req.query.key);
    res.header('Content-Type', 'application/json');
    if (key.endsWith('roc.csv')) {
      res.sendFile(_path.resolve(__dirname, rocDataPath));
    } else if (key.endsWith('roc2.csv')) {
      res.sendFile(_path.resolve(__dirname, rocDataPath2));
    } else if (key.endsWith('confusion_matrix.csv')) {
      res.sendFile(_path.resolve(__dirname, confusionMatrixPath));
    } else if (key.endsWith('table.csv')) {
      res.sendFile(_path.resolve(__dirname, tableDataPath));
    } else if (key.endsWith('hello-world.html')) {
      res.sendFile(_path.resolve(__dirname, helloWorldHtmlPath));
    } else if (key.endsWith('hello-world-big.html')) {
      res.sendFile(_path.resolve(__dirname, helloWorldBigHtmlPath));
    } else if (key === 'analysis') {
      res.sendFile(_path.resolve(__dirname, confusionMatrixMetadataJsonPath));
    } else if (key === 'analysis2') {
      res.sendFile(_path.resolve(__dirname, confusionMatrixMetadataJsonPath));
    } else if (key === 'model') {
      res.sendFile(_path.resolve(__dirname, rocMetadataJsonPath));
    } else if (key === 'model2') {
      res.sendFile(_path.resolve(__dirname, rocMetadataJsonPath2));
    } else {
      // TODO: what does production return here?
      res.send('dummy file for key: ' + key);
    }
  });

  app.get('/apps/tensorboard', (req, res) => {
    res.send(tensorboardPod);
  });

  app.post('/apps/tensorboard', (req, res) => {
    tensorboardPod = 'http://tensorboardserver:port';
    setTimeout(() => {
      res.send('ok');
    }, 1000);
  });

  app.get('/k8s/pod/logs', (req, res) => {
    const podName = decodeURIComponent(req.query.podname);
    if (podName === 'json-12abc') {
      res.status(404).send('pod not found');
      return;
    }
    if (podName === 'coinflip-recursive-q7dqb-3721646052') {
      res.status(500).send('Failed to retrieve log');
      return;
    }
    const shortLog = fs.readFileSync('./mock-backend/shortlog.txt', 'utf-8');
    const longLog = fs.readFileSync('./mock-backend/longlog.txt', 'utf-8');
    const log = podName === 'coinflip-recursive-q7dqb-3466727817' ? longLog : shortLog;
    setTimeout(() => {
      res.send(log);
    }, 300);
  });

  app.get('/visualizations/allowed', (req, res) => {
    res.send(true);
  });

  // Uncomment this instead to test 404 endpoints.
  // app.get('/system/cluster-name', (_, res) => {
  //   res.status(404).send('404 Not Found');
  // });
  // app.get('/system/project-id', (_, res) => {
  //   res.status(404).send('404 Not Found');
  // });
  app.get('/system/cluster-name', (_, res) => {
    res.send('mock-cluster-name');
  });
  app.get('/system/project-id', (_, res) => {
    res.send('mock-project-id');
  });

  app.all(v1beta1Prefix + '*', (req, res) => {
    res.status(404).send('Bad request endpoint.');
  });
};
