#!/usr/bin/python3
#
# Univention Configuration Registry
#
# SPDX-FileCopyrightText: 2011-2025 Univention GmbH
# SPDX-License-Identifier: AGPL-3.0-only

"""List modified and not updated UCR templates."""

import os
import sys
from argparse import ArgumentParser, Namespace  # noqa: F401
from collections.abc import Iterator  # noqa: F401
from hashlib import md5
from textwrap import dedent

from debian.deb822 import Deb822


PREFIX = '/etc/univention'
SUFFIX = ('.dpkg-new', '.dpkg-dist')
K64 = 1 << 16


def main():
    # type: () -> int
    opt = parse_cmdline()
    modified = set()
    if opt.dpkg:
        modified |= check_find(opt.verbose)
    if opt.md5:
        modified |= check_md5(opt.verbose)
    if modified:
        print(dedent("""\
            WARNING: The following UCR files are modified locally.
            Updated versions will be named FILENAME.dpkg-*.
            The files should be checked for differences.
            """), file=sys.stderr)
        print('\n'.join(sorted(modified)))
        return 1
    return 0


def parse_cmdline():
    # type: () -> Namespace
    parser = ArgumentParser(description=__doc__)
    parser.add_argument(
        '--md5',
        action='store_false',
        help='Disable checking MD5 sums.')
    parser.add_argument(
        '--dpkg',
        action='store_false',
        help='Disable checking for renamed files.')
    parser.add_argument(
        '--verbose', '-v',
        action='store_true',
        help='Enable verbose output.')
    return parser.parse_args()


def check_find(verbose=False):
    # type: (bool) -> Set[str]
    modified = set()
    for dirpath, _dirnames, filenames in os.walk(PREFIX):
        for filename in filenames:
            for suffix in SUFFIX:
                if filename.endswith(suffix):
                    filepath = os.path.join(dirpath, filename)
                    if verbose:
                        print(filepath, file=sys.stderr)
                    basepath = filepath[:-len(suffix)]
                    if not os.path.exists(basepath):
                        continue
                    modified.add(basepath)
    return modified


def check_md5(verbose=False):
    # type: (bool) -> Set[str]
    modified = set()  # type: Set[str]
    original = set()  # type: Set[str]
    try:
        for filepath, expected in iter_templates():
            if filepath in original or filepath in modified:
                continue

            current = md5sum(filepath)
            if verbose:
                print("%s %s %s" % (filepath, expected, current), file=sys.stderr)
            if expected == current:
                original.add(filepath)
            elif current is None:
                continue
            else:
                modified.add(filepath)
        return modified
    except OSError as ex:
        print(ex, file=sys.stderr)
        sys.exit(2)


def iter_templates():
    # type: () -> Iterator[Tuple[str, str]]
    with open('/var/lib/dpkg/status') as dpkg_status:
        for pkg in Deb822.iter_paragraphs(dpkg_status, ["Conffiles"], use_apt_pkg=True):
            try:
                conffiles = pkg["Conffiles"]
            except KeyError:
                continue
            for conffile in conffiles.splitlines():
                fields = [_.strip() for _ in conffile.rsplit(' ', 1) if _]
                # skip obsolete and new conffiles
                if not fields or 'newconffile' in fields or 'obsolete' in fields:
                    continue
                filepath, fmd5 = fields
                if filepath.startswith(PREFIX):
                    yield filepath, fmd5


def md5sum(filepath):
    # type: (str) -> Optional[str]
    digest = md5()
    try:
        with open(filepath, 'rb') as stream:
            while True:
                buf = stream.read(65536)
                if not buf:
                    break
                digest.update(buf)
    except OSError:
        return None

    return digest.hexdigest()


if __name__ == '__main__':
    sys.exit(main())
