#!/usr/bin/env python3
#
# autodist-distroquery - query the distroquery REST API and output
#   bash-compatible variable assignments for use with eval.
#
# Copyright (c) 2026 by Silvan Calarco <silvan@openmamba.org>
#
# DISTROQUERY_API_URL is read from /etc/autodist/config; the script exits
# with an error if the variable is not set or empty.
#
# Usage:
#   autodist-distroquery package REPO PKG [ARCH]
#   autodist-distroquery repository REPO
#
# Bash variable assignments are written to stdout.

import sys
import json
import shlex
import urllib.request

CONFIG_FILE = '/etc/autodist/config'
SECRETS_FILE = '/etc/autodist/secrets'


def read_config(path=CONFIG_FILE):
    """Parse a bash key=value config file and return a dict."""
    config = {}
    try:
        with open(path) as f:
            for line in f:
                line = line.strip()
                if not line or line.startswith('#') or '=' not in line:
                    continue
                key, _, value = line.partition('=')
                value = value.strip()
                # Strip surrounding quotes
                if len(value) >= 2 and value[0] == value[-1] and value[0] in ('"', "'"):
                    value = value[1:-1]
                config[key.strip()] = value
    except OSError:
        pass
    return config


def get_api_url():
    """Return DISTROQUERY_API_URL from config, or exit with an error."""
    config = read_config()
    api_url = config.get('DISTROQUERY_API_URL', '')
    if not api_url:
        print(f"ERROR: DISTROQUERY_API_URL is not set in {CONFIG_FILE}", file=sys.stderr)
        sys.exit(1)
    return api_url


def fetch_json(url):
    """Fetch and parse JSON from a URL. Returns None on any error."""
    try:
        with urllib.request.urlopen(url, timeout=10) as resp:
            return json.load(resp)
    except Exception:
        return None


def get_social_token():
    """Return DISTROQUERY_API_TOKEN from secrets file, or exit with an error."""
    config = read_config(SECRETS_FILE)
    token = config.get('DISTROQUERY_API_TOKEN', '')
    if not token:
        print(f"ERROR: DISTROQUERY_API_TOKEN is not set in {SECRETS_FILE}", file=sys.stderr)
        sys.exit(1)
    return token


def fetch_json_auth(url, token, method='GET', data=None):
    """Fetch and parse JSON from a URL with Bearer token auth.

    For POST requests, pass data as a dict (will be JSON-encoded).
    Returns None on any error.
    """
    import urllib.error
    body = json.dumps(data).encode() if data is not None else None
    req = urllib.request.Request(
        url,
        data=body,
        method=method,
        headers={
            'Authorization': f'Bearer {token}',
            'Content-Type': 'application/json',
            'Accept': 'application/json',
        })
    try:
        with urllib.request.urlopen(req, timeout=10) as resp:
            return json.load(resp)
    except urllib.error.HTTPError as e:
        try:
            err = json.load(e)
            print(f"ERROR: API returned {e.code}: {err}", file=sys.stderr)
        except Exception:
            print(f"ERROR: API returned {e.code}", file=sys.stderr)
        return None
    except Exception as e:
        print(f"ERROR: {e}", file=sys.stderr)
        return None


def cmd_package(repo, pkg, buildarch=''):
    """Query GET /package/{repo}/{pkg}, resolve the effective RPM arch, and
    output bash variable assignments.

    Outputs: pkg_name, pkg_version, pkg_release, pkg_summary, pkg_group,
             pkg_license, pkg_url, pkg_description, pkg_size, pkg_buildtime,
             pkg_arch, pkg_archs (array), pkg_builds (array).

    buildarch selects which build architecture's binary packages go into
    pkg_builds; "any" or "" auto-selects the first available architecture.
    noarch packages are appended to pkg_builds for any non-noarch arch.

    The effective RPM arch is resolved via a secondary call to
    GET /package/{repo}/{binarypackage}/{arch}, since the source package
    response only records build arches and a package built under x86_64 may
    still produce noarch RPMs.
    """
    api_url = get_api_url()

    d = fetch_json(f"{api_url}/package/{repo}/{pkg}")
    if d is None:
        print(f"ERROR: failed to fetch package data from API for {pkg} in {repo}", file=sys.stderr)
        sys.exit(2)
    if 'error' in d:
        print('pkg_name=')
        print('pkg_version=')
        print('pkg_release=')
        print('pkg_buildtime=')
        print('pkg_arch=')
        print('pkg_archs=()')
        print('pkg_builds=()')
        sys.exit(0)

    archs_data = d.get('children', {}).get('archs', {})
    non_noarch = [a for a in archs_data if a != 'noarch']
    effective_archs = non_noarch if non_noarch else (['noarch'] if 'noarch' in archs_data else [])

    if not buildarch or buildarch == 'any':
        buildarch = effective_archs[0] if effective_archs else ''

    builds = []
    if buildarch in archs_data:
        builds = [p['name'] for p in archs_data[buildarch]]
        if buildarch != 'noarch' and 'noarch' in archs_data:
            builds += [p['name'] for p in archs_data['noarch']]

    # Resolve actual RPM arch via the binary package endpoint
    #pkg_arch_val = buildarch
    pkg_arch_val = ""
    if builds and buildarch and buildarch != 'noarch':
        pkg_data = fetch_json(f"{api_url}/package/{repo}/{builds[0]}/{buildarch}")
        if pkg_data is None:
            print(f"ERROR: failed to fetch arch data from API for {builds[0]}/{buildarch} in {repo}", file=sys.stderr)
            sys.exit(2)
        if 'arch' in pkg_data and pkg_data['arch'] in [buildarch, 'noarch']:
            pkg_arch_val = pkg_data['arch']
        else:
            # No build for requested arch, return nothing
            print('pkg_name=')
            print('pkg_version=')
            print('pkg_release=')
            print('pkg_buildtime=')
            print('pkg_arch=')
            print('pkg_archs=()')
            print('pkg_builds=()')
            sys.exit(0)
    elif not builds:
        # No builds, return nothing
        print('pkg_name=')
        print('pkg_version=')
        print('pkg_release=')
        print('pkg_buildtime=')
        print('pkg_arch=')
        print('pkg_archs=()')
        print('pkg_builds=()')
        sys.exit(0)

    print('pkg_name='        + shlex.quote(d.get('name', '')))
    print('pkg_version='     + shlex.quote(d.get('version', '')))
    print('pkg_release='     + shlex.quote(d.get('release', '')))
    print('pkg_epoch='       + shlex.quote(str(d.get('epoch', ''))))
    print('pkg_repository='  + shlex.quote(d.get('repository', repo)))
    print('pkg_summary='     + shlex.quote(d.get('summary', '')))
    print('pkg_group='       + shlex.quote(d.get('group', '')))
    print('pkg_license='     + shlex.quote(d.get('license', '')))
    print('pkg_url='         + shlex.quote(d.get('url', '')))
    print('pkg_description=' + shlex.quote(d.get('description', '')))
    print('pkg_size='        + shlex.quote(str(d.get('size', ''))))
    print('pkg_buildtime='   + shlex.quote(str(d.get('buildtime', ''))))
    print('pkg_arch='        + shlex.quote(pkg_arch_val))
    print('pkg_archs=('      + ' '.join(shlex.quote(a) for a in effective_archs) + ')')
    print('pkg_builds=('     + ' '.join(shlex.quote(b) for b in builds) + ')')


def cmd_package_all_archs(repo, pkg):
    """Query GET /package/{repo}/{pkg} for all architectures and output
    bash variable assignments with per-arch builds info.

    Outputs: pkg_name, pkg_version, pkg_release, pkg_summary, pkg_group,
             pkg_license, pkg_url, pkg_description, pkg_size, pkg_buildtime,
             pkg_archs (array), and for each arch in pkg_archs:
             pkg_builds_<arch> (array), pkg_arch_<arch>.
    """
    api_url = get_api_url()

    d = fetch_json(f"{api_url}/package/{repo}/{pkg}")
    if d is None:
        print(f"ERROR: failed to fetch package data from API for {pkg} in {repo}", file=sys.stderr)
        sys.exit(2)
    if 'error' in d:
        print('pkg_name=')
        print('pkg_version=')
        print('pkg_release=')
        print('pkg_buildtime=')
        print('pkg_archs=()')
        sys.exit(0)

    archs_data = d.get('children', {}).get('archs', {})
    non_noarch = [a for a in archs_data if a != 'noarch']
    effective_archs = non_noarch if non_noarch else (['noarch'] if 'noarch' in archs_data else [])

    if not effective_archs:
        print('pkg_name=')
        print('pkg_version=')
        print('pkg_release=')
        print('pkg_buildtime=')
        print('pkg_archs=()')
        sys.exit(0)

    print('pkg_name='        + shlex.quote(d.get('name', '')))
    print('pkg_version='     + shlex.quote(d.get('version', '')))
    print('pkg_release='     + shlex.quote(d.get('release', '')))
    print('pkg_epoch='       + shlex.quote(str(d.get('epoch', ''))))
    print('pkg_repository='  + shlex.quote(d.get('repository', repo)))
    print('pkg_summary='     + shlex.quote(d.get('summary', '')))
    print('pkg_group='       + shlex.quote(d.get('group', '')))
    print('pkg_license='     + shlex.quote(d.get('license', '')))
    print('pkg_url='         + shlex.quote(d.get('url', '')))
    print('pkg_description=' + shlex.quote(d.get('description', '')))
    print('pkg_size='        + shlex.quote(str(d.get('size', ''))))
    print('pkg_buildtime='   + shlex.quote(str(d.get('buildtime', ''))))
    print('pkg_archs=('      + ' '.join(shlex.quote(a) for a in effective_archs) + ')')

    for arch in effective_archs:
        builds = [p['name'] for p in archs_data.get(arch, [])]
        if arch != 'noarch' and 'noarch' in archs_data:
            builds += [p['name'] for p in archs_data['noarch']]

        if not builds:
            continue

        pkg_arch_val = arch
        if arch != 'noarch':
            pkg_data = fetch_json(f"{api_url}/package/{repo}/{builds[0]}/{arch}")
            if pkg_data is None:
                print(f"ERROR: failed to fetch arch data from API for {builds[0]}/{arch} in {repo}", file=sys.stderr)
                sys.exit(2)
            if 'arch' not in pkg_data or pkg_data['arch'] not in [arch, 'noarch']:
                continue
            pkg_arch_val = pkg_data['arch']

        safe_arch = arch.replace('-', '_')
        print(f'pkg_builds_{safe_arch}=(' + ' '.join(shlex.quote(b) for b in builds) + ')')
        print(f'pkg_arch_{safe_arch}=' + shlex.quote(pkg_arch_val))


def cmd_repository(repo, sort='', order=''):
    """Query GET /repository/{repo} and output pkg_list.

    Outputs: pkg_list (array of source package names), sorted by `sort`
    field in `order` direction when specified (e.g. sort='buildtime',
    order='desc').
    """
    api_url = get_api_url()

    url = f"{api_url}/repository/{repo}?per_page=100000&page=1&arch=src"
    if sort:
        url += f"&sort={sort}&order={order or 'asc'}"
    d = fetch_json(url)
    if d is None:
        print(f"ERROR: failed to fetch repository data from API for {repo}", file=sys.stderr)
        sys.exit(2)
    if 'error' in d:
        sys.exit(0)

    pkgs = [p['name'] for p in d.get('packages', []) if p.get('arch') == 'src']
    print('pkg_list=(' + ' '.join(shlex.quote(p) for p in pkgs) + ')')


def cmd_recent_html(repo, count=100):
    """Query GET /repository/{repo} sorted by buildtime and output HTML
    compatible with the _recent.inc format generated by distromatic.

    The output can be used as a drop-in replacement for the static
    _recent.inc file in the webbuild CGI interface.
    """
    import html as html_mod
    from datetime import datetime

    api_url = get_api_url()

    url = (f"{api_url}/repository/{repo}"
           f"?per_page={count}&page=1"
           f"&sort=buildtime&order=desc&arch=src&add=changelog,children,problems,updates")
    d = fetch_json(url)
    if d is None:
        print(f"ERROR: failed to fetch recent packages from API for {repo}",
              file=sys.stderr)
        sys.exit(2)
    if 'error' in d:
        sys.exit(0)

    # Title with repository update timestamp and statistics from metadata
    metadata = d.get('metadata', {})
    repos = metadata.get('repositories', [])
    repo_escaped = html_mod.escape(repo)
    if repos:
        current = max(repos, key=lambda r: r.get('index', 0))
        archs_data = current.get('archs', {})
        # Format update timestamp from the most recent arch
        ts_str = ''
        ts = max(
            (info.get('timestamp', '') for info in archs_data.values()),
            default='')
        if ts:
            try:
                dt = datetime.fromisoformat(ts.replace('Z', '+00:00'))
                local_dt = dt.astimezone()
                ts_str = local_dt.strftime(' (%Y-%m-%d %H:%M)')
            except Exception:
                pass
        print(f'<b>Recent packages in {repo_escaped}{ts_str}:</b><br>')
        stats_parts = []
        for arch in ['src', 'x86_64', 'aarch64', 'i586']:
            info = archs_data.get(arch)
            if info:
                count_pkgs = info.get('packages', 0)
                size_gb = info.get('size', 0) / (1024 ** 3)
                label = 'SRPMS' if arch == 'src' else arch
                stats_parts.append(
                    f'<a href="/en/rpms/{repo_escaped}/">'
                    f'{label}</a>({count_pkgs};{size_gb:.2f} GB)')
        if stats_parts:
            print(' '.join(stats_parts) + ' <br>')
    else:
        print(f'<b>Recent packages in {repo_escaped}:</b><br>')

    for pkg in d.get('packages', []):
        name = html_mod.escape(pkg.get('name', ''))
        ver = html_mod.escape(
            pkg.get('version', '') + '-' + pkg.get('release', ''))
        summary = html_mod.escape(pkg.get('summary', ''), quote=True)
        repo_escaped = html_mod.escape(repo)

        # Format date as MM/DD or MM/DD/YYYY if not current year
        date_str = ''
        try:
            dt = datetime.fromisoformat(
                pkg['buildtime'].replace('Z', '+00:00'))
            if dt.year != datetime.now().year:
                date_str = dt.strftime('%Y/%m/%d')
            else:
                date_str = dt.strftime('%m/%d')
        except Exception:
            pass

        # Build icon and tooltip from changelog and/or updates
        tip_lines = []
        changelog = pkg.get('changelog', [])
        for entry in changelog:
            try:
                edt = datetime.fromisoformat(
                    entry['date'].replace('Z', '+00:00'))
                edate = edt.strftime('%a %b %d %Y')
            except Exception:
                edate = ''
            packager = entry.get('packager', '')
            erelease = entry.get('release', '')
            etext = entry.get('text', '')
            tip_lines.append(
                f"{edate} - {packager} ({erelease})")
            tip_lines.append(etext)
        children = pkg.get('children', {}).get('archs', {}) or {}
        archs = [a for a in children if a != 'noarch']
        if archs:
            tip_lines.append(f"\nArchs: {' '.join(archs)}")
        updates = pkg.get('updates', [])
        has_updates = bool(updates)
        if has_updates:
            for u in updates:
                urepo = u.get('repository', '')
                uver = u.get('version', '')
                urel = u.get('release', '')
                tip_lines.append(
                    f"\nUpdates {pkg.get('name', '')}"
                    f"({urepo},src,0:{uver}-{urel})")
        tip = html_mod.escape('\n'.join(tip_lines), quote=True)
        icon = '\u2B06\uFE0F' if has_updates else '\U0001F4E6'
        print(f'<span class="tip" data-tip="{tip}">{icon}</span>', end='')

        # Build warnings tooltip if available
        problems = pkg.get('problems', {})
        warnings = problems.get('warnings', [])
        needrebuild = problems.get('needrebuild', [])
        warn_html = ''
        onlyin_html = ''
        obsoletes_html = ''
        warn_lines = []
        onlyin_lines = []
        obsoletes_lines = []
        def recent_warn_order(w):
            text = w.get('text', '')
            if 'need to be rebuilt' in text:
                return 0
            return 1
        for w in sorted(warnings, key=recent_warn_order):
            text = html_mod.escape(w.get('text', ''), quote=True)
            if w.get('arch', 'src') != 'src':
                wname = html_mod.escape(w.get('name', ''), quote=True)
                warch = html_mod.escape(w.get('arch', ''), quote=True)
                wrepo = html_mod.escape(w.get('repository', repo), quote=True)
                text = f'{wname}({warch},{wrepo}): {text}'
            if 'which is only in this repository' in w.get('text', ''):
                onlyin_lines.append(f'&nbsp;&bull;&nbsp;{text}')
            elif ' obsoletes ' in w.get('text', ''):
                obsoletes_lines.append(f'&nbsp;&bull;&nbsp;{text}')
            else:
                warn_lines.append(f'&nbsp;&bull;&nbsp;{text}')
        if warn_lines:
            warn_tip = '\n'.join(warn_lines).replace('"', '&quot;')
            warn_html = (f'<span class="tip" data-tip="{warn_tip}">'
                         f'\u26A0\uFE0F</span>')
        if onlyin_lines:
            onlyin_tip = '\n'.join(onlyin_lines).replace('"', '&quot;')
            onlyin_html = (f'<span class="tip" data-tip="{onlyin_tip}">'
                           f'\U0001F517</span>')
        if obsoletes_lines:
            obsoletes_tip = '\n'.join(obsoletes_lines).replace('"', '&quot;')
            obsoletes_html = (f'<span class="tip" data-tip="{obsoletes_tip}">'
                              f'⚰️</span>')

        needrebuild_html = ''
        needrebuild_lines = []
        for entry in needrebuild:
            seen_pkgs = set()
            unique_torebuild = []
            for p in entry.get('torebuild', []):
                key = (p['name'], p.get('arch', ''), p.get('repository', repo))
                if key not in seen_pkgs:
                    seen_pkgs.add(key)
                    unique_torebuild.append(p)
            pkgs_str = ' '.join(
                f'{html_mod.escape(p["name"], quote=True)}'
                f'({html_mod.escape(p.get("arch", ""), quote=True)}'
                f',{html_mod.escape(p.get("repository", repo), quote=True)})'
                for p in unique_torebuild
            )
            if pkgs_str:
                needrebuild_lines.append(f'&nbsp;&bull;&nbsp;need to be rebuilt: {pkgs_str}')
        if needrebuild_lines:
            needrebuild_tip = '\n'.join(needrebuild_lines).replace('"', '&quot;')
            needrebuild_html = (f'<span class="tip" data-tip="{needrebuild_tip}">'
                                f'\u26D4</span>')

        needport_raw = pkg.get('problems', {}).get('needport', [])
        needport_archs = list(dict.fromkeys(
            entry['arch']
            for entry in needport_raw
            if entry.get('arch')
        ))
        needport_html = ''
        if needport_archs:
            archs_str = html_mod.escape(' '.join(needport_archs), quote=True)
            needport_tip = f'requires port to arch(s): {archs_str}'
            needport_html = (f'<span class="tip" data-tip="{needport_tip}">'
                             f'\u23F3</span>')

        print(f'<a href="/en/rpms/{repo_escaped}/{name}/"'
              f' title="{summary}">'
              f'{date_str} {name} {ver}</a>{needrebuild_html}{warn_html}{obsoletes_html}{onlyin_html}{needport_html}<br>')


def cmd_repository_problems(repo, arch):
    """Query GET /problems/{repo} and output package problem lists.

    Filters needport[].archs entries matching arch and outputs the
    corresponding source package names as a bash array.

    Outputs:
      needport_list   (array of source package names needing porting to arch)
      warnings_list   (array of source package names that have warnings)
      needrebuild_list (array of source package names that need rebuild)
    """
    api_url = get_api_url()

    d = fetch_json(f"{api_url}/problems/{repo}")
    if d is None:
        print(f"ERROR: failed to fetch problems from API for {repo}", file=sys.stderr)
        sys.exit(2)

    pkgs = [
        entry['source']
        for entry in d.get('needport', [])
        if arch in entry.get('archs', [])
    ]
    print('needport_list=(' + ' '.join(shlex.quote(p) for p in pkgs) + ')')

    warned = list(dict.fromkeys(
        name
        for w in d.get('warnings', [])
        for name in ([w['name']] if w.get('name') else []) +
                    ([w['srcname']] if w.get('srcname') else [])
    ))
    print('warnings_list=(' + ' '.join(shlex.quote(p) for p in warned) + ')')

    rebuild = list(dict.fromkeys(
        p['name']
        for entry in d.get('needrebuild', [])
        for p in entry.get('torebuild', [])
        if p.get('name')
    ))
    print('needrebuild_list=(' + ' '.join(shlex.quote(p) for p in rebuild) + ')')


def cmd_package_needrebuild(repo, pkg, arch):
    """Query GET /package/{repo}/{pkg}?add=problems and output pkg_needrebuild.

    Uses the per-package problems data to avoid fetching the full repository
    problems list. Collects torebuild entries matching the given arch,
    formatted as "name@provider" tokens (compatible with the pkg_needrebuild
    array format consumed by autoport).
    Outputs an empty array when no needrebuild entry is found for the package.

    Outputs: pkg_needrebuild (array of name@provider tokens).
    """
    api_url = get_api_url()

    d = fetch_json(f"{api_url}/package/{repo}/{pkg}?add=problems")
    if d is None:
        print(f"ERROR: failed to fetch package data from API for {pkg} in {repo}", file=sys.stderr)
        sys.exit(2)

    needrebuild = []
    for entry in d.get('problems', {}).get('needrebuild', []):
        for item in entry.get('torebuild', []):
            if item.get('arch', '') == arch:
                needrebuild.append(f"{item['name']}@{item.get('provider', pkg)}")

    print('pkg_needrebuild=(' + ' '.join(shlex.quote(t) for t in needrebuild) + ')')


def cmd_package_binary(repo, pkg, arch):
    """Query GET /package/{repo}/{pkg}/{arch} for a binary package and output
    bash variable assignments.

    Outputs: pkg_name, pkg_version, pkg_release, pkg_size, pkg_repository,
    pkg_arch. All variables are set to empty string when the package is not
    found so callers can test [ "$pkg_name" ] for existence.
    """
    api_url = get_api_url()

    d = fetch_json(f"{api_url}/package/{repo}/{pkg}/{arch}")
    if d is None:
        print(f"ERROR: failed to fetch binary package data from API for {pkg}/{arch} in {repo}", file=sys.stderr)
        sys.exit(2)
    if 'error' in d or not d.get('name'):
        print('pkg_name=')
        print('pkg_version=')
        print('pkg_release=')
        print('pkg_size=')
        print('pkg_repository=')
        print('pkg_arch=')
        sys.exit(0)

    print('pkg_name='      + shlex.quote(d.get('name', '')))
    print('pkg_version='   + shlex.quote(d.get('version', '')))
    print('pkg_release='   + shlex.quote(d.get('release', '')))
    print('pkg_size='      + shlex.quote(str(d.get('size', ''))))
    print('pkg_repository='+ shlex.quote(d.get('repository', repo)))
    print('pkg_arch='      + shlex.quote(d.get('arch', arch)))


def cmd_repository_srclist(repo, pkg=None):
    """Query GET /repository/{repo} and output one line per source package:
    name version buildtime repository epoch release.

    Uses add=parents to include packages from parent repositories, so the
    output mirrors the srcpkglist behaviour of returning the most up-to-date
    version of every package visible in the repository hierarchy.
    The repository field reflects which repository the package actually
    belongs to (as returned by the API).

    If pkg is specified, only the matching package is returned.

    Intended for use in shell while-read loops that need per-package
    name/version/buildtime/repository/epoch/release without making individual
    API calls per package.
    """
    api_url = get_api_url()

    d = fetch_json(f"{api_url}/repository/{repo}?per_page=100000&page=1&arch=src&add=parents")
    if d is None:
        print(f"ERROR: failed to fetch repository data from API for {repo}", file=sys.stderr)
        sys.exit(2)
    if 'error' in d:
        sys.exit(0)

    for p in d.get('packages', []):
        if p.get('arch') == 'src':
            name = p.get('name', '')
            if pkg and name != pkg:
                continue
            bt = p.get('buildtime')
            if bt:
                from datetime import datetime, timezone
                builddate = datetime.strptime(bt, '%Y-%m-%dT%H:%M:%SZ').strftime('%Y%m%d')
            else:
                builddate = '0'
            print(name, p.get('repository', repo), p.get('epoch', ''),
                  p.get('version', ''), p.get('release', ''), builddate)


def cmd_repository_log(repo):
    """Query GET /problems/{repo} and output an HTML log compatible with
    the distromatic.log section used by webbuild.
    """
    import html as html_mod
    from datetime import datetime

    api_url = get_api_url()

    d = fetch_json(f"{api_url}/problems/{repo}")
    if d is None:
        print(f"ERROR: failed to fetch problems from API for {repo}",
              file=sys.stderr)
        sys.exit(2)

    def warn_item(name, arch, wrepo, text, srcname=None):
        n = html_mod.escape(name)
        a = html_mod.escape(arch)
        r = html_mod.escape(wrepo)
        t = html_mod.escape(text)
        if srcname and srcname != name:
            src_label = f'{html_mod.escape(srcname)}({a},{r}): '
        else:
            src_label = ''
        return (f'<li><b>{n}</b>({a},{r}): '
                f'{src_label}<span style="color:#c00">{t}</span></li>')

    warnings = d.get('warnings', [])

    def src_warn_order(w):
        text = w.get('text', '')
        if 'is older or same release' in text:
            return (0, 0)
        if 'missing build provider' in text:
            return (1, 0)
        if 'requires port to' in text:
            return (2, 0)
        return (3, 0)

    def bin_warn_order(w):
        text = w.get('text', '')
        if text.startswith('missing SRPM'):
            return 0
        if text.startswith('missing provider'):
            return 1
        return 2

    src_warnings = sorted(
        [w for w in warnings if w.get('arch') == 'src'],
        key=src_warn_order)
    bin_warnings = sorted(
        [w for w in warnings if w.get('arch') != 'src'],
        key=bin_warn_order)

    print(f'<b>Package warnings in {html_mod.escape(repo)}:</b>')
    if src_warnings or bin_warnings:
        print('<ul style="margin:2px 0;padding-left:20px">')
        prev_order = None
        for w in src_warnings:
            cur_order = src_warn_order(w)
            if prev_order is not None and cur_order != prev_order:
                print('<hr style="margin:4px 0">')
            prev_order = cur_order
            print(warn_item(w.get('name', ''), w.get('arch', ''),
                            w.get('repository', repo), w.get('text', ''),
                            srcname=w.get('srcname', '')))
        if src_warnings and bin_warnings:
            print('<hr style="margin:4px 0">')
        # Group bin_warnings by source package (srcname when available,
        # otherwise name), preserving bin_warn_order within each group.
        from itertools import groupby as _groupby

        def bin_src_key(w):
            return (w.get('srcname') or w.get('name', ''),
                    w.get('repository', repo))

        bin_sorted = sorted(bin_warnings,
                            key=lambda w: (bin_src_key(w), bin_warn_order(w)))
        for grp_key, grp_iter in _groupby(bin_sorted, key=bin_src_key):
            grp = list(grp_iter)
            src, wrep = grp_key
            print(f'<li style="list-style:none;margin-left:-13px"><details open>'
                  f'<summary><b>{html_mod.escape(src)}</b>'
                  f'(src,{html_mod.escape(wrep)})</summary>')
            print('<ul style="margin:2px 0;padding-left:20px">')
            for w in grp:
                wname = html_mod.escape(w.get('name', ''))
                warch = html_mod.escape(w.get('arch', ''))
                wr = html_mod.escape(w.get('repository', repo))
                t = html_mod.escape(w.get('text', ''))
                print(f'<li>{wname}({warch},{wr}): '
                      f'<span style="color:#c00">{t}</span></li>')
            print('</ul></details></li>')
        print('</ul>')
    else:
        print(' <b style="color:green">no problems found.</b><br>')

    needrebuild = d.get('needrebuild', [])
    if needrebuild:
        print(f'<br><hr style="margin:4px 0"><br>')
        print(f'<b>Need to be rebuilt:</b>')
        print('<ul style="margin:2px 0;padding-left:20px">')
        for entry in sorted(needrebuild, key=lambda e: e.get('source', '')):
            source = html_mod.escape(entry.get('source', ''))
            seen_pkgs = set()
            unique_torebuild = []
            for p in entry.get('torebuild', []):
                key = (p['name'], p.get('arch', ''), p.get('repository', repo))
                if key not in seen_pkgs:
                    seen_pkgs.add(key)
                    unique_torebuild.append(p)
            pkgs = ' '.join(
                f'{html_mod.escape(p["name"])}'
                f'({html_mod.escape(p["arch"])}'
                f',{html_mod.escape(p.get("repository", repo))})'
                for p in unique_torebuild
            )
            print(f'<li><b>{source}</b>(src,{html_mod.escape(repo)}): '
                  f'<span style="color:#c00">need to be rebuilt: {pkgs}</span></li>')
        print('</ul>')

    needport = d.get('needport', [])
    if needport:
        print(f'<br><hr style="margin:4px 0"><br>')
        print(f'<b>Need porting to other archs:</b>')
        print('<ul style="margin:2px 0;padding-left:20px">')
        for entry in sorted(needport, key=lambda e: e.get('source', '')):
            source = html_mod.escape(entry.get('source', ''))
            archs = ', '.join(html_mod.escape(a) for a in entry.get('archs', []))
            print(f'<li><b>{source}</b>(src,{html_mod.escape(repo)}): '
                  f'<span style="color:#c00">requires port to arch(s): {archs}</span></li>')
        print('</ul>')


def cmd_provide_sources(repo, provide, arch=''):
    """Query GET /providers/{repo}/{provide}[/{arch}] and print source package names.

    When arch is empty the endpoint is queried without an arch suffix so results
    from all architectures are returned — this is needed when the provide is not
    yet available for the target arch (e.g. during a port).

    Slashes in provide are double-encoded (%252F) so the server receives a literal
    %2F in the path segment rather than treating it as a path separator.

    Outputs one source package name per line (plain text, not bash assignments).
    Duplicate entries are suppressed.
    """
    import urllib.parse
    api_url = get_api_url()

    # Double-encode slashes (%2F → %252F) so the server receives a literal %2F
    # in the path segment rather than treating it as a path separator.
    encoded = urllib.parse.quote(provide, safe='').replace('%2F', '%252F')
    url = f"{api_url}/providers/{repo}/{encoded}/{arch}" if arch else f"{api_url}/providers/{repo}/{encoded}"

    d = fetch_json(url)
    if d is None:
        print(f"ERROR: failed to fetch providers from API for {provide!r} in {repo}",
              file=sys.stderr)
        sys.exit(2)
    if isinstance(d, dict) and 'error' in d:
        sys.exit(0)

    seen = set()

    def emit(pkg):
        if isinstance(pkg, dict):
            name = pkg.get('source') or pkg.get('srcname') or pkg.get('name', '')
        else:
            name = str(pkg)
        if name and name not in seen:
            seen.add(name)
            print(name)

    if isinstance(d, list):
        for pkg in d:
            emit(pkg)
    else:
        providers = d.get('providers', d)
        if isinstance(providers, dict) and 'archs' in providers:
            for pkg_list in providers['archs'].values():
                for pkg in pkg_list:
                    emit(pkg)
        else:
            for pkg in (providers if isinstance(providers, list) else []):
                emit(pkg)


def cmd_social_log_get(from_id=None, limit=100, target=None):
    """Query GET /social_log and output bash variable assignments.

    Outputs: social_log_entries (array of '|'-separated fields per entry:
    id|privacy|user|type|target|email|text|time).
    """
    api_url = get_api_url()
    token = get_social_token()

    params = [f"limit={limit}", "order=asc"]
    if from_id:
        params.append(f"from_id={from_id}")
    if target:
        params.append(f"target={target}")
    url = f"{api_url}/social_log?{'&'.join(params)}"

    d = fetch_json_auth(url, token)
    if d is None:
        sys.exit(2)
    if 'error' in d:
        print(f"ERROR: {d['error']}", file=sys.stderr)
        sys.exit(2)

    from datetime import datetime, timezone

    entries = d if isinstance(d, list) else d.get('entries', [])
    out = []
    for e in entries:
        ts = e.get('time', '')
        try:
            dt = datetime.fromisoformat(ts.replace('Z', '+00:00'))
            unix_ts = int(dt.timestamp())
        except Exception:
            unix_ts = ''
        fields = '|'.join([
            str(e.get('id', '')),
            str(e.get('privacy', '')),
            str(e.get('user', '')),
            str(e.get('type', '')),
            str(e.get('target', '')),
            str(e.get('email', '')),
            str(e.get('text', '')),
            str(unix_ts),
        ])
        out.append(shlex.quote(fields))
    print('social_log_entries=(' + ' '.join(out) + ')')


def cmd_social_log_post(user, text, stype='', target='', privacy='', email=''):
    """POST /social_log to add a new entry."""
    api_url = get_api_url()
    token = get_social_token()

    payload = {'user': user, 'text': text}
    if stype:
        payload['type'] = stype
    if target:
        payload['target'] = target
    if privacy:
        payload['privacy'] = privacy.lower() == 'true' if isinstance(privacy, str) else bool(privacy)
    if email:
        payload['email'] = email

    d = fetch_json_auth(f"{api_url}/social_log", token, method='POST', data=payload)
    if d is None:
        sys.exit(2)
    if 'error' in d:
        print(f"ERROR: {d['error']}", file=sys.stderr)
        sys.exit(2)


def _search_fetch(query, per_page, page, sort, order, arch):
    """Shared fetch for cmd_search / cmd_search_html. Returns the parsed JSON dict."""
    import urllib.parse
    api_url = get_api_url()
    params = {'q': query, 'per_page': str(per_page), 'page': str(page),
              'sort': sort, 'order': order}
    if arch:
        params['arch'] = arch
    url = f"{api_url}/search?{urllib.parse.urlencode(params)}"
    return fetch_json(url)


def _search_group(packages):
    """Group API package list by (repo, name, version, release), collecting archs."""
    groups = {}
    group_order = []
    for pkg in packages:
        key = (pkg.get('repository', ''), pkg.get('name', ''),
               pkg.get('version', ''), pkg.get('release', ''))
        if key not in groups:
            groups[key] = {'archs': [], 'summary': pkg.get('summary', '')}
            group_order.append(key)
        groups[key]['archs'].append(pkg.get('arch', ''))
    return group_order, groups


def cmd_search(query, per_page=100, page=1, sort='name', order='asc', arch=''):
    """Query GET /search and output plain text results."""
    d = _search_fetch(query, per_page, page, sort, order, arch)
    if d is None:
        print("ERROR: failed to fetch search results from API", file=sys.stderr)
        sys.exit(2)
    if 'error' in d:
        print(f"ERROR: {d['error']}", file=sys.stderr)
        sys.exit(2)

    api_url = get_api_url()
    query_info = d.get('query', {})
    total = query_info.get('total', 0)
    print(f"Search results for '{query}': {total} package(s) found")

    group_order, groups = _search_group(d.get('packages', []))
    for key in group_order:
        repo, name, version, release = key
        g = groups[key]
        archs = ' '.join(g['archs'])
        print(f"{repo}  {name}-{version}-{release}  [{archs}]  {g['summary']}")
        if 'src' in g['archs']:
            rd = fetch_json(f"{api_url}/package/{repo}/{name}"
                            f"?add=requiredby,buildrequiredby,recommendedby")
            if rd:
                for field in ('buildrequiredby', 'requiredby', 'recommendedby'):
                    items = list(rd.get(field) or [])
                    if items:
                        shown = items[:10]
                        names = ', '.join(
                            p['name'] if isinstance(p, dict) else str(p)
                            for p in shown)
                        if len(items) > 10:
                            names += ',…'
                        print(f"  {field}({len(items)}): {names}")


def cmd_search_html(query, per_page=100, page=1, sort='name', order='asc', arch=''):
    """Query GET /search and output HTML results for the webbuild interface."""
    import html as html_mod

    d = _search_fetch(query, per_page, page, sort, order, arch)
    if d is None:
        print("<b style='color:red'>ERROR: failed to fetch search results from API</b>")
        return
    if 'error' in d:
        print(f"<b style='color:red'>ERROR: {html_mod.escape(d['error'])}</b>")
        return

    config = read_config()
    site_base_url = config.get('SITE_BASE_URL', '')

    query_info = d.get('query', {})
    total = query_info.get('total', 0)
    pages = query_info.get('pages', 1)
    from_n = query_info.get('from', 1)
    to_n = query_info.get('to', len(d.get('packages', [])))
    query_esc = html_mod.escape(query)

    print(f'<b>Search results for &quot;{query_esc}&quot;: {total} package(s) found</b>')
    if total == 0:
        return
    if pages > 1:
        print(f'<small>(showing {from_n}-{to_n} of {total})</small>')

    api_url = get_api_url()
    print('<br>')
    for pkg in d.get('packages', []):
        repo = pkg.get('repository', '')
        name = pkg.get('name', '')
        version = pkg.get('version', '')
        release = pkg.get('release', '')
        arch = pkg.get('arch', '')
        summary = html_mod.escape(pkg.get('summary', ''), quote=True)
        name_esc = html_mod.escape(name)
        repo_esc = html_mod.escape(repo)
        ver_esc = html_mod.escape(f"{version}-{release}")
        arch_esc = html_mod.escape(arch)
        if arch == 'src':
            href = f"{site_base_url}/en/rpms/{repo_esc}/{name_esc}/"
            print(f'<b><a href="{href}" title="{summary}" target="_blank">'
                  f'{name_esc}-{ver_esc}</a>'
                  f' ({arch_esc}, {repo_esc})</b><br>')
            rd = fetch_json(f"{api_url}/package/{repo}/{name}"
                            f"?add=requiredby,buildrequiredby,recommendedby")
            if rd:
                for field in ('buildrequiredby', 'requiredby', 'recommendedby'):
                    items = list(rd.get(field) or [])
                    if items:
                        shown = items[:10]
                        names_html = ', '.join(
                            html_mod.escape(
                                p['name'] if isinstance(p, dict) else str(p))
                            for p in shown)
                        if len(items) > 10:
                            names_html += ',&hellip;'
                        field_esc = html_mod.escape(field)
                        print(f'&nbsp;&nbsp;<b>{field_esc}({len(items)})</b>:'
                              f' {names_html}<br>')
        else:
            href = f"{site_base_url}/en/rpms/{repo_esc}/{name_esc}/{arch_esc}/"
            print(f'<a href="{href}" title="{summary}" target="_blank">'
                  f'{name_esc}-{ver_esc}</a>'
                  f' <small>({arch_esc}, {repo_esc})</small><br>')


def usage():
    print("Usage:", file=sys.stderr)
    print("  autodist-distroquery package REPO PKG [ARCH]", file=sys.stderr)
    print("  autodist-distroquery package-all-archs REPO PKG", file=sys.stderr)
    print("  autodist-distroquery package-binary REPO PKG ARCH", file=sys.stderr)
    print("  autodist-distroquery package-needrebuild REPO PKG ARCH", file=sys.stderr)
    print("  autodist-distroquery provide-sources REPO PROVIDE [ARCH]", file=sys.stderr)
    print("  autodist-distroquery repository-problems REPO ARCH", file=sys.stderr)
    print("  autodist-distroquery repository REPO [SORT [ORDER]]", file=sys.stderr)
    print("  autodist-distroquery repository-srclist REPO [PKG]", file=sys.stderr)
    print("  autodist-distroquery recent-html REPO [COUNT]", file=sys.stderr)
    print("  autodist-distroquery repository-log REPO", file=sys.stderr)
    print("  autodist-distroquery search QUERY [PER_PAGE [PAGE [SORT [ORDER [ARCH]]]]]", file=sys.stderr)
    print("  autodist-distroquery search-html QUERY [PER_PAGE [PAGE [SORT [ORDER [ARCH]]]]]", file=sys.stderr)
    print("  autodist-distroquery social-log get [FROM_ID [LIMIT [TARGET]]]", file=sys.stderr)
    print("  autodist-distroquery social-log post USER TEXT [TYPE [TARGET [PRIVACY [EMAIL]]]]", file=sys.stderr)
    sys.exit(1)


if len(sys.argv) < 2:
    usage()

command = sys.argv[1]

if command == 'package':
    if len(sys.argv) < 4:
        usage()
    repo      = sys.argv[2]
    pkg       = sys.argv[3]
    buildarch = sys.argv[4] if len(sys.argv) > 4 else ''
    cmd_package(repo, pkg, buildarch)
elif command == 'package-all-archs':
    if len(sys.argv) < 4:
        usage()
    repo = sys.argv[2]
    pkg  = sys.argv[3]
    cmd_package_all_archs(repo, pkg)
elif command == 'repository':
    if len(sys.argv) < 3:
        usage()
    repo  = sys.argv[2]
    sort  = sys.argv[3] if len(sys.argv) > 3 else ''
    order = sys.argv[4] if len(sys.argv) > 4 else ''
    cmd_repository(repo, sort, order)
elif command == 'recent-html':
    if len(sys.argv) < 3:
        usage()
    repo = sys.argv[2]
    count = int(sys.argv[3]) if len(sys.argv) > 3 else 400
    cmd_recent_html(repo, count)
elif command == 'repository-problems':
    if len(sys.argv) < 4:
        usage()
    repo = sys.argv[2]
    arch = sys.argv[3]
    cmd_repository_problems(repo, arch)
elif command == 'package-needrebuild':
    if len(sys.argv) < 5:
        usage()
    repo = sys.argv[2]
    pkg  = sys.argv[3]
    arch = sys.argv[4]
    cmd_package_needrebuild(repo, pkg, arch)
elif command == 'package-binary':
    if len(sys.argv) < 5:
        usage()
    repo = sys.argv[2]
    pkg  = sys.argv[3]
    arch = sys.argv[4]
    cmd_package_binary(repo, pkg, arch)
elif command == 'provide-sources':
    if len(sys.argv) < 4:
        usage()
    repo    = sys.argv[2]
    provide = sys.argv[3]
    arch    = sys.argv[4] if len(sys.argv) > 4 else ''
    cmd_provide_sources(repo, provide, arch)
elif command == 'repository-srclist':
    if len(sys.argv) < 3:
        usage()
    repo = sys.argv[2]
    pkg  = sys.argv[3] if len(sys.argv) > 3 else None
    cmd_repository_srclist(repo, pkg)
elif command == 'repository-log':
    if len(sys.argv) < 3:
        usage()
    repo = sys.argv[2]
    cmd_repository_log(repo)
elif command in ('search', 'search-html'):
    if len(sys.argv) < 3:
        usage()
    query    = sys.argv[2]
    per_page = int(sys.argv[3]) if len(sys.argv) > 3 else 100
    page     = int(sys.argv[4]) if len(sys.argv) > 4 else 1
    sort     = sys.argv[5] if len(sys.argv) > 5 else 'name'
    order    = sys.argv[6] if len(sys.argv) > 6 else 'asc'
    arch     = sys.argv[7] if len(sys.argv) > 7 else ''
    if command == 'search':
        cmd_search(query, per_page, page, sort, order, arch)
    else:
        cmd_search_html(query, per_page, page, sort, order, arch)
elif command == 'social-log':
    if len(sys.argv) < 3:
        usage()
    subcommand = sys.argv[2]
    if subcommand == 'get':
        from_id = sys.argv[3] if len(sys.argv) > 3 else None
        limit   = int(sys.argv[4]) if len(sys.argv) > 4 else 100
        target  = sys.argv[5] if len(sys.argv) > 5 else None
        cmd_social_log_get(from_id, limit, target)
    elif subcommand == 'post':
        if len(sys.argv) < 5:
            usage()
        user    = sys.argv[3]
        text    = sys.argv[4]
        stype   = sys.argv[5] if len(sys.argv) > 5 else ''
        target  = sys.argv[6] if len(sys.argv) > 6 else ''
        privacy = sys.argv[7] if len(sys.argv) > 7 else ''
        email   = sys.argv[8] if len(sys.argv) > 8 else ''
        cmd_social_log_post(user, text, stype, target, privacy, email)
    else:
        print(f"Unknown social-log subcommand: {subcommand}", file=sys.stderr)
        usage()
else:
    print(f"Unknown command: {command}", file=sys.stderr)
    usage()
