/**
 * AWS Signature Version 4 for S3 requests — zero external deps, built on
 * `node:crypto` + `fetch`. Matches the framework's zero-dep ethos (like
 * `@wrnexus/ai`) and works with any S3-compatible service (AWS, Cloudflare R2,
 * Backblaze B2, MinIO, DigitalOcean Spaces).
 *
 * Reference: docs.aws.amazon.com/general/latest/gr/sigv4_signing.html
 */

import { createHash, createHmac } from "node:crypto";

/** Hex-encoded SHA-256 of a payload. */
export function sha256Hex(data: Uint8Array | string): string {
  return createHash("sha256").update(data).digest("hex");
}

function hmac(key: Uint8Array | string, data: string): Buffer {
  return createHmac("sha256", key as Buffer)
    .update(data, "utf8")
    .digest();
}

/** `2024-01-02T03:04:05.678Z` → `20240102T030405Z`. */
function amzDate(d: Date): string {
  return d
    .toISOString()
    .replace(/[.]\d{3}/, "")
    .replace(/[:-]/g, "");
}

/**
 * Percent-encode an S3 object key for the request path. Every character except
 * the RFC 3986 unreserved set is encoded; `/` between segments is preserved.
 */
export function encodeKey(key: string): string {
  return key
    .split("/")
    .map((seg) =>
      encodeURIComponent(seg).replace(
        /[!*'()]/g,
        (c) => "%" + c.charCodeAt(0).toString(16).toUpperCase(),
      ),
    )
    .join("/");
}

export interface SignInput {
  method: string;
  host: string;
  /** Canonical URI — already `%`-encoded, begins with `/`. */
  path: string;
  region: string;
  accessKeyId: string;
  secretAccessKey: string;
  /** Hex SHA-256 of the body, or `"UNSIGNED-PAYLOAD"`. */
  payloadHash: string;
  /** Extra headers to sign (e.g. `content-type`). `host`/`x-amz-*` are added here. */
  headers?: Record<string, string>;
  date: Date;
  service?: string;
}

/**
 * Compute the signed header set for an S3 request. Returns the headers to send
 * (lowercased names, including `authorization`, `host`, `x-amz-date`,
 * `x-amz-content-sha256`).
 */
export function signS3(input: SignInput): Record<string, string> {
  const service = input.service ?? "s3";
  const now = amzDate(input.date);
  const stamp = now.slice(0, 8);

  // Normalize headers to lowercase names + trimmed values; add the required ones.
  const headers: Record<string, string> = {};
  for (const [k, v] of Object.entries(input.headers ?? {})) {
    headers[k.toLowerCase()] = String(v).trim();
  }
  headers["host"] = input.host;
  headers["x-amz-content-sha256"] = input.payloadHash;
  headers["x-amz-date"] = now;

  const names = Object.keys(headers).sort();
  const canonicalHeaders = names.map((n) => `${n}:${headers[n]}\n`).join("");
  const signedHeaders = names.join(";");

  const canonicalRequest = [
    input.method.toUpperCase(),
    input.path,
    "", // canonical query string (none for our object operations)
    canonicalHeaders,
    signedHeaders,
    input.payloadHash,
  ].join("\n");

  const scope = `${stamp}/${input.region}/${service}/aws4_request`;
  const stringToSign = ["AWS4-HMAC-SHA256", now, scope, sha256Hex(canonicalRequest)].join("\n");

  const kDate = hmac("AWS4" + input.secretAccessKey, stamp);
  const kRegion = hmac(kDate, input.region);
  const kService = hmac(kRegion, service);
  const kSigning = hmac(kService, "aws4_request");
  const signature = createHmac("sha256", kSigning).update(stringToSign, "utf8").digest("hex");

  headers["authorization"] =
    `AWS4-HMAC-SHA256 Credential=${input.accessKeyId}/${scope}, ` +
    `SignedHeaders=${signedHeaders}, Signature=${signature}`;

  return headers;
}
