/**
 * AWS SigV4 query-string presigner for Cloudflare R2 object reads.
 *
 * R2 speaks the S3 API, so a presigned GET is a plain SigV4 query-string
 * signature over an unsigned payload.
 */

import crypto from 'crypto';

export type PresignGetObjectParams = {
  endpoint: string;
  bucket: string;
  objectKey: string;
  region: string;
  accessKeyId: string;
  secretAccessKey: string;
  expiresIn: number;
  now?: Date;
};

const ALGORITHM = 'AWS4-HMAC-SHA256';
const SERVICE = 's3';

/** RFC 3986 encoding; S3 canonical requests keep "/" literal in the path. */
function uriEncode(value: string, keepSlash = false): string {
  const encoded = encodeURIComponent(value).replace(
    /[!'()*]/g,
    (char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`
  );
  return keepSlash ? encoded.replace(/%2F/g, '/') : encoded;
}

function sha256Hex(value: string): string {
  return crypto.createHash('sha256').update(value, 'utf8').digest('hex');
}

function hmac(key: crypto.BinaryLike, value: string): Buffer {
  return crypto.createHmac('sha256', key).update(value, 'utf8').digest();
}

function signingKey(secretAccessKey: string, date: string, region: string): Buffer {
  const dateKey = hmac(`AWS4${secretAccessKey}`, date);
  const regionKey = hmac(dateKey, region);
  const serviceKey = hmac(regionKey, SERVICE);
  return hmac(serviceKey, 'aws4_request');
}

export type CanonicalRequest = {
  canonicalRequest: string;
  canonicalUri: string;
  canonicalQuery: string;
  amzDate: string;
  scope: string;
};

export function buildCanonicalRequest(params: PresignGetObjectParams): CanonicalRequest {
  const now = params.now ?? new Date();
  const amzDate = now.toISOString().replace(/[-:]/g, '').replace(/\.\d{3}/, '');
  const shortDate = amzDate.slice(0, 8);
  const scope = `${shortDate}/${params.region}/${SERVICE}/aws4_request`;

  const endpoint = new URL(params.endpoint);
  const pathSegments = params.bucket ? [params.bucket, params.objectKey] : [params.objectKey];
  const canonicalUri = `/${pathSegments.join('/')}`
    .split('/')
    .map((segment) => uriEncode(segment))
    .join('/');

  const query: Record<string, string> = {
    'X-Amz-Algorithm': ALGORITHM,
    'X-Amz-Credential': `${params.accessKeyId}/${scope}`,
    'X-Amz-Date': amzDate,
    'X-Amz-Expires': String(params.expiresIn),
    'X-Amz-SignedHeaders': 'host',
  };

  const canonicalQuery = Object.keys(query)
    .sort()
    .map((key) => `${uriEncode(key)}=${uriEncode(query[key])}`)
    .join('&');

  const canonicalRequest = [
    'GET',
    canonicalUri,
    canonicalQuery,
    `host:${endpoint.host}\n`,
    'host',
    'UNSIGNED-PAYLOAD',
  ].join('\n');

  return { canonicalRequest, canonicalUri, canonicalQuery, amzDate, scope };
}

export function presignGetObjectUrl(params: PresignGetObjectParams): string {
  const { canonicalRequest, canonicalUri, canonicalQuery, amzDate, scope } =
    buildCanonicalRequest(params);
  const shortDate = amzDate.slice(0, 8);
  const endpoint = new URL(params.endpoint);

  const stringToSign = [ALGORITHM, amzDate, scope, sha256Hex(canonicalRequest)].join('\n');
  const signature = crypto
    .createHmac('sha256', signingKey(params.secretAccessKey, shortDate, params.region))
    .update(stringToSign, 'utf8')
    .digest('hex');

  return `${endpoint.origin}${canonicalUri}?${canonicalQuery}&X-Amz-Signature=${signature}`;
}
