import type { CryptoProvider } from '@shade/core';
import { fromBase64, UnauthorizedError, ValidationError, ReplayError } from '@shade/core';

/**
 * Self-authenticated prekey server.
 *
 * Each write request must include a signature over a canonical
 * representation of the body (excluding the signature field itself)
 * using the identity's Ed25519 signing key.
 *
 * On register: the signing key is extracted from the body itself
 * (TOFU — first registration establishes the identity).
 *
 * On replenish/delete: the signing key is looked up from the stored
 * identity record for the address.
 */

/** Maximum age of a signed request, in milliseconds */
const MAX_SIGNATURE_AGE_MS = 5 * 60 * 1000; // 5 minutes

/** Maximum clock skew tolerated (future timestamps) */
const MAX_FUTURE_SKEW_MS = 1 * 60 * 1000; // 1 minute

interface SignedPayload {
  signedAt: number;
  signature: string; // base64
  [key: string]: unknown;
}

/**
 * Canonicalize a payload for signing by omitting the `signature` field
 * and using deterministic JSON key ordering.
 *
 * Both signer and verifier must produce identical bytes.
 */
export function canonicalizePayload(payload: SignedPayload): Uint8Array {
  const { signature, ...rest } = payload;
  // Sort keys to be deterministic
  const sorted = Object.keys(rest)
    .sort()
    .reduce<Record<string, unknown>>((acc, key) => {
      acc[key] = (rest as Record<string, unknown>)[key];
      return acc;
    }, {});
  return new TextEncoder().encode(JSON.stringify(sorted));
}

/**
 * Sign a payload with an Ed25519 signing private key.
 * Returns the payload with `signature` and `signedAt` set.
 */
export async function signPayload<T extends Record<string, unknown>>(
  crypto: CryptoProvider,
  signingPrivateKey: Uint8Array,
  payload: T,
): Promise<T & { signedAt: number; signature: string }> {
  const signedAt = Date.now();
  const bytes = canonicalizePayload({ ...payload, signedAt, signature: '' });
  const sig = await crypto.sign(signingPrivateKey, bytes);
  return {
    ...payload,
    signedAt,
    signature: Buffer.from(sig).toString('base64'),
  };
}

/**
 * Verify a signed payload against a given signing public key.
 *
 * Throws:
 *  - ValidationError if signedAt or signature fields are missing/malformed
 *  - ReplayError if signedAt is outside the allowed window
 *  - UnauthorizedError if the signature does not verify
 */
export async function verifyPayload(
  crypto: CryptoProvider,
  signingPublicKey: Uint8Array,
  payload: unknown,
): Promise<void> {
  if (!payload || typeof payload !== 'object') {
    throw new ValidationError('Payload must be an object');
  }
  const p = payload as SignedPayload;

  if (typeof p.signedAt !== 'number') {
    throw new ValidationError('Missing or invalid signedAt', 'signedAt');
  }
  if (typeof p.signature !== 'string') {
    throw new ValidationError('Missing or invalid signature', 'signature');
  }

  const now = Date.now();
  const age = now - p.signedAt;
  if (age > MAX_SIGNATURE_AGE_MS) {
    throw new ReplayError(`Signature too old: ${age}ms`);
  }
  if (age < -MAX_FUTURE_SKEW_MS) {
    throw new ReplayError(`Signature in the future: ${-age}ms ahead`);
  }

  let sigBytes: Uint8Array;
  try {
    sigBytes = fromBase64(p.signature);
  } catch {
    throw new ValidationError('Signature is not valid base64', 'signature');
  }

  const canonical = canonicalizePayload(p);
  const valid = await crypto.verify(signingPublicKey, canonical, sigBytes);
  if (!valid) {
    throw new UnauthorizedError('Invalid signature');
  }
}

// ─── Address validation ─────────────────────────────────────

const ADDRESS_REGEX = /^[a-zA-Z0-9][a-zA-Z0-9:_\-.]{0,255}$/;

/**
 * Validate a peer address string.
 * Allows alphanumeric, :, _, -, . with max 256 chars and must start with alphanumeric.
 * Rejects path traversal, unicode tricks, and empty strings.
 */
export function validateAddress(address: unknown): string {
  if (typeof address !== 'string') {
    throw new ValidationError('Address must be a string', 'address');
  }
  const normalized = address.normalize('NFKC');
  if (!ADDRESS_REGEX.test(normalized)) {
    throw new ValidationError('Invalid address format', 'address');
  }
  return normalized;
}
