class JenkinsJob {
  const JenkinsJob({
    required this.name,
    required this.url,
    this.fullName,
    this.color,
    this.isFolder = false,
  });

  final String name;
  final Uri url;
  final String? fullName;
  final String? color;
  final bool isFolder;

  factory JenkinsJob.fromJson(Map<String, Object?> json) {
    final rawClass = json['_class'];
    return JenkinsJob(
      name: json['name'] as String,
      fullName: json['fullName'] as String?,
      url: Uri.parse(json['url'] as String),
      color: json['color'] as String?,
      isFolder: rawClass is String && rawClass.contains('Folder'),
    );
  }
}

class JenkinsBuild {
  const JenkinsBuild({
    required this.number,
    required this.jobName,
    required this.url,
    required this.startedAt,
    required this.result,
    required this.artifacts,
    this.requestedBy,
    this.branch,
    this.flavor,
  });

  final int number;
  final String jobName;
  final Uri url;
  final DateTime? startedAt;
  final String? result;
  final List<BuildArtifact> artifacts;
  final String? requestedBy;
  final String? branch;
  final String? flavor;

  bool get isRunning => result == null;
  List<BuildArtifact> get apkArtifacts =>
      artifacts.where((artifact) => artifact.isApk).toList(growable: false);

  factory JenkinsBuild.fromJson(
    Map<String, Object?> json, {
    required String jobName,
  }) {
    final timestamp = json['timestamp'];
    final artifactsJson = json['artifacts'];

    return JenkinsBuild(
      number: json['number'] as int,
      jobName: jobName,
      url: Uri.parse(json['url'] as String),
      startedAt: timestamp is int
          ? DateTime.fromMillisecondsSinceEpoch(timestamp)
          : null,
      result: json['result'] as String?,
      requestedBy: _requestedBy(json),
      branch:
          _parameterValue(json, 'BRANCH_NAME') ??
          _parameterValue(json, 'branch'),
      flavor:
          _parameterValue(json, 'FLAVOR') ?? _parameterValue(json, 'flavor'),
      artifacts: artifactsJson is List
          ? artifactsJson
                .whereType<Map<String, Object?>>()
                .map(BuildArtifact.fromJson)
                .toList()
          : const [],
    );
  }

  static String? _parameterValue(Map<String, Object?> json, String name) {
    final actions = json['actions'];
    if (actions is! List) {
      return null;
    }

    for (final action in actions.whereType<Map<String, Object?>>()) {
      final parameters = action['parameters'];
      if (parameters is! List) {
        continue;
      }

      for (final parameter in parameters.whereType<Map<String, Object?>>()) {
        if (parameter['name'] == name) {
          final value = parameter['value'];
          return value == null ? null : '$value';
        }
      }
    }

    return null;
  }

  static String? _requestedBy(Map<String, Object?> json) {
    return _causeUser(json) ?? _causeTrigger(json) ?? _commitAuthor(json);
  }

  static String? _causeUser(Map<String, Object?> json) {
    for (final cause in _causes(json)) {
      final userName = _stringValue(cause['userName']);
      if (userName != null) {
        return userName;
      }

      final userId = _stringValue(cause['userId']);
      if (userId != null) {
        return userId;
      }
    }

    return null;
  }

  static String? _causeTrigger(Map<String, Object?> json) {
    for (final cause in _causes(json)) {
      final shortDescription = _stringValue(cause['shortDescription']);
      if (shortDescription != null) {
        return shortDescription;
      }

      final upstreamProject = _stringValue(cause['upstreamProject']);
      if (upstreamProject != null) {
        final upstreamBuild = _stringValue(cause['upstreamBuild']);
        return upstreamBuild == null
            ? upstreamProject
            : '$upstreamProject #$upstreamBuild';
      }
    }

    return null;
  }

  static String? _commitAuthor(Map<String, Object?> json) {
    final changeSet = json['changeSet'];
    if (changeSet is! Map<String, Object?>) {
      return null;
    }

    final items = changeSet['items'];
    if (items is! List) {
      return null;
    }

    for (final item in items.whereType<Map<String, Object?>>()) {
      final author = item['author'];
      if (author is Map<String, Object?>) {
        final fullName = _stringValue(author['fullName']);
        if (fullName != null) {
          return fullName;
        }

        final id = _stringValue(author['id']);
        if (id != null) {
          return id;
        }
      }

      final authorEmail = _stringValue(item['authorEmail']);
      if (authorEmail != null) {
        return authorEmail;
      }
    }

    return null;
  }

  static Iterable<Map<String, Object?>> _causes(
    Map<String, Object?> json,
  ) sync* {
    final actions = json['actions'];
    if (actions is! List) {
      return;
    }

    for (final action in actions.whereType<Map<String, Object?>>()) {
      final causes = action['causes'];
      if (causes is! List) {
        continue;
      }

      yield* causes.whereType<Map<String, Object?>>();
    }
  }

  static String? _stringValue(Object? value) {
    if (value == null) {
      return null;
    }

    final text = '$value'.trim();
    return text.isEmpty ? null : text;
  }
}

class BuildArtifact {
  const BuildArtifact({required this.fileName, required this.relativePath});

  final String fileName;
  final String relativePath;

  bool get isApk {
    final lowerFileName = fileName.toLowerCase();
    final lowerRelativePath = relativePath.toLowerCase();
    return lowerFileName.endsWith('.apk') || lowerRelativePath.endsWith('.apk');
  }

  factory BuildArtifact.fromJson(Map<String, Object?> json) {
    return BuildArtifact(
      fileName: json['fileName'] as String,
      relativePath: json['relativePath'] as String,
    );
  }
}
