#!/usr/bin/env python3
#
# Copyright (c) 2024-2025 The NetBSD Foundation, Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS
# ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
# TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS
# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#

"""require_user_login is a hook to verify new changeset authorship
matches login name as determined by the LOGNAME environment variable.

Usage:
  [netbsd_hooks]
  domain = pkgsrc.org

  [hooks]
  pretxnchangegroup.require_user_login = python:....require_user_login.hook

"""

import os

from typing import Optional

from mercurial.i18n import _
from mercurial import (
    error,
    pycompat,
    registrar,
)
from mercurial.utils import stringutil


configtable = {}
configitem = registrar.configitem(configtable)

# [netbsd_hooks]
# domain = pkgsrc.org
configitem(
    b'netbsd_hooks',
    b'domain',
    default=None,
)


MAX_ERRORS = 3


def emaildomain(email: bytes) -> Optional[bytes]:
    at = email.find(b'@')
    if at < 0:
        return None
    return email[at + 1:]


def hook(ui, repo, hooktype, node=None, **kwargs):
    if hooktype != b'pretxnchangegroup':
        raise error.Abort(
            _(b'Unsupported hook type %r') % pycompat.bytestr(hooktype)
        )
    accepted_domains = ui.configlist(b'netbsd_hooks', b'domain')
    if not accepted_domains:
        raise error.Abort(_(b'netbsd_hooks.domain not configured'))
    accepted_user = os.getenvb(b'LOGNAME') or os.getenvb(b'USER')
    if not accepted_user:
        raise error.Abort(_(b'LOGNAME and USER unset or empty'))
    ctx = repo.unfiltered()[node]
    email_errors = []
    domain_errors = []
    seen_domains = set()
    user_errors = []
    seen_users = set()
    for revid in repo.changelog.revs(start=ctx.rev()):
        rev = repo[revid]
        email = stringutil.email(rev.user())
        user = stringutil.emailuser(email)
        domain = emaildomain(email)
        if not domain:
            if len(email_errors) <= MAX_ERRORS:
                if len(email_errors) == MAX_ERRORS:
                    email_errors.append(b'...')
                else:
                    email_errors.append(rev.user())
                continue
        if domain not in accepted_domains:
            if domain not in seen_domains:
                seen_domains.add(domain)
                if len(domain_errors) <= MAX_ERRORS:
                    if len(domain_errors) == MAX_ERRORS:
                        domain_errors.append(b'...')
                    else:
                        domain_errors.append(b'@' + domain)
        elif user != accepted_user:
            if user not in seen_users:
                seen_users.add(user)
                if len(user_errors) <= MAX_ERRORS:
                    if len(user_errors) == MAX_ERRORS:
                        user_errors.append(b'...')
                    else:
                        user_errors.append(rev.user())
    errors = []
    if email_errors:
        errors.append(_(
            b'Changeset authors missing email domain: %s' % (
                b', '.join(email_errors)
            )
        ))
    if domain_errors:
        errors.append(_(
            b'Changesets must have author address %s, not %s' % (
                b' or '.join(b'@' + domain for domain in accepted_domains),
                b' or '.join(domain_errors),
            )
        ))
    if user_errors:
        errors.append(_(
            b'User %s must not impersonate others: %s' % (
                accepted_user,
                b', '.join(user_errors)
            )
        ))
    if errors:
        raise error.Abort(b'\n'.join(errors))
