import { CliUsageError, CliTerseError } from '@alwaysai/alwayscli';
import * as crypto from 'crypto';
import { ALWAYSAI_CLI_EXECUTABLE_NAME } from '../../constants';
import { checkUserIsLoggedInComponent } from '../user';
import {
  runWithSpinner,
  JsSpawner,
  tarFiles,
  streamToBuffer
} from '../../util';
import { appCheckComponent } from './app-check-component';
import { appConfigureComponent } from './app-configure-component';
import { getCurrentProjectId, ProjectJsonFile } from '../../core/project';
import {
  insertAppRecord,
  getPresignedUrlAppUpload,
  usePresignedUrlAppUpload
} from '../../infrastructure';
import { APP_IGNORE_FILES } from './app-install-component';
import { checkForInvalidModelsComponent } from './models/app-invalid-models-check-component';

type ApplicationData = {
  filename: string;
  tarBuffer: Buffer;
  releaseManifest: string;
  projectId: string;
  releaseHash: string;
};

export async function appPublishComponent(props: {
  yes: boolean;
  name?: string;
  excludes?: string[];
}) {
  const { yes, name, excludes } = props;

  await checkUserIsLoggedInComponent({ yes });
  try {
    await appCheckComponent({ ignoreTargetJsonFile: true });
  } 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 invalidModels = await checkForInvalidModelsComponent();
  if (Object.keys(invalidModels).length > 0) {
    throw new CliTerseError(
      `You do not have permission to use the following models, or the model version does not exist:\n\n` +
        `${Object.entries(invalidModels)
          .map(([model, version]) => `- ${model}: version ${version}`)
          .join('\n')}\n\n` +
        `Please remove these models before publishing the application again.`
    );
  }

  const applicationPackage = await runWithSpinner(
    createApplicationFiles,
    [{ excludes: excludes || [] }],
    'Create application package'
  );
  await runWithSpinner(
    publishApplicationPackage,
    [applicationPackage, name],
    'Publish application package'
  );

  return applicationPackage.releaseHash;
}

async function createApplicationFiles(params: { excludes: string[] }) {
  const { excludes } = params;
  // Get project ID
  const projectJsonFile = ProjectJsonFile().read();
  const projectId = projectJsonFile.project
    ? projectJsonFile.project.id
    : undefined;
  if (projectId === undefined) {
    throw new CliTerseError('Please set up a project for this app');
  }

  // Put src in tarball
  const spawner = JsSpawner();
  const ignore = APP_IGNORE_FILES.concat(['alwaysai.target.json'], excludes);
  const tarfile = await tarFiles(spawner, ignore);
  const tarBuffer = await streamToBuffer(tarfile);

  // Generate release hash
  const hashSum = crypto.createHash('sha256');
  hashSum.update(tarBuffer);
  const releaseHash = hashSum.digest('hex');

  const releaseDate = new Date();
  const filename = `${projectId}/${releaseHash}.tgz`;

  // Generate release manifest
  const releaseData = {
    releaseHash,
    releaseDate,
    filename
  };
  const releaseManifest = JSON.stringify(releaseData, null, 2);
  const applicationPackage: ApplicationData = {
    filename,
    tarBuffer,
    releaseManifest,
    projectId,
    releaseHash
  };
  return applicationPackage;
}

async function publishApplicationPackage(
  applicationPackage: ApplicationData,
  name?: string
) {
  const fileData = {
    tarFileName: applicationPackage.filename,
    tarFile: applicationPackage.tarBuffer,
    releaseManifestName: `${applicationPackage.projectId}/release.json`,
    releaseManifestFile: applicationPackage.releaseManifest
  };

  const presignedUrlTarFile = await getPresignedUrlAppUpload(
    fileData.tarFileName,
    'application/octet-stream'
  );

  const presignedUrlManifestFile = await getPresignedUrlAppUpload(
    fileData.releaseManifestName,
    'application/octet-stream'
  );

  await usePresignedUrlAppUpload(
    presignedUrlTarFile.uploadURL,
    fileData.tarFile
  );
  await usePresignedUrlAppUpload(
    presignedUrlManifestFile.uploadURL,
    fileData.releaseManifestFile
  );

  const record = {
    hash: applicationPackage.releaseHash,
    s3Path: applicationPackage.filename,
    projectId: await getCurrentProjectId(),
    name: name ?? ''
  };
  await insertAppRecord(record);
}
