final class DomainException implements Exception {
  const DomainException(this.message);

  final String message;

  @override
  String toString() => message;
}

final class ValidationException extends DomainException {
  const ValidationException(super.message);
}

final class EmailAlreadyTakenException extends DomainException {
  const EmailAlreadyTakenException() : super('Email already taken.');
}

final class InvalidCredentialsException extends DomainException {
  const InvalidCredentialsException() : super('Invalid credentials.');
}

final class UnauthorizedException extends DomainException {
  const UnauthorizedException() : super('Unauthorized.');
}

final class ShareNotFoundException extends DomainException {
  const ShareNotFoundException() : super('Share not found.');
}

final class ShareConflictException extends DomainException {
  const ShareConflictException(super.message);
}

final class UserAccount {
  UserAccount({
    required String id,
    required String email,
    required String passwordHash,
    this.displayName,
    required DateTime createdAt,
    required DateTime updatedAt,
    DateTime? disabledAt,
  }) : id = _nonBlank(id, 'User id'),
       email = _nonBlank(email, 'Email').toLowerCase(),
       passwordHash = _nonBlank(passwordHash, 'Password hash'),
       createdAt = createdAt.toUtc(),
       updatedAt = updatedAt.toUtc(),
       disabledAt = disabledAt?.toUtc();

  final String id;
  final String email;
  final String passwordHash;
  final String? displayName;
  final DateTime createdAt;
  final DateTime updatedAt;
  final DateTime? disabledAt;

  bool get isDisabled => disabledAt != null;
}

final class AuthSession {
  AuthSession({
    required String id,
    required String userId,
    required String tokenHash,
    required DateTime issuedAt,
    required DateTime expiresAt,
    DateTime? revokedAt,
    this.userAgent,
    this.deviceLabel,
  }) : id = _nonBlank(id, 'Auth session id'),
       userId = _nonBlank(userId, 'User id'),
       tokenHash = _nonBlank(tokenHash, 'Token hash'),
       issuedAt = issuedAt.toUtc(),
       expiresAt = expiresAt.toUtc(),
       revokedAt = revokedAt?.toUtc() {
    if (!this.expiresAt.isAfter(this.issuedAt)) {
      throw const ValidationException(
        'Auth session expiration must be after issue time.',
      );
    }
  }

  final String id;
  final String userId;
  final String tokenHash;
  final DateTime issuedAt;
  final DateTime expiresAt;
  final DateTime? revokedAt;
  final String? userAgent;
  final String? deviceLabel;

  bool isActiveAt(DateTime now) {
    final instant = now.toUtc();
    return revokedAt == null && expiresAt.isAfter(instant);
  }
}

enum SyncedResourceType {
  exercise('exercise'),
  program('program'),
  workoutTemplate('workoutTemplate'),
  workoutHistory('workoutHistory'),
  mediaAsset('mediaAsset');

  const SyncedResourceType(this.wireName);

  final String wireName;

  static SyncedResourceType parse(String value) {
    for (final type in values) {
      if (type.wireName == value) {
        return type;
      }
    }
    throw ValidationException('Unsupported resource type: $value.');
  }
}

final class SyncedResource {
  SyncedResource({
    required String serverId,
    required String ownerUserId,
    required this.resourceType,
    required String clientId,
    required Map<String, Object?> payloadJson,
    required this.schemaVersion,
    required DateTime clientUpdatedAt,
    required DateTime serverUpdatedAt,
    DateTime? deletedAt,
    this.originDeviceId,
  }) : serverId = _nonBlank(serverId, 'Server resource id'),
       ownerUserId = _nonBlank(ownerUserId, 'Owner user id'),
       clientId = _nonBlank(clientId, 'Client resource id'),
       payloadJson = Map.unmodifiable(payloadJson),
       clientUpdatedAt = clientUpdatedAt.toUtc(),
       serverUpdatedAt = serverUpdatedAt.toUtc(),
       deletedAt = deletedAt?.toUtc() {
    if (schemaVersion <= 0) {
      throw const ValidationException('Schema version must be positive.');
    }
  }

  final String serverId;
  final String ownerUserId;
  final SyncedResourceType resourceType;
  final String clientId;
  final Map<String, Object?> payloadJson;
  final int schemaVersion;
  final DateTime clientUpdatedAt;
  final DateTime serverUpdatedAt;
  final DateTime? deletedAt;
  final String? originDeviceId;
}

enum ShareRecipientStatus {
  pending('pending'),
  accepted('accepted'),
  declined('declined'),
  revoked('revoked');

  const ShareRecipientStatus(this.wireName);

  final String wireName;

  static ShareRecipientStatus parse(String value) {
    for (final status in values) {
      if (status.wireName == value) {
        return status;
      }
    }
    throw ValidationException('Unsupported share recipient status: $value.');
  }
}

final class Share {
  Share({
    required String id,
    required String senderUserId,
    required this.resourceType,
    required Map<String, Object?> payloadJson,
    required DateTime createdAt,
    DateTime? revokedAt,
  }) : id = _nonBlank(id, 'Share id'),
       senderUserId = _nonBlank(senderUserId, 'Sender user id'),
       payloadJson = Map.unmodifiable(payloadJson),
       createdAt = createdAt.toUtc(),
       revokedAt = revokedAt?.toUtc() {
    if (resourceType != SyncedResourceType.program &&
        resourceType != SyncedResourceType.workoutTemplate) {
      throw const ValidationException(
        'Shares only support program and workoutTemplate resources.',
      );
    }
  }

  final String id;
  final String senderUserId;
  final SyncedResourceType resourceType;
  final Map<String, Object?> payloadJson;
  final DateTime createdAt;
  final DateTime? revokedAt;

  bool get isRevoked => revokedAt != null;
}

final class ShareRecipient {
  ShareRecipient({
    required String id,
    required String shareId,
    required String recipientUserId,
    this.status = ShareRecipientStatus.pending,
    DateTime? respondedAt,
  }) : id = _nonBlank(id, 'Share recipient id'),
       shareId = _nonBlank(shareId, 'Share id'),
       recipientUserId = _nonBlank(recipientUserId, 'Recipient user id'),
       respondedAt = respondedAt?.toUtc();

  final String id;
  final String shareId;
  final String recipientUserId;
  final ShareRecipientStatus status;
  final DateTime? respondedAt;
}

String _nonBlank(String value, String label) {
  final trimmed = value.trim();
  if (trimmed.isEmpty) {
    throw ValidationException('$label must not be blank.');
  }
  return trimmed;
}
