/**
 * Shade error hierarchy.
 *
 * All errors have:
 *  - `name`: class name for `instanceof` checks
 *  - `code`: stable string code for error mapping (e.g. HTTP status)
 *  - `toJSON()`: serializable form for network transmission
 */
export class ShadeError extends Error {
  /** Stable error code for mapping to HTTP status or UI strings */
  readonly code: string;

  constructor(code: string, message: string) {
    super(message);
    this.name = 'ShadeError';
    this.code = code;
  }

  toJSON(): { name: string; code: string; message: string } {
    return { name: this.name, code: this.code, message: this.message };
  }
}

// ─── Crypto / Protocol Errors ────────────────────────────────

export class InvalidSignatureError extends ShadeError {
  constructor(message = 'Signature verification failed') {
    super('SHADE_INVALID_SIGNATURE', message);
    this.name = 'InvalidSignatureError';
  }
}

export class DecryptionError extends ShadeError {
  constructor(message = 'Decryption failed') {
    super('SHADE_DECRYPTION_FAILED', message);
    this.name = 'DecryptionError';
  }
}

export class MaxSkipExceededError extends ShadeError {
  constructor(requested: number, max: number) {
    super('SHADE_MAX_SKIP_EXCEEDED', `Cannot skip ${requested} messages (max: ${max})`);
    this.name = 'MaxSkipExceededError';
  }
}

export class DuplicateMessageError extends ShadeError {
  constructor() {
    super('SHADE_DUPLICATE_MESSAGE', 'Duplicate message: key already consumed');
    this.name = 'DuplicateMessageError';
  }
}

export class ReplayError extends ShadeError {
  constructor(message = 'Message timestamp outside replay window') {
    super('SHADE_REPLAY', message);
    this.name = 'ReplayError';
  }
}

// ─── Session / Identity Errors ───────────────────────────────

export class NoSessionError extends ShadeError {
  constructor(address: string) {
    super('SHADE_NO_SESSION', `No session for address: ${address}`);
    this.name = 'NoSessionError';
  }
}

export class UntrustedIdentityError extends ShadeError {
  constructor(address: string) {
    super('SHADE_UNTRUSTED_IDENTITY', `Untrusted identity key for: ${address}`);
    this.name = 'UntrustedIdentityError';
  }
}

export class PreKeyNotFoundError extends ShadeError {
  constructor(keyId: number, type: 'signed' | 'one-time') {
    super('SHADE_PREKEY_NOT_FOUND', `${type} prekey not found: ${keyId}`);
    this.name = 'PreKeyNotFoundError';
  }
}

export class IdentityRotationError extends ShadeError {
  constructor(message = 'Identity rotation failed') {
    super('SHADE_IDENTITY_ROTATION', message);
    this.name = 'IdentityRotationError';
  }
}

/**
 * Thrown when a fingerprint gate (V3.3) blocks an operation because the
 * peer's safety number has not been verified, or the registered handler
 * returned `false`.
 */
export class FingerprintNotVerifiedError extends ShadeError {
  constructor(
    public readonly peerAddress: string,
    public readonly gate: 'first-large-file' | 'backup-import' | 'new-device-trust' | 'inbox-fanout',
    message?: string,
  ) {
    super(
      'SHADE_FINGERPRINT_NOT_VERIFIED',
      message ?? `Fingerprint not verified for ${peerAddress} (gate: ${gate})`,
    );
    this.name = 'FingerprintNotVerifiedError';
  }
}

/**
 * Thrown when `seal()` / `open()` is called on a {@link StreamRatchet}
 * that has already been torn down via `close()`. The stream's ratchet
 * secrets have been zeroized and cannot be revived — open a fresh
 * stream instead.
 */
export class StreamClosedError extends ShadeError {
  constructor(message = 'Stream is closed') {
    super('SHADE_STREAM_CLOSED', message);
    this.name = 'StreamClosedError';
  }
}

/**
 * Thrown when a stream handshake frame is malformed, arrives in the
 * wrong order, or references a streamId that does not match the stream
 * it was fed to.
 */
export class StreamHandshakeError extends ShadeError {
  constructor(message = 'Stream handshake failed') {
    super('SHADE_STREAM_HANDSHAKE', message);
    this.name = 'StreamHandshakeError';
  }
}

// ─── Infrastructure Errors ───────────────────────────────────

export class NetworkError extends ShadeError {
  constructor(message: string, public readonly statusCode?: number) {
    super('SHADE_NETWORK', message);
    this.name = 'NetworkError';
  }
}

export class StorageError extends ShadeError {
  constructor(message: string, public override readonly cause?: unknown) {
    super('SHADE_STORAGE', message);
    this.name = 'StorageError';
  }
}

export class ValidationError extends ShadeError {
  constructor(message: string, public readonly field?: string) {
    super('SHADE_VALIDATION', message);
    this.name = 'ValidationError';
  }
}

export class TimeoutError extends ShadeError {
  constructor(message = 'Operation timed out') {
    super('SHADE_TIMEOUT', message);
    this.name = 'TimeoutError';
  }
}

export class RateLimitError extends ShadeError {
  constructor(message = 'Rate limit exceeded', public readonly retryAfterSeconds?: number) {
    super('SHADE_RATE_LIMIT', message);
    this.name = 'RateLimitError';
  }
}

export class ConfigurationError extends ShadeError {
  constructor(message: string) {
    super('SHADE_CONFIG', message);
    this.name = 'ConfigurationError';
  }
}

export class UnauthorizedError extends ShadeError {
  constructor(message = 'Unauthorized') {
    super('SHADE_UNAUTHORIZED', message);
    this.name = 'UnauthorizedError';
  }
}

/**
 * 409 Conflict — caller wrote to a resource that already exists without
 * supplying an If-Match precondition. V4.9: the encrypted blob primitive
 * uses this to force read-then-write on already-occupied slots.
 */
export class ConflictError extends ShadeError {
  constructor(message = 'Conflict') {
    super('SHADE_CONFLICT', message);
    this.name = 'ConflictError';
  }
}

/**
 * 412 Precondition Failed — caller supplied an If-Match etag that does
 * not match the current state. V4.9: the encrypted blob primitive uses
 * this to surface stale-CAS so clients can re-read, merge, and retry.
 */
export class PreconditionFailedError extends ShadeError {
  constructor(message = 'Precondition failed') {
    super('SHADE_PRECONDITION_FAILED', message);
    this.name = 'PreconditionFailedError';
  }
}

// ─── Error → HTTP Status Mapping ────────────────────────────

/**
 * Map a ShadeError to an HTTP status code.
 * Used by the prekey server's global error handler.
 */
export function errorToHttpStatus(error: unknown): number {
  if (!(error instanceof ShadeError)) return 500;
  switch (error.code) {
    case 'SHADE_INVALID_SIGNATURE':
    case 'SHADE_UNAUTHORIZED':
      return 401;
    case 'SHADE_UNTRUSTED_IDENTITY':
    case 'SHADE_FINGERPRINT_NOT_VERIFIED':
      return 403;
    case 'SHADE_NO_SESSION':
    case 'SHADE_PREKEY_NOT_FOUND':
      return 404;
    case 'SHADE_VALIDATION':
      return 400;
    case 'SHADE_REPLAY':
    case 'SHADE_DUPLICATE_MESSAGE':
    case 'SHADE_CONFLICT':
      return 409;
    case 'SHADE_PRECONDITION_FAILED':
      return 412;
    case 'SHADE_RATE_LIMIT':
      return 429;
    case 'SHADE_TIMEOUT':
      return 504;
    case 'SHADE_NETWORK':
    case 'SHADE_STORAGE':
    case 'SHADE_CONFIG':
      return 503;
    default:
      return 500;
  }
}
