import { z } from 'zod';
import { ShadeError } from '@shade/core';

export const FileErrorCodeSchema = z.enum([
  'NOT_FOUND',
  'PERMISSION_DENIED',
  'CONFLICT',
  'QUOTA_EXCEEDED',
  'RATE_LIMIT',
  'PATH_VALIDATION',
  'FINGERPRINT_REQUIRED',
  'OPERATION_TIMEOUT',
  'IDEMPOTENCY_CONFLICT',
  'CANCELLED',
  'INTERNAL',
  'NOT_IMPLEMENTED',
  'CUSTOM_OP_REJECTED',
  'INVALID_SIGNATURE',
  'INVALID_ARGS',
]);
export type FileErrorCode = z.infer<typeof FileErrorCodeSchema>;

export const FileErrorPayloadSchema = z.object({
  code: FileErrorCodeSchema,
  message: z.string().max(2048),
  /** Suggested retry delay in ms; only set for retriable errors (rate limit, transient). */
  retryAfterMs: z.number().int().nonnegative().optional(),
  /** Path or arg field the error refers to. */
  field: z.string().max(128).optional(),
  /** Optional cause string (sanitized — no stack traces leak from server). */
  cause: z.string().max(2048).optional(),
});
export type FileErrorPayload = z.infer<typeof FileErrorPayloadSchema>;

// ─── Class hierarchy ─────────────────────────────────────────

/**
 * Base class for all `@shade/files` errors. Extends `ShadeError` so the
 * existing `errorToHttpStatus()` mapping in `@shade/core` continues to apply.
 */
export class FileError extends ShadeError {
  readonly payload: FileErrorPayload;

  constructor(payload: FileErrorPayload) {
    super(`SHADE_FS_${payload.code}`, payload.message);
    this.name = 'FileError';
    this.payload = payload;
  }

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

export class NotFoundError extends FileError {
  constructor(message = 'Not found', field?: string) {
    super({ code: 'NOT_FOUND', message, ...(field !== undefined ? { field } : {}) });
    this.name = 'NotFoundError';
  }
}

export class PermissionDeniedError extends FileError {
  constructor(message = 'Permission denied') {
    super({ code: 'PERMISSION_DENIED', message });
    this.name = 'PermissionDeniedError';
  }
}

export class ConflictError extends FileError {
  constructor(message: string) {
    super({ code: 'CONFLICT', message });
    this.name = 'ConflictError';
  }
}

export class QuotaExceededError extends FileError {
  constructor(message = 'Quota exceeded', retryAfterMs?: number) {
    super({
      code: 'QUOTA_EXCEEDED',
      message,
      ...(retryAfterMs !== undefined ? { retryAfterMs } : {}),
    });
    this.name = 'QuotaExceededError';
  }
}

export class FsRateLimitError extends FileError {
  constructor(message = 'Rate limit exceeded', retryAfterMs?: number) {
    super({
      code: 'RATE_LIMIT',
      message,
      ...(retryAfterMs !== undefined ? { retryAfterMs } : {}),
    });
    this.name = 'FsRateLimitError';
  }
}

export class PathValidationError extends FileError {
  constructor(message: string, field = 'path') {
    super({ code: 'PATH_VALIDATION', message, field });
    this.name = 'PathValidationError';
  }
}

export class FingerprintRequiredError extends FileError {
  constructor(message = 'Peer fingerprint must be verified before this operation') {
    super({ code: 'FINGERPRINT_REQUIRED', message });
    this.name = 'FingerprintRequiredError';
  }
}

export class OperationTimeoutError extends FileError {
  constructor(message = 'Operation timed out') {
    super({ code: 'OPERATION_TIMEOUT', message });
    this.name = 'OperationTimeoutError';
  }
}

export class IdempotencyConflictError extends FileError {
  constructor(
    message = 'Idempotency key reused with different arguments',
  ) {
    super({ code: 'IDEMPOTENCY_CONFLICT', message });
    this.name = 'IdempotencyConflictError';
  }
}

export class CancelledError extends FileError {
  constructor(message = 'Cancelled') {
    super({ code: 'CANCELLED', message });
    this.name = 'CancelledError';
  }
}

export class InternalFileError extends FileError {
  constructor(message = 'Internal server error', cause?: string) {
    super({
      code: 'INTERNAL',
      message,
      ...(cause !== undefined ? { cause } : {}),
    });
    this.name = 'InternalFileError';
  }
}

export class NotImplementedError extends FileError {
  constructor(op: string) {
    super({ code: 'NOT_IMPLEMENTED', message: `Operation not implemented: ${op}` });
    this.name = 'NotImplementedError';
  }
}

export class CustomOpRejectedError extends FileError {
  constructor(message: string) {
    super({ code: 'CUSTOM_OP_REJECTED', message });
    this.name = 'CustomOpRejectedError';
  }
}

export class InvalidSignatureError extends FileError {
  constructor(message = 'RPC envelope signature verification failed') {
    super({ code: 'INVALID_SIGNATURE', message });
    this.name = 'InvalidSignatureError';
  }
}

export class InvalidArgsError extends FileError {
  constructor(message: string, field?: string) {
    super({ code: 'INVALID_ARGS', message, ...(field !== undefined ? { field } : {}) });
    this.name = 'InvalidArgsError';
  }
}

/**
 * Reconstruct the right `FileError` subclass from a wire payload. Used by
 * the client to surface typed errors from server responses.
 */
export function fileErrorFromPayload(payload: FileErrorPayload): FileError {
  switch (payload.code) {
    case 'NOT_FOUND':
      return new NotFoundError(payload.message, payload.field);
    case 'PERMISSION_DENIED':
      return new PermissionDeniedError(payload.message);
    case 'CONFLICT':
      return new ConflictError(payload.message);
    case 'QUOTA_EXCEEDED':
      return new QuotaExceededError(payload.message, payload.retryAfterMs);
    case 'RATE_LIMIT':
      return new FsRateLimitError(payload.message, payload.retryAfterMs);
    case 'PATH_VALIDATION':
      return new PathValidationError(payload.message, payload.field);
    case 'FINGERPRINT_REQUIRED':
      return new FingerprintRequiredError(payload.message);
    case 'OPERATION_TIMEOUT':
      return new OperationTimeoutError(payload.message);
    case 'IDEMPOTENCY_CONFLICT':
      return new IdempotencyConflictError(payload.message);
    case 'CANCELLED':
      return new CancelledError(payload.message);
    case 'INTERNAL':
      return new InternalFileError(payload.message, payload.cause);
    case 'NOT_IMPLEMENTED':
      return new NotImplementedError(payload.message);
    case 'CUSTOM_OP_REJECTED':
      return new CustomOpRejectedError(payload.message);
    case 'INVALID_SIGNATURE':
      return new InvalidSignatureError(payload.message);
    case 'INVALID_ARGS':
      return new InvalidArgsError(payload.message, payload.field);
  }
}

/** Convert any thrown value into a `FileErrorPayload` for wire serialization. */
export function payloadFromError(err: unknown): FileErrorPayload {
  if (err instanceof FileError) return err.payload;
  if (err instanceof Error) {
    return {
      code: 'INTERNAL',
      message: err.message.slice(0, 2048),
    };
  }
  return { code: 'INTERNAL', message: 'unknown error' };
}
