#!/usr/bin/python3
#
# This file is part of OpenMediaVault.
#
# @license   http://www.gnu.org/licenses/gpl.html GPL Version 3
# @author    Volker Theile <volker.theile@openmediavault.org>
# @copyright Copyright (c) 2009-2025 Volker Theile
#
# OpenMediaVault is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# any later version.
#
# OpenMediaVault is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with OpenMediaVault. If not, see <http://www.gnu.org/licenses/>.

import json
import platform
import re
# The following error might happen from time to time.
#
# Traceback (most recent call last):
#   File "/usr/sbin/omv-mkaptidx", line 27, in <module>
#     import openmediavault
# EOFError: EOF read where not expected
#
# To analyse the error execute:
# python3 -vc 'import openmediavault'
#
# To fix this error simply execute the following command:
# rm -f /usr/lib/python3/dist-packages/__pycache__/openmediavault.cpython-32.pyc
import sys

import apt
import click
import openmediavault.productinfo

import openmediavault

pi = openmediavault.productinfo.ProductInfo()


class OpenMediaVaultPluginsFilter(apt.cache.Filter):
    """
    Filter that returns all openmediavault plugins except the packages:
    - openmediavault
    - openmediavault-keyring
    """

    def apply(self, pkg):
        m = re.match(r"^%s-(\S+)$" % pi.package_name, pkg.name)
        if not m:
            return False
        if m.group(1) == "keyring":
            return False
        if pkg.candidate is not None:
            # The 'Plugin-Architecture' is mostly used by plugins that are
            # using container images. The architectures that are supported
            # by those images are listed as comma separated values. Note,
            # the list contains the architecture labels used by Debian.
            arch = explode_csv(pkg.candidate.record.get(
                "Plugin-Architecture"))
            if arch:
                # Map the platform architecture to the Debian
                # architecture string used in 'debian/control'.
                machine_2_debian = {
                    "i386": "i386",
                    "x86_64": "amd64",
                    "armv6l": "armel",
                    "armv7l": "armhf",
                    "aarch64": "arm64"
                }
                # Skip this package if the host architecture is
                # not in the list of the supported architectures.
                if machine_2_debian.get(platform.machine()) not in arch:
                    return False
        return True


def get_extended_description(raw_description):
    """
    Return the extended description according to the Debian policy
    (Chapter 5.6.13).
    See http://www.debian.org/doc/debian-policy/ch-controlfields.html
    for more information.
    """
    parts = raw_description.partition("\n")
    lines = parts[2].split("\n")
    for i, line in enumerate(lines):
        lines[i] = line.strip()
        if lines[i] == ".":
            lines[i] = "\n"
    return "\n".join(lines)


def explode_csv(value):
    """
    Split a comma separated string into parts.
    """
    if not isinstance(value, str):
        return []
    return [part.strip() for part in value.split(",") if part.strip()]


def get_value(f, default=None):
    """
    :param f: The function to be executed.
    :type f: callable
    :param default: The default value. Defaults to ``None``.
    :type default: str|int|None
    :return: Returns the specified value, otherwise the default on failure.
    """
    try:
        return f()
    except SystemError:
        pass
    return default


@click.command()
@click.option('--verbose', is_flag=True, help='Shows verbose output.')
def main(verbose: bool):
    cache = apt.cache.Cache()

    # Create the '/var/lib/openmediavault/apt/upgradeindex.json' file.
    print("Creating index of upgradeable packages ...")
    data = []
    cache.upgrade(True)
    for pkg in cache.get_changes():
        if pkg.candidate is None:
            continue
        data.append({
            "name": pkg.name,
            "oldversion": pkg.installed.version if pkg.is_installed and pkg.installed is not None else "",
            "repository": "{}/{}".format(pkg.candidate.origins[0].label, pkg.candidate.origins[0].archive)
            if pkg.candidate.origins is not None else "",
            "package": pkg.candidate.record.get("Package"),
            "source": pkg.candidate.source_name,
            "sourceversion": pkg.candidate.source_version,
            "version": pkg.candidate.version,
            "installedsize": pkg.candidate.size,
            "maintainer": pkg.candidate.record.get("Maintainer", ""),
            "architecture": pkg.candidate.architecture,
            "depends": pkg.candidate.record.get("Depends", ""),
            "suggests": pkg.candidate.record.get("Suggests", ""),
            "conflicts": pkg.candidate.record.get("Conflicts", ""),
            "breaks": pkg.candidate.record.get("Breaks", ""),
            "abstract": pkg.candidate.summary,  # Deprecated
            "summary": pkg.candidate.summary,
            "description": pkg.candidate.record.get("Description", ""),
            "extendeddescription": get_extended_description(pkg.candidate.raw_description),
            "homepage": pkg.candidate.homepage,
            "descriptionmd5": pkg.candidate.record.get("Description-md5", ""),
            "multiarch": pkg.candidate.record.get("Multi-Arch", ""),
            "predepends": pkg.candidate.record.get("Pre-Depends", ""),
            "section": pkg.candidate.section,
            "priority": pkg.candidate.priority,
            "filename": pkg.candidate.filename,
            "size": pkg.candidate.size,
            "md5sum": get_value(lambda: pkg.candidate.md5),
            "sha1": get_value(lambda: pkg.candidate.sha1),
            "sha256": get_value(lambda: pkg.candidate.sha256),
            "uri": pkg.candidate.uri,
            "uris": pkg.candidate.uris
        })
        verbose and print(f"Added package '{pkg.name}'")
    with open(
        openmediavault.getenv(
            'OMV_APT_UPGRADE_INDEX_FILE',
            '/var/lib/openmediavault/apt/upgradeindex.json'
        ), 'w'
    ) as outfile:
        json.dump(data, outfile, sort_keys=True, indent=4)
        verbose and print(f"Index file '{outfile.name}' successfully updated.")

    # Create the '/var/lib/openmediavault/apt/pluginsindex.json' file.
    print("Creating index of plugins ...")
    data = []
    cache = apt.cache.Cache()
    filtered = apt.cache.FilteredCache(cache)
    filtered.set_filter(OpenMediaVaultPluginsFilter())
    for pkg in filtered:
        if pkg.candidate is None:
            continue
        data.append({
            "name": pkg.name,
            "repository": "{}/{}".format(pkg.candidate.origins[0].label, pkg.candidate.origins[0].archive)
            if pkg.candidate.origins is not None else "",
            "package": pkg.candidate.record.get("Package"),
            "version": pkg.candidate.version,
            "installedsize": pkg.candidate.size,
            "maintainer": pkg.candidate.record.get("Maintainer", ""),
            "architecture": pkg.candidate.architecture,
            "depends": pkg.candidate.record.get("Depends", ""),
            "suggests": pkg.candidate.record.get("Suggests", ""),
            "conflicts": pkg.candidate.record.get("Conflicts", ""),
            "breaks": pkg.candidate.record.get("Breaks", ""),
            "abstract": pkg.candidate.summary,  # Deprecated
            "summary": pkg.candidate.summary,
            "description": pkg.candidate.record.get("Description", ""),
            "extendeddescription": get_extended_description(pkg.candidate.raw_description),
            "homepage": pkg.candidate.homepage,
            "descriptionmd5": pkg.candidate.record.get("Description-md5", ""),
            "multiarch": pkg.candidate.record.get("Multi-Arch", ""),
            "predepends": pkg.candidate.record.get("Pre-Depends", ""),
            "section": pkg.candidate.section,
            "pluginsection": pkg.candidate.record.get("Plugin-Section", ""),
            "pluginarchitecture": explode_csv(pkg.candidate.record.get("Plugin-Architecture")),
            "priority": pkg.candidate.priority,
            "filename": pkg.candidate.filename,
            "size": pkg.candidate.size,
            "md5sum": get_value(lambda: pkg.candidate.md5),
            "sha1": get_value(lambda: pkg.candidate.sha1),
            "sha256": get_value(lambda: pkg.candidate.sha256),
            "installed": pkg.is_installed
        })
        verbose and print(f"Added package '{pkg.name}'")
    with open(
        openmediavault.getenv(
            'OMV_APT_PLUGINS_INDEX_FILE',
            '/var/lib/openmediavault/apt/pluginsindex.json'
        ), 'w'
    ) as outfile:
        json.dump(data, outfile, sort_keys=True, indent=4)
        verbose and print(f"Index file '{outfile.name}' successfully updated.")

    sys.exit(0)


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