// src/services/storage.ts
// Copyright (C) 2026 Robert Colbert <rob.colbert@openplatform.us>
// All Rights Reserved

import env from "../config/env.js";

import { Stream as NodeStream } from "node:stream";

import { Client as MinioClient, BucketItemStat } from "minio";

/*
 * WebApp needs the type, and it's not exposed any other way. It's not even an
 * uncommon type. If there's a better way to expose the type, I don't know it.
 */
import { UploadedObjectInfo } from "../../node_modules/minio/dist/esm/internal/type.mjs";

export type MinioObjectMetadata = Record<string, string>;

export interface MinioFile {
  bucket: string;
  key: string;
  filePath?: string;
  metadata?: MinioObjectMetadata;
}

export interface MinioStreamFileRange {
  offset: number;
  length: number;
}

export interface MinioStreamFile extends MinioFile {
  range?: MinioStreamFileRange;
}

export interface MinioBuffer {
  bucket: string;
  key: string;
  buffer: Buffer;
  metadata?: MinioObjectMetadata;
}

import { DtpService } from "../lib/service.js";

class StorageService extends DtpService {
  minio?: MinioClient;

  get name(): string {
    return "StorageService";
  }
  get slug(): string {
    return "storage";
  }

  constructor() {
    super();
  }

  async start(): Promise<void> {
    try {
      this.log.info("creating MinIO transport", {
        endPoint: env.minio.endpoint,
        port: env.minio.port,
        useSSL: env.minio.useSsl,
        accessKey: env.minio.accessKey,
      });
      this.minio = new MinioClient({
        endPoint: env.minio.endpoint,
        port: env.minio.port,
        useSSL: env.minio.useSsl,
        accessKey: env.minio.accessKey,
        secretKey: env.minio.secretKey,
      });
    } catch (error) {
      this.log.error("failed to connect to S3/MinIO", { error });
      const e = new Error("Not connected to storage", { cause: error });
      e.statusCode = 500;
      throw e;
    }
  }

  async stop(): Promise<void> {}

  async uploadBuffer(buffer: MinioBuffer): Promise<void> {
    if (!this.minio) {
      const error = new Error("Not connected to storage");
      error.statusCode = 503;
      throw error;
    }
    const response = await this.minio.putObject(
      buffer.bucket,
      buffer.key,
      buffer.buffer,
      buffer.buffer.length,
      buffer.metadata,
    );
    this.log.debug("uploaded buffer to storage", { response });
  }

  async uploadFile(fileInfo: MinioFile): Promise<UploadedObjectInfo> {
    if (!this.minio) {
      const error = new Error("Not connected to storage");
      error.statusCode = 503;
      throw error;
    }
    if (!fileInfo.filePath) {
      const error = new Error("Must specify filePath of file to upload");
      error.statusCode = 400;
      throw error;
    }
    return this.minio.fPutObject(
      fileInfo.bucket,
      fileInfo.key,
      fileInfo.filePath,
      fileInfo.metadata,
    );
  }

  async statFile(file: MinioFile): Promise<BucketItemStat> {
    return this.statObject(file.bucket, file.key);
  }

  async statObject(bucket: string, key: string): Promise<BucketItemStat> {
    if (!this.minio) {
      const error = new Error("Not connected to storage");
      error.statusCode = 503;
      throw error;
    }
    this.log.debug("retrieving object status", { bucket, key });
    return this.minio.statObject(bucket, key);
  }

  async downloadFile(fileInfo: MinioFile): Promise<void> {
    if (!this.minio) {
      const error = new Error("Not connected to storage");
      error.statusCode = 503;
      throw error;
    }
    if (!fileInfo.filePath) {
      const error = new Error("Must specify filePath of file to download");
      error.statusCode = 400;
      throw error;
    }
    this.log.debug("downloading object to file", {
      bucket: fileInfo.bucket,
      key: fileInfo.key,
      filePath: fileInfo.filePath,
    });
    return this.minio.fGetObject(
      fileInfo.bucket,
      fileInfo.key,
      fileInfo.filePath,
    );
  }

  async openDownloadStream(fileInfo: MinioStreamFile): Promise<NodeStream> {
    if (!this.minio) {
      const error = new Error("Not connected to storage");
      error.statusCode = 503;
      throw error;
    }

    if (fileInfo.range) {
      this.log.debug("fetching partial object", {
        bucket: fileInfo.bucket,
        key: fileInfo.key,
        range: fileInfo.range,
      });
      const stream: NodeStream = await this.minio.getPartialObject(
        fileInfo.bucket,
        fileInfo.key,
        fileInfo.range.offset,
        fileInfo.range.length,
      );
      return stream;
    }

    this.log.debug("fetching object", {
      bucket: fileInfo.bucket,
      key: fileInfo.key,
    });
    const stream: NodeStream = await this.minio.getObject(
      fileInfo.bucket,
      fileInfo.key,
    );
    return stream;
  }

  async removeObject(bucket: string, key: string): Promise<void> {
    if (!this.minio) {
      const error = new Error("Not connected to storage");
      error.statusCode = 503;
      throw error;
    }
    this.log.debug("removing object", { bucket, key });
    await this.minio.removeObject(bucket, key);
  }
}

export { NodeStream };

export default new StorageService();
