import crypto from 'node:crypto';
import { FragmentNotFoundError, PageNotFoundError } from '../content/provider.js';
import { normalizeRoute } from '../content/routes.js';
// ---------------------------------------------------------------------------
// S3 content provider — reads compiled content files from S3
// ---------------------------------------------------------------------------
export class S3ContentProvider {
    config;
    #manifest;
    constructor(config = {}) {
        this.config = resolveS3Config(config);
    }
    async listRoutes() {
        const manifest = await this.manifest();
        return Object.values(manifest.routes).sort((a, b) => a.path.localeCompare(b.path));
    }
    async getSiteConfig() {
        const manifest = await this.manifest();
        return await this.getJSON(manifest.siteConfigRef);
    }
    async getPage(route) {
        const manifest = await this.manifest();
        const normalized = normalizeRoute(route);
        const entry = Object.values(manifest.routes).find((candidate) => normalizeRoute(candidate.path) === normalized);
        if (!entry) {
            throw new PageNotFoundError(route);
        }
        try {
            return await this.getJSON(entry.payloadRef);
        }
        catch (error) {
            throw new PageNotFoundError(route, error);
        }
    }
    async getFragment(id) {
        const manifest = await this.manifest();
        const entry = manifest.fragments[id];
        if (!entry) {
            throw new FragmentNotFoundError(id);
        }
        try {
            return await this.getJSON(entry.payloadRef);
        }
        catch (error) {
            throw new FragmentNotFoundError(id, error);
        }
    }
    async manifest() {
        this.#manifest ??= this.getJSON('manifest.json');
        return await this.#manifest;
    }
    async fetchRaw(key) {
        return await getS3Raw(key, this.config);
    }
    async getJSON(key) {
        return await getS3JSON(joinKey(this.config.keyPrefix, key), this.config);
    }
}
// ---------------------------------------------------------------------------
// S3 fetch with AWS SigV4 signing
// ---------------------------------------------------------------------------
export async function getS3JSON(key, config) {
    const url = s3ObjectURL(config, key);
    const signedHeaders = signedS3Headers('GET', url, config);
    const res = await fetch(url, {
        method: 'GET',
        headers: signedHeaders,
        cache: 'no-store'
    });
    if (!res.ok) {
        throw new Error(`S3 GET ${key} failed with status ${res.status}: ${await res.text()}`);
    }
    return (await res.json());
}
export async function getS3Raw(key, config) {
    const url = s3ObjectURL(config, key);
    const signedHeaders = signedS3Headers('GET', url, config);
    return await fetch(url, {
        method: 'GET',
        headers: signedHeaders,
        cache: 'no-store'
    });
}
// ---------------------------------------------------------------------------
// Internal helpers
// ---------------------------------------------------------------------------
function resolveS3Config(config) {
    const resolved = {
        bucket: config.bucket || process.env.ACI_S3_BUCKET || '',
        keyPrefix: config.keyPrefix || process.env.ACI_S3_KEY_PREFIX || '',
        region: config.region || process.env.AWS_REGION || 'us-east-1',
        accessKeyId: config.accessKeyId || process.env.AWS_ACCESS_KEY_ID || '',
        secretAccessKey: config.secretAccessKey || process.env.AWS_SECRET_ACCESS_KEY || '',
        sessionToken: config.sessionToken || process.env.AWS_SESSION_TOKEN || '',
        endpoint: trimSlash(config.endpoint || process.env.ACI_S3_ENDPOINT || '')
    };
    for (const key of ['bucket', 'accessKeyId', 'secretAccessKey']) {
        if (!resolved[key])
            throw new Error(`S3 config missing ${key}`);
    }
    return resolved;
}
function s3ObjectURL(config, key) {
    const encodedKey = encodeKey(key);
    if (config.endpoint) {
        return new URL(`${config.endpoint}/${encodeURIComponent(config.bucket)}${encodedKey}`);
    }
    return new URL(`https://${config.bucket}.s3.${config.region}.amazonaws.com${encodedKey}`);
}
function signedS3Headers(method, url, config) {
    const now = new Date();
    const amzDate = amzTimestamp(now);
    const dateStamp = amzDate.slice(0, 8);
    const payloadHash = sha256Hex('');
    const headers = new Headers({
        host: url.host,
        'x-amz-content-sha256': payloadHash,
        'x-amz-date': amzDate
    });
    if (config.sessionToken)
        headers.set('x-amz-security-token', config.sessionToken);
    const signedHeaderNames = Array.from(headers.keys()).sort();
    const canonicalHeaders = signedHeaderNames.map((k) => `${k}:${headers.get(k)?.trim()}\n`).join('');
    const canonicalRequest = [
        method,
        url.pathname,
        url.searchParams.toString(),
        canonicalHeaders,
        signedHeaderNames.join(';'),
        payloadHash
    ].join('\n');
    const credentialScope = `${dateStamp}/${config.region}/s3/aws4_request`;
    const stringToSign = [
        'AWS4-HMAC-SHA256',
        amzDate,
        credentialScope,
        sha256Hex(canonicalRequest)
    ].join('\n');
    const signingKey = awsSigningKey(config.secretAccessKey, dateStamp, config.region, 's3');
    const signature = hmacHex(signingKey, stringToSign);
    headers.set('authorization', `AWS4-HMAC-SHA256 Credential=${config.accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaderNames.join(';')}, Signature=${signature}`);
    return headers;
}
function awsSigningKey(secret, dateStamp, region, service) {
    const kDate = hmac(Buffer.from(`AWS4${secret}`, 'utf8'), dateStamp);
    const kRegion = hmac(kDate, region);
    const kService = hmac(kRegion, service);
    return hmac(kService, 'aws4_request');
}
function amzTimestamp(date) {
    return date.toISOString().replace(/[:-]|\.\d{3}/g, '');
}
function hmac(key, value) {
    return crypto.createHmac('sha256', key).update(value, 'utf8').digest();
}
function hmacHex(key, value) {
    return crypto.createHmac('sha256', key).update(value, 'utf8').digest('hex');
}
function sha256Hex(value) {
    return crypto.createHash('sha256').update(value, 'utf8').digest('hex');
}
function trimSlash(value) {
    return value.replace(/\/+$/, '');
}
function encodeKey(key) {
    return `/${key.split('/').map(encodeURIComponent).join('/')}`;
}
function joinKey(...parts) {
    return parts
        .flatMap((part) => part.split('/'))
        .map((part) => part.trim())
        .filter(Boolean)
        .join('/');
}
