import * as plugins from './plugins.js';
import * as interfaces from './interfaces/index.js';
import { DockerHost } from './classes.host.js';
import { DockerResource } from './classes.base.js';
import { logger } from './logger.js';

/**
 * represents a docker image on the remote docker host
 */
export class DockerImage extends DockerResource {
  // STATIC (Internal - prefixed with _ to indicate internal use)

  /**
   * Internal: Get all images
   * Public API: Use dockerHost.listImages() instead
   */
  public static async _list(dockerHost: DockerHost) {
    const images: DockerImage[] = [];
    const response = await dockerHost.request('GET', '/images/json');
    for (const imageObject of response.body) {
      images.push(new DockerImage(dockerHost, imageObject));
    }
    return images;
  }

  /**
   * Internal: Get image by name
   * Public API: Use dockerHost.getImageByName(name) instead
   */
  public static async _fromName(
    dockerHost: DockerHost,
    imageNameArg: string,
  ) {
    const images = await this._list(dockerHost);
    const result = images.find((image) => {
      if (image.RepoTags) {
        return image.RepoTags.includes(imageNameArg);
      } else {
        return false;
      }
    });
    return result;
  }

  /**
   * Internal: Create image from registry
   * Public API: Use dockerHost.createImageFromRegistry(descriptor) instead
   */
  public static async _createFromRegistry(
    dockerHostArg: DockerHost,
    optionsArg: {
      creationObject: interfaces.IImageCreationDescriptor;
    },
  ): Promise<DockerImage> {
    // lets create a sanatized imageUrlObject
    const imageUrlObject: {
      imageUrl: string;
      imageTag: string;
      imageOriginTag: string;
    } = {
      imageUrl: optionsArg.creationObject.imageUrl,
      imageTag: optionsArg.creationObject.imageTag ?? '',
      imageOriginTag: '',
    };
    if (imageUrlObject.imageUrl.includes(':')) {
      const imageUrl = imageUrlObject.imageUrl.split(':')[0];
      const imageTag = imageUrlObject.imageUrl.split(':')[1];
      if (imageUrlObject.imageTag) {
        throw new Error(
          `imageUrl ${imageUrlObject.imageUrl} can't be tagged with ${imageUrlObject.imageTag} because it is already tagged with ${imageTag}`,
        );
      } else {
        imageUrlObject.imageUrl = imageUrl;
        imageUrlObject.imageTag = imageTag;
      }
    } else if (!imageUrlObject.imageTag) {
      imageUrlObject.imageTag = 'latest';
    }
    imageUrlObject.imageOriginTag = `${imageUrlObject.imageUrl}:${imageUrlObject.imageTag}`;

    // lets actually create the image
    const response = await dockerHostArg.request(
      'POST',
      `/images/create?fromImage=${encodeURIComponent(
        imageUrlObject.imageUrl,
      )}&tag=${encodeURIComponent(imageUrlObject.imageTag)}`,
    );
    if (response.statusCode < 300) {
      logger.log(
        'info',
        `Successfully pulled image ${imageUrlObject.imageUrl} from the registry`,
      );
      const image = await DockerImage._fromName(
        dockerHostArg,
        imageUrlObject.imageOriginTag,
      );
      if (!image) {
        throw new Error(`Image ${imageUrlObject.imageOriginTag} not found after pull`);
      }
      return image;
    } else {
      // Pull failed — check if the image already exists locally
      const existingImage = await DockerImage._fromName(
        dockerHostArg,
        imageUrlObject.imageOriginTag,
      );
      if (existingImage) {
        logger.log(
          'warn',
          `Pull failed for ${imageUrlObject.imageUrl}, using locally cached image`,
        );
        return existingImage;
      }
      throw new Error(`Failed to pull image ${imageUrlObject.imageOriginTag} and no local copy exists`);
    }
  }

  /**
   * Internal: Create image from tar stream
   * Public API: Use dockerHost.createImageFromTarStream(stream, descriptor) instead
   */
  public static async _createFromTarStream(
    dockerHostArg: DockerHost,
    optionsArg: {
      creationObject: interfaces.IImageCreationDescriptor;
      tarStream: plugins.smartstream.stream.Readable;
    },
  ): Promise<DockerImage> {
    // Start the request for importing an image
    const response = await dockerHostArg.requestStreaming(
      'POST',
      '/images/load',
      optionsArg.tarStream,
    );

    // requestStreaming now returns Node.js stream
    const nodeStream = response as plugins.smartstream.stream.Readable;

    /**
     * Docker typically returns lines like:
     * {"stream":"Loaded image: myrepo/myimage:latest"}
     *
     * So we will collect those lines and parse out the final image name.
     */
    let rawOutput = '';
    nodeStream.on('data', (chunk) => {
      rawOutput += chunk.toString();
    });

    // Wrap the end event in a Promise for easier async/await usage
    await new Promise<void>((resolve, reject) => {
      nodeStream.on('end', () => {
        resolve();
      });
      nodeStream.on('error', (err) => {
        reject(err);
      });
    });

    // Attempt to parse each line to find something like "Loaded image: ..."
    let loadedImageTag: string | undefined;
    const lines = rawOutput.trim().split('\n').filter(Boolean);

    for (const line of lines) {
      try {
        const jsonLine = JSON.parse(line);
        if (
          jsonLine.stream &&
          (jsonLine.stream.startsWith('Loaded image:') ||
            jsonLine.stream.startsWith('Loaded image ID:'))
        ) {
          // Examples:
          // "Loaded image: your-image:latest"
          // "Loaded image ID: sha256:...."
          loadedImageTag = jsonLine.stream
            .replace('Loaded image: ', '')
            .replace('Loaded image ID: ', '')
            .trim();
        }
      } catch {
        // not valid JSON, ignore
      }
    }

    if (!loadedImageTag) {
      throw new Error(
        `Could not parse the loaded image info from Docker response.\nResponse was:\n${rawOutput}`,
      );
    }

    // Now try to look up that image by the "loadedImageTag".
    // Depending on Docker's response, it might be something like:
    //   "myrepo/myimage:latest"  OR  "sha256:someHash..."
    // If Docker gave you an ID (e.g. "sha256:..."), you may need a separate
    // DockerImage.getImageById method; or if you prefer, you can treat it as a name.
    const newlyImportedImage = await DockerImage._fromName(
      dockerHostArg,
      loadedImageTag,
    );

    if (!newlyImportedImage) {
      throw new Error(
        `Image load succeeded, but no local reference found for "${loadedImageTag}".`,
      );
    }

    logger.log('info', `Successfully imported image "${loadedImageTag}".`);

    return newlyImportedImage;
  }

  public static async tagImageByIdOrName(
    dockerHost: DockerHost,
    idOrNameArg: string,
    newTagArg: string,
  ) {
    const response = await dockerHost.request(
      'POST',
      `/images/${encodeURIComponent(idOrNameArg)}/${encodeURIComponent(newTagArg)}`,
    );
  }

  /**
   * Internal: Build image from Dockerfile
   * Public API: Use dockerHost.buildImage(tag) instead
   */
  public static async _build(dockerHostArg: DockerHost, dockerImageTag) {
    // TODO: implement building an image
  }

  // INSTANCE PROPERTIES
  /**
   * the tags for an image
   */
  public Containers!: number;
  public Created!: number;
  public Id!: string;
  public Labels!: interfaces.TLabels;
  public ParentId!: string;
  public RepoDigests!: string[];
  public RepoTags!: string[];
  public SharedSize!: number;
  public Size!: number;
  public VirtualSize!: number;

  constructor(dockerHostArg: DockerHost, dockerImageObjectArg: any) {
    super(dockerHostArg);
    Object.keys(dockerImageObjectArg).forEach((keyArg) => {
      this[keyArg] = dockerImageObjectArg[keyArg];
    });
  }

  // INSTANCE METHODS

  /**
   * Refreshes this image's state from the Docker daemon
   */
  public async refresh(): Promise<void> {
    if (!this.RepoTags || this.RepoTags.length === 0) {
      throw new Error('Cannot refresh image without RepoTags');
    }
    const updated = await DockerImage._fromName(this.dockerHost, this.RepoTags[0]);
    if (updated) {
      Object.assign(this, updated);
    }
  }

  /**
   * tag an image
   * @param newTag
   */
  public async tagImage(newTag) {
    throw new Error('.tagImage is not yet implemented');
  }

  /**
   * pulls the latest version from the registry
   */
  public async pullLatestImageFromRegistry(): Promise<boolean> {
    const updatedImage = await DockerImage._createFromRegistry(this.dockerHost, {
      creationObject: {
        imageUrl: this.RepoTags[0],
      },
    });
    Object.assign(this, updatedImage);
    // TODO: Compare image digists before and after
    return true;
  }

  /**
   * Removes this image from the Docker daemon
   */
  public async remove(options?: { force?: boolean; noprune?: boolean }): Promise<void> {
    const queryParams = new URLSearchParams();
    if (options?.force) queryParams.append('force', '1');
    if (options?.noprune) queryParams.append('noprune', '1');

    const queryString = queryParams.toString();
    const response = await this.dockerHost.request(
      'DELETE',
      `/images/${encodeURIComponent(this.Id)}${queryString ? '?' + queryString : ''}`,
    );

    if (response.statusCode >= 300) {
      throw new Error(`Failed to remove image: ${response.statusCode}`);
    }
  }

  // get stuff
  public async getVersion() {
    if (this.Labels && this.Labels.version) {
      return this.Labels.version;
    } else {
      return '0.0.0';
    }
  }

  /**
   * exports an image to a tar ball
   */
  public async exportToTarStream(): Promise<plugins.smartstream.stream.Readable> {
    logger.log('info', `Exporting image ${this.RepoTags[0]} to tar stream.`);
    const response = await this.dockerHost.requestStreaming(
      'GET',
      `/images/${encodeURIComponent(this.RepoTags[0])}/get`,
    );

    // requestStreaming now returns Node.js stream
    const nodeStream = response as plugins.smartstream.stream.Readable;

    let counter = 0;
    const webduplexStream = new plugins.smartstream.SmartDuplex({
      writeFunction: async (chunk, tools) => {
        if (counter % 1000 === 0) console.log(`Got chunk: ${counter}`);
        counter++;
        return chunk;
      },
    });

    nodeStream.on('data', (chunk) => {
      if (!webduplexStream.write(chunk)) {
        nodeStream.pause();
        webduplexStream.once('drain', () => {
          nodeStream.resume();
        });
      }
    });

    nodeStream.on('end', () => {
      webduplexStream.end();
    });

    nodeStream.on('error', (error) => {
      logger.log('error', `Error during image export: ${error.message}`);
      webduplexStream.destroy(error);
    });

    return webduplexStream;
  }
}
