import { Readable } from 'node:stream';
import {
  DeleteObjectCommand,
  GetObjectCommand,
  HeadObjectCommand,
  PutObjectCommand,
  S3Client
} from '@aws-sdk/client-s3';
import { getSignedUrl as signS3Url } from '@aws-sdk/s3-request-presigner';
import type {
  DeleteObjectInput,
  GetObjectInput,
  GetSignedUrlInput,
  ObjectExistsInput,
  PutObjectInput,
  StorageProvider
} from '../types';

function requireS3Env(name: string) {
  const value = process.env[name]?.trim();

  if (!value) {
    throw new Error(`Missing required S3 storage config: ${name}`);
  }

  return value;
}

function getS3Client() {
  return new S3Client({
    region: requireS3Env('S3_REGION'),
    endpoint: requireS3Env('S3_ENDPOINT'),
    credentials: {
      accessKeyId: requireS3Env('S3_ACCESS_KEY_ID'),
      secretAccessKey: requireS3Env('S3_SECRET_ACCESS_KEY')
    },
    forcePathStyle: process.env.S3_FORCE_PATH_STYLE !== 'false'
  });
}

function getBucket() {
  return requireS3Env('S3_BUCKET');
}

export function createS3StorageProvider(): StorageProvider {
  const client = getS3Client();
  const bucket = getBucket();

  return {
    async putObject(input: PutObjectInput) {
      await client.send(
        new PutObjectCommand({
          Bucket: bucket,
          Key: input.key,
          Body: input.body,
          ContentType: input.contentType
        })
      );

      return {
        provider: 's3',
        key: input.key,
        contentType: input.contentType,
        fileName: input.fileName,
        size: input.body.byteLength
      };
    },
    async getObject(input: GetObjectInput) {
      const response = await client.send(
        new GetObjectCommand({
          Bucket: bucket,
          Key: input.key
        })
      );

      if (!response.Body) {
        throw new Error('S3 object body is empty');
      }

      const nodeStream = response.Body.transformToWebStream
        ? response.Body.transformToWebStream()
        : (Readable.toWeb(response.Body as Readable) as ReadableStream<Uint8Array>);

      return {
        provider: 's3',
        key: input.key,
        contentType: response.ContentType || 'application/octet-stream',
        size: Number(response.ContentLength || 0),
        body: nodeStream
      };
    },
    async deleteObject(input: DeleteObjectInput) {
      await client.send(
        new DeleteObjectCommand({
          Bucket: bucket,
          Key: input.key
        })
      );
    },
    async objectExists(input: ObjectExistsInput) {
      try {
        await client.send(
          new HeadObjectCommand({
            Bucket: bucket,
            Key: input.key
          })
        );
        return true;
      } catch {
        return false;
      }
    },
    async getSignedUrl(input: GetSignedUrlInput) {
      return signS3Url(
        client,
        new GetObjectCommand({
          Bucket: bucket,
          Key: input.key
        }),
        {
          expiresIn: input.expiresInSeconds ?? 300
        }
      );
    }
  };
}
