import { Inject, Injectable, Logger, NotFoundException } from '@nestjs/common';
import { ConfigType } from '@nestjs/config';
import { Client } from 'minio';
import { Readable } from 'stream';

import { minioConfig } from './minio.config';

/**
 * Minimal object-storage client for the passenger API.
 *
 * A deliberate subset of the freight MinioService — only what chat attachments
 * need (put, sign, stream, key-from-url). Freight's extra surface (delete,
 * public URLs for unauthenticated links) is omitted rather than copied
 * speculatively.
 */
@Injectable()
export class MinioService {
  private readonly client: Client;
  private readonly logger = new Logger(MinioService.name);
  private readonly bucket: string;

  constructor(
    @Inject(minioConfig.KEY)
    private readonly config: ConfigType<typeof minioConfig>,
  ) {
    this.bucket = config.bucket;
    this.client = new Client({
      endPoint: config.endPoint,
      port: config.port,
      useSSL: config.useSSL,
      accessKey: config.accessKey,
      secretKey: config.secretKey,
      region: config.region,
    });
  }

  async uploadFile(objectName: string, buffer: Buffer, contentType: string): Promise<string> {
    await this.client.putObject(this.bucket, objectName, buffer, buffer.length, {
      'Content-Type': contentType,
    });
    return this.getObjectUrl(objectName);
  }

  /** Unsigned object URL — what gets persisted. Not browser-fetchable. */
  getObjectUrl(objectName: string): string {
    const protocol = this.config.useSSL ? 'https' : 'http';
    return `${protocol}://${this.config.endPoint}:${this.config.port}/${this.bucket}/${objectName}`;
  }

  getObjectNameFromUrl(value: string): string {
    const trimmed = value.trim();
    if (!trimmed) throw new NotFoundException('File object path is empty');
    if (!/^https?:\/\//i.test(trimmed)) return trimmed.replace(/^\/+/, '');

    const url = new URL(trimmed);
    // pathname percent-encodes the key (a space becomes "%20") but MinIO stores
    // the literal characters, so decode each segment or a file whose name had
    // spaces 404s with "specified key does not exist".
    const parts = url.pathname
      .split('/')
      .filter(Boolean)
      .map((segment) => decodeURIComponent(segment));
    if (parts[0] === this.bucket) parts.shift();

    const objectName = parts.join('/');
    if (!objectName) throw new NotFoundException('File object path is empty');
    return objectName;
  }

  async getFileStream(objectName: string): Promise<Readable> {
    return this.client.getObject(this.bucket, objectName);
  }

  /**
   * Short-lived signed URL for inline preview.
   *
   * Unlike the freight twin this does NOT degrade to an unsigned public URL when
   * signing fails: a chat attachment is another passenger's file, and quietly
   * handing back a URL that only works if the bucket is world-readable trades a
   * visible error for a silent access-control surprise. Fail loudly instead.
   */
  async getSignedUrl(objectName: string, expirySeconds: number): Promise<string> {
    try {
      return await this.client.presignedGetObject(this.bucket, objectName, expirySeconds);
    } catch (error) {
      this.logger.error(`Failed to sign URL for ${objectName}: ${(error as Error).message}`);
      throw error;
    }
  }
}
