import { AwsClient } from "aws4fetch";

export const PRESIGN_TTL_SECONDS = 900;

export interface WasabiCreds {
  accessKeyId: string;
  secretAccessKey: string;
}

export interface BucketLocation {
  name: string;
  region: string;
}

export interface PresignedUrl {
  url: string;
  expiresAt: string;
}

const SHA256_RE = /^[a-f0-9]{64}$/;
const SITE_ID_RE = /^[A-Za-z0-9_-]+$/;

export function blobKey(siteId: string, sha256: string): string {
  if (!SITE_ID_RE.test(siteId)) {
    throw new Error("invalid siteId for S3 key");
  }
  if (!SHA256_RE.test(sha256)) {
    throw new Error("invalid sha256 for S3 key");
  }
  return `s/${siteId}/blobs/${sha256}`;
}

function objectUrl(bucket: BucketLocation, key: string): string {
  return `https://s3.${bucket.region}.wasabisys.com/${bucket.name}/${key}`;
}

function client(creds: WasabiCreds, region: string): AwsClient {
  return new AwsClient({
    accessKeyId: creds.accessKeyId,
    secretAccessKey: creds.secretAccessKey,
    service: "s3",
    region,
    // 10 retries par défaut = ~25 s bloqués dans la requête sur un 5xx Wasabi
    retries: 1,
  });
}

async function presign(
  creds: WasabiCreds,
  bucket: BucketLocation,
  key: string,
  method: "PUT" | "GET",
): Promise<PresignedUrl> {
  const url = new URL(objectUrl(bucket, key));
  url.searchParams.set("X-Amz-Expires", String(PRESIGN_TTL_SECONDS));
  const signed = await client(creds, bucket.region).sign(
    new Request(url, { method }),
    { aws: { signQuery: true } },
  );
  return {
    url: signed.url,
    expiresAt: new Date(Date.now() + PRESIGN_TTL_SECONDS * 1000).toISOString(),
  };
}

export function presignPut(
  creds: WasabiCreds,
  bucket: BucketLocation,
  key: string,
): Promise<PresignedUrl> {
  return presign(creds, bucket, key, "PUT");
}

export function presignGet(
  creds: WasabiCreds,
  bucket: BucketLocation,
  key: string,
): Promise<PresignedUrl> {
  return presign(creds, bucket, key, "GET");
}

export async function headObject(
  creds: WasabiCreds,
  bucket: BucketLocation,
  key: string,
): Promise<number | null> {
  const res = await client(creds, bucket.region).fetch(objectUrl(bucket, key), {
    method: "HEAD",
  });
  if (res.status === 404) return null;
  if (!res.ok) throw new Error(`HEAD ${key} failed with ${res.status}`);
  return Number(res.headers.get("content-length") ?? "0");
}

export async function getObjectJson<T>(
  creds: WasabiCreds,
  bucket: BucketLocation,
  key: string,
): Promise<T> {
  const res = await client(creds, bucket.region).fetch(objectUrl(bucket, key));
  if (!res.ok) throw new Error(`GET ${key} failed with ${res.status}`);
  const bytes = new Uint8Array(await res.arrayBuffer());
  if (bytes[0] === 0x1f && bytes[1] === 0x8b) {
    const stream = new Response(bytes).body!.pipeThrough(
      new DecompressionStream("gzip"),
    );
    return (await new Response(stream).json()) as T;
  }
  return JSON.parse(new TextDecoder().decode(bytes)) as T;
}

export async function deleteObject(
  creds: WasabiCreds,
  bucket: BucketLocation,
  key: string,
): Promise<void> {
  const res = await client(creds, bucket.region).fetch(objectUrl(bucket, key), {
    method: "DELETE",
  });
  if (!res.ok && res.status !== 404) {
    throw new Error(`DELETE ${key} failed with ${res.status}`);
  }
}
