# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License.  You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied.  See the License for the
# specific language governing permissions and limitations
# under the License.

"""This is the notifications feature for .asf.yaml. It validates and sets up mailing list targets for repository events."""

from collections.abc import Mapping

import asfyaml.mappings as mappings
from asfyaml.asfyaml import ASFYamlFeature
import re
import fnmatch
import json
import os
import yaml
import asfpy

# Notification settings are stored locally in repo-dir.git/notifications.yaml
NOTIFICATION_SETTINGS_FILE = "notifications.yaml"
# This JSON file contains all valid mailing lists we manage.
VALID_LISTS_FILE = "/x1/gitbox/mailinglists.json"
# These are the schemes we can set.
VALID_NOTIFICATION_SCHEMES = [
    "commits",
    "issues",
    "pullrequests",
    "issues_status",
    "issues_comment",
    "pullrequests_status",
    "pullrequests_comment",
    "jira_options",
    "jobs",
    "commits_by_path",
    "discussions",
    # The rules below are for INFRA-23186
    "pullrequests_bot_*",
    "pullrequests_status_bot_*",
    "pullrequests_comment_bot_*",
    "issues_bot_*",
    "issues_status_bot_*",
    "issues_comment_bot_*",
]

# These are the only valid targets for private repo events
VALID_PRIVATE_TARGETS = [
    "private@*",
    "security@*",
    "commits@infra.apache.org",
    "private-commits@*",
]

# regex for valid ASF mailing list
RE_VALID_MAILING_LIST = re.compile(r"[-a-z0-9]+@([-a-z0-9]+\.)?(incubator\.)?apache\.org$")


class ASFNotificationsFeature(ASFYamlFeature, name="notifications", priority=0):
    """.asf.yaml notifications feature class. Runs before anything else."""

    valid_targets: Mapping[str, str] = {}  # Placeholder for self.valid_targets. Will be re-initialized on run.

    def run(self):
        # Test if we need to process this (only works on the default branch)
        if self.instance.branch != self.repository.default_branch:
            print("Saw notifications meta-data in .asf.yaml, but not in default branch of repository, not updating...")
            return
        self.valid_targets = {}  # Set to a brand-new instance-local dict for valid scheme entries.
        # Read the list of valid mailing list targets from disk
        valid_lists = json.load(open(VALID_LISTS_FILE))
        # For each setting in our YAML, validate and then set.
        for key, value in self.yaml.items():
            # commits_by_path is handled elsewhere and is a dict, so we disregard that here.
            # jira_options isn't super necessary to verify yet, so ignore as well.
            if key == "commits_by_path" or key == "jira_options":
                continue
            # if there is a '.incubator' bit in the mailing list target, crop it out. We're done with those!
            value = value.replace(".incubator.apache.org", ".apache.org")
            if not isinstance(value, str):
                raise Exception(
                    f"[ERROR] Found bad value set for notifications::{key}. Notification targets must be string values."
                )
            # Ensure we allow this scheme to be configured
            if not any(fnmatch.fnmatch(key, pattern) for pattern in VALID_NOTIFICATION_SCHEMES):
                raise Exception(f"[ERROR] Found unknown notification scheme, notifications::{key}")
            # Ensure this is a valid (existing) list target
            if not RE_VALID_MAILING_LIST.match(value) or value not in valid_lists:
                raise Exception(
                    f"[ERROR] The mailing list target, {value}, set in notifications::{key}, is not an existing ASF mailing list."
                )
            if self.repository.is_private:
                if not any(fnmatch.fnmatch(value, pattern) for pattern in VALID_PRIVATE_TARGETS):
                    raise Exception(
                        f"[ERROR] The mailing list target for notifications::{key} MUST be a private mailing list."
                    )

            # Ensure the right project is contacted, but allow for overrides
            mapped_override = mappings.ML_OVERRIDES.get(self.repository.name)
            if mapped_override and value == mapped_override:
                pass
            elif not value.endswith(f"@{self.repository.hostname}.apache.org"):
                raise Exception(
                    f"[ERROR] Target for notifications::{key} is set to {value}, but must be a valid @{self.repository.hostname}.apache.org mailing list!"
                )

            # All is well??
            self.valid_targets[key] = value

        # Check for commits_by_path and validate if found
        if "commits_by_path" in self.yaml:
            if not isinstance(self.yaml.commits_by_path, dict):
                raise Exception(
                    f"[ERROR] notifications::commits_by_path must be a dictionary, but was a {self.yaml.commits_by_path.__class__.__name__}"
                )
            for pattern, target in self.yaml.commits_by_path.items():
                # All mail targets must be strings. Either a single target or a list of targets.
                email_targets = (isinstance(target, list) and target) or [target]
                if not all(isinstance(x, str) for x in email_targets):
                    raise Exception(
                        f"[ERROR] Notification target for notifications::commits_by_path::{pattern} must be either a single email address or a list of email addresses."
                    )
                for email_address in email_targets:
                    if not fnmatch.fnmatch(email_address, "*@*.*"):  # Super simple email address validation
                        raise Exception(
                            f"[ERROR] Notification target for notifications::commits_by_path::{pattern} must be valid email addresses, but found target: {email_address}"
                        )

        # Update the notifications file on disk
        scheme_path = os.path.join(self.repository.path, NOTIFICATION_SETTINGS_FILE)
        old_yml = {}
        if os.path.exists(scheme_path):
            old_yml = yaml.safe_load(open(scheme_path))
        if old_yml == self.yaml:  # No changes, just return straight away.
            return
        else:  # Changes made, save to disk
            with open(scheme_path, "w") as fp:
                yaml.dump(self.yaml_raw, fp, default_flow_style=False)
                print("Dumped yaml")

        print(f"Updating notification schemes for repository {self.repository.name}: ")
        changes = ""
        # Figure out what changed since last
        all_schemes = set(list(self.yaml.keys()) + list(old_yml.keys()))  # Every scheme in old + new set.
        for key in all_schemes:
            if key == "commits_by_path":
                continue  # We don't handle these just yet
            if key not in old_yml and key in self.yaml:
                changes += "- adding new scheme (%s): %r\n" % (key, self.yaml[key])
            elif key in old_yml and key not in self.yaml:
                changes += "- removing old scheme (%s) - was %r\n" % (key, old_yml[key])
            elif key in old_yml and key in self.yaml and old_yml[key] != self.yaml[key]:
                changes += "- updating scheme %s: %r -> %r\n" % (key, old_yml[key], self.yaml[key])
        # Print the changes to the git client
        print(changes)

        changesets = ""
        for push in self.repository.changesets:
            for commit in push.commits:
                if ".asf.yaml" in commit.files:
                    perp = commit.committer_email
                    if commit.committer_email != commit.author_email:
                        perp = f"{commit.committer_email}/{commit.author_email}"
                    changesets += f"{commit.sha}: [{perp}] {commit.subject}\n"
        # If in test mode, bail!
        if "quietmode" in self.instance.environments_enabled:
            return

        # Tell project what happened, on private@
        msg = f"""The following notification schemes have been changed on {self.repository.name} by {self.committer.email}:
{changes}

These changes were caused by the following commits to the {self.instance.branch} branch:

{changesets}

With regards,
ASF Infra.
"""
        asfpy.messaging.mail(
            sender="GitBox <gitbox@apache.org>",
            recipients=[f"private@{self.repository.hostname}.apache.org"],
            subject=f"Notification schemes for {self.repository.name}.git updated",
            message=msg,
        )
