import * as plugins from './plugins.js';
import * as paths from './paths.js';
import { logger } from './logger.js';
import type { DockerHost } from './classes.host.js';

const smartfileFactory = plugins.smartfile.SmartFileFactory.nodeFs();

export interface IDockerImageStoreConstructorOptions {
  /**
   * used for preparing images for longer term storage
   */
  localDirPath: string;
  /**
   * a smartbucket dir for longer term storage.
   */
  bucketDir: plugins.smartbucket.Directory;
}

export class DockerImageStore {
  public options: IDockerImageStoreConstructorOptions;

  constructor(optionsArg: IDockerImageStoreConstructorOptions) {
    this.options = optionsArg;
  }

  // Method to store tar stream
  public async storeImage(
    imageName: string,
    tarStream: plugins.smartstream.stream.Readable,
  ): Promise<void> {
    logger.log('info', `Storing image ${imageName}...`);
    const uniqueProcessingId = plugins.smartunique.shortId();

    const initialTarDownloadPath = plugins.path.join(
      this.options.localDirPath,
      `${uniqueProcessingId}.tar`,
    );
    const extractionDir = plugins.path.join(
      this.options.localDirPath,
      uniqueProcessingId,
    );
    // Create a write stream to store the tar file
    const writeStream = plugins.fs.createWriteStream(initialTarDownloadPath);

    // lets wait for the write stream to finish
    await new Promise<void>((resolve, reject) => {
      tarStream.pipe(writeStream);
      writeStream.on('finish', () => resolve());
      writeStream.on('error', reject);
    });
    logger.log(
      'info',
      `Image ${imageName} stored locally for processing. Extracting...`,
    );

    // lets process the image
    await plugins.smartarchive.SmartArchive.create()
      .file(initialTarDownloadPath)
      .extract(extractionDir);
    logger.log('info', `Image ${imageName} extracted.`);
    await plugins.fs.promises.rm(initialTarDownloadPath, { force: true });
    logger.log('info', `deleted original tar to save space.`);
    logger.log('info', `now repackaging for s3...`);
    const smartfileIndexJson = await smartfileFactory.fromFilePath(
      plugins.path.join(extractionDir, 'index.json'),
    );
    const smartfileManifestJson = await smartfileFactory.fromFilePath(
      plugins.path.join(extractionDir, 'manifest.json'),
    );
    const smartfileOciLayoutJson = await smartfileFactory.fromFilePath(
      plugins.path.join(extractionDir, 'oci-layout'),
    );

    // repositories file is optional in OCI image tars
    const repositoriesPath = plugins.path.join(extractionDir, 'repositories');
    const hasRepositories = plugins.fs.existsSync(repositoriesPath);
    const smartfileRepositoriesJson = hasRepositories
      ? await smartfileFactory.fromFilePath(repositoriesPath)
      : null;

    const indexJson = JSON.parse(smartfileIndexJson.contents.toString());
    const manifestJson = JSON.parse(smartfileManifestJson.contents.toString());
    const ociLayoutJson = JSON.parse(
      smartfileOciLayoutJson.contents.toString(),
    );

    if (indexJson.manifests?.[0]?.annotations) {
      indexJson.manifests[0].annotations['io.containerd.image.name'] = imageName;
    }
    if (manifestJson?.[0]?.RepoTags) {
      manifestJson[0].RepoTags[0] = imageName;
    }

    if (smartfileRepositoriesJson) {
      const repositoriesJson = JSON.parse(
        smartfileRepositoriesJson.contents.toString(),
      );
      const repoFirstKey = Object.keys(repositoriesJson)[0];
      const repoFirstValue = repositoriesJson[repoFirstKey];
      repositoriesJson[imageName] = repoFirstValue;
      delete repositoriesJson[repoFirstKey];
      smartfileRepositoriesJson.contents = Buffer.from(
        JSON.stringify(repositoriesJson, null, 2),
      );
    }

    smartfileIndexJson.contents = Buffer.from(
      JSON.stringify(indexJson, null, 2),
    );
    smartfileManifestJson.contents = Buffer.from(
      JSON.stringify(manifestJson, null, 2),
    );
    smartfileOciLayoutJson.contents = Buffer.from(
      JSON.stringify(ociLayoutJson, null, 2),
    );

    const writePromises = [
      smartfileIndexJson.write(),
      smartfileManifestJson.write(),
      smartfileOciLayoutJson.write(),
    ];
    if (smartfileRepositoriesJson) {
      writePromises.push(smartfileRepositoriesJson.write());
    }
    await Promise.all(writePromises);

    logger.log('info', 'repackaging archive for s3...');
    const tartools = new plugins.smartarchive.TarTools();
    const newTarPack = await tartools.getDirectoryPackStream(extractionDir);
    const finalTarName = `${uniqueProcessingId}.processed.tar`;
    const finalTarPath = plugins.path.join(
      this.options.localDirPath,
      finalTarName,
    );
    const finalWriteStream = plugins.fs.createWriteStream(finalTarPath);
    await new Promise<void>((resolve, reject) => {
      newTarPack.pipe(finalWriteStream);
      finalWriteStream.on('finish', () => resolve());
      finalWriteStream.on('error', reject);
    });
    logger.log('ok', `Repackaged image ${imageName} for s3.`);
    await plugins.fs.promises.rm(extractionDir, { recursive: true, force: true });
    // Remove existing file in bucket if it exists (smartbucket v4 no longer silently overwrites)
    try {
      await this.options.bucketDir.fastRemove({ path: `${imageName}.tar` });
    } catch (e) {
      // File may not exist, which is fine
    }
    const finalTarReadStream = plugins.fs.createReadStream(finalTarPath);
    await this.options.bucketDir.fastPutStream({
      stream: finalTarReadStream,
      path: `${imageName}.tar`,
    });
    await plugins.fs.promises.rm(finalTarPath, { force: true });
  }

  public async start() {
    // Ensure the local directory exists and is empty
    await plugins.fs.promises.rm(this.options.localDirPath, { recursive: true, force: true });
    await plugins.fs.promises.mkdir(this.options.localDirPath, { recursive: true });
  }

  public async stop() {}

  // Method to retrieve tar stream
  public async getImage(
    imageName: string,
  ): Promise<plugins.smartstream.stream.Readable> {
    const imagePath = plugins.path.join(
      this.options.localDirPath,
      `${imageName}.tar`,
    );

    if (!plugins.fs.existsSync(imagePath)) {
      throw new Error(`Image ${imageName} does not exist.`);
    }

    return plugins.fs.createReadStream(imagePath);
  }
}
