/**
 * @license
 * Copyright 2018 Google LLC
 *
 * 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
 *
 * https://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 execa from 'execa';
import {Options} from 'execa';
import * as fs from 'mz/fs';
import * as path from 'path';
import terminalLink from 'terminal-link';
import {PWD} from '../constants';
import * as files from '../files';
import {AuthType, ConnectorConfig, Template} from '../types';
import * as util from '../util';
import {format} from '../util';
import * as appsscript from './appsscript';
import * as validation from './validation';

const OAUTH2_LIBRARY = {
  userSymbol: 'OAuth2',
  libraryId: '1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF',
  version: '33',
};

const getTemplates = (config: ConnectorConfig): Template[] => {
  return [
    {match: /{{MANIFEST_NAME}}/, replace: config.projectName},
    {match: /{{MANIFEST_LOGO_URL}}/, replace: config.manifestLogoUrl!},
    {match: /{{MANIFEST_COMPANY}}/, replace: config.manifestCompany!},
    {match: /{{MANIFEST_COMPANY_URL}}/, replace: config.manifestCompanyUrl!},
    {match: /{{MANIFEST_ADDON_URL}}/, replace: config.manifestAddonUrl!},
    {match: /{{MANIFEST_SUPPORT_URL}}/, replace: config.manifestSupportUrl!},
    {match: /{{MANIFEST_DESCRIPTION}}/, replace: config.manifestDescription!},
    {
      match: /{{MANIFEST_SOURCES}}/,
      replace: `[${config
        .manifestSources!.split(',')
        .map((a: string) => `"${a}"`)
        .join(',')}]`,
    },
  ];
};

const ensureAuthenticated = async (execOptions: Options): Promise<void> => {
  const authenticated = await util.spinnify(
    'Ensuring clasp is authenticated...',
    async () => {
      return validation.claspAuthenticated();
    }
  );
  if (authenticated === false) {
    const infoText = format.yellow(
      'Clasp must be globally authenticated for dscc-gen.'
    );
    const claspLogin = format.green('npx @google/clasp login');
    console.log(`${infoText}\nrunning ${claspLogin} ...\n`);
    await execa(
      'npx',
      ['@google/clasp', 'login'],
      Object.assign({}, execOptions, {stdio: 'inherit'})
    );
  }
};

const installDependencies = async (
  projectPath: string,
  config: ConnectorConfig
) => {
  return util.spinnify('Installing project dependencies...', async () =>
    util.npmInstall(projectPath, config)
  );
};

const createAppsScriptProject = async (
  projectPath: string,
  projectName: string,
  execOptions: Options,
  config: ConnectorConfig
): Promise<void> => {
  return util.spinnify('Creating Apps Script project...', async () => {
    await appsscript.create(projectPath, projectName);
    // Since clasp creating a new project overwrites the manifest, we want to
    // copy the template manifest over the one generated by clasp.
    files.mv(
      [execOptions.cwd!, 'temp', 'appsscript.json'],
      [execOptions.cwd!, 'src']
    );
    if (config.authType === AuthType.OAUTH2) {
      const fileOptions = {encoding: 'utf8'};
      const manifestPath = path.resolve(projectPath, 'src', 'appsscript.json');
      const manifestString = await fs.readFile(manifestPath, fileOptions);
      const manifest = JSON.parse(manifestString);
      // Add the OAUTH2_LIBRARY dependency.
      manifest.dependencies.libraries.push(OAUTH2_LIBRARY);
      await fs.writeFile(
        manifestPath,
        JSON.stringify(manifest, undefined, '  '),
        fileOptions
      );
    }
    await appsscript.push(projectPath);
  });
};

const cloneAppsScriptProject = async (
  projectPath: string,
  config: ConnectorConfig
): Promise<void> => {
  const scriptId = config.scriptId!;
  if (config.ts === true) {
    // The user is trying to migrate an existing project to be an appsscript
    // one.
    return util.spinnify('Cloning existing project...', async () => {
      await appsscript.clone(projectPath, scriptId, 'old_js');
      files.cp(
        [projectPath, 'old_js', 'appsscript.json'],
        [projectPath, 'src', 'appsscript.json']
      );
    });
  } else {
    // We don't need the template source files since we want the Apps Scripts project's
    return util.spinnify('Cloning existing project...', async () => {
      files.remove(projectPath, 'src');
      await appsscript.clone(projectPath, scriptId, 'src');
    });
  }
};

const manageDeployments = async (
  projectPath: string,
  config: ConnectorConfig
) => {
  let productionDeploymentId: string | undefined;
  if (config.scriptId !== undefined) {
    // See if there is already a 'Production' deployment.
    productionDeploymentId = await util.spinnify(
      'Checking for a production deployment',
      async () => {
        return appsscript.getDeploymentIdByName(projectPath, 'Production');
      }
    );
  }
  if (productionDeploymentId === undefined) {
    productionDeploymentId = await util.spinnify(
      'Creating a production deployment',
      async () => {
        return await appsscript.deploy(projectPath, 'Production');
      }
    );
  }
  const latestDeploymentId = await util.spinnify(
    'Getting the latest deploymentId',
    async () => {
      return await appsscript.getDeploymentIdByName(projectPath, '@HEAD');
    }
  );
  if (latestDeploymentId === undefined) {
    throw new Error(
      `Wasn't able to get the latest deploymentId. This is probably a bug with dscc-gen.`
    );
  }
  return util.spinnify('Updating templates with your values...', async () => {
    return files.fixTemplates(projectPath, [
      {match: /{{PRODUCTION_DEPLOYMENT_ID}}/, replace: productionDeploymentId!},
      {match: /{{LATEST_DEPLOYMENT_ID}}/, replace: latestDeploymentId!},
    ]);
  });
};

type AuthTypeFileMap = {[TKey in AuthType]: string};
const authTypeToFile: AuthTypeFileMap = {
  [AuthType.NONE]: 'NONE_auth',
  [AuthType.USER_PASS]: 'USER_PASS_auth',
  [AuthType.USER_TOKEN]: 'USER_TOKEN_auth',
  [AuthType.OAUTH2]: 'OAUTH2_auth',
  [AuthType.KEY]: 'KEY_auth',
  [AuthType.PATH_USER_PASS]: 'PATH_USER_PASS_auth',
};

const removeExcessAuthFiles = async (
  projectPath: string,
  config: ConnectorConfig
) => {
  const extension = config.ts ? '.ts' : '.js';
  const chosenAuthType = config.authType;
  return Promise.all(
    Object.values(AuthType).map(async (authType: AuthType) => {
      const authFile = authTypeToFile[authType] + extension;
      if (authType !== chosenAuthType) {
        return files.remove(projectPath, 'src', authFile);
      } else {
        const projectAuthFile = 'auth' + extension;
        return files.rename(
          [projectPath, 'src', authFile],
          [projectPath, 'src', projectAuthFile]
        );
      }
    })
  );
};

export const createFromTemplate = async (
  config: ConnectorConfig
): Promise<number> => {
  const {projectName, basePath} = config;
  const templatePath = path.join(
    basePath,
    'templates',
    config.projectChoice.toString() + (config.ts ? '-ts' : '')
  );
  const projectPath = path.join(PWD, projectName);
  await files.createAndCopyFiles(projectPath, templatePath, projectName);
  try {
    await util.spinnify('Updating templates with your values...', async () => {
      await files.fixTemplates(projectPath, getTemplates(config));
      await removeExcessAuthFiles(projectPath, config);
    });

    const execOptions: Options = {cwd: projectPath};

    await installDependencies(projectPath, config);
    await ensureAuthenticated(execOptions);

    if (config.scriptId !== undefined) {
      await cloneAppsScriptProject(projectPath, config);
    } else {
      await createAppsScriptProject(
        projectPath,
        projectName,
        execOptions,
        config
      );
    }
    await manageDeployments(projectPath, config);

    // Remove temp directory.
    files.remove(projectPath, 'temp');

    const connectorOverview = format.blue(
      terminalLink(
        'connector overview',
        'https://developers.google.com/datastudio/connector/'
      )
    );
    const styledProjectName = format.green(projectName);
    const cdDirection = format.yellow(`cd ${projectName}`);
    const runCmd = config.yarn ? 'yarn' : 'npm run';
    const open = format.red(`${runCmd} open`);
    const push = format.blue(`${runCmd} push`);
    const watch = format.green(`${runCmd} watch`);
    const prettier = format.yellow(`${runCmd} prettier`);
    const tryLatest = format.red(`${runCmd} try_latest`);
    const tryProduction = format.blue(`${runCmd} try_production`);
    const updateProduction = format.green(`${runCmd} update_production`);

    console.log(
      `\
Created a new community connector: ${styledProjectName}\n\
\n\
If this is your first connector, see ${connectorOverview}\n\
\n\
${cdDirection} to start working on your connector\n\
\n\
Scripts are provided to simplify development:\n\
\n\
${open} - open your project in Apps Script.\n\
${push} - push your local changes to Apps Script.\n\
${watch} - watches for local changes & pushes them to Apps Script.\n\
${prettier} - formats your code using community standards.\n\
${tryLatest} - opens the deployment with your latest code.\n\
${tryProduction} - opens your production deployment.\n\
${updateProduction} - updates your production deployment to use the latest code.\n\
`
    );
    return 0;
  } catch (e) {
    files.remove(projectPath);
    throw e;
  }
};
