import {
  Injectable,
  Logger,
  OnModuleInit,
  ServiceUnavailableException,
} from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import {
  CreateBucketCommand,
  DeleteObjectCommand,
  GetObjectCommand,
  HeadBucketCommand,
  PutObjectCommand,
  S3Client,
} from "@aws-sdk/client-s3";
import type { Readable } from "node:stream";

/**
 * S3 / MinIO object storage for document blobs. MySQL keeps only the pointer
 * (`storageKey`) + metadata; the bytes live here. Same bucket the migration's
 * `blob_extract.py` writes to, so keys stay under the `service/…` and
 * `policy/…` prefixes it established.
 *
 * Env (see deploy/.env.dev): S3_ENDPOINT, S3_BUCKET, and creds — S3_ACCESS_KEY
 * / S3_SECRET_KEY, falling back to MINIO_ROOT_USER / MINIO_ROOT_PASSWORD so a
 * single MinIO credential set drives both the migration and the API.
 */
@Injectable()
export class StorageService implements OnModuleInit {
  private readonly logger = new Logger(StorageService.name);
  private readonly client: S3Client | null;
  readonly bucket: string;

  constructor(config: ConfigService) {
    const endpoint = config.get<string>("S3_ENDPOINT");
    this.bucket = config.get<string>("S3_BUCKET") ?? "jorgecuadros-documents";
    const accessKeyId =
      config.get<string>("S3_ACCESS_KEY") ?? config.get<string>("MINIO_ROOT_USER");
    const secretAccessKey =
      config.get<string>("S3_SECRET_KEY") ?? config.get<string>("MINIO_ROOT_PASSWORD");

    if (!endpoint || !accessKeyId || !secretAccessKey) {
      this.logger.warn(
        "Object storage not configured (missing S3_ENDPOINT / credentials); " +
          "document upload & download are disabled.",
      );
      this.client = null;
      return;
    }

    this.client = new S3Client({
      endpoint,
      region: config.get<string>("S3_REGION") ?? "us-east-1",
      credentials: { accessKeyId, secretAccessKey },
      forcePathStyle: true, // MinIO needs path-style addressing
    });
  }

  /** Best-effort bucket check on boot; never blocks API startup. */
  async onModuleInit() {
    if (!this.client) return;
    try {
      await this.client.send(new HeadBucketCommand({ Bucket: this.bucket }));
    } catch {
      try {
        await this.client.send(new CreateBucketCommand({ Bucket: this.bucket }));
        this.logger.log(`Created bucket "${this.bucket}".`);
      } catch (err) {
        this.logger.warn(
          `Could not verify/create bucket "${this.bucket}": ${(err as Error).message}`,
        );
      }
    }
  }

  /**
   * Whether the deployment has object storage at all. Callers use this to
   * refuse work up front instead of failing halfway through — a recibo batch
   * that dies on its first `put` leaves a FAILED batch and no explanation the
   * office can act on.
   */
  get available(): boolean {
    return this.client !== null;
  }

  private require(): S3Client {
    if (!this.client) {
      throw new ServiceUnavailableException(
        "El almacenamiento de documentos no está configurado.",
      );
    }
    return this.client;
  }

  async put(key: string, body: Buffer, contentType?: string): Promise<void> {
    await this.require().send(
      new PutObjectCommand({
        Bucket: this.bucket,
        Key: key,
        Body: body,
        ContentType: contentType,
      }),
    );
  }

  async getStream(key: string): Promise<{
    stream: Readable;
    contentType?: string;
    contentLength?: number;
  }> {
    const out = await this.require().send(
      new GetObjectCommand({ Bucket: this.bucket, Key: key }),
    );
    return {
      stream: out.Body as Readable,
      contentType: out.ContentType,
      contentLength: out.ContentLength,
    };
  }

  /** Best-effort blob delete; a missing object is not an error. */
  async delete(key: string): Promise<void> {
    if (!this.client) return;
    try {
      await this.client.send(
        new DeleteObjectCommand({ Bucket: this.bucket, Key: key }),
      );
    } catch (err) {
      this.logger.warn(`Failed to delete blob "${key}": ${(err as Error).message}`);
    }
  }
}
