/// Type definitions for Synor Compute SDK
library synor_compute.types;

import 'package:collection/collection.dart';

/// Numeric precision for compute operations
enum Precision {
  fp64('fp64'),
  fp32('fp32'),
  fp16('fp16'),
  bf16('bf16'),
  int8('int8'),
  int4('int4');

  const Precision(this.value);
  final String value;

  static Precision fromString(String s) =>
      Precision.values.firstWhere((p) => p.value == s, orElse: () => fp32);
}

/// Target processor type for compute operations
enum ProcessorType {
  cpu('cpu'),
  gpu('gpu'),
  tpu('tpu'),
  npu('npu'),
  lpu('lpu'),
  fpga('fpga'),
  dsp('dsp'),
  webgpu('webgpu'),
  wasm('wasm'),
  auto('auto');

  const ProcessorType(this.value);
  final String value;

  static ProcessorType fromString(String s) =>
      ProcessorType.values.firstWhere((p) => p.value == s, orElse: () => auto);
}

/// Job priority levels
enum Priority {
  low('low'),
  normal('normal'),
  high('high'),
  critical('critical');

  const Priority(this.value);
  final String value;

  static Priority fromString(String s) =>
      Priority.values.firstWhere((p) => p.value == s, orElse: () => normal);
}

/// Job execution status
enum JobStatus {
  pending('pending'),
  queued('queued'),
  running('running'),
  completed('completed'),
  failed('failed'),
  cancelled('cancelled');

  const JobStatus(this.value);
  final String value;

  bool get isTerminal =>
      this == completed || this == failed || this == cancelled;

  static JobStatus fromString(String s) =>
      JobStatus.values.firstWhere((p) => p.value == s, orElse: () => pending);
}

/// Balancing strategy for load distribution
enum BalancingStrategy {
  speed('speed'),
  energy('energy'),
  balanced('balanced'),
  cost('cost'),
  latency('latency');

  const BalancingStrategy(this.value);
  final String value;

  static BalancingStrategy fromString(String s) => BalancingStrategy.values
      .firstWhere((p) => p.value == s, orElse: () => balanced);
}

/// Tensor data type
enum DType {
  float64('float64'),
  float32('float32'),
  float16('float16'),
  bfloat16('bfloat16'),
  int64('int64'),
  int32('int32'),
  int16('int16'),
  int8('int8'),
  uint8('uint8'),
  bool_('bool');

  const DType(this.value);
  final String value;

  static DType fromString(String s) =>
      DType.values.firstWhere((p) => p.value == s, orElse: () => float32);
}

/// Configuration for SDK client
class SynorConfig {
  final String apiKey;
  final String baseUrl;
  final Duration timeout;
  final int maxRetries;
  final ProcessorType defaultProcessor;
  final Precision defaultPrecision;
  final Priority defaultPriority;

  const SynorConfig({
    required this.apiKey,
    this.baseUrl = 'https://compute.synor.io',
    this.timeout = const Duration(seconds: 30),
    this.maxRetries = 3,
    this.defaultProcessor = ProcessorType.auto,
    this.defaultPrecision = Precision.fp32,
    this.defaultPriority = Priority.normal,
  });

  SynorConfig copyWith({
    String? apiKey,
    String? baseUrl,
    Duration? timeout,
    int? maxRetries,
    ProcessorType? defaultProcessor,
    Precision? defaultPrecision,
    Priority? defaultPriority,
  }) {
    return SynorConfig(
      apiKey: apiKey ?? this.apiKey,
      baseUrl: baseUrl ?? this.baseUrl,
      timeout: timeout ?? this.timeout,
      maxRetries: maxRetries ?? this.maxRetries,
      defaultProcessor: defaultProcessor ?? this.defaultProcessor,
      defaultPrecision: defaultPrecision ?? this.defaultPrecision,
      defaultPriority: defaultPriority ?? this.defaultPriority,
    );
  }
}

/// Matrix multiplication options
class MatMulOptions {
  final Precision? precision;
  final ProcessorType? processor;
  final Priority? priority;
  final bool transposeA;
  final bool transposeB;

  const MatMulOptions({
    this.precision,
    this.processor,
    this.priority,
    this.transposeA = false,
    this.transposeB = false,
  });

  Map<String, dynamic> toJson() => {
        if (precision != null) 'precision': precision!.value,
        if (processor != null) 'processor': processor!.value,
        if (priority != null) 'priority': priority!.value,
        'transpose_a': transposeA,
        'transpose_b': transposeB,
      };
}

/// Convolution options
class Conv2dOptions {
  final List<int> kernel;
  final List<int> stride;
  final List<int> padding;
  final List<int> dilation;
  final int groups;
  final Precision? precision;
  final ProcessorType? processor;
  final Priority? priority;

  const Conv2dOptions({
    this.kernel = const [3, 3],
    this.stride = const [1, 1],
    this.padding = const [0, 0],
    this.dilation = const [1, 1],
    this.groups = 1,
    this.precision,
    this.processor,
    this.priority,
  });

  Map<String, dynamic> toJson() => {
        'kernel': kernel,
        'stride': stride,
        'padding': padding,
        'dilation': dilation,
        'groups': groups,
        if (precision != null) 'precision': precision!.value,
        if (processor != null) 'processor': processor!.value,
        if (priority != null) 'priority': priority!.value,
      };
}

/// Flash attention options
class AttentionOptions {
  final int numHeads;
  final double? scale;
  final bool causal;
  final double? dropoutP;
  final Precision? precision;
  final ProcessorType? processor;
  final Priority? priority;

  const AttentionOptions({
    required this.numHeads,
    this.scale,
    this.causal = false,
    this.dropoutP,
    this.precision,
    this.processor,
    this.priority,
  });

  Map<String, dynamic> toJson() => {
        'num_heads': numHeads,
        if (scale != null) 'scale': scale,
        'causal': causal,
        if (dropoutP != null) 'dropout_p': dropoutP,
        if (precision != null) 'precision': precision!.value,
        if (processor != null) 'processor': processor!.value,
        if (priority != null) 'priority': priority!.value,
      };
}

/// LLM inference options
class InferenceOptions {
  final int maxTokens;
  final double temperature;
  final double topP;
  final int? topK;
  final double? frequencyPenalty;
  final double? presencePenalty;
  final List<String>? stopSequences;
  final bool stream;
  final ProcessorType? processor;
  final Priority? priority;

  const InferenceOptions({
    this.maxTokens = 256,
    this.temperature = 0.7,
    this.topP = 1.0,
    this.topK,
    this.frequencyPenalty,
    this.presencePenalty,
    this.stopSequences,
    this.stream = false,
    this.processor,
    this.priority,
  });

  Map<String, dynamic> toJson() => {
        'max_tokens': maxTokens,
        'temperature': temperature,
        'top_p': topP,
        if (topK != null) 'top_k': topK,
        if (frequencyPenalty != null) 'frequency_penalty': frequencyPenalty,
        if (presencePenalty != null) 'presence_penalty': presencePenalty,
        if (stopSequences != null) 'stop_sequences': stopSequences,
        'stream': stream,
        if (processor != null) 'processor': processor!.value,
        if (priority != null) 'priority': priority!.value,
      };
}

/// Pricing information for compute resources
class PricingInfo {
  final ProcessorType processor;
  final double pricePerSecond;
  final double pricePerGflop;
  final int availableUnits;
  final double utilizationPercent;
  final String region;

  const PricingInfo({
    required this.processor,
    required this.pricePerSecond,
    required this.pricePerGflop,
    required this.availableUnits,
    required this.utilizationPercent,
    required this.region,
  });

  factory PricingInfo.fromJson(Map<String, dynamic> json) => PricingInfo(
        processor: ProcessorType.fromString(json['processor'] as String),
        pricePerSecond: (json['price_per_second'] as num).toDouble(),
        pricePerGflop: (json['price_per_gflop'] as num).toDouble(),
        availableUnits: json['available_units'] as int,
        utilizationPercent: (json['utilization_percent'] as num).toDouble(),
        region: json['region'] as String,
      );
}

/// Compute usage statistics
class UsageStats {
  final int totalJobs;
  final int completedJobs;
  final int failedJobs;
  final double totalComputeSeconds;
  final double totalCost;
  final Map<ProcessorType, double> costByProcessor;

  const UsageStats({
    required this.totalJobs,
    required this.completedJobs,
    required this.failedJobs,
    required this.totalComputeSeconds,
    required this.totalCost,
    required this.costByProcessor,
  });

  factory UsageStats.fromJson(Map<String, dynamic> json) {
    final costMap = <ProcessorType, double>{};
    final rawCostMap = json['cost_by_processor'] as Map<String, dynamic>?;
    if (rawCostMap != null) {
      for (final entry in rawCostMap.entries) {
        costMap[ProcessorType.fromString(entry.key)] =
            (entry.value as num).toDouble();
      }
    }

    return UsageStats(
      totalJobs: json['total_jobs'] as int,
      completedJobs: json['completed_jobs'] as int,
      failedJobs: json['failed_jobs'] as int,
      totalComputeSeconds: (json['total_compute_seconds'] as num).toDouble(),
      totalCost: (json['total_cost'] as num).toDouble(),
      costByProcessor: costMap,
    );
  }
}

/// Model category.
enum ModelCategory {
  llm('llm'),
  embedding('embedding'),
  imageClassification('image_classification'),
  objectDetection('object_detection'),
  segmentation('segmentation'),
  imageGeneration('image_generation'),
  speechToText('speech_to_text'),
  textToSpeech('text_to_speech'),
  videoGeneration('video_generation'),
  multiModal('multi_modal'),
  custom('custom');

  const ModelCategory(this.value);
  final String value;

  static ModelCategory fromString(String s) =>
      ModelCategory.values.firstWhere((c) => c.value == s, orElse: () => custom);
}

/// Model format.
enum ModelFormat {
  onnx('onnx'),
  pytorch('pytorch'),
  torchScript('torchscript'),
  tensorflow('tensorflow'),
  tfLite('tflite'),
  safeTensors('safetensors'),
  gguf('gguf'),
  ggml('ggml'),
  custom('custom');

  const ModelFormat(this.value);
  final String value;

  static ModelFormat fromString(String s) =>
      ModelFormat.values.firstWhere((f) => f.value == s, orElse: () => custom);
}

/// ML framework for training.
enum MlFramework {
  pytorch('pytorch'),
  tensorflow('tensorflow'),
  jax('jax'),
  onnx('onnx');

  const MlFramework(this.value);
  final String value;

  static MlFramework fromString(String s) =>
      MlFramework.values.firstWhere((f) => f.value == s, orElse: () => pytorch);
}

/// Model information.
class ModelInfo {
  final String id;
  final String name;
  final String description;
  final ModelCategory category;
  final String cid;
  final ModelFormat format;
  final int sizeBytes;
  final int parameters;
  final List<Precision> supportedPrecisions;
  final ProcessorType recommendedProcessor;
  final int? contextLength;
  final String license;
  final String provider;
  final String version;
  final bool isPublic;

  const ModelInfo({
    required this.id,
    required this.name,
    required this.description,
    required this.category,
    required this.cid,
    required this.format,
    required this.sizeBytes,
    required this.parameters,
    required this.supportedPrecisions,
    required this.recommendedProcessor,
    this.contextLength,
    required this.license,
    required this.provider,
    required this.version,
    required this.isPublic,
  });

  factory ModelInfo.fromJson(Map<String, dynamic> json) => ModelInfo(
        id: json['id'] as String,
        name: json['name'] as String,
        description: json['description'] as String,
        category: ModelCategory.fromString(json['category'] as String),
        cid: json['cid'] as String,
        format: ModelFormat.fromString(json['format'] as String),
        sizeBytes: json['size_bytes'] as int,
        parameters: json['parameters'] as int,
        supportedPrecisions: (json['supported_precisions'] as List)
            .map((p) => Precision.fromString(p as String))
            .toList(),
        recommendedProcessor:
            ProcessorType.fromString(json['recommended_processor'] as String),
        contextLength: json['context_length'] as int?,
        license: json['license'] as String,
        provider: json['provider'] as String,
        version: json['version'] as String,
        isPublic: json['is_public'] as bool? ?? true,
      );

  /// Format parameters for display (e.g., "70B", "7B").
  String get formattedParameters {
    if (parameters >= 1000000000000) {
      return '${(parameters / 1e12).toStringAsFixed(1)}T';
    } else if (parameters >= 1000000000) {
      return '${(parameters / 1e9).toStringAsFixed(1)}B';
    } else if (parameters >= 1000000) {
      return '${(parameters / 1e6).toStringAsFixed(1)}M';
    }
    return '$parameters';
  }
}

/// Training options.
class TrainingOptions {
  final MlFramework framework;
  final int epochs;
  final int batchSize;
  final double learningRate;
  final String? optimizer;
  final Map<String, dynamic>? hyperparameters;
  final bool distributed;
  final int? checkpointEvery;
  final ProcessorType? processor;
  final Priority? priority;

  const TrainingOptions({
    this.framework = MlFramework.pytorch,
    this.epochs = 1,
    this.batchSize = 32,
    this.learningRate = 0.001,
    this.optimizer,
    this.hyperparameters,
    this.distributed = false,
    this.checkpointEvery,
    this.processor,
    this.priority,
  });

  Map<String, dynamic> toJson() => {
        'framework': framework.value,
        'epochs': epochs,
        'batch_size': batchSize,
        'learning_rate': learningRate,
        if (optimizer != null) 'optimizer': optimizer,
        if (hyperparameters != null) 'hyperparameters': hyperparameters,
        'distributed': distributed,
        if (checkpointEvery != null) 'checkpoint_every': checkpointEvery,
        if (processor != null) 'processor': processor!.value,
        if (priority != null) 'priority': priority!.value,
      };
}

/// Training job result.
class TrainingResult {
  final String jobId;
  final String modelCid;
  final int epochs;
  final double finalLoss;
  final Map<String, double> metrics;
  final int durationMs;
  final double cost;
  final List<String>? checkpointCids;

  const TrainingResult({
    required this.jobId,
    required this.modelCid,
    required this.epochs,
    required this.finalLoss,
    required this.metrics,
    required this.durationMs,
    required this.cost,
    this.checkpointCids,
  });

  factory TrainingResult.fromJson(Map<String, dynamic> json) => TrainingResult(
        jobId: json['job_id'] as String,
        modelCid: json['model_cid'] as String,
        epochs: json['epochs'] as int,
        finalLoss: (json['final_loss'] as num).toDouble(),
        metrics: (json['metrics'] as Map<String, dynamic>)
            .map((k, v) => MapEntry(k, (v as num).toDouble())),
        durationMs: json['duration_ms'] as int,
        cost: (json['cost'] as num).toDouble(),
        checkpointCids: (json['checkpoint_cids'] as List?)?.cast<String>(),
      );
}

/// Model upload options.
class ModelUploadOptions {
  final String name;
  final String? description;
  final ModelCategory category;
  final ModelFormat format;
  final String? alias;
  final bool isPublic;
  final String? license;

  const ModelUploadOptions({
    required this.name,
    this.description,
    this.category = ModelCategory.custom,
    this.format = ModelFormat.onnx,
    this.alias,
    this.isPublic = false,
    this.license,
  });

  Map<String, dynamic> toJson() => {
        'name': name,
        if (description != null) 'description': description,
        'category': category.value,
        'format': format.value,
        if (alias != null) 'alias': alias,
        'is_public': isPublic,
        if (license != null) 'license': license,
      };
}

/// Model upload result.
class ModelUploadResult {
  final String modelId;
  final String cid;
  final int sizeBytes;

  const ModelUploadResult({
    required this.modelId,
    required this.cid,
    required this.sizeBytes,
  });

  factory ModelUploadResult.fromJson(Map<String, dynamic> json) =>
      ModelUploadResult(
        modelId: json['model_id'] as String,
        cid: json['cid'] as String,
        sizeBytes: json['size_bytes'] as int,
      );
}

/// Dataset format for training.
enum DatasetFormat {
  /// JSON Lines format - one JSON object per line
  jsonl('jsonl'),
  /// CSV format with headers
  csv('csv'),
  /// Parquet columnar format (efficient for large datasets)
  parquet('parquet'),
  /// Apache Arrow format
  arrow('arrow'),
  /// HuggingFace datasets format
  huggingface('huggingface'),
  /// TFRecord format (TensorFlow)
  tfrecord('tfrecord'),
  /// WebDataset format (PyTorch)
  webdataset('webdataset'),
  /// Raw text files (one sample per line)
  text('text'),
  /// Image folder structure
  imagefolder('imagefolder'),
  /// Custom binary format
  custom('custom');

  const DatasetFormat(this.value);
  final String value;

  static DatasetFormat fromString(String s) =>
      DatasetFormat.values.firstWhere((f) => f.value == s, orElse: () => jsonl);
}

/// Dataset type/task.
enum DatasetType {
  /// Text completion (prompt → completion)
  textCompletion('text_completion'),
  /// Instruction following (instruction, input, output)
  instructionTuning('instruction_tuning'),
  /// Chat/conversation format
  chat('chat'),
  /// Question answering
  questionAnswering('question_answering'),
  /// Text classification
  textClassification('text_classification'),
  /// Named entity recognition
  ner('ner'),
  /// Image classification
  imageClassification('image_classification'),
  /// Object detection
  objectDetection('object_detection'),
  /// Image segmentation
  imageSegmentation('image_segmentation'),
  /// Image-text pairs
  imageText('image_text'),
  /// Audio transcription
  audioTranscription('audio_transcription'),
  /// Custom format
  custom('custom');

  const DatasetType(this.value);
  final String value;

  static DatasetType fromString(String s) =>
      DatasetType.values.firstWhere((t) => t.value == s, orElse: () => custom);
}

/// Dataset upload options.
class DatasetUploadOptions {
  /// Dataset name
  final String name;
  /// Description
  final String? description;
  /// Dataset format
  final DatasetFormat format;
  /// Dataset type/task
  final DatasetType type;
  /// Column mapping (for CSV/Parquet)
  final Map<String, String>? columnMapping;
  /// Train/validation/test split ratios
  final DatasetSplit? split;
  /// Preprocessing options
  final DatasetPreprocessing? preprocessing;
  /// Is public
  final bool isPublic;

  const DatasetUploadOptions({
    required this.name,
    this.description,
    this.format = DatasetFormat.jsonl,
    this.type = DatasetType.textCompletion,
    this.columnMapping,
    this.split,
    this.preprocessing,
    this.isPublic = false,
  });

  Map<String, dynamic> toJson() => {
        'name': name,
        if (description != null) 'description': description,
        'format': format.value,
        'type': type.value,
        if (columnMapping != null) 'column_mapping': columnMapping,
        if (split != null) 'split': split!.toJson(),
        if (preprocessing != null) 'preprocessing': preprocessing!.toJson(),
        'is_public': isPublic,
      };
}

/// Dataset split configuration.
class DatasetSplit {
  /// Training set ratio (0.0-1.0)
  final double train;
  /// Validation set ratio (0.0-1.0)
  final double validation;
  /// Test set ratio (0.0-1.0)
  final double test;
  /// Random seed for reproducibility
  final int? seed;

  const DatasetSplit({
    this.train = 0.8,
    this.validation = 0.1,
    this.test = 0.1,
    this.seed,
  });

  Map<String, dynamic> toJson() => {
        'train': train,
        'validation': validation,
        'test': test,
        if (seed != null) 'seed': seed,
      };
}

/// Dataset preprocessing options.
class DatasetPreprocessing {
  /// Maximum sequence length (for text)
  final int? maxLength;
  /// Truncation strategy
  final String? truncation;
  /// Tokenizer to use
  final String? tokenizer;
  /// Image size (for vision datasets)
  final List<int>? imageSize;
  /// Normalize images
  final bool? normalizeImages;
  /// Shuffle dataset
  final bool shuffle;
  /// Remove duplicates
  final bool deduplicate;

  const DatasetPreprocessing({
    this.maxLength,
    this.truncation,
    this.tokenizer,
    this.imageSize,
    this.normalizeImages,
    this.shuffle = true,
    this.deduplicate = false,
  });

  Map<String, dynamic> toJson() => {
        if (maxLength != null) 'max_length': maxLength,
        if (truncation != null) 'truncation': truncation,
        if (tokenizer != null) 'tokenizer': tokenizer,
        if (imageSize != null) 'image_size': imageSize,
        if (normalizeImages != null) 'normalize_images': normalizeImages,
        'shuffle': shuffle,
        'deduplicate': deduplicate,
      };
}

/// Dataset upload result.
class DatasetUploadResult {
  /// Dataset CID (use this for training)
  final String cid;
  /// Dataset ID
  final String datasetId;
  /// Total samples
  final int totalSamples;
  /// Train samples
  final int trainSamples;
  /// Validation samples
  final int validationSamples;
  /// Test samples
  final int testSamples;
  /// Size in bytes
  final int sizeBytes;
  /// Schema detected
  final Map<String, String>? schema;

  const DatasetUploadResult({
    required this.cid,
    required this.datasetId,
    required this.totalSamples,
    required this.trainSamples,
    required this.validationSamples,
    required this.testSamples,
    required this.sizeBytes,
    this.schema,
  });

  factory DatasetUploadResult.fromJson(Map<String, dynamic> json) =>
      DatasetUploadResult(
        cid: json['cid'] as String,
        datasetId: json['dataset_id'] as String,
        totalSamples: json['total_samples'] as int,
        trainSamples: json['train_samples'] as int,
        validationSamples: json['validation_samples'] as int,
        testSamples: json['test_samples'] as int,
        sizeBytes: json['size_bytes'] as int,
        schema: (json['schema'] as Map<String, dynamic>?)?.cast<String, String>(),
      );
}

/// Dataset info.
class DatasetInfo {
  final String id;
  final String name;
  final String? description;
  final String cid;
  final DatasetFormat format;
  final DatasetType type;
  final int totalSamples;
  final int sizeBytes;
  final Map<String, String>? schema;
  final bool isPublic;
  final DateTime createdAt;

  const DatasetInfo({
    required this.id,
    required this.name,
    this.description,
    required this.cid,
    required this.format,
    required this.type,
    required this.totalSamples,
    required this.sizeBytes,
    this.schema,
    required this.isPublic,
    required this.createdAt,
  });

  factory DatasetInfo.fromJson(Map<String, dynamic> json) => DatasetInfo(
        id: json['id'] as String,
        name: json['name'] as String,
        description: json['description'] as String?,
        cid: json['cid'] as String,
        format: DatasetFormat.fromString(json['format'] as String),
        type: DatasetType.fromString(json['type'] as String),
        totalSamples: json['total_samples'] as int,
        sizeBytes: json['size_bytes'] as int,
        schema: (json['schema'] as Map<String, dynamic>?)?.cast<String, String>(),
        isPublic: json['is_public'] as bool? ?? false,
        createdAt: DateTime.parse(json['created_at'] as String),
      );
}

/// Training progress update.
class TrainingProgress {
  final String jobId;
  final int epoch;
  final int totalEpochs;
  final int step;
  final int totalSteps;
  final double loss;
  final Map<String, double> metrics;
  final double? learningRate;
  final double? gradientNorm;
  final int samplesPerSecond;
  final int? estimatedRemainingMs;

  const TrainingProgress({
    required this.jobId,
    required this.epoch,
    required this.totalEpochs,
    required this.step,
    required this.totalSteps,
    required this.loss,
    required this.metrics,
    this.learningRate,
    this.gradientNorm,
    required this.samplesPerSecond,
    this.estimatedRemainingMs,
  });

  factory TrainingProgress.fromJson(Map<String, dynamic> json) =>
      TrainingProgress(
        jobId: json['job_id'] as String,
        epoch: json['epoch'] as int,
        totalEpochs: json['total_epochs'] as int,
        step: json['step'] as int,
        totalSteps: json['total_steps'] as int,
        loss: (json['loss'] as num).toDouble(),
        metrics: (json['metrics'] as Map<String, dynamic>?)
                ?.map((k, v) => MapEntry(k, (v as num).toDouble())) ??
            {},
        learningRate: (json['learning_rate'] as num?)?.toDouble(),
        gradientNorm: (json['gradient_norm'] as num?)?.toDouble(),
        samplesPerSecond: json['samples_per_second'] as int? ?? 0,
        estimatedRemainingMs: json['estimated_remaining_ms'] as int?,
      );

  /// Progress percentage (0.0 - 1.0).
  double get progress {
    if (totalSteps == 0) return 0.0;
    return step / totalSteps;
  }

  /// Formatted progress string.
  String get progressText =>
      'Epoch ${epoch + 1}/$totalEpochs - Step $step/$totalSteps - Loss: ${loss.toStringAsFixed(4)}';
}

/// Exception thrown by Synor Compute operations
class SynorException implements Exception {
  final String message;
  final String? code;
  final int? statusCode;
  final Map<String, dynamic>? details;

  const SynorException(
    this.message, {
    this.code,
    this.statusCode,
    this.details,
  });

  @override
  String toString() => 'SynorException: $message (code: $code)';

  factory SynorException.fromJson(Map<String, dynamic> json) => SynorException(
        json['message'] as String? ?? 'Unknown error',
        code: json['code'] as String?,
        statusCode: json['status_code'] as int?,
        details: json['details'] as Map<String, dynamic>?,
      );
}
