#!/usr/bin/env python
"""
Update the CI file.
"""

import logging
import re

import yaml

from ..file import update_content

LOGGER = logging.getLogger(__name__)


class GitlabCI:
    """
    Wrapper around the gitlab-ci file.
    """

    def __init__(self, project, path=".gitlab-ci.yml"):
        """
        Args:
            project:
                A Project instance.

            path:
                The path to the gitlab-ci file, relative to the repository root
                directory.
        """
        self.project = project
        self.path = project.git_repo.path / path
        self._data = None

    @property
    def data(self):
        """
        The CI configuration data.
        """
        if self._data is None:
            self._data = self.load()
        return self._data

    def load(self):
        """
        Load the file data.

        Returns:
            The loaded data, or an empty dict if the file does not exist.
        """
        try:
            with self.path.open("rb") as handle:
                return yaml.safe_load(handle)
        except FileNotFoundError:
            return {}

    def update_pages(self):
        """
        Update the pages job.
        """
        pages = self.data.get("pages")
        if not pages:
            return
        pages["artifacts"] = {"paths": ["public"]}
        pages["only"] = ["main"]
        pages["stage"] = "deploy"

    def add_register_pip_pkg(self):
        """
        Add a jot to register a pip package.
        """
        key = "register_pip_pkg"
        if self.project.pyproject_toml_path.exists():
            self.data[key] = {
                "image": "python:latest",
                "only": ["main"],
                "script": [
                    "pip install build twine",
                    "python -m build",
                    "TWINE_PASSWORD=${CI_JOB_TOKEN} TWINE_USERNAME=gitlab-ci-token "
                    "python -m twine upload --repository-url "
                    "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/pypi "
                    "dist/*",
                ],
                "stage": "deploy",
            }
        else:
            self.data.pop(key)

    def add_release_job(self):
        """
        Add a release job that triggers when Git tags are pushed.
        """
        key = "release_job"
        regex = self.project.config.get("gitlab", "release_tag_regex")
        if regex:
            self.data[key] = {
                "image": "registry.gitlab.com/gitlab-org/release-cli:latest",
                "release": {
                    "description": "Release $CI_COMMIT_TAG",
                    "tag_name": "$CI_COMMIT_TAG",
                },
                "rules": [{"if": f"$CI_COMMIT_TAG =~ /{regex}/"}],
                "script": ['echo "running release_job"'],
                "stage": "release",
            }
        else:
            self.data.pop(key, None)

    def _manage_swh_hook(self, gproj):
        """
        Manage the SWH webhooks.

        Args:
            gproj:
                The python-gitlab Project instance.
        """
        # Reset to releases_events when SoftwareHeritage updates their API.
        #  events_key = 'releases_events'
        events_key = "tag_push_events"
        url = "https://archive.softwareheritage.org/api/1/origin/save/webhook/gitlab/"

        swh_hook = None
        add = self.project.codemeta_json_path.exists()

        for hook in gproj.hooks.list(iterator=True):
            if hook.attributes["url"] == url:
                if add and swh_hook is None:
                    swh_hook = hook
                else:
                    LOGGER.info("Deleting SWH hook for %s.", gproj.name)
                    hook.delete()

        if add and swh_hook is None:
            LOGGER.info("Creating SWH webhook for %s", gproj.name)
            swh_hook = gproj.hooks.create({"url": url, events_key: True})

        if swh_hook:
            changed = False
            for key, value in swh_hook.attributes.items():
                if key.endswith("_events"):
                    expected_value = key == events_key
                    if value != expected_value:
                        changed = True
                        setattr(swh_hook, key, expected_value)
            if changed:
                LOGGER.info("Updating SWH hook for %s.", gproj.name)
                swh_hook.save()

    def manage_hooks(self):
        """
        Add or remove hooks depending on the current repository configuration.
        """
        if not self.project.config.get("gitlab", "manage_hooks", default=False):
            return
        with self.project.config.gitlab as glab:
            glab.auth()
            gproj = self.project.git_repo.get_gitlab_project(glab)
            self._manage_swh_hook(gproj)

    def add_stages(self):
        """
        Add the list of stages for all added jobs.
        """
        recognized_stages = [".pre", "build", "test", "release", "deploy", ".post"]
        stages = set()
        for key, value in self.data.items():
            if not isinstance(value, dict):
                continue
            stage = value.get("stage")
            if stage is None:
                LOGGER.error("No stage specified for job %s in %s", key, self.path)
                continue
            stages.add(stage)
        self.data["stages"] = [stage for stage in recognized_stages if stage in stages]

    def add_tags(self):
        """
        Add runner tags. This will deduplicate tags and also ensure that jobs
        using the same tags reference each other in the YAML output.
        """
        tag_map = self.project.config.get("gitlab", "ci_tags")
        if tag_map:
            tag_map = tuple(
                (re.compile(regex), tags) for regex, tags in tag_map.items()
            )
            previous_sets = []
            for name, data in self.data.items():
                if not isinstance(data, dict):
                    continue
                collected_tags = set()
                for regex, new_tags in tag_map:
                    if regex.search(name):
                        collected_tags.update(new_tags)
                collected_tags = sorted(collected_tags)
                # PyYAML seems to only emit references when nested items refer
                # to the same instance so ensure that identical lists do.
                for prev in previous_sets:
                    if collected_tags == prev:
                        collected_tags = prev
                        break
                else:
                    previous_sets.append(collected_tags)
                self.data[name]["tags"] = collected_tags

    def update(self):
        """
        Update the CI file by detecting which jobs should be added.
        """
        self.update_pages()
        self.add_register_pip_pkg()
        self.add_release_job()
        self.add_stages()
        self.add_tags()
        update_content(yaml.dump(self.data), self.path)
        self.manage_hooks()
