import * as plugins from './plugins.js';
import { Readable, Writable } from 'stream';

type TStreamSource = (streamFile: StreamFile) => Promise<Readable | ReadableStream>;

/**
 * The StreamFile class represents a file as a stream.
 * It allows creating streams from a file path, a URL, or a buffer.
 * Use SmartFileFactory to create instances of this class.
 */
export class StreamFile {
  // STATIC

  public static async fromPath(filePath: string, smartFs?: any): Promise<StreamFile> {
    if (!smartFs) {
      throw new Error('No SmartFs instance available. Create StreamFile through SmartFileFactory.');
    }

    const streamSource: TStreamSource = async (streamFileArg) => {
      return await streamFileArg.smartFs.file(filePath).readStream();
    };
    const streamFile = new StreamFile(streamSource, filePath, smartFs);
    streamFile.multiUse = true;
    streamFile.byteLengthComputeFunction = async () => {
      const stats = await smartFs.file(filePath).stat();
      return stats.size;
    };
    return streamFile;
  }

  public static async fromUrl(url: string, smartFs?: any): Promise<StreamFile> {
    const streamSource: TStreamSource = async (streamFileArg) => {
      const response = await plugins.smartrequest.SmartRequest.create()
        .url(url)
        .get();
      const responseStream = response.stream();
      if (!responseStream) {
        throw new Error(`No response stream available for ${url}`);
      }
      return responseStream;
    };
    const streamFile = new StreamFile(streamSource, undefined, smartFs);
    streamFile.multiUse = true;
    streamFile.byteLengthComputeFunction = async () => {
      const response = await plugins.smartrequest.SmartRequest.create()
        .url(url)
        .accept('binary')
        .get();
      const buffer = Buffer.from(await response.arrayBuffer());
      return buffer.length;
    };
    return streamFile;
  }

  public static fromBuffer(
    buffer: Buffer,
    relativeFilePath?: string,
    smartFs?: any
  ): StreamFile {
    const streamSource: TStreamSource = async (streamFileArg) => {
      const stream = new Readable();
      stream.push(buffer);
      stream.push(null); // End of stream
      return stream;
    };
    const streamFile = new StreamFile(streamSource, relativeFilePath, smartFs);
    streamFile.multiUse = true;
    streamFile.byteLengthComputeFunction = async () => buffer.length;
    return streamFile;
  }

  /**
   * Creates a StreamFile from an existing Readable stream with an option for multiple uses.
   * @param stream A Node.js Readable stream.
   * @param relativeFilePath Optional file path for the stream.
   * @param multiUse If true, the stream can be read multiple times, caching its content.
   * @param smartFs Optional SmartFs instance for filesystem operations
   * @returns A StreamFile instance.
   */
  public static fromStream(
    stream: Readable,
    relativeFilePath?: string,
    multiUse: boolean = false,
    smartFs?: any
  ): StreamFile {
    const streamSource: TStreamSource = (streamFileArg) => {
      if (streamFileArg.multiUse) {
        // If multi-use is enabled and we have cached content, create a new readable stream from the buffer
        const bufferedStream = new Readable();
        bufferedStream.push(streamFileArg.cachedStreamBuffer);
        bufferedStream.push(null); // No more data to push
        return Promise.resolve(bufferedStream);
      } else {
        return Promise.resolve(stream);
      }
    };

    const streamFile = new StreamFile(streamSource, relativeFilePath, smartFs);
    streamFile.multiUse = multiUse;

    // If multi-use is enabled, cache the stream when it's first read
    if (multiUse) {
      const chunks: Buffer[] = [];
      stream.on('data', (chunk) => chunks.push(Buffer.from(chunk)));
      stream.on('end', () => {
        streamFile.cachedStreamBuffer = Buffer.concat(chunks);
      });
      // It's important to handle errors that may occur during streaming
      stream.on('error', (err) => {
        console.error('Error while caching stream:', err);
      });
    }

    return streamFile;
  }

  // INSTANCE
  relativeFilePath?: string;
  private streamSource: TStreamSource;
  private smartFs?: any;

  // enable stream based multi use
  private cachedStreamBuffer?: Buffer;
  public multiUse: boolean = false;
  public used: boolean = false;
  public byteLengthComputeFunction?: () => Promise<number>;

  private constructor(streamSource: TStreamSource, relativeFilePath?: string, smartFs?: any) {
    this.streamSource = streamSource;
    this.relativeFilePath = relativeFilePath;
    this.smartFs = smartFs;
  }

  // METHODS

  private checkMultiUse() {
    if (!this.multiUse && this.used) {
      throw new Error('This stream can only be used once.');
    }
    this.used = true;
  }

  /**
   * Creates a new readable stream from the source.
   */
  public async createReadStream(): Promise<Readable> {
    const stream = await this.streamSource(this);
    
    // Check if it's a Web ReadableStream and convert to Node.js Readable
    if (stream && typeof (stream as any).getReader === 'function') {
      // This is a Web ReadableStream, convert it to Node.js Readable
      return Readable.fromWeb(stream as any);
    }
    
    // It's already a Node.js Readable stream
    return stream as Readable;
  }

  /**
   * Writes the stream to the disk at the specified path.
   * @param filePathArg The file path where the stream should be written.
   */
  public async writeToDisk(filePathArg: string): Promise<void> {
    if (!this.smartFs) {
      throw new Error('No SmartFs instance available. Create StreamFile through SmartFileFactory.');
    }

    this.checkMultiUse();
    const readStream = await this.createReadStream();
    let writeStream = await this.smartFs.file(filePathArg).writeStream();

    // Check if it's a Web WritableStream and convert to Node.js Writable
    if (writeStream && typeof (writeStream as any).getWriter === 'function') {
      // This is a Web WritableStream, convert it to Node.js Writable
      writeStream = Writable.fromWeb(writeStream as any);
    }

    return new Promise((resolve, reject) => {
      readStream.pipe(writeStream);
      readStream.on('error', reject);
      writeStream.on('error', reject);
      writeStream.on('finish', resolve);
    });
  }

  public async writeToDir(dirPathArg: string) {
    if (!this.smartFs) {
      throw new Error('No SmartFs instance available. Create StreamFile through SmartFileFactory.');
    }

    this.checkMultiUse();
    if (!this.relativeFilePath) {
      throw new Error('Cannot write stream to directory without a relative file path.');
    }

    const filePath = plugins.path.join(dirPathArg, this.relativeFilePath);
    const dirPath = plugins.path.parse(filePath).dir;
    await this.smartFs.directory(dirPath).recursive().create();
    return this.writeToDisk(filePath);
  }

  public async getContentAsBuffer() {
    this.checkMultiUse();
    const done = plugins.smartpromise.defer<Buffer>();
    const readStream = await this.createReadStream();
    const chunks: Buffer[] = [];
    readStream.on('data', (chunk) => chunks.push(Buffer.from(chunk)));
    readStream.on('error', done.reject);
    readStream.on('end', () => {
      const contentBuffer = Buffer.concat(chunks);
      done.resolve(contentBuffer);
    });
    return done.promise;
  }

  public async getContentAsString(formatArg: 'utf8' | 'binary' = 'utf8') {
    const contentBuffer = await this.getContentAsBuffer();
    return contentBuffer.toString(formatArg);
  }

  /**
   * Returns the size of the file content in bytes.
   */
  public async getSize(): Promise<number | null> {
    if (this.byteLengthComputeFunction) {
      return this.byteLengthComputeFunction();
    } else {
      return null;
    }
  }

  /**
   * Converts the StreamFile to a SmartFile by loading content into memory
   */
  public async toSmartFile(): Promise<any> {
    const { SmartFile } = await import('./classes.smartfile.js');
    const buffer = await this.getContentAsBuffer();
    return new SmartFile({
      path: this.relativeFilePath || 'stream',
      contentBuffer: buffer,
      base: process.cwd()
    }, this.smartFs);
  }
}
