import { createHash } from 'node:crypto';
import { Injectable } from '@nestjs/common';
import { DeleteObjectCommand, GetObjectCommand, PutObjectCommand, S3Client } from '@aws-sdk/client-s3';
import type { StoragePort } from './storage.port';

export interface S3Config {
  endpoint?: string;      // MinIO/self-hosted URL; omit for AWS
  region: string;
  bucket: string;
  accessKeyId: string;
  secretAccessKey: string;
  forcePathStyle: boolean; // true for MinIO
}

/** The one method this adapter uses — lets tests inject a stub client (no live S3/MinIO). */
export interface S3Like {
  send(command: unknown): Promise<unknown>;
}

/** Build an S3Config from env, or null if the required bits are missing (→ fall back to disk). */
export function s3ConfigFromEnv(env: NodeJS.ProcessEnv = process.env): S3Config | null {
  const bucket = env.IIOS_S3_BUCKET;
  const accessKeyId = env.IIOS_S3_ACCESS_KEY;
  const secretAccessKey = env.IIOS_S3_SECRET_KEY;
  if (!bucket || !accessKeyId || !secretAccessKey) return null;
  return {
    ...(env.IIOS_S3_ENDPOINT ? { endpoint: env.IIOS_S3_ENDPOINT } : {}),
    region: env.IIOS_S3_REGION ?? 'us-east-1',
    bucket,
    accessKeyId,
    secretAccessKey,
    // MinIO/self-hosted needs path-style; default true unless explicitly disabled for AWS.
    forcePathStyle: env.IIOS_S3_FORCE_PATH_STYLE !== 'false',
  };
}

/**
 * S3-compatible object storage (AWS S3, MinIO, R2, Supabase Storage). A drop-in for LocalDiskStorage:
 * same StoragePort contract. sha256 is computed locally on put (S3 doesn't return it), matching the
 * disk adapter. A missing object reads back as null (not an error).
 */
@Injectable()
export class S3Storage implements StoragePort {
  private readonly client: S3Like;
  private readonly bucket: string;

  constructor(config: S3Config, client?: S3Like) {
    this.bucket = config.bucket;
    this.client = client ?? new S3Client({
      region: config.region,
      ...(config.endpoint ? { endpoint: config.endpoint } : {}),
      forcePathStyle: config.forcePathStyle,
      credentials: { accessKeyId: config.accessKeyId, secretAccessKey: config.secretAccessKey },
    });
  }

  async put(objectKey: string, data: Buffer, mime: string): Promise<{ sizeBytes: number; checksumSha256: string }> {
    const checksumSha256 = createHash('sha256').update(data).digest('hex');
    await this.client.send(new PutObjectCommand({ Bucket: this.bucket, Key: objectKey, Body: data, ContentType: mime }));
    return { sizeBytes: data.length, checksumSha256 };
  }

  async get(objectKey: string): Promise<{ data: Buffer; mime: string; sizeBytes: number } | null> {
    try {
      const res = (await this.client.send(new GetObjectCommand({ Bucket: this.bucket, Key: objectKey }))) as {
        Body?: { transformToByteArray(): Promise<Uint8Array> };
        ContentType?: string;
      };
      if (!res.Body) return null;
      const bytes = await res.Body.transformToByteArray();
      const data = Buffer.from(bytes);
      return { data, mime: res.ContentType ?? 'application/octet-stream', sizeBytes: data.length };
    } catch (err) {
      if (isNotFound(err)) return null;
      throw err;
    }
  }

  async remove(objectKey: string): Promise<void> {
    await this.client.send(new DeleteObjectCommand({ Bucket: this.bucket, Key: objectKey }));
  }
}

function isNotFound(err: unknown): boolean {
  const e = err as { name?: string; $metadata?: { httpStatusCode?: number } };
  return e.name === 'NoSuchKey' || e.name === 'NotFound' || e.$metadata?.httpStatusCode === 404;
}
