text: from typing import Tuple
  from functools import reduce
  
  from django.contrib.auth.models import User
  
  from hknweb.coursesemester.models import Semester
  from hknweb.events.models import Rsvp
  
  from hknweb.candidate.constants import ATTR, EVENT_NAMES, REQUIREMENT_TITLES_TEMPLATE
  from hknweb.candidate.models import (
      Announcement,
      RequirementMergeRequirement,
  )
  
  from hknweb.candidate.utils_candportal.merged_events import MergedEvents
  from hknweb.candidate.utils_candportal.count import (
      count_challenges,
      count_num_bitbytes,
  )
  from hknweb.candidate.utils_candportal.get_events import (
      get_required_events,
      get_upcoming_events,
  )
  from hknweb.candidate.utils_candportal.process_misc_req import (
      CandidateFormProcessor,
      CommitteeProjectProcessor,
      DuePaymentProjectProcessor,
  )
  from hknweb.candidate.utils_candportal.req_info import ReqInfo
  
  
  class CandidatePortalData:
      def __init__(self, user: User):
          self.user = user
  
      def get_merge_info(self, candidate_semester: Semester) -> Tuple[set, list]:
          if not candidate_semester:
              return set(), []
  
          merger_reqs = RequirementMergeRequirement.objects.filter(
              candidateSemesterActive=candidate_semester.id,
              enable=True,
          )
          if not merger_reqs:
              return set(), []
  
          seen_merger_nodes = set()
          merger_nodes = [
              MergedEvents(m, candidate_semester, seen_merger_nodes) for m in merger_reqs
          ]
          required_events_merger = reduce(set.__or__, (n.events() for n in merger_nodes))
          return required_events_merger, merger_nodes
  
      def process_merge_node(self, node, req_info: ReqInfo) -> str:
          node_string = node.get_events_str()
  
          key = node_string
          if key in req_info.titles:
              key = f"{key} {sum(s.startswith(key) for s in req_info.titles) + 1}"
              req_info.colors[key] = req_info.colors[node_string]
  
          remaining_count, grand_total = node.get_counts(req_info.remaining, req_info.lst)
          req_info.statuses[key] = round(remaining_count, 2) < 0.05
  
          # num_required_hangouts is None, since Merger nodes should not use it
          if node.all_required:
              # TODO Support for All Required for Merged Requirement (probably not a huge priority)
              req_info.titles[key] = (
                  node_string
                  + " - Looped Merged Requirements for all required currently unsupported"
              )
          else:
              req_info.titles[key] = REQUIREMENT_TITLES_TEMPLATE.format(
                  node_string, grand_total, remaining_count
              )
  
          events = node.events()
          confirmed, unconfirmed = req_info.confirmed_events, req_info.unconfirmed_events
          confirmed[key] = reduce(list.__add__, (confirmed[e] for e in events))
          unconfirmed[key] = reduce(list.__add__, (unconfirmed[e] for e in events))
  
          return key
  
      def get_user_cand_data(self) -> dict:
          candidate_semester = self.user.profile.candidate_semester
  
          required_events_merger, merger_nodes = self.get_merge_info(candidate_semester)
  
          challenges = count_challenges(self.user, candidate_semester)
          num_bitbytes = count_num_bitbytes(self.user, candidate_semester)
  
          event_types = list(get_required_events(candidate_semester, set()))
          rsvps = Rsvp.objects.filter(user__exact=self.user)
  
          req_info = ReqInfo()
          req_info.set_confirmed_unconfirmed_events(
              rsvps, candidate_semester, required_events_merger
          )
          req_info.set_list(candidate_semester)
          req_info.set_confirmed_reqs(challenges[ATTR.NUM_CONFIRMED], num_bitbytes)
          req_info.set_remaining()
          req_info.set_statuses()
          req_info.set_titles()
          req_info.set_colors(event_types, merger_nodes)
  
          merge_names = [self.process_merge_node(node, req_info) for node in merger_nodes]
  
          return {
              "req_statuses": {e: req_info.statuses[e] for e in event_types},
              **self._get_interactivities_context(req_info, challenges),
              **self._get_bitbyte_context(req_info, num_bitbytes),
              **self._get_misc_context(self.user, candidate_semester),
              **self._get_event_related_context(
                  self.user, req_info, event_types + merge_names
              ),
              **self._get_misc_req_related_context(self.user, candidate_semester),
          }
  
      @staticmethod
      def _get_interactivities_context(req_info: ReqInfo, challenges: dict) -> dict:
          return {
              EVENT_NAMES.INTERACTIVITIES: {
                  **req_info.titles[EVENT_NAMES.INTERACTIVITIES],
                  **challenges,
                  ATTR.STATUS: req_info.statuses[EVENT_NAMES.INTERACTIVITIES],
              }
          }
  
      @staticmethod
      def _get_bitbyte_context(req_info: ReqInfo, num_bitbytes: int) -> dict:
          return {
              "bitbyte": {
                  ATTR.TITLE: req_info.titles[EVENT_NAMES.BITBYTE],
                  ATTR.STATUS: req_info.statuses[EVENT_NAMES.BITBYTE],
                  ATTR.NUM_BITBYTES: num_bitbytes,
              }
          }
  
      @staticmethod
      def _get_misc_context(user: User, candidate_semester: Semester) -> dict:
          announcements = Announcement.objects.filter(visible=True).order_by(
              "-release_date"
          )
  
          return {
              "announcements": announcements,
              "username": user.username,
              "user_self": True,
              "candidate_semester": candidate_semester
              or "Please set your candidate semester in your Account Settings",
          }
  
      @staticmethod
      def _get_event_related_context(user: User, r: ReqInfo, event_types: list) -> dict:
          info = (
              r.titles,
              r.statuses,
              r.colors,
              r.confirmed_events,
              r.unconfirmed_events,
          )
          attrs = (ATTR.TITLE, ATTR.STATUS, ATTR.COLOR, ATTR.CONFIRMED, ATTR.UNCONFIRMED)
          events = [dict(zip(attrs, (i[t] for i in info))) for t in event_types]
  
          return {
              "events": events,
              "confirmed_events": r.confirmed_events,
              "unconfirmed_events": r.unconfirmed_events,
              "upcoming_events": get_upcoming_events(user),
          }
  
      @staticmethod
      def _get_misc_req_related_context(user: User, candidate_semester: Semester) -> dict:
          return {
              "committee_project": CommitteeProjectProcessor.process_status(
                  user, candidate_semester
              ),
              "candidate_forms": CandidateFormProcessor.process_status(
                  user, candidate_semester
              ),
              "due_payments": DuePaymentProjectProcessor.process_status(
                  user, candidate_semester
              ),
          }
  