// Thin wrapper around @aws-sdk/client-s3 configured for Dezky's audit
// archive bucket. Same code talks to MinIO in dev and Hetzner Object
// Storage in prod — only the endpoint + credentials change via env.
//
// We deliberately keep the surface small (put, head, list) so the archive
// worker doesn't depend on AWS SDK specifics that might shift between
// versions. Streaming reads / restore-from-cold are a follow-up; today's
// scope is the write path.

import { Injectable, Logger } from '@nestjs/common'
import { ConfigService } from '@nestjs/config'
import {
  HeadObjectCommand,
  ListObjectsV2Command,
  PutObjectCommand,
  S3Client,
} from '@aws-sdk/client-s3'

export interface ColdPutInput {
  key: string
  body: Buffer | string
  contentType?: string
  // Server-side encryption for prod (Hetzner SSE-S3). MinIO ignores when
  // unset; setting it explicitly is a no-op for dev but documents intent.
  serverSideEncryption?: 'AES256'
}

export interface ColdHeadResult {
  exists: boolean
  size?: number
  etag?: string
}

@Injectable()
export class ColdStorageClient {
  private readonly logger = new Logger(ColdStorageClient.name)
  private readonly client: S3Client
  private readonly bucket: string

  constructor(config: ConfigService) {
    const endpoint = config.getOrThrow<string>('AUDIT_COLD_ENDPOINT')
    const region = config.get<string>('AUDIT_COLD_REGION') ?? 'us-east-1'
    const accessKeyId = config.getOrThrow<string>('AUDIT_COLD_ACCESS_KEY')
    const secretAccessKey = config.getOrThrow<string>('AUDIT_COLD_SECRET_KEY')
    this.bucket = config.getOrThrow<string>('AUDIT_COLD_BUCKET')

    this.client = new S3Client({
      endpoint,
      region,
      credentials: { accessKeyId, secretAccessKey },
      // forcePathStyle is mandatory for MinIO and harmless for Hetzner (which
      // accepts both virtual-host + path-style). Without it the SDK tries
      // `bucket.minio:9000` which doesn't resolve.
      forcePathStyle: true,
    })
  }

  get bucketName(): string {
    return this.bucket
  }

  // Upload an object. Returns the ETag the server assigned. Throws on
  // network/auth failure; callers should not catch and pretend success —
  // the archive worker uses throw-on-failure to keep events in hot storage.
  async put(input: ColdPutInput): Promise<{ etag?: string }> {
    const res = await this.client.send(
      new PutObjectCommand({
        Bucket: this.bucket,
        Key: input.key,
        Body: input.body,
        ContentType: input.contentType,
        ServerSideEncryption: input.serverSideEncryption,
      }),
    )
    return { etag: res.ETag }
  }

  // Existence + size check. The archive worker calls this after every put
  // to confirm the upload actually landed before deleting from hot Mongo.
  async head(key: string): Promise<ColdHeadResult> {
    try {
      const res = await this.client.send(
        new HeadObjectCommand({ Bucket: this.bucket, Key: key }),
      )
      return { exists: true, size: res.ContentLength, etag: res.ETag }
    } catch (err) {
      // S3 returns 404 for missing keys; the SDK surfaces it as a NotFound
      // error. Treat 404 as "doesn't exist" without logging — anything else
      // is real failure.
      const e = err as { name?: string; $metadata?: { httpStatusCode?: number } }
      if (e?.name === 'NotFound' || e?.$metadata?.httpStatusCode === 404) {
        return { exists: false }
      }
      throw err
    }
  }

  // List objects under a prefix. Used by the operator UI to show recent
  // archive batches. Paginated via continuation token; for the v1 UI we cap
  // at a single page since we only show the latest N.
  async list(prefix: string, max = 100): Promise<{ keys: string[]; truncated: boolean }> {
    const res = await this.client.send(
      new ListObjectsV2Command({ Bucket: this.bucket, Prefix: prefix, MaxKeys: max }),
    )
    return {
      keys: (res.Contents ?? []).map((c) => c.Key!).filter(Boolean),
      truncated: !!res.IsTruncated,
    }
  }
}
