#!/usr/bin/env python3
# -*- coding: utf-8 -*-

# Copyright (C) 2015-2021 Xyne
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# (version 2) as published by the Free Software Foundation.
#
#
# This program 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 this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

import errno
import glob
import itertools
import json
import logging
import os
import platform
import re
import shutil
import sqlite3
import subprocess
import sys
import textwrap
import time
import urllib.error
import urllib.request

import xdg.BaseDirectory

import MemoizeDB
import pyalpm
import pycman

import XCGF


# -------------------------------- Constants --------------------------------- #

NAME = 'XCPF'
PKGINFO_DBNAME = 'pkginfo.sqlite3'

# Variant of ISO 8601 (2015-10-05T09:23:04.883Z)
ARCHLINUX_ORG_JSON_TIME_FORMAT = '%Y-%m-%dT%H:%M:%S.%fZ'


ARCHLINUX_ORG_JSON_URL = 'https://www.archlinux.org/packages/{repo}/{arch}/{pkgname}/json/'

ARCHLINUX_ORG_ABS_RSYNC_HOST = 'rsync.archlinux.org'
ARCHLINUX_ORG_ABS_RSYNC_PATH = '::abs/{arch}/{repo}/'

# Cache subdirectory for caching git repos.
ABS_GIT_CACHE = 'abs_git'

ARCHLINUX_ORG_ABS_GIT_HOST = 'https://github.com/archlinux'
ARCHLINUX_ORG_ABS_GIT_REPO = ARCHLINUX_ORG_ABS_GIT_HOST + '/svntogit-{gitrepo}.git'
ARCHLINUX_ORG_ABS_GIT_TRUNK = ARCHLINUX_ORG_ABS_GIT_REPO + '/plain/{pkgname}/trunk'

# Tuple elements:
# * repo name
# * repo architecture
# * is testing repo
# * Git repo name
ARCHLINUX_OFFICIAL_REPOS = (
    ('testing', 'any', True, 'packages'),
    ('core', 'any', False, 'packages'),
    ('extra', 'any', False, 'packages'),
    ('community-testing', 'any', True, 'community'),
    ('community', 'any', False, 'community'),
    ('multilib-testing', 'x86_64', True, 'community'),
    ('multilib', 'x86_64', False, 'community'),
)

ARCHLINUX_OFFICIAL_ARCHITECTURES = ('x86_64', 'i686')

ABS_GIT_REPOS = ('packages', 'community')

WRAP_WIDTH = 80


# --------------------------------- Caching ---------------------------------- #

def cachepath(what):
    '''
    Create a standard cache directory for this module and return a full path to
    the given subpath. Use this to re-use cached data across different scripts.
    '''
    cachedir = xdg.BaseDirectory.save_cache_path(NAME)
    return os.path.join(cachedir, what)


def pkginfo_dbpath():
    '''
    Return a standard path for the cached package info.
    '''
    return cachepath(PKGINFO_DBNAME)


def abs_git_cache(pkgbase):
    '''
    Return a standard cache path for the Arch Linux Git repositories.
    '''
    return cachepath(os.path.join(ABS_GIT_CACHE, pkgbase))


def clear_abs_git_cache():
    '''
    Clear the ABS Git cache.
    '''
    shutil.rmtree(cachepath(ABS_GIT_CACHE))


# ------------------------------ PkgInfoHandler ------------------------------ #

class OfficialPkgInfo():  # pylint: disable=too-many-instance-attributes
    '''
    A class to handle package information retrieval. It provides optional
    transparent caching of the remote data.
    '''

    def __init__(
        self,
        ttl=0,
        arch=None,
        testing=False,
        dbpath=None,
        mdb=None,
        handle=None,
        config=None,
        dbs=None,
        log_level=logging.DEBUG,
        resolve_pkgbases=False
    ):  # pylint: disable=too-many-locals,too-many-arguments
        self.log_level = log_level
        self.resolve_pkgbases = resolve_pkgbases

        self.ttl = ttl
        if arch is None:
            arch = platform.machine()
        self.arch = arch
        self.testing = testing

        self.config = config
        if handle is None and config is not None:
            handle = pycman.config.init_with_config(config)
        self.handle = handle
        if dbs is None and handle is not None:
            dbs = handle.get_syncdbs()
        # Restrict databases to official databases.
        official_repos = set(r[0] for r in ARCHLINUX_OFFICIAL_REPOS)
        self.dbs = list(db for db in dbs if db.name in official_repos)

        if ttl > 0 or mdb is not None:
            if mdb is None:
                if dbpath is None:
                    dbpath = pkginfo_dbpath()

                def func(urls):
                    for url in urls:
                        yield url, (XCGF.text_from_url(url),)
                glue = {
                    'request': (func, (('data', 'TEXT'),), ttl)
                }
                conn = sqlite3.connect(
                    dbpath,
                    detect_types=(sqlite3.PARSE_DECLTYPES | sqlite3.PARSE_COLNAMES),
                    isolation_level=None
                )
                mdb = MemoizeDB.MemoizeDB(conn, glue)
                mdb.db_initialize()

            self.mdb = mdb

            def memoized_archlinux_org_pkg_info(repo, arch, pkgname):
                url = archlinux_org_pkg_info_url(repo, arch, pkgname)
                txt = mdb.get_one('request', url)
                return json.loads(txt)

            self.retrieve_pkginfo = memoized_archlinux_org_pkg_info

        else:
            self.retrieve_pkginfo = archlinux_org_pkg_info

    def pkginfo_by_pkgname(self, pkgname):
        '''
        Retrieve package information by pkgname. This will search through the
        official repos until the package is found or the repos are exhausted.
        '''
        for repo, arch, testing, _ in ARCHLINUX_OFFICIAL_REPOS:
            if not testing and arch in ('any', self.arch):
                try:
                    return self.retrieve_pkginfo(repo, self.arch, pkgname)
                except urllib.error.HTTPError as err:
                    if err.code == 404:
                        continue
                    raise err
        return None

    def pkginfo_by_pyalpm_package(self, pkg):
        '''
        Retrieve package information for an instance of pyalpm.Package.
        '''
        try:
            return self.retrieve_pkginfo(pkg.db.name, pkg.arch, pkg.name)
        except urllib.error.HTTPError as err:
            if err.code == 404:
                return None
            raise err

    def pkginfo_by_pkgname_or_pyalpm_package(self, pkg):
        '''
        Retrieve package information by pkgname or pyalpm.Package.
        '''
        if isinstance(pkg, pyalpm.Package):
            return self.pkginfo_by_pyalpm_package(pkg)
        pyalpm_pkg = self.pyalpm_package(pkg)
        if pyalpm_pkg is None:
            return self.pkginfo_by_pkgname(pkg)
        return self.pkginfo_by_pyalpm_package(pyalpm_pkg)

    def pyalpm_package(self, pkgname):
        '''
        Search databases for a package name.
        '''
        if isinstance(pkgname, pyalpm.Package):
            return pkgname
        if not self.dbs:
            return None
        return find_pkgname(self.dbs, pkgname)

    def pyalpm_package_and_pkginfo(self, pkgname):
        '''
        Attempt to retrieve the pyalpm.Package and the package information.
        '''
        pkg = self.pyalpm_package(pkgname)
        if pkg is None:
            pkginfo = self.pkginfo_by_pkgname(pkgname)
        else:
            pkginfo = self.pkginfo_by_pyalpm_package(pkg)
        return pkg, pkginfo

    def map_to_pkgbases(self, pkgs):
        '''
        Iterate over tuples mapping pkgnames to pkgbases. If the pkginfo could not
        be found, then the pkgbase will be None.
        '''
        for pkg in pkgs:
            pkginfo = self.pkginfo_by_pkgname_or_pyalpm_package(pkg)
            try:
                yield pkg, pkginfo['base']
            except KeyError:
                try:
                    yield pkg, None
                except AttributeError:
                    yield pkg, None

    def map_to_pkgname_pkgbase_repo(self, pkgname):
        '''
        Attempt to map a pkgname or pyalpm.Package to a pkgname, repo and pkgbase.

        Use this to collect package bases for ABS retrieval.
        '''
        pkg = self.pyalpm_package(pkgname)
        if pkg is None:
            name = pkgname
            pkginfo = self.pkginfo_by_pkgname(pkgname)
            try:
                base = pkginfo['pkgbase']
            except (TypeError, KeyError):
                base = None
            try:
                repo = pkginfo['repo']
            except (TypeError, KeyError):
                repo = None
        else:
            name = pkg.name
            repo = pkg.db.name
            pkginfo = self.pkginfo_by_pyalpm_package(pkg)
            try:
                base = pkginfo['pkgbase']
            except (TypeError, KeyError):
                base = None
        return name, repo, base

    def collect_pkgbases_by_repo(self, pkgnames):
        '''
        Attempt to map packages to package bases and organize them by repo.
        '''
        repos = {}
        pkgbases = {}
        if self.resolve_pkgbases:
            for name, repo, base in (
                self.map_to_pkgname_pkgbase_repo(p) for p in pkgnames
            ):
                try:
                    pkgbases[base].add(name)
                except KeyError:
                    pkgbases[base] = set((name,))

                if base is not None and name != base:
                    logging.log(
                        self.log_level,
                        'mapped %s to %s in %s',
                        name, base, repo
                    )
                if base is None:
                    base = name
                try:
                    repos[repo].add(base)
                except KeyError:
                    repos[repo] = set((base,))
        else:
            repos[None] = set(
                p.name if isinstance(p, pyalpm.Package) else p for p in pkgnames
            )
        return repos, pkgbases

    def retrieve_abs_via_git(self, output_dir, pkgnames, pull=False, trunk=False):
        '''
        Retrieve the requested files from ABS via the online Git interface if
        possible. Iterates over the found packages and their URLs.
        '''
        if self.resolve_pkgbases:
            repos, pkgbase_map = self.collect_pkgbases_by_repo(pkgnames)
        else:
            repos = {
                None: set(pkgnames)
            }
            pkgbase_map = {}
        for repo, pkgbases in repos.items():
            for pkgbase in pkgbases:
                url = retrieve_abs_via_git(output_dir, pkgbase, repo=repo,
                                           ttl=self.ttl, pull=pull, trunk=trunk)
                if url:
                    try:
                        for pkgname in pkgbase_map[pkgbase]:
                            yield pkgname, url
                    except KeyError:
                        yield pkgbase, url

    def get_blind_abs_rsync_paths(self, repos=None):
        '''
        Get a list of ABS rsync paths for the given architecture and repos.
        '''
        for repo, arch, is_testing, _ in ARCHLINUX_OFFICIAL_REPOS:
            if repos and repo not in repos:
                continue
            if is_testing and not self.testing:
                continue
            if arch == 'any':
                yield ARCHLINUX_ORG_ABS_RSYNC_PATH.format(arch=arch, repo=repo)
                if self.arch != arch:
                    yield ARCHLINUX_ORG_ABS_RSYNC_PATH.format(arch=self.arch, repo=repo)
            elif arch == self.arch:
                yield ARCHLINUX_ORG_ABS_RSYNC_PATH.format(arch=arch, repo=repo)

    def retrieve_abs_via_rsync(
        self,
        output_dir,
        pkgnames,
        rsync_path='rsync',
    ):  # pylint: disable=too-many-branches,too-many-locals
        '''
        Retrieve the requested files from ABS via rsync.
        '''

        cmd = [rsync_path, '-rt']

        repos, pkgbase_map = self.collect_pkgbases_by_repo(pkgnames)
        pkgbases = set()
        for pbs in repos.values():
            pkgbases |= pbs

#     if None in repos:
        abs_paths = list(self.get_blind_abs_rsync_paths())
        cmd.extend(f'--include=/{x}/***' for x in pkgbases)
        cmd.append('--exclude=/*')
#     else:
#       abs_paths = list(self.get_abs_rsync_paths(repos))

        if abs_paths:
            abs_paths[0] = ARCHLINUX_ORG_ABS_RSYNC_HOST + abs_paths[0]
        cmd.extend(abs_paths)
        cmd.append(output_dir)

        # Use mtimes to determine if rsync retrieved a file.
        # This avoid piping and parsing rsync output.
        pkgdirs = tuple(os.path.join(output_dir, x) for x in pkgbases)
        mtimes = {}
        for pdir in pkgdirs:
            try:
                mtime = os.path.getmtime(pdir)
                mtimes[pdir] = mtime
                os.utime(pdir, (0, 0))
            except OSError as err:
                if err.errno != errno.ENOENT:
                    raise err

        logging.debug('invoking %s', cmd)
        with open(os.devnull, 'wb') as handle:
            subprocess.call(cmd, stdout=handle)

        for pdir in pkgdirs:
            try:
                mtime = os.path.getmtime(pdir)
            except OSError as err:
                if err.errno != errno.ENOENT:
                    raise err
            else:
                if mtime != 0:
                    pkgbase = os.path.basename(pdir)
                    try:
                        yield from pkgbase_map[pkgbase]
                    except KeyError:
                        yield pkgbase
                else:
                    mtime = mtimes[pdir]
                    os.utime(pdir, (mtime, mtime))


# --------------------------------- Network ---------------------------------- #

def archlinux_org_pkg_info_url(repo, arch, pkgname):
    '''
    Retrieve information about official packages from archlinux.org.
    '''
    return ARCHLINUX_ORG_JSON_URL.format(repo=repo, arch=arch, pkgname=pkgname)


def archlinux_org_pkg_info(repo, arch, pkgname):
    '''
    Retrieve information about official packages from archlinux.org.
    '''
    url = archlinux_org_pkg_info_url(repo, arch, pkgname)
    return XCGF.load_json_from_url(url)


def archlinux_abs_git_repos(repo=None):
    '''
    Iterate over the ABS git repos.
    '''
    if repo:
        for rep, _, _, grepo in ARCHLINUX_OFFICIAL_REPOS:
            if repo == rep:
                yield grepo
                break
    else:
        yield from ABS_GIT_REPOS


def search_abs_git_repos(pkgname, repo=None):
    '''
    Scrape the online Git interface to retrieve URLs and names.
    '''
    for grepo in archlinux_abs_git_repos(repo=repo):
        url = ARCHLINUX_ORG_ABS_GIT_TRUNK.format(
            gitrepo=grepo,
            pkgname=pkgname
        )
        try:
            with urllib.request.urlopen(url) as handle:
                yield url, None
                for line in handle:
                    match = re.search(r"href='(.+?)'>(.+?)<".encode('utf-8'), line)
                    if match:
                        href = match.group(1).decode()
                        name = match.group(2).decode()
                        if name[:2] != '..':
                            yield ARCHLINUX_ORG_ABS_GIT_HOST + href, name
        except urllib.error.HTTPError as err:
            if err.code != 404:
                raise err
        else:
            return


def retrieve_abs_via_git_web_interface(output_dir, pkgbase, repo=None):
    '''
    Retrieve ABS files via the Git interface. Return the page url if successful.
    '''
    urls = search_abs_git_repos(pkgbase, repo=repo)
    try:
        url, _ = next(urls)
    except StopIteration:
        return None
    else:
        pkgdir = os.path.join(output_dir, pkgbase)
        os.makedirs(pkgdir, exist_ok=True)
        for href, name in urls:
            XCGF.mirror(href, os.path.join(pkgdir, name), cache_time=0)
        return url


def retrieve_abs_via_git(output_dir, pkgbase, repo=None, ttl=0, pull=False, trunk=False):  # pylint: disable=too-many-branches,too-many-locals
    '''
    Retrieve ABS files via the Git interface. Return the page url if successful.
    '''
    gr_path = abs_git_cache(pkgbase)
    os.makedirs(os.path.dirname(gr_path), exist_ok=True)
    gr_url = None
    if os.path.exists(os.path.join(gr_path, '.git')):
        git_cmd = ['git', '-C', gr_path]
        try:
            fetch = (ttl <= 0) or (os.path.getmtime(os.path.join(
                gr_path,
                '.git',
                'FETCH_HEAD'
            )) + ttl) < time.time()
        except FileNotFoundError:
            fetch = True
        if fetch or pull:
            if pull:
                git_args = ['pull']
            else:
                git_args = ['fetch']
            subprocess.check_call(
                git_cmd + git_args,
                stdout=subprocess.DEVNULL,
                stderr=subprocess.DEVNULL
            )
        gr_url = subprocess.check_output(
            git_cmd + ['config', '--get', 'remote.origin.url']).decode().strip()
    else:
        for gitrepo in archlinux_abs_git_repos(repo=repo):
            gr_url = ARCHLINUX_ORG_ABS_GIT_REPO.format(gitrepo=gitrepo)
            try:
                subprocess.check_call(
                    [
                        'git', 'clone',
                        gr_url,
                        '--single-branch', '-b', f'packages/{pkgbase}',
                        gr_path
                    ],
                    stdout=subprocess.DEVNULL,
                    stderr=subprocess.DEVNULL
                )
            except subprocess.CalledProcessError:
                continue
            else:
                break
        else:
            return None

    # Use the trunk subdir by default.
    gr_subdir = os.path.join(gr_path, 'trunk')
    # Otherwise check for an existing non-testing repo.
    if not trunk:
        for rep, _, testing, _ in ARCHLINUX_OFFICIAL_REPOS:
            if testing:
                continue
            pattern = os.path.join(gr_path, 'repos', f'{rep}-*')
            for matched_subdir in glob.iglob(pattern):
                gr_subdir = matched_subdir
                break
            else:
                continue
            break

    logging.debug('extracting files from %s', gr_subdir)
    with subprocess.Popen(
        ['git', '-C', gr_subdir, 'archive', 'HEAD'],
        stdout=subprocess.PIPE,
        stderr=subprocess.DEVNULL
    ) as proc1:
        output_dir = os.path.join(output_dir, pkgbase)
        os.makedirs(output_dir, exist_ok=True)
        with subprocess.Popen(
            ['tar', '-x', '-C', output_dir],
            stdin=proc1.stdout,
            stdout=subprocess.DEVNULL,
            stderr=subprocess.DEVNULL
        ):
            proc1.stdout.close()
            return f'{gr_url} [packages/{pkgbase}]'


# -------------------------------- Exceptions -------------------------------- #

class XcpfError(Exception):
    '''
    Parent class of all custom exceptions raised by this module.
    '''

    def __init__(self, msg, error=None):
        super().__init__(msg)
        self.msg = msg
        self.error = error

    def __str__(self):
        string = f'{self.__class__.__name__}: {self.msg}'
        if self.error:
            string += f' [{self.error}]'
        return string


# ----------------------------- pyalpm Database ------------------------------ #

def join_db_pkgcaches(dbs):
    '''
    Create a unified list of packages from the given databases. The order is
    preserved.
    '''
    return list(itertools.chain.from_iterable(db.pkgcache for db in dbs))


def find_pkgname(dbs, pkgname):
    '''
    Search for a package in the given databases.
    '''
    for db in dbs:  # pylint: disable=invalid-name
        pkg = db.get_pkg(pkgname)
        if pkg is not None:
            return pkg
    return None


# ----------------------------- Version Parsing ------------------------------ #

def split_version_requirement(name):
    '''
    Split a name with a version requirement into a name, equality relation and
    version.
    '''
    for rel in ('>=', '<=', '>', '<', '='):
        try:
            name, ver = name.split(rel, 1)
            return name, rel, ver
        except ValueError:
            continue
    return name, None, None


def satisfies_version_requirement(version, relation, required_version):
    '''
    Check if a version requirement is satisfied by the given version.
    '''
    if relation is None:
        return True
    vcmp = pyalpm.vercmp(version, required_version)
    return \
        (vcmp > 0 and '>' in relation) or \
        (vcmp == 0 and '=' in relation) or \
        (vcmp < 0 and '<' in relation)


def satisfies_all_version_requirements(version, reqs):
    '''
    Check if all version requirements are satisfied.
    '''
    return all(
        satisfies_version_requirement(version, *req) for req in reqs
    )


def collect_version_requirements(args):
    '''
    Collect version requirements per package.

    Returns a dictionary of package names mapped to lists of (relation, version)
    tuples for that name.
    '''
    ver_reqs = {}

    for arg in args:
        name, relation, version = split_version_requirement(arg)
        if relation is None:
            if name not in ver_reqs:
                ver_reqs[name] = set()
        else:
            try:
                ver_reqs[name].add((relation, version))
            except KeyError:
                ver_reqs[name] = set(((relation, version),))

    return ver_reqs


def get_required_version_strings(ver_reqs, names):
    '''
    Return an iterator over the strings specifying the version requirements for
    the named package.
    '''
    for name in names:
        yielded = False
        try:
            for rel, ver in ver_reqs[name]:
                yield '{}{}{}'.format(name, rel, ver)
                yielded = True
        except KeyError:
            yield name
        else:
            if not yielded:
                yield name


def select_name_or_first(name, pkgs):
    '''
    Given a list of packages, return the package with the exact name if it exists,
    otherwise return the first package in the list.
    '''
    try:
        first = next(pkgs)
    except StopIteration:
        return None

    if first.name == name:
        return first

    for pkg in pkgs:
        if pkg.name == name:
            return pkg

    return first


def repo_and_package(pkg):
    '''
    Split repo prefix from package name, e.g. "core/foo" -> "core", "foo".
    '''
    try:
        repo, version = pkg.split('/', 1)
        return repo, version
    except ValueError:
        return None, pkg


def find_all_satisfiers(pkgs, req):
    '''
    Wrapper around pyalpm.find_satisfier that considers repo prefixes, e.g.
    core/foo, and return all satisfiers.
    '''
    repo, req = repo_and_package(req)
    while pkgs:
        pkg = pyalpm.find_satisfier(pkgs, req)
        if not pkg:
            break
        if repo is None or (pkg.db.name == repo):
            yield pkg
        # Direct comparison doesn't work here even though "pkg == pkg" evaluates to True.
        pkgs = list(p for p in pkgs if not (p.name == pkg.name and p.db.name == pkg.db.name))


def find_satisfiers_in_dbs(dbs, reqs):
    '''
    Search for packages that satisfy all of the given version requirements in the
    given databases.
    '''
    for db in dbs:
        for pkg in find_satisfiers_in_pkgs(db.pkgcache, reqs):
            yield pkg


def find_satisfiers_in_pkgs(pkgs, reqs):
    '''
    Search for packages that satisfy all of the given version requirements among
    the given packages.
    '''
    # The package that satisfies one version requirement may not satisfy them
    # all. Different packages may provide different versions of the
    # requirement yet the "find_satisfier" function will only return the
    # first. All requirements must therefore be checked against the result of
    # each query.
    if pkgs:
        for req in reqs:
            for pkg in find_all_satisfiers(pkgs, req):
                if pkg:
                    ps = (pkg,)
                    for r in reqs:
                        if r != req:
                            if not pyalpm.find_satisfier(ps, r):
                                break
                    else:
                        # Control reaches here only if all requirements have been met.
                        yield pkg


# ----------------------------- Argument Parsing ----------------------------- #

def get_args_from_stdin():
    '''
    Get arguments from STDIN. This should emulate pacman's parsing of the "-"
    argument to insert package names piped via STDIN.
    '''
    for line in sys.stdin:
        yield from line.strip().split()


def maybe_insert_args_from_stdin(args):
    '''
    Replace "-" with arguments from STDIN if it appears in the given arguments.
    '''
    first = True
    for arg in args:
        if arg == '-' and first:
            first = False
            yield from get_args_from_stdin()
        else:
            yield arg


# --------------------------------- Display ---------------------------------- #

# From pkg-query (fmtlst)
# margin = max field name length in format_pkginfo.
def format_pkginfo_list(xs, per_line=False, margin=18):  # pylint: disable=invalid-name
    '''
    Format a list for displayed package information.
    '''
    if xs:
        if per_line:
            insert = '\n' + ' ' * margin
            return insert.join(format_pkginfo_string(x, margin=margin) for x in xs)
        return format_pkginfo_string(' '.join(xs), margin=margin)
    return 'None'


def format_pkginfo_string(desc, margin=18):
    '''
    Format a description for displayed package information.
    '''
    lines = textwrap.wrap(desc, width=WRAP_WIDTH-margin, break_long_words=False)
    insert = '\n' + ' ' * margin
    return insert.join(lines)


# From pkg-query
def format_pkginfo(pkg):
    '''
    Format package information for display similarly to "pacman -Si".
    '''
    return f'''Repository      : {pkg.db.name}
Name            : {pkg.name}
Version         : {pkg.version}
Description     : {format_pkginfo_string(pkg.desc)}
Architecture    : {pkg.arch}
URL             : {pkg.url}
Licenses        : {format_pkginfo_string(pkg.licenses)}
Packager        : {pkg.packager}
Groups          : {format_pkginfo_list(pkg.groups)}
Provides        : {format_pkginfo_list(pkg.provides)}
Depends On      : {format_pkginfo_list(pkg.depends)}
Optional Deps   : {format_pkginfo_list(pkg.optdepends, per_line=True)}
Conflicts With  : {format_pkginfo_list(pkg.conflicts)}
Replaces        : {format_pkginfo_list(pkg.replaces)}
Download Size   : {pkg.download_size}
Installed Size  : {pkg.isize}
Build Date      : {pkg.installdate}
'''
