/**
 * S3 (and S3-compatible) storage driver — zero deps, SigV4-signed `fetch`.
 * Works with AWS S3, Cloudflare R2, Backblaze B2, MinIO, DigitalOcean Spaces.
 *
 * Path-style vs virtual-hosted: AWS defaults to virtual-hosted
 * (`bucket.s3.region.amazonaws.com`); custom endpoints (R2/MinIO) default to
 * path-style (`endpoint/bucket/key`). Override with `forcePathStyle`.
 */

import type { S3StoreConfig, StorageDriver } from "../driver.ts";
import { contentTypeOf } from "../mime.ts";
import { encodeKey, sha256Hex, signS3 } from "../sigv4.ts";

export function s3Driver(config: S3StoreConfig): StorageDriver {
  if (!config.bucket || /[\\/\0\r\n]/.test(config.bucket)) {
    throw new TypeError("S3 bucket must be a non-empty name without path separators");
  }
  const pathStyle = config.forcePathStyle ?? !!config.endpoint;
  const endpoint = config.endpoint ?? `https://s3.${config.region}.amazonaws.com`;
  const parsed = new URL(endpoint);
  if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
    throw new TypeError("S3 endpoint must use http or https");
  }
  if (parsed.username || parsed.password || parsed.search || parsed.hash) {
    throw new TypeError("S3 endpoint cannot contain credentials, query parameters, or fragments");
  }
  const scheme = parsed.protocol.replace(":", "");
  const endpointHost = parsed.host;
  const endpointPath = parsed.pathname.replace(/\/+$/, "");
  if (!pathStyle && endpointPath) {
    throw new TypeError("Virtual-hosted S3 endpoints cannot contain a path prefix");
  }

  function target(key: string): { host: string; path: string; url: string } {
    const ek = encodeKey(key);
    if (pathStyle) {
      const host = endpointHost;
      const path = `${endpointPath}/${encodeURIComponent(config.bucket)}/${ek}`;
      return { host, path, url: `${scheme}://${host}${path}` };
    }
    const host = `${config.bucket}.${endpointHost}`;
    const path = `/${ek}`;
    return { host, path, url: `${scheme}://${host}${path}` };
  }

  async function send(
    method: string,
    key: string,
    body?: Uint8Array,
    contentType?: string,
  ): Promise<Response> {
    const { host, path, url } = target(key);
    const headers = signS3({
      method,
      host,
      path,
      region: config.region,
      accessKeyId: config.accessKeyId,
      secretAccessKey: config.secretAccessKey,
      payloadHash: sha256Hex(body ?? new Uint8Array()),
      headers: contentType ? { "content-type": contentType } : {},
      date: new Date(),
    });
    return fetch(url, { method, headers, body: body as BodyInit | undefined });
  }

  return {
    async put(key, data, meta) {
      const res = await send("PUT", key, data, meta.contentType);
      if (!res.ok) throw new Error(`S3 put failed (${res.status}): ${await peek(res)}`);
    },
    async get(key) {
      const res = await send("GET", key);
      if (res.status === 404 || res.status === 403) return null;
      if (!res.ok) throw new Error(`S3 get failed (${res.status})`);
      const len = res.headers.get("content-length");
      return {
        body: res.body ?? new Uint8Array(),
        contentType: res.headers.get("content-type") || contentTypeOf(key),
        size: len ? Number(len) : undefined,
      };
    },
    async delete(key) {
      const res = await send("DELETE", key);
      if (!res.ok && res.status !== 404) throw new Error(`S3 delete failed (${res.status})`);
    },
    publicUrl(key) {
      if (config.publicBaseUrl) {
        return `${config.publicBaseUrl.replace(/\/+$/, "")}/${encodeKey(key)}`;
      }
      return config.access === "public" ? target(key).url : null;
    },
  };
}

async function peek(res: Response): Promise<string> {
  try {
    return (await res.text()).slice(0, 200);
  } catch {
    return "";
  }
}
