/**
 * Backblaze B2 client (S3-compatible) for the LogLift evidence pipeline.
 *
 * Implements AWS Signature Version 4 presigned URLs (matches the n8n
 * collector's expectations) for both downloads (Pulse fetching uploaded
 * payloads) and uploads (Pulse handing the collector a presigned PUT
 * target so the script doesn't carry credentials).
 *
 * Port of the SigV4 implementation from `docs/LogLift Review.json` —
 * battle-tested in production via the existing n8n flow.
 */

import { createHash, createHmac } from 'crypto';

export interface B2Config {
  keyId: string;
  secret: string;
  bucket: string;
  region: string;
  /** S3-compatible endpoint, e.g. `s3.us-west-002.backblazeb2.com` (no scheme). */
  endpoint: string;
}

/** Hard cap on bytes Pulse will read from a B2 object. */
export const MAX_DOWNLOAD_BYTES = 25 * 1024 * 1024; // 25 MB

/**
 * Object-key shape we accept from inbound webhooks. Path-traversal guard
 * — must be `{client_id_or_uuid}/{computer_name}/eventlogs_{timestamp}.json.gz`.
 */
export const OBJECT_KEY_REGEX =
  /^[A-Za-z0-9_-]+\/[A-Za-z0-9_.-]+\/eventlogs_[0-9_]+\.json\.gz$/;

/**
 * Object-key shape for raw `.eml` evidence uploads (Phase 16 / D-05):
 * `phishing/{reportId}/{attachmentId}.eml`. This is a SEPARATE regex from
 * OBJECT_KEY_REGEX — per the B2 evidence skill doc, never loosen the
 * existing LogLift guard to accommodate a new shape. Path-traversal safe:
 * each segment is restricted to `[A-Za-z0-9_-]+`, so `..` cannot appear.
 */
export const EML_OBJECT_KEY_REGEX =
  /^phishing\/[A-Za-z0-9_-]+\/[A-Za-z0-9_-]+\.eml$/;

export class B2NotConfiguredError extends Error {
  constructor() {
    super(
      'Backblaze B2 is not configured. Set B2_KEY_ID + B2_APP_KEY (and optionally B2_BUCKET / B2_REGION / B2_ENDPOINT).'
    );
    this.name = 'B2NotConfiguredError';
  }
}

export class B2InvalidObjectKeyError extends Error {
  constructor(objectKey: string) {
    super(`Invalid object key shape: ${objectKey.slice(0, 200)}`);
    this.name = 'B2InvalidObjectKeyError';
  }
}

export function isB2Configured(): boolean {
  return !!(process.env.B2_KEY_ID && process.env.B2_APP_KEY);
}

export function getB2Config(): B2Config {
  const keyId = process.env.B2_KEY_ID;
  const secret = process.env.B2_APP_KEY;
  if (!keyId || !secret) throw new B2NotConfiguredError();
  return {
    keyId,
    secret,
    bucket: process.env.B2_BUCKET || 'wulf-audits',
    region: process.env.B2_REGION || 'us-west-002',
    endpoint: process.env.B2_ENDPOINT || 's3.us-west-002.backblazeb2.com',
  };
}

function sign(key: Buffer | string, msg: string): Buffer {
  return createHmac('sha256', key).update(msg, 'utf8').digest();
}

function deriveSigningKey(
  secret: string,
  dateStamp: string,
  region: string,
  service: string
): Buffer {
  const kDate = sign('AWS4' + secret, dateStamp);
  const kRegion = sign(kDate, region);
  const kService = sign(kRegion, service);
  return sign(kService, 'aws4_request');
}

interface PresignParams {
  method: 'GET' | 'PUT';
  objectKey: string;
  expiresInSeconds: number;
  config: B2Config;
}

function presign(params: PresignParams): string {
  const { method, objectKey, expiresInSeconds, config } = params;
  const host = config.endpoint;
  // We do NOT URL-encode slashes in the path itself; SigV4 wants the
  // literal canonical URI with the object key as-is (slashes intact).
  const canonicalUri = '/' + config.bucket + '/' + objectKey;
  const algorithm = 'AWS4-HMAC-SHA256';

  const now = new Date();
  const amzDate = now.toISOString().replace(/[-:]/g, '').split('.')[0] + 'Z';
  const dateStamp = amzDate.slice(0, 8);
  const credentialScope = `${dateStamp}/${config.region}/s3/aws4_request`;

  const canonicalHeaders = `host:${host}\n`;
  const signedHeaders = 'host';

  const qs: Record<string, string> = {
    'X-Amz-Algorithm': algorithm,
    'X-Amz-Credential': encodeURIComponent(`${config.keyId}/${credentialScope}`),
    'X-Amz-Date': amzDate,
    'X-Amz-Expires': String(expiresInSeconds),
    'X-Amz-SignedHeaders': signedHeaders,
  };

  const canonicalQueryString = Object.keys(qs)
    .sort()
    .map((k) => `${k}=${qs[k]}`)
    .join('&');

  const payloadHash = 'UNSIGNED-PAYLOAD';

  const canonicalRequest = [
    method,
    canonicalUri,
    canonicalQueryString,
    canonicalHeaders,
    signedHeaders,
    payloadHash,
  ].join('\n');

  const stringToSign = [
    algorithm,
    amzDate,
    credentialScope,
    createHash('sha256').update(canonicalRequest, 'utf8').digest('hex'),
  ].join('\n');

  const signingKey = deriveSigningKey(config.secret, dateStamp, config.region, 's3');
  const signature = createHmac('sha256', signingKey)
    .update(stringToSign, 'utf8')
    .digest('hex');

  return `https://${host}${canonicalUri}?${canonicalQueryString}&X-Amz-Signature=${signature}`;
}

export function presignDownload(
  objectKey: string,
  expiresInSeconds = 600,
  cfg: B2Config = getB2Config(),
  keyRegex: RegExp = OBJECT_KEY_REGEX
): string {
  if (!keyRegex.test(objectKey)) throw new B2InvalidObjectKeyError(objectKey);
  return presign({ method: 'GET', objectKey, expiresInSeconds, config: cfg });
}

export function presignUpload(
  objectKey: string,
  expiresInSeconds = 1800,
  cfg: B2Config = getB2Config(),
  keyRegex: RegExp = OBJECT_KEY_REGEX
): string {
  if (!keyRegex.test(objectKey)) throw new B2InvalidObjectKeyError(objectKey);
  return presign({ method: 'PUT', objectKey, expiresInSeconds, config: cfg });
}

/**
 * Stream a B2 object to a Buffer. Caps at MAX_DOWNLOAD_BYTES — refuses to
 * read past that even if the server returns more.
 */
export async function downloadToBuffer(
  objectKey: string,
  cfg: B2Config = getB2Config(),
  keyRegex: RegExp = OBJECT_KEY_REGEX
): Promise<Buffer> {
  const url = presignDownload(objectKey, 600, cfg, keyRegex);
  const res = await fetch(url);
  if (!res.ok) {
    const text = await res.text().catch(() => '');
    throw new Error(`B2 GET ${objectKey} → ${res.status} ${res.statusText}: ${text.slice(0, 300)}`);
  }
  // Best-effort content-length check before reading the body.
  const contentLength = res.headers.get('content-length');
  if (contentLength && Number(contentLength) > MAX_DOWNLOAD_BYTES) {
    throw new Error(
      `B2 object ${objectKey} too large: ${contentLength} bytes (cap ${MAX_DOWNLOAD_BYTES})`
    );
  }
  if (!res.body) {
    throw new Error(`B2 GET ${objectKey} returned no body`);
  }

  const reader = res.body.getReader();
  const chunks: Uint8Array[] = [];
  let total = 0;
  for (;;) {
    const { value, done } = await reader.read();
    if (done) break;
    if (!value) continue;
    total += value.byteLength;
    if (total > MAX_DOWNLOAD_BYTES) {
      try { await reader.cancel(); } catch { /* ignore */ }
      throw new Error(
        `B2 object ${objectKey} exceeded ${MAX_DOWNLOAD_BYTES} bytes mid-stream`
      );
    }
    chunks.push(value);
  }
  return Buffer.concat(chunks.map((c) => Buffer.from(c.buffer, c.byteOffset, c.byteLength)));
}

// Test-only exports.
export const _B2_INTERNALS = {
  deriveSigningKey,
  presign,
};
