import { existsSync } from 'fs';
import { CliUsageError, CliTerseError } from '@alwaysai/alwayscli';
import { buildDockerImageComponent } from '../docker';
import { checkUserIsLoggedInComponent } from '../user';
import { appCheckComponent } from './app-check-component';
import { appCleanComponent } from './app-clean-component';
import { appConfigureComponent } from './app-configure-component';
import { createTargetDirectoryComponent } from './target';
import { appInstallModelsComponent } from './models/app-install-models-component';
import {
  ALWAYSAI_CLI_EXECUTABLE_NAME,
  DOCKER_TEST_IMAGE_ID,
  PLEASE_REPORT_THIS_ERROR_MESSAGE
} from '../../constants';
import {
  JsSpawner,
  SshSpawner,
  runWithSpinner,
  logger,
  Spawner,
  Spinner,
  SshDockerSpawner,
  copyFiles,
  stringifyError
} from '../../util';
import {
  TargetJsonFile,
  getTargetHardwareUuid,
  TargetConfig,
  TargetJsonFileReturnType,
  getPythonVenvPaths,
  createPythonVenv,
  installPythonReqs
} from '../../core/app';
import { getDeviceByUuid } from '../../infrastructure';
import {
  connectBySshComponent,
  findOrWritePrivateKeyFileComponent
} from '../general';
import { PYTHON_REQUIREMENTS_FILE_NAME } from '../../paths';

export const APP_IGNORE_FILES = ['models', 'node_modules', '.git', 'venv'];

export async function appInstallComponent(props: {
  yes: boolean;
  clean: boolean;
  pull: boolean;
  source: boolean;
  models: boolean;
  docker: boolean;
  venv: boolean;
  excludes?: string[];
}) {
  const { yes, clean, pull, source, models, docker, venv, excludes } = props;
  const steps: string[] = [];
  // When any flag is set, only add steps determined by flags
  if ([source, models, docker, venv].some((element) => element === true)) {
    if (source) {
      steps.push('source');
    }
    if (models) {
      steps.push('models');
    }
    if (docker) {
      steps.push('docker');
    }
    if (venv) {
      steps.push('venv');
    }
  } else {
    // Otherwise, add all steps
    steps.push(...['source', 'models', 'docker', 'venv']);
  }
  await checkUserIsLoggedInComponent({ yes });
  try {
    await appCheckComponent();
  } catch (err) {
    if (yes) {
      throw new CliUsageError(
        `App is not properly configured. Did you run \`${ALWAYSAI_CLI_EXECUTABLE_NAME} app configure\`?`
      );
    } else {
      await appConfigureComponent({ yes });
    }
  }

  const targetJsonFile = TargetJsonFile();
  const targetHostSpawner = targetJsonFile.readHostSpawner();
  const targetCfg = targetJsonFile.read();
  const sourceSpawner = JsSpawner();

  switch (targetCfg.targetProtocol) {
    case 'native:':
    case 'docker:': {
      if (clean) {
        await appCleanComponent({ yes });
      }
      break;
    }
    case 'ssh+docker:': {
      const { targetHostname, targetPath } = targetCfg;
      await findOrWritePrivateKeyFileComponent({ yes });
      await connectBySshComponent({ targetHostname });
      if (clean) {
        await appCleanComponent({ yes });
      }
      await createTargetDirectoryComponent({ targetHostname, targetPath });

      const projectDevice = targetCfg.deviceId;
      let getDevice;
      try {
        getDevice = await getDeviceByUuid({
          uuid: projectDevice
        });
      } catch (error) {
        logger.error(stringifyError(error));
        throw new CliTerseError(
          'Device does not exist in the selected project.'
        );
      }

      const hardwareId = await getTargetHardwareUuid(
        SshSpawner({ targetHostname })
      );

      if (hardwareId !== getDevice.hardware_ids) {
        throw new CliTerseError(
          `Target device does not match the one selected. Please run ${ALWAYSAI_CLI_EXECUTABLE_NAME} app configure again.`
        );
      }
      break;
    }
    default:
  }

  if (steps.includes('source')) {
    await installSource({
      targetCfg,
      sourceSpawner,
      targetHostSpawner,
      excludes
    });
  }

  if (steps.includes('docker')) {
    await buildDocker({
      targetCfg,
      targetJsonFile,
      targetHostSpawner,
      pull
    });
  }

  if (steps.includes('models')) {
    await installModels({ targetJsonFile });
  }

  if (steps.includes('venv')) {
    await installVenv({
      targetCfg,
      sourceSpawner,
      targetJsonFile
    });
  }
}

async function installSource(props: {
  targetCfg: TargetConfig;
  sourceSpawner: Spawner;
  targetHostSpawner: Spawner;
  excludes?: string[];
}) {
  const { targetCfg, sourceSpawner, targetHostSpawner } = props;
  switch (targetCfg.targetProtocol) {
    case 'ssh+docker:': {
      const { targetHostname, targetPath } = targetCfg;
      const busyboxSpawner = SshDockerSpawner({
        targetHostname,
        targetPath,
        dockerImageId: DOCKER_TEST_IMAGE_ID
      });

      const excludes = props.excludes
        ? props.excludes.concat(APP_IGNORE_FILES)
        : APP_IGNORE_FILES;

      await runWithSpinner(
        async () => {
          await copyFiles(sourceSpawner, busyboxSpawner, excludes);
          logger.debug(
            await targetHostSpawner.run({
              exe: 'docker',
              args: [
                'run',
                '--rm',
                '--workdir',
                '/app',
                '--volume',
                '$(pwd):/app',
                DOCKER_TEST_IMAGE_ID,
                'chown',
                '-R',
                '$(id -u ${USER}):$(id -g ${USER})',
                '/app'
              ],
              cwd: '.'
            })
          );
        },
        [],
        'Copy application to target'
      );
      break;
    }
    default:
  }
}

export async function buildDocker(props: {
  targetCfg: TargetConfig;
  targetJsonFile: TargetJsonFileReturnType;
  targetHostSpawner: Spawner;
  pull: boolean;
}) {
  const { targetCfg, targetJsonFile, targetHostSpawner, pull } = props;
  switch (targetCfg.targetProtocol) {
    case 'docker:':
    case 'ssh+docker:': {
      const targetHardware = targetCfg.targetHardware;
      const dockerImageId = await buildDockerImageComponent({
        targetHostSpawner,
        targetHardware,
        pullBaseImage: pull
      });
      targetCfg.dockerImageId = dockerImageId;
      targetJsonFile.update((targetCfg) => {
        switch (targetCfg.targetProtocol) {
          case 'docker:':
          case 'ssh+docker:': {
            targetCfg.dockerImageId = dockerImageId;
            break;
          }

          case 'native:':
          default: {
            throw new CliTerseError(
              `Invalid target protocol (${targetCfg.targetProtocol})! ${PLEASE_REPORT_THIS_ERROR_MESSAGE}`
            );
          }
        }
      });
    }
  }
}

async function installModels(props: {
  targetJsonFile: TargetJsonFileReturnType;
}) {
  const { targetJsonFile } = props;
  const targetSpawner = targetJsonFile.readHostSpawner();
  await appInstallModelsComponent(targetSpawner);
}

export async function installVenv(props: {
  targetCfg: TargetConfig;
  sourceSpawner: Spawner;
  targetJsonFile: TargetJsonFileReturnType;
}) {
  const { targetCfg, sourceSpawner, targetJsonFile } = props;
  const pythonVenvPaths = await getPythonVenvPaths({ targetCfg });
  let targetSpawner: Spawner;
  switch (targetCfg.targetProtocol) {
    case 'native:': {
      targetSpawner = sourceSpawner;
      break;
    }
    case 'docker:':
    case 'ssh+docker:': {
      targetSpawner = targetJsonFile.readContainerSpawner({
        ignoreTargetHardware: true
      });
      break;
    }
    default:
      throw new CliTerseError(
        `Invalid target protocol(${targetCfg})! ${PLEASE_REPORT_THIS_ERROR_MESSAGE}`
      );
  }

  const spinner = Spinner('Create python virtual environment');
  try {
    const installed = await createPythonVenv({
      targetSpawner,
      pythonVenvPaths,
      logger
    });
    if (installed === false) {
      spinner.succeed('Found python virtual environment');
    } else {
      spinner.succeed();
    }
  } catch (exception) {
    spinner.fail();
    throw exception;
  }

  if (existsSync(PYTHON_REQUIREMENTS_FILE_NAME)) {
    await runWithSpinner(
      installPythonReqs,
      [
        {
          reqFilePath: PYTHON_REQUIREMENTS_FILE_NAME,
          targetSpawner,
          pythonVenvPaths,
          logger
        }
      ],
      'Install python dependencies'
    );
  }
}
