text: # Copyright Contributors to the Packit project.
  # SPDX-License-Identifier: MIT
  
  import logging
  from datetime import datetime
  from typing import List, Optional, Set, Tuple
  
  from copr.v3 import CoprRequestException
  from ogr.abstract import GitProject
  from ogr.parsing import parse_git_repo
  from ogr.services.github import GithubProject
  from packit.config import JobConfig, JobType
  from packit.config.aliases import get_valid_build_targets
  from packit.config.package_config import PackageConfig
  from packit.exceptions import PackitCoprException, PackitCoprSettingsException
  
  from packit_service import sentry_integration
  from packit_service.celerizer import celery_app
  from packit_service.config import Deployment, ServiceConfig
  from packit_service.constants import MSG_RETRIGGER
  from packit_service.models import AbstractTriggerDbType, CoprBuildModel
  from packit_service.worker.events import EventData
  from packit_service.service.urls import (
      get_copr_build_info_url,
      get_srpm_build_info_url,
  )
  from packit_service.worker.build.build_helper import BaseBuildJobHelper
  from packit_service.worker.monitoring import Pushgateway
  from packit_service.worker.result import TaskResults
  from packit_service.worker.reporting import BaseCommitStatus
  
  logger = logging.getLogger(__name__)
  
  
  class CoprBuildJobHelper(BaseBuildJobHelper):
      job_type_build = JobType.copr_build
      job_type_test = JobType.tests
      status_name_build: str = "rpm-build"
      status_name_test: str = "testing-farm"
  
      def __init__(
          self,
          service_config: ServiceConfig,
          package_config: PackageConfig,
          project: GitProject,
          metadata: EventData,
          db_trigger: AbstractTriggerDbType,
          job_config: JobConfig,
          targets_override: Optional[Set[str]] = None,
          pushgateway: Optional[Pushgateway] = None,
      ):
          super().__init__(
              service_config=service_config,
              package_config=package_config,
              project=project,
              metadata=metadata,
              db_trigger=db_trigger,
              job_config=job_config,
              targets_override=targets_override,
              pushgateway=pushgateway,
          )
  
          self.msg_retrigger: str = MSG_RETRIGGER.format(
              job="build",
              command="copr-build" if self.job_build else "build",
              place="pull request",
          )
  
      @property
      def default_project_name(self) -> str:
          """
          Project name for copr.
  
          * use hostname prefix for non-github service
          * replace slash in namespace with dash
          * add `-stg` suffix for the stg app
          """
  
          service_hostname = parse_git_repo(self.project.service.instance_url).hostname
          service_prefix = (
              "" if isinstance(self.project, GithubProject) else f"{service_hostname}-"
          )
  
          namespace = self.project.namespace.replace("/", "-")
          stg = "-stg" if self.service_config.deployment == Deployment.stg else ""
          # We want to share project between all releases.
          # More details: https://github.com/packit/packit-service/issues/1044
          identifier = "releases" if self.metadata.tag_name else self.metadata.identifier
  
          return f"{service_prefix}{namespace}-{self.project.repo}-{identifier}{stg}"
  
      @property
      def job_project(self) -> Optional[str]:
          """
          The job definition from the config file.
          """
          if self.job_build and self.job_build.metadata.project:
              return self.job_build.metadata.project
  
          if self.job_tests and self.job_tests.metadata.project:
              return self.job_tests.metadata.project
  
          return self.default_project_name
  
      @property
      def job_owner(self) -> Optional[str]:
          """
          Owner used for the copr build -- search the config or use the copr's config.
          """
          if self.job_build and self.job_build.metadata.owner:
              return self.job_build.metadata.owner
  
          if self.job_tests and self.job_tests.metadata.owner:
              return self.job_tests.metadata.owner
  
          return self.api.copr_helper.copr_client.config.get("username")
  
      @property
      def preserve_project(self) -> Optional[bool]:
          """
          If the project will be preserved or can be removed after 60 days.
          """
          return self.job_build.metadata.preserve_project if self.job_build else None
  
      @property
      def list_on_homepage(self) -> Optional[bool]:
          """
          If the project will be shown on the copr home page.
          """
          return self.job_build.metadata.list_on_homepage if self.job_build else None
  
      @property
      def additional_repos(self) -> Optional[List[str]]:
          """
          Additional repos that will be enable for copr build.
          """
          return self.job_build.metadata.additional_repos if self.job_build else None
  
      @property
      def build_targets_all(self) -> Set[str]:
          """
          Return all valid Copr build targets/chroots from config.
          """
          return get_valid_build_targets(*self.configured_build_targets, default=None)
  
      @property
      def tests_targets_all(self) -> Set[str]:
          """
          Return all valid test targets/chroots from config.
          """
          return get_valid_build_targets(*self.configured_tests_targets, default=None)
  
      @property
      def available_chroots(self) -> Set[str]:
          """
          Returns set of available COPR targets.
          """
          return {
              *filter(
                  lambda chroot: not chroot.startswith("_"),
                  self.api.copr_helper.get_copr_client()
                  .mock_chroot_proxy.get_list()
                  .keys(),
              )
          }
  
      def get_built_packages(self, build_id: int, chroot: str) -> List:
          return self.api.copr_helper.copr_client.build_chroot_proxy.get_built_packages(
              build_id, chroot
          ).packages
  
      def get_build(self, build_id: int):
          return self.api.copr_helper.copr_client.build_proxy.get(build_id)
  
      def monitor_not_submitted_copr_builds(self, number_of_builds: int, reason: str):
          """
          Measure the time it took to set the failed status in case of event (e.g. failed SRPM)
          that prevents Copr build to be submitted.
          """
          time = (datetime.now() - self.metadata.task_accepted_time).total_seconds()
          for _ in range(number_of_builds):
              self.pushgateway.copr_build_not_submitted_time.labels(
                  reason=reason
              ).observe(time)
  
      def run_copr_build(self) -> TaskResults:
          self.report_status_to_all(
              description="Building SRPM ...",
              state=BaseCommitStatus.running,
              # pagure requires "valid url"
              url="",
          )
          if results := self.create_srpm_if_needed():
              return results
  
          if not self.srpm_model.success:
              msg = "SRPM build failed, check the logs for details."
              self.report_status_to_all(
                  state=BaseCommitStatus.failure,
                  description=msg,
                  url=get_srpm_build_info_url(self.srpm_model.id),
              )
              self.monitor_not_submitted_copr_builds(
                  len(self.build_targets), "srpm_failure"
              )
              return TaskResults(success=False, details={"msg": msg})
  
          try:
              build_id, web_url = self.run_build()
          except Exception as ex:
              sentry_integration.send_to_sentry(ex)
              # TODO: Where can we show more info about failure?
              # TODO: Retry
              self.report_status_to_all(
                  state=BaseCommitStatus.error,
                  description=f"Submit of the build failed: {ex}",
              )
              self.monitor_not_submitted_copr_builds(
                  len(self.build_targets), "submit_failure"
              )
              return TaskResults(
                  success=False,
                  details={"msg": "Submit of the Copr build failed.", "error": str(ex)},
              )
  
          unprocessed_chroots = []
          for chroot in self.build_targets:
              if chroot not in self.available_chroots:
                  self.report_status_to_all_for_chroot(
                      state=BaseCommitStatus.error,
                      description=f"Not supported target: {chroot}",
                      url=get_srpm_build_info_url(self.srpm_model.id),
                      chroot=chroot,
                  )
                  self.monitor_not_submitted_copr_builds(1, "not_supported_target")
                  unprocessed_chroots.append(chroot)
                  continue
  
              copr_build = CoprBuildModel.create(
                  build_id=str(build_id),
                  commit_sha=self.metadata.commit_sha,
                  project_name=self.job_project,
                  owner=self.job_owner,
                  web_url=web_url,
                  target=chroot,
                  status="pending",
                  run_model=self.run_model,
                  task_accepted_time=self.metadata.task_accepted_time,
              )
              url = get_copr_build_info_url(id_=copr_build.id)
              self.report_status_to_all_for_chroot(
                  state=BaseCommitStatus.running,
                  description="Starting RPM build...",
                  url=url,
                  chroot=chroot,
              )
  
          if unprocessed_chroots:
              unprocessed = "\n".join(sorted(unprocessed_chroots))
              available = "\n".join(sorted(self.available_chroots))
              self.status_reporter.comment(
                  body="There are build targets that are not supported by COPR.\n"
                  "<details>\n<summary>Unprocessed build targets</summary>\n\n"
                  f"```\n{unprocessed}\n```\n</details>\n"
                  "<details>\n<summary>Available build targets</summary>\n\n"
                  f"```\n{available}\n```\n</details>",
              )
  
          # release the hounds!
          celery_app.send_task(
              "task.babysit_copr_build",
              args=(build_id,),
              countdown=120,  # do the first check in 120s
          )
  
          return TaskResults(success=True, details={})
  
      def run_build(
          self, target: Optional[str] = None
      ) -> Tuple[Optional[int], Optional[str]]:
          """
          Trigger the build and return id and web_url
          :param target: str, run for all if not set
          :return: task_id, task_url
          """
  
          owner = self.job_owner or self.api.copr_helper.configured_owner
          if not owner:
              raise PackitCoprException(
                  "Copr owner not set. Use Copr config file or `--owner` when calling packit CLI."
              )
  
          try:
              overwrite_booleans = owner == "packit"
              self.api.copr_helper.create_copr_project_if_not_exists(
                  project=self.job_project,
                  chroots=list(self.build_targets_all),
                  owner=owner,
                  description=None,
                  instructions=None,
                  list_on_homepage=self.list_on_homepage if overwrite_booleans else None,
                  preserve_project=self.preserve_project if overwrite_booleans else None,
                  additional_repos=self.additional_repos,
                  request_admin_if_needed=True,
              )
          except PackitCoprSettingsException as ex:
              # notify user first, PR if exists, commit comment otherwise
              table = (
                  "| field | old value | new value |\n"
                  "| ----- | --------- | --------- |\n"
              )
              for field, (old, new) in ex.fields_to_change.items():
                  table += f"| {field} | {old} | {new} |\n"
  
              boolean_note = ""
              if "unlisted_on_hp" in ex.fields_to_change:
                  boolean_note += (
                      "The `unlisted_on_hp` field is represented as `list_on_homepage`"
                      " in the packit config."
                      "By default we create projects with `list_on_homepage: False`.\n"
                  )
  
              if "delete_after_days" in ex.fields_to_change:
                  boolean_note += (
                      "The `delete_after_days` field is represented as `preserve_project`"
                      " in the packit config (`True` is `-1` and `False` is `60`)."
                      "By default we create projects with `preserve: True` "
                      "which means `delete_after_days=60`.\n"
                  )
  
              permissions_url = self.api.copr_helper.get_copr_settings_url(
                  owner, self.job_project, section="permissions"
              )
              settings_url = self.api.copr_helper.get_copr_settings_url(
                  owner, self.job_project
              )
  
              msg = (
                  "Based on your Packit configuration the settings "
                  f"of the {owner}/{self.job_project} "
                  "Copr project would need to be updated as follows:\n"
                  "\n"
                  f"{table}"
                  "\n"
                  f"{boolean_note}"
                  "\n"
                  "Packit was unable to update the settings above as it is missing `admin` "
                  f"permissions on the {owner}/{self.job_project} Copr project.\n"
                  "\n"
                  "To fix this you can do one of the following:\n"
                  "\n"
                  f"- Grant Packit `admin` permissions on the {owner}/{self.job_project} "
                  f"Copr project on the [permissions page]({permissions_url}).\n"
                  "- Change the above Copr project settings manually "
                  f"on the [settings page]({settings_url}) "
                  "to match the Packit configuration.\n"
                  "- Update the Packit configuration to match the Copr project settings.\n"
                  "\n"
                  "Please retrigger the build, once the issue above is fixed.\n"
              )
              self.status_reporter.comment(body=msg)
              raise ex
  
          logger.debug(
              f"owner={owner}, project={self.job_project}, path={self.srpm_path}"
          )
  
          try:
              build = self.api.copr_helper.copr_client.build_proxy.create_from_file(
                  ownername=owner,
                  projectname=self.job_project,
                  path=self.srpm_path,
                  buildopts={
                      "chroots": list(self.build_targets),
                  },
              )
          except CoprRequestException as ex:
              if "You don't have permissions to build in this copr." in str(
                  ex
              ) or "is not allowed to build in the copr" in str(ex):
                  self.api.copr_helper.copr_client.project_proxy.request_permissions(
                      ownername=owner,
                      projectname=self.job_project,
                      permissions={"builder": True},
                  )
  
                  # notify user, PR if exists, commit comment otherwise
                  permissions_url = self.api.copr_helper.get_copr_settings_url(
                      owner, self.job_project, section="permissions"
                  )
                  self.status_reporter.comment(
                      body="We have requested the `builder` permissions "
                      f"for the {owner}/{self.job_project} Copr project.\n"
                      "\n"
                      "Please confirm the request on the "
                      f"[{owner}/{self.job_project} Copr project permissions page]"
                      f"({permissions_url})"
                      " and retrigger the build.",
                  )
              raise ex
  
          return build.id, self.api.copr_helper.copr_web_build_url(build)
  