import { argon2id } from '@noble/hashes/argon2.js';
import { bytesToBase64, base64ToBytes } from '../lib/base64.js';
import type { MultidealEnv } from '../lib/env.js';

/**
 * PasswordHashDO — argon2id password hashing under the DO's 30s CPU budget.
 *
 * Why a DO: argon2id at OWASP params (m=19456 KiB ≈ 19 MB, t=2, p=1) cannot run
 * in the ~10ms free-tier request worker. A SQLite-backed DO gets a 30s CPU budget.
 * The caller keys the instance by `idFromName(userId)` so load distributes across
 * users (parallel) while staying serial per-user — a free per-user rate-limit.
 *
 * Hash format (PHC-style, the version marker IS the format):
 *   argon2id$v=19$m=19456,t=2,p=1$<saltB64>$<hmacB64>
 *
 * The argon2 output is STILL peppered with the same after-KDF discipline as the
 * pep1 scheme: hmac = HMAC-SHA256(key = hex-decoded PASSWORD_PEPPER_V1, message =
 * raw argon2id output). The pepper is a CF secret, never stored, never logged.
 *
 * Routes (all secret-gated on header `x-md-hash-do` === env.HASH_DO_SECRET):
 *   POST /hash   { userId, password }          -> { hash }
 *   POST /verify { userId, password, stored }  -> { ok }
 */

// OWASP argon2id parameters (RFC 9106). Memory in KiB, time in iterations.
const ARGON2_M = 19456; // 19 MiB
const ARGON2_T = 2;
const ARGON2_P = 1;
const ARGON2_VERSION = 19; // 0x13
const ARGON2_DKLEN = 32;
const SALT_BYTES = 16;
const HMAC_BYTES = 32; // HMAC-SHA256 output width (conceptually distinct from dkLen)

// Sane bounds for parsed PHC cost params. Any out-of-bounds value is treated as
// malformed input → parsePhc returns null → /verify fails closed with {ok:false}.
// This rejects degenerate (m=0,t=0,p=0) and absurd (huge m) params BEFORE the
// derive, so malformed stored hashes can never throw a 500 or exhaust the DO.
const PHC_T_MIN = 1;
const PHC_T_MAX = 10;
const PHC_P_MIN = 1;
const PHC_P_MAX = 4;
const PHC_M_MIN = 8; // KiB
const PHC_M_MAX = 1048576; // KiB (~1 GiB ceiling)

interface ParsedPhc {
  m: number;
  t: number;
  p: number;
  version: number;
  salt: Uint8Array;
  hmac: Uint8Array;
}

/**
 * Parse a PHC-style argon2id string. Returns null on ANY malformation so the
 * verify path can fail closed without throwing.
 */
function parsePhc(stored: string): ParsedPhc | null {
  // argon2id$v=19$m=19456,t=2,p=1$<saltB64>$<hmacB64>
  const parts = stored.split('$');
  if (parts.length !== 5) return null;
  const [scheme, vSeg, costSeg, saltB64, hmacB64] = parts;
  if (scheme !== 'argon2id') return null;

  const vMatch = /^v=(\d+)$/.exec(vSeg!);
  if (!vMatch) return null;
  const version = Number(vMatch[1]);

  const costMatch = /^m=(\d+),t=(\d+),p=(\d+)$/.exec(costSeg!);
  if (!costMatch) return null;
  const m = Number(costMatch[1]);
  const t = Number(costMatch[2]);
  const p = Number(costMatch[3]);
  if (!Number.isInteger(m) || !Number.isInteger(t) || !Number.isInteger(p)) return null;
  if (!Number.isInteger(version)) return null;

  // Bounds: reject degenerate/absurd params BEFORE the derive (fail closed).
  // Re-deriving with m=0/t=0 throws; a huge m would exhaust the DO. Either is a
  // malformed stored value, not a credential — return null → /verify {ok:false}.
  if (version !== ARGON2_VERSION) return null;
  if (t < PHC_T_MIN || t > PHC_T_MAX) return null;
  if (p < PHC_P_MIN || p > PHC_P_MAX) return null;
  if (m < PHC_M_MIN || m > PHC_M_MAX) return null;

  let salt: Uint8Array;
  let hmac: Uint8Array;
  try {
    salt = base64ToBytes(saltB64!);
    hmac = base64ToBytes(hmacB64!);
  } catch (err) {
    void err;
    return null;
  }
  // Salt and hmac must decode to exactly the expected widths.
  if (salt.length !== SALT_BYTES || hmac.length !== HMAC_BYTES) return null;

  return { m, t, p, version, salt, hmac };
}

/** Constant-time byte comparison. Never use === on secret material. */
function timingSafeEqualBytes(a: Uint8Array, b: Uint8Array): boolean {
  if (a.length !== b.length) return false;
  let diff = 0;
  for (let i = 0; i < a.length; i++) {
    diff |= a[i]! ^ b[i]!;
  }
  return diff === 0;
}

/**
 * Apply the after-KDF pepper: HMAC-SHA256(key = hex-decoded pepper, message =
 * raw argon2id bytes). Byte-identical discipline to credentials.ts hmacPepper.
 */
async function hmacPepper(argonRaw: Uint8Array, pepperHex: string): Promise<Uint8Array> {
  if (pepperHex.length % 2 !== 0 || !/^[0-9a-fA-F]+$/.test(pepperHex)) {
    throw new Error('hmacPepper: invalid pepper hex');
  }
  const keyBytes = new Uint8Array(pepperHex.length / 2);
  for (let i = 0; i < pepperHex.length; i += 2) {
    keyBytes[i / 2] = parseInt(pepperHex.slice(i, i + 2), 16);
  }
  const key = await crypto.subtle.importKey(
    'raw',
    keyBytes,
    { name: 'HMAC', hash: 'SHA-256' },
    false,
    ['sign'],
  );
  const sig = await crypto.subtle.sign('HMAC', key, argonRaw as unknown as ArrayBuffer);
  return new Uint8Array(sig);
}

/**
 * Derive the peppered argon2id digest for a password against a given salt and
 * cost params. The argon2id call is the heavy step the DO exists to host.
 */
async function derivePepperedDigest(
  password: string,
  salt: Uint8Array,
  pepperHex: string,
  m: number,
  t: number,
  p: number,
  version: number,
): Promise<Uint8Array> {
  const argonRaw = argon2id(password, salt, {
    m,
    t,
    p,
    version,
    dkLen: ARGON2_DKLEN,
  });
  return hmacPepper(argonRaw, pepperHex);
}

export class PasswordHashDO {
  private env: MultidealEnv;

  constructor(_ctx: DurableObjectState, env: MultidealEnv) {
    this.env = env;
  }

  async fetch(request: Request): Promise<Response> {
    // Secret-gate: reject unless the caller carries the shared secret header.
    const secret = this.env.HASH_DO_SECRET;
    const header = request.headers.get('x-md-hash-do');
    if (!header || !secret || header !== secret) {
      return new Response('Forbidden', { status: 403 });
    }

    const pepperHex = this.env.PASSWORD_PEPPER_V1;
    if (!pepperHex) {
      return Response.json({ error: 'pepper_unconfigured' }, { status: 500 });
    }

    const url = new URL(request.url);

    if (request.method === 'POST' && url.pathname === '/hash') {
      const body = (await request.json()) as { userId?: unknown; password?: unknown };
      if (typeof body.password !== 'string' || body.password.length === 0) {
        return Response.json({ error: 'bad_request' }, { status: 400 });
      }
      const salt = new Uint8Array(SALT_BYTES);
      crypto.getRandomValues(salt);
      const digest = await derivePepperedDigest(
        body.password,
        salt,
        pepperHex,
        ARGON2_M,
        ARGON2_T,
        ARGON2_P,
        ARGON2_VERSION,
      );
      const hash =
        `argon2id$v=${ARGON2_VERSION}$m=${ARGON2_M},t=${ARGON2_T},p=${ARGON2_P}$` +
        `${bytesToBase64(salt)}$${bytesToBase64(digest)}`;
      return Response.json({ hash });
    }

    if (request.method === 'POST' && url.pathname === '/verify') {
      const body = (await request.json()) as {
        userId?: unknown;
        password?: unknown;
        stored?: unknown;
      };
      if (typeof body.password !== 'string' || typeof body.stored !== 'string') {
        return Response.json({ error: 'bad_request' }, { status: 400 });
      }
      // Re-derive with the STORED salt + params (not the current constants) so a
      // future param bump cannot break verification of already-persisted hashes.
      const parsed = parsePhc(body.stored);
      if (!parsed) {
        // Malformed stored value — fail closed, never throw.
        return Response.json({ ok: false });
      }
      const candidate = await derivePepperedDigest(
        body.password,
        parsed.salt,
        pepperHex,
        parsed.m,
        parsed.t,
        parsed.p,
        parsed.version,
      );
      return Response.json({ ok: timingSafeEqualBytes(candidate, parsed.hmac) });
    }

    return Response.json({ error: 'not_found' }, { status: 404 });
  }
}
