/**
 * Node-backed implementation of `S3ClientLike`.
 *
 * Wraps `@aws-sdk/client-s3`. Resolvable on both Node and Bun, so it
 * can serve as the package's default fallback for resolvers that do
 * not honor the `"bun"` exports condition.
 *
 * Design notes:
 *
 * 1. **Lazy SDK loading.** `@aws-sdk/client-s3` is large (~1 MB on
 *    disk) and imports several hundred files at startup. To keep the
 *    cost off the cold-start path of callers that import this module
 *    but never actually upload/download, the SDK is loaded via dynamic
 *    `import()` on first method call.
 *
 * 2. **Streamed uploads track byte counts via a counting passthrough.**
 *    `PutObjectCommand` does not report bytes uploaded. For
 *    fixed-size bodies (`Uint8Array`, `ArrayBuffer`, `Buffer`,
 *    `string`, `Blob`) we use `.byteLength` / `.size`. For
 *    `ReadableStream` bodies we wrap the source in a counting
 *    `Transform` so the returned byte count matches what was actually
 *    sent on the wire.
 *
 * 3. **Bucket-in-endpoint addressing.** Agentuity buckets use
 *    virtual-host-style endpoints (`<bucket>.<host>`), so the bucket
 *    name is implicit in the URL. The SDK still requires a `Bucket`
 *    parameter on each command; we extract it from the endpoint's
 *    leading hostname label and rely on the endpoint override to route
 *    correctly. `forcePathStyle` is `false`.
 */
import { Buffer } from 'node:buffer';
import { Readable, Transform } from 'node:stream';
import { resolveEndpoint } from "./types.js";
export { bucketConfigFromEnv } from "./types.js";
let sdkModule = null;
let sdkPromise = null;
async function loadSdk() {
    if (sdkModule)
        return sdkModule;
    if (!sdkPromise) {
        sdkPromise = import('@aws-sdk/client-s3').then((mod) => {
            sdkModule = mod;
            return mod;
        });
    }
    return sdkPromise;
}
export function createS3Client(bucket) {
    const endpoint = resolveEndpoint(bucket);
    // The SDK's `Bucket` parameter is essentially a placeholder for our
    // purposes — we always send virtual-hosted-style requests, so the
    // `endpoint` URL is what actually routes. Prefer the explicit
    // `bucket` field when supplied; otherwise fall back to parsing the
    // leading hostname label out of the composed endpoint.
    const bucketLabel = bucket.bucket ?? extractBucketLabel(endpoint);
    const state = {
        endpoint,
        bucketLabel,
        region: bucket.region || 'auto',
        credentials: {
            accessKeyId: bucket.access_key,
            secretAccessKey: bucket.secret_key,
        },
        clientPromise: null,
    };
    const getClient = async () => {
        if (!state.clientPromise) {
            const sdk = await loadSdk();
            state.clientPromise = Promise.resolve(new sdk.S3Client({
                endpoint: state.endpoint,
                region: state.region,
                credentials: state.credentials,
                forcePathStyle: false,
            }));
        }
        return state.clientPromise;
    };
    return {
        async list(opts) {
            const sdk = await loadSdk();
            const client = await getClient();
            const out = await client.send(new sdk.ListObjectsV2Command({
                Bucket: state.bucketLabel,
                Prefix: opts?.prefix,
                MaxKeys: opts?.maxKeys,
                ContinuationToken: opts?.continuationToken,
            }));
            return {
                contents: (out.Contents ?? []).map((o) => ({
                    key: o.Key ?? '',
                    size: o.Size ?? 0,
                    lastModified: o.LastModified?.toISOString() ?? '',
                    etag: o.ETag,
                })),
                isTruncated: out.IsTruncated ?? false,
                nextContinuationToken: out.NextContinuationToken,
            };
        },
        async stat(key) {
            const sdk = await loadSdk();
            const client = await getClient();
            const out = await client.send(new sdk.HeadObjectCommand({ Bucket: state.bucketLabel, Key: key }));
            return {
                size: out.ContentLength ?? 0,
                type: out.ContentType,
                lastModified: out.LastModified,
                etag: out.ETag,
            };
        },
        file(key) {
            // Mutable holder for the most recently observed Content-Type.
            // Populated by the first arrayBuffer/text/stream call; matches
            // the contract documented on S3FileLike.type.
            const meta = {};
            return {
                get type() {
                    return meta.type;
                },
                async arrayBuffer() {
                    const sdk = await loadSdk();
                    const client = await getClient();
                    const out = await client.send(new sdk.GetObjectCommand({ Bucket: state.bucketLabel, Key: key }));
                    if (out.ContentType)
                        meta.type = out.ContentType;
                    return readBodyAsArrayBuffer(out.Body);
                },
                async text() {
                    const buf = await this.arrayBuffer();
                    return new TextDecoder().decode(buf);
                },
                stream() {
                    // Lazily fetch on first pull so callers can hold a handle
                    // without triggering a network call upfront.
                    let inner = null;
                    let reader = null;
                    return new ReadableStream({
                        async pull(controller) {
                            if (!inner) {
                                const sdk = await loadSdk();
                                const client = await getClient();
                                const out = await client.send(new sdk.GetObjectCommand({
                                    Bucket: state.bucketLabel,
                                    Key: key,
                                }));
                                if (out.ContentType)
                                    meta.type = out.ContentType;
                                inner = bodyToWebStream(out.Body);
                                reader = inner.getReader();
                            }
                            const { value, done } = await reader.read();
                            if (done)
                                controller.close();
                            else
                                controller.enqueue(value);
                        },
                        async cancel() {
                            await reader?.cancel();
                        },
                    });
                },
            };
        },
        async write(key, body, opts) {
            const sdk = await loadSdk();
            const client = await getClient();
            const { Body, getBytesUploaded } = prepareBody(body);
            await client.send(new sdk.PutObjectCommand({
                Bucket: state.bucketLabel,
                Key: key,
                Body,
                ContentType: opts?.type,
            }));
            return getBytesUploaded();
        },
        async delete(key) {
            const sdk = await loadSdk();
            const client = await getClient();
            await client.send(new sdk.DeleteObjectCommand({ Bucket: state.bucketLabel, Key: key }));
        },
    };
}
/**
 * Convert one of our accepted `S3Body` shapes into something the AWS
 * SDK can consume, while also preparing a way to report the number of
 * bytes that flowed to S3.
 *
 * For fixed-size bodies the count is known up front. For streaming
 * bodies we attach a counting `Transform` and read the tally back after
 * the upload completes.
 */
function prepareBody(body) {
    if (typeof body === 'string') {
        const bytes = Buffer.byteLength(body, 'utf-8');
        return { Body: body, getBytesUploaded: () => bytes };
    }
    if (body instanceof Uint8Array) {
        return { Body: body, getBytesUploaded: () => body.byteLength };
    }
    if (body instanceof ArrayBuffer) {
        const buf = Buffer.from(body);
        return { Body: buf, getBytesUploaded: () => buf.byteLength };
    }
    if (typeof Blob !== 'undefined' && body instanceof Blob) {
        // Convert the Blob to a Node stream, counting bytes as it flows.
        const webStream = body.stream();
        const nodeStream = Readable.fromWeb(webStream);
        const { stream, getBytes } = countingPassthrough(nodeStream);
        return { Body: stream, getBytesUploaded: getBytes };
    }
    if (isWebReadableStream(body)) {
        const nodeStream = Readable.fromWeb(body);
        const { stream, getBytes } = countingPassthrough(nodeStream);
        return { Body: stream, getBytesUploaded: getBytes };
    }
    // Fallthrough: pass through whatever it is. Should be unreachable
    // given the `S3Body` union, but keeps the function total.
    return {
        Body: body,
        getBytesUploaded: () => 0,
    };
}
function isWebReadableStream(value) {
    return (typeof value === 'object' &&
        value !== null &&
        typeof value.getReader === 'function');
}
/**
 * Wrap a Node `Readable` in a passthrough that counts bytes flowing
 * through it. The returned `getBytes()` callback returns the running
 * tally; call it after the consumer has finished reading.
 */
function countingPassthrough(source) {
    let count = 0;
    const counter = new Transform({
        transform(chunk, _enc, cb) {
            count += chunk.length;
            cb(null, chunk);
        },
    });
    source.pipe(counter);
    source.on('error', (err) => counter.destroy(err));
    return { stream: counter, getBytes: () => count };
}
/**
 * Read an SDK `GetObjectCommand` response body fully into an
 * `ArrayBuffer`. The SDK returns `unknown` for the body shape because
 * it varies by runtime; in Node it's an `IncomingMessage`-style
 * `Readable`.
 */
async function readBodyAsArrayBuffer(body) {
    if (!body)
        return new ArrayBuffer(0);
    if (body instanceof Uint8Array) {
        // Some shapes return a Uint8Array directly. Copy into a fresh
        // ArrayBuffer so the return type is the strict ArrayBuffer rather
        // than ArrayBuffer | SharedArrayBuffer (which `.buffer` may be).
        const out = new Uint8Array(body.byteLength);
        out.set(body);
        return out.buffer;
    }
    if (isWebReadableStream(body)) {
        const reader = body.getReader();
        const chunks = [];
        while (true) {
            const { value, done } = await reader.read();
            if (done)
                break;
            if (value)
                chunks.push(value);
        }
        return concatUint8Arrays(chunks).buffer;
    }
    // Otherwise treat as a Node Readable.
    const chunks = [];
    for await (const chunk of body) {
        chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
    }
    const merged = Buffer.concat(chunks);
    // Copy into a tightly-sized ArrayBuffer to avoid sharing the underlying
    // pool buffer with other Buffer instances.
    const out = new Uint8Array(merged.byteLength);
    out.set(merged);
    return out.buffer;
}
/**
 * Convert an SDK response body into a Web `ReadableStream<Uint8Array>`.
 * Returns an empty stream when the body is missing.
 */
function bodyToWebStream(body) {
    if (!body) {
        return new ReadableStream({
            start(controller) {
                controller.close();
            },
        });
    }
    if (isWebReadableStream(body))
        return body;
    if (body instanceof Uint8Array) {
        return new ReadableStream({
            start(controller) {
                controller.enqueue(body);
                controller.close();
            },
        });
    }
    // Assume Node Readable.
    return Readable.toWeb(body);
}
function concatUint8Arrays(parts) {
    let total = 0;
    for (const p of parts)
        total += p.byteLength;
    // Allocate against a concrete ArrayBuffer (not SharedArrayBuffer) so
    // the resulting `.buffer` is typed as ArrayBuffer.
    const out = new Uint8Array(new ArrayBuffer(total));
    let offset = 0;
    for (const p of parts) {
        out.set(p, offset);
        offset += p.byteLength;
    }
    return out;
}
/**
 * Pull the leading hostname label off a virtual-hosted-style endpoint
 * URL. Falls back to `'bucket'` if parsing fails — which is fine
 * because the endpoint override controls actual routing; the SDK only
 * uses `Bucket` to construct path-style URLs (which we never do).
 */
function extractBucketLabel(endpoint) {
    try {
        const url = new URL(endpoint);
        const firstLabel = url.hostname.split('.')[0];
        return firstLabel || 'bucket';
    }
    catch {
        return 'bucket';
    }
}
//# sourceMappingURL=node.js.map