import {
  CopyObjectCommand,
  CreateBucketCommand,
  DeleteObjectsCommand,
  GetObjectCommand,
  HeadBucketCommand,
  ListObjectsV2Command,
  PutObjectCommand,
  S3Client
} from "@aws-sdk/client-s3";
import { env } from "../config/env.js";
import type { PutObjectInput, StorageObjectSummary, StorageProvider } from "./StorageProvider.js";

export class S3StorageProvider implements StorageProvider {
  private readonly client = new S3Client({
    endpoint: env.s3.endpoint,
    region: env.s3.region,
    forcePathStyle: env.s3.forcePathStyle,
    credentials: {
      accessKeyId: env.s3.accessKeyId,
      secretAccessKey: env.s3.secretAccessKey
    }
  });

  async ensureReady() {
    try {
      await this.client.send(new HeadBucketCommand({ Bucket: env.s3.bucket }));
    } catch {
      await this.client.send(new CreateBucketCommand({ Bucket: env.s3.bucket }));
    }
  }

  async putObject(input: PutObjectInput) {
    await this.ensureReady();

    await this.client.send(
      new PutObjectCommand({
        Bucket: env.s3.bucket,
        Key: input.key,
        Body: input.body,
        ContentType: input.contentType
      })
    );
  }

  async getObject(key: string) {
    await this.ensureReady();

    const result = await this.client.send(
      new GetObjectCommand({
        Bucket: env.s3.bucket,
        Key: key
      })
    );
    const bytes = result.Body ? await result.Body.transformToByteArray() : new Uint8Array();

    return {
      body: bytes,
      contentType: result.ContentType
    };
  }

  async getObjectText(key: string) {
    const result = await this.getObject(key);

    return new TextDecoder().decode(result.body);
  }

  async listPrefix(prefix: string) {
    let continuationToken: string | undefined;
    const objects: StorageObjectSummary[] = [];

    await this.ensureReady();

    do {
      const listedObjects = await this.client.send(
        new ListObjectsV2Command({
          Bucket: env.s3.bucket,
          Prefix: prefix,
          ContinuationToken: continuationToken
        })
      );

      for (const object of listedObjects.Contents ?? []) {
        if (!object.Key) {
          continue;
        }

        objects.push({
          key: object.Key,
          size: object.Size ?? 0,
          lastModified: object.LastModified?.toISOString(),
          etag: object.ETag
        });
      }

      continuationToken = listedObjects.NextContinuationToken;
    } while (continuationToken);

    return objects;
  }

  async copyPrefix(sourcePrefix: string, destinationPrefix: string) {
    let continuationToken: string | undefined;
    let copiedObjects = 0;

    await this.ensureReady();

    do {
      const listedObjects = await this.client.send(
        new ListObjectsV2Command({
          Bucket: env.s3.bucket,
          Prefix: sourcePrefix,
          ContinuationToken: continuationToken
        })
      );

      for (const object of listedObjects.Contents ?? []) {
        if (!object.Key) {
          continue;
        }

        const sourceKey = object.Key;
        const destinationKey = `${destinationPrefix}${sourceKey.slice(sourcePrefix.length)}`;

        await this.client.send(
          new CopyObjectCommand({
            Bucket: env.s3.bucket,
            CopySource: `${env.s3.bucket}/${encodeURIComponent(sourceKey).replace(/%2F/g, "/")}`,
            Key: destinationKey
          })
        );
        copiedObjects += 1;
      }

      continuationToken = listedObjects.NextContinuationToken;
    } while (continuationToken);

    return copiedObjects;
  }

  async deletePrefix(prefix: string) {
    let continuationToken: string | undefined;

    await this.ensureReady();

    do {
      const listedObjects = await this.client.send(
        new ListObjectsV2Command({
          Bucket: env.s3.bucket,
          Prefix: prefix,
          ContinuationToken: continuationToken
        })
      );
      const objects = listedObjects.Contents?.map((object) => ({ Key: object.Key })).filter((object) => object.Key);

      if (objects && objects.length > 0) {
        await this.client.send(
          new DeleteObjectsCommand({
            Bucket: env.s3.bucket,
            Delete: {
              Objects: objects
            }
          })
        );
      }

      continuationToken = listedObjects.NextContinuationToken;
    } while (continuationToken);
  }
}
