<?php
declare(strict_types=1);

/**
 * Generate a Gentoo installed-package USE flag report.
 *
 * This is a PHP port of gentoo_useflag_report.py. It reads installed metadata
 * from /var/db/pkg and explicit requests from /etc/portage/package.use*.
 * The output is analysis-only; it does not modify Portage configuration.
 */

const DEPENDENCY_FIELDS = ['DEPEND', 'RDEPEND', 'PDEPEND', 'BDEPEND', 'IDEPEND'];

const USE_FLAGS_WITHOUT_REMOVE_PROPOSALS = [
    'wayland' => 'manual skip: keep out of automatic removal proposals',
    'zlib' => 'manual skip: keep out of automatic removal proposals',
    'readline' => 'manual skip: keep out of automatic removal proposals',
    'bzip2' => 'manual skip: keep out of automatic removal proposals',
    'openmp' => 'manual skip: 2026-05-27 keep out of automatic removal proposals',
    'cxx' => 'manual skip: 2026-05-27 keep out of automatic removal proposals',
    'zstd' => 'manual skip: 2026-05-27 keep out of automatic removal proposals',
    'unicode' => 'manual skip: 2026-05-27 keep out of automatic removal proposals',
    'xattr' => 'manual skip: 2026-05-27 keep out of automatic removal proposals',
    'asm' => 'manual skip: 2026-05-27 keep out of automatic removal proposals',
    'cet' => 'manual skip: 2026-05-27 keep out of automatic removal proposals',
    'ncurses' => 'manual skip: 2026-05-27 keep out of automatic removal proposals',
    'png' => 'manual skip: 2026-05-27 keep out of automatic removal proposals',
    'jpeg' => 'manual skip: 2026-05-27 keep out of automatic removal proposals',
];

const PROTECTED_FLAG_PATTERNS = [
    '/^abi_/',
    '/^amd64$/',
    '/^x86$/',
    '/^arm64$/',
    '/^ppc/',
    '/^cpu_flags_/',
    '/^input_devices_/',
    '/^video_cards_/',
    '/^python_targets_/',
    '/^python_single_target_/',
    '/^php_targets_/',
    '/^ruby_targets_/',
    '/^lua_targets_/',
    '/^l10n_/',
    '/^linguas_/',
    '/^kernel_/',
    '/^elibc_/',
    '/^userland_/',
    '/^pwphpbytecode_abi_/',
];

const DEFAULT_NOT_DISABLED_FLAGS = [
    'acl' => 'basic filesystem/security behavior; review manually before global disable',
    'amd64' => 'architecture pseudo-flag from installed metadata',
    'caps' => 'basic privilege/security behavior; review manually before global disable',
    'crypt' => 'common crypto support; review manually before global disable',
    'dbus' => 'desktop/session integration; review manually before global disable',
    'elogind' => 'session/login integration; review manually before global disable',
    'gnome' => 'desktop stack marker; review manually before global disable',
    'gtk' => 'desktop UI stack; review manually before global disable',
    'introspection' => 'GNOME/GObject integration; review manually before global disable',
    'ipv6' => 'network baseline; review manually before global disable',
    'nls' => 'localization baseline; review manually before global disable',
    'pam' => 'login/security integration; review manually before global disable',
    'policykit' => 'desktop authorization integration; review manually before global disable',
    'ssl' => 'TLS baseline; review manually before global disable',
    'systemd' => 'system integration; review manually before global disable',
    'udev' => 'device management baseline; review manually before global disable',
    'X' => 'desktop/X11 baseline; review manually before global disable',
];

function read_text(string $path): string
{
    if (!is_file($path)) {
        return '';
    }
    $text = file_get_contents($path);
    return trim($text === false ? '' : $text);
}

function split_flags(string $text): array
{
    $flags = [];
    foreach (preg_split('/\s+/', trim($text)) ?: [] as $token) {
        $token = trim($token);
        if ($token === '' || str_starts_with($token, '-')) {
            continue;
        }
        $flags[] = ltrim($token, '+');
    }
    return $flags;
}

function parse_iuse_defaults(string $text): array
{
    $defaults = [];
    foreach (preg_split('/\s+/', trim($text)) ?: [] as $token) {
        if (str_starts_with($token, '+') && strlen($token) > 1) {
            $defaults[substr($token, 1)] = true;
        }
    }
    return array_keys($defaults);
}

function parse_iuse_flags(string $text): array
{
    $flags = [];
    foreach (preg_split('/\s+/', trim($text)) ?: [] as $token) {
        $token = trim($token);
        if ($token === '') {
            continue;
        }
        $flags[ltrim($token, '+-')] = true;
    }
    return array_keys($flags);
}

function package_name_from_pf(string $pf): string
{
    $versionRe = '/^(?P<pn>.+)-(?P<ver>(?:\d+)(?:\.\d+)*(?:[a-z]?)(?:_(?:alpha|beta|pre|rc|p)\d*)*(?:-r\d+)?)$/';
    if (preg_match($versionRe, $pf, $match)) {
        return $match['pn'];
    }
    if (preg_match('/^(.+)-\d/', $pf, $match)) {
        return $match[1];
    }
    return $pf;
}

function sorted_dir_children(string $path): array
{
    if (!is_dir($path)) {
        return [];
    }
    $items = array_values(array_filter(scandir($path) ?: [], static fn(string $name): bool => $name !== '.' && $name !== '..'));
    usort($items, 'strcasecmp');
    return array_map(static fn(string $name): string => $path . DIRECTORY_SEPARATOR . $name, $items);
}

function set_union(array ...$sets): array
{
    $out = [];
    foreach ($sets as $set) {
        foreach ($set as $item) {
            $out[(string)$item] = true;
        }
    }
    return array_keys($out);
}

function iter_installed(string $varDbPkg, bool $includeWithoutUse = false): array
{
    $packages = [];
    foreach (sorted_dir_children($varDbPkg) as $categoryDir) {
        if (!is_dir($categoryDir)) {
            continue;
        }
        $category = basename($categoryDir);
        foreach (sorted_dir_children($categoryDir) as $packageDir) {
            if (!is_dir($packageDir)) {
                continue;
            }
            $pf = read_text($packageDir . '/PF') ?: basename($packageDir);
            $categoryFile = read_text($packageDir . '/CATEGORY') ?: $category;
            $pn = package_name_from_pf($pf);
            $iuseText = read_text($packageDir . '/IUSE');
            $iuseEffectiveText = read_text($packageDir . '/IUSE_EFFECTIVE');
            $activeFlags = split_flags(read_text($packageDir . '/USE'));
            if (!$activeFlags && !$includeWithoutUse) {
                continue;
            }
            $dependencyTexts = [];
            foreach (DEPENDENCY_FIELDS as $field) {
                $dependencyTexts[$field] = read_text($packageDir . '/' . $field);
            }
            $packages[] = [
                'category' => $categoryFile,
                'pf' => $pf,
                'pn' => $pn,
                'cp' => $categoryFile . '/' . $pn,
                'cpv' => $categoryFile . '/' . $pf,
                'active_flags' => $activeFlags,
                'iuse_defaults' => set_union(parse_iuse_defaults($iuseText), parse_iuse_defaults($iuseEffectiveText)),
                'iuse_flags' => set_union(parse_iuse_flags($iuseText), parse_iuse_flags($iuseEffectiveText)),
                'dependency_texts' => $dependencyTexts,
                'required_use' => read_text($packageDir . '/REQUIRED_USE'),
            ];
        }
    }
    return $packages;
}

function recursive_files(string $dir): array
{
    if (!is_dir($dir)) {
        return [];
    }
    $files = [];
    $it = new RecursiveIteratorIterator(
        new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS),
        RecursiveIteratorIterator::SELF_FIRST
    );
    foreach ($it as $entry) {
        if ($entry->isFile()) {
            $files[] = $entry->getPathname();
        }
    }
    usort($files, 'strcasecmp');
    return $files;
}

function package_use_files(string $portageDir): array
{
    $candidates = [];
    foreach (['package.use', 'package.use.force', 'package.use.mask'] as $name) {
        $path = $portageDir . '/' . $name;
        if (is_file($path)) {
            $candidates[] = $path;
        } elseif (is_dir($path)) {
            $candidates = array_merge($candidates, recursive_files($path));
        }
    }
    return $candidates;
}

function cp_from_atom(string $atom): ?string
{
    $atom = trim($atom);
    if ($atom === '' || str_starts_with($atom, '#') || str_starts_with($atom, '*/*')) {
        return null;
    }
    $atom = preg_replace('/^(?:[<>=~]{1,2})/', '', $atom) ?? $atom;
    $atom = explode(':', $atom, 2)[0];
    $atom = explode('[', $atom, 2)[0];
    $atom = explode('::', $atom, 2)[0];
    if (!str_contains($atom, '/')) {
        return null;
    }
    [$category, $rest] = explode('/', $atom, 2);
    if ($category === '' || $rest === '') {
        return null;
    }
    return $category . '/' . package_name_from_pf($rest);
}

function expand_package_use_tokens(array $tokens): array
{
    $flags = [];
    $useExpandPrefix = null;
    foreach ($tokens as $token) {
        if (str_ends_with($token, ':')) {
            $useExpandPrefix = strtolower(substr($token, 0, -1)) . '_';
            continue;
        }
        if (str_starts_with($token, '-')) {
            continue;
        }
        $flag = ltrim($token, '+');
        if ($flag === '') {
            continue;
        }
        if ($useExpandPrefix !== null) {
            $flag = $useExpandPrefix . $flag;
        }
        $flags[] = $flag;
    }
    return $flags;
}

function relative_path(string $path, string $base): string
{
    $base = rtrim($base, '/') . '/';
    return str_starts_with($path, $base) ? substr($path, strlen($base)) : $path;
}

function parse_package_use(string $portageDir): array
{
    $explicit = [];
    $globalExplicit = [];
    foreach (package_use_files($portageDir) as $filePath) {
        $source = relative_path($filePath, $portageDir);
        $lines = file($filePath, FILE_IGNORE_NEW_LINES);
        if ($lines === false) {
            continue;
        }
        foreach ($lines as $idx => $raw) {
            $line = trim(explode('#', $raw, 2)[0]);
            if ($line === '') {
                continue;
            }
            $parts = preg_split('/\s+/', $line) ?: [];
            if (count($parts) < 2) {
                continue;
            }
            $flags = expand_package_use_tokens(array_slice($parts, 1));
            $detail = $source . ':' . ($idx + 1);
            if (trim($parts[0]) === '*/*') {
                foreach ($flags as $flag) {
                    $globalExplicit[$flag] ??= [];
                    if (!in_array($detail, $globalExplicit[$flag], true)) {
                        $globalExplicit[$flag][] = $detail;
                    }
                }
                continue;
            }
            $cp = cp_from_atom($parts[0]);
            if ($cp === null) {
                continue;
            }
            foreach ($flags as $flag) {
                $key = $cp . "\0" . $flag;
                $explicit[$key] ??= [];
                if (!in_array($detail, $explicit[$key], true)) {
                    $explicit[$key][] = $detail;
                }
            }
        }
    }
    return [$explicit, $globalExplicit];
}

function normalize_use_dep_token(string $token): ?array
{
    $raw = trim($token);
    if ($raw === '') {
        return null;
    }
    $raw = explode('(', $raw, 2)[0];
    if (str_starts_with($raw, '-') || str_starts_with($raw, '!')) {
        return null;
    }
    $kind = 'hard';
    if (str_ends_with($raw, '?')) {
        $kind = 'conditional';
        $raw = substr($raw, 0, -1);
    }
    if (str_ends_with($raw, '=')) {
        $kind = 'matching';
        $raw = substr($raw, 0, -1);
    }
    $flag = ltrim($raw, '+');
    if ($flag === '') {
        return null;
    }
    return ['flag' => $flag, 'kind' => $kind, 'raw' => trim($token)];
}

function parse_use_dep_atoms(string $text): array
{
    $items = [];
    $pattern = '/(?P<atom>(?:!?=?[<>=~]{0,2})?[A-Za-z0-9+_.-]+\/[A-Za-z0-9+_.-]+)(?::[A-Za-z0-9+_.-]+(?:\/[A-Za-z0-9+_.-]+)?)?\[(?P<uses>[^\]]+)\]/';
    if (!preg_match_all($pattern, $text, $matches, PREG_SET_ORDER)) {
        return $items;
    }
    foreach ($matches as $match) {
        $cp = cp_from_atom($match['atom']);
        if ($cp === null) {
            continue;
        }
        foreach (explode(',', $match['uses']) as $rawToken) {
            $token = normalize_use_dep_token($rawToken);
            if ($token !== null) {
                $items[] = [$cp, $token];
            }
        }
    }
    return $items;
}

function build_reverse_use_dependencies(array $installedPackages): array
{
    $reverse = [];
    foreach ($installedPackages as $depender) {
        foreach ($depender['dependency_texts'] as $field => $text) {
            if ($text === '') {
                continue;
            }
            foreach (parse_use_dep_atoms((string)$text) as [$targetCp, $token]) {
                $key = $targetCp . "\0" . $token['flag'];
                $reverse[$key] ??= [];
                $reverse[$key][] = [
                    'depender' => (string)$depender['cpv'],
                    'depender_cp' => (string)$depender['cp'],
                    'field' => (string)$field,
                    'kind' => $token['kind'],
                    'token' => $token['raw'],
                ];
            }
        }
    }
    return $reverse;
}

function required_use_mentions(string $requiredUse, string $flag): bool
{
    return preg_match('/(?<![A-Za-z0-9_+.-])' . preg_quote($flag, '/') . '(?![A-Za-z0-9_+.-])/', $requiredUse) === 1;
}

function summarize_dep_refs(array $refs, int $limit = 8): string
{
    if (!$refs) {
        return '';
    }
    $chunks = [];
    foreach (array_slice($refs, 0, $limit) as $ref) {
        $chunks[] = "{$ref['depender']}:{$ref['field']}[{$ref['token']}:{$ref['kind']}]";
    }
    if (count($refs) > $limit) {
        $chunks[] = '+' . (count($refs) - $limit) . ' more';
    }
    return implode('; ', $chunks);
}

function resolution_for_row(array $row, array $depRefs, bool $requiredUseMention): array
{
    $source = $row['source'];
    if ($source === 'package.use/zzz-dynamic') {
        return ['explicit-maintain-deps', 'explicit via package.use/zzz-dynamic'];
    }
    if ($source === 'package.use') {
        return ['explicit-package.use', 'explicit via package.use'];
    }
    foreach ($depRefs as $ref) {
        if ($ref['kind'] === 'hard') {
            return ['reverse-dependency-hard', 'required by installed package USE dependency'];
        }
    }
    if ($depRefs) {
        return ['reverse-dependency-conditional', 'mentioned by installed package USE dependency'];
    }
    if ($requiredUseMention) {
        return ['required-use-mentions-flag', 'same package REQUIRED_USE mentions this flag'];
    }
    if ($source === '+IUSE package default') {
        return ['package-default-optional', 'package default; no installed reverse USE dependency found'];
    }
    return ['profile-or-implicit-optional', 'profile/implicit active; no installed reverse USE dependency found'];
}

function is_protected_flag(string $flag): bool
{
    foreach (PROTECTED_FLAG_PATTERNS as $pattern) {
        if (preg_match($pattern, $flag) === 1) {
            return true;
        }
    }
    return false;
}

function proposal_for_row(array $row, string $resolution, bool $protected, array $policyFlags): string
{
    $flag = $row['use_flag'];
    if ($protected) {
        return 'keep-protected';
    }
    if (array_key_exists($flag, USE_FLAGS_WITHOUT_REMOVE_PROPOSALS)) {
        return 'keep-skip-list';
    }
    if (array_key_exists($flag, DEFAULT_NOT_DISABLED_FLAGS) || array_key_exists($flag, $policyFlags)) {
        return 'keep-policy';
    }
    if (in_array($resolution, ['explicit-maintain-deps', 'explicit-package.use', 'reverse-dependency-hard', 'reverse-dependency-conditional', 'required-use-mentions-flag'], true)) {
        return 'keep-or-package-enable';
    }
    return 'remove-candidate';
}

function read_policy_flags(string $path): array
{
    if (!is_file($path)) {
        return [];
    }
    $flags = [];
    foreach (file($path, FILE_IGNORE_NEW_LINES) ?: [] as $line) {
        if (preg_match('/\s*-\s*`([^`]+)`\s*:?\s*(.*)$/', $line, $match)) {
            $flags[$match[1]] = trim($match[2]) ?: 'listed in policy file';
        }
    }
    return $flags;
}

function read_gentoo_useflag_descriptions(string $portageDir): array
{
    $profilesDirs = [
        '/var/db/repos/gentoo/profiles',
        '/usr/portage/profiles',
        dirname($portageDir) . '/repos/gentoo/profiles',
    ];
    $descriptions = [];
    foreach ($profilesDirs as $profilesDir) {
        if (!is_dir($profilesDir)) {
            continue;
        }
        $useDesc = $profilesDir . '/use.desc';
        if (is_file($useDesc)) {
            foreach (file($useDesc, FILE_IGNORE_NEW_LINES) ?: [] as $raw) {
                $line = trim($raw);
                if ($line === '' || str_starts_with($line, '#') || !str_contains($line, ' - ')) {
                    continue;
                }
                [$flag, $desc] = explode(' - ', $line, 2);
                $descriptions[trim($flag)] ??= ['description' => trim($desc), 'source' => $useDesc];
            }
        }
        $descDir = $profilesDir . '/desc';
        if (is_dir($descDir)) {
            foreach (glob($descDir . '/*.desc') ?: [] as $descFile) {
                $prefix = strtolower(pathinfo($descFile, PATHINFO_FILENAME)) . '_';
                foreach (file($descFile, FILE_IGNORE_NEW_LINES) ?: [] as $raw) {
                    $line = trim($raw);
                    if ($line === '' || str_starts_with($line, '#') || !str_contains($line, ' - ')) {
                        continue;
                    }
                    [$value, $desc] = explode(' - ', $line, 2);
                    $descriptions[$prefix . trim($value)] ??= ['description' => trim($desc), 'source' => $descFile];
                }
            }
        }
        $localDesc = $profilesDir . '/use.local.desc';
        if (is_file($localDesc)) {
            foreach (file($localDesc, FILE_IGNORE_NEW_LINES) ?: [] as $raw) {
                $line = trim($raw);
                if ($line === '' || str_starts_with($line, '#') || !str_contains($line, ' - ') || !str_contains($line, ':')) {
                    continue;
                }
                [$left, $desc] = explode(' - ', $line, 2);
                [$package, $flag] = explode(':', $left, 2);
                $flag = trim($flag);
                $descriptions[$flag] ??= ['description' => 'Local USE flag; see package-specific descriptions.', 'source' => $localDesc];
                $samples = $descriptions[$flag]['local_samples'] ?? '';
                if (count(array_filter(explode('; ', $samples))) < 5) {
                    $descriptions[$flag]['local_samples'] = implode('; ', array_filter([$samples, trim($package) . ': ' . trim($desc)]));
                }
            }
        }
        return $descriptions;
    }
    return $descriptions;
}

function gentoo_useflag_url(string $flag): string
{
    return 'https://packages.gentoo.org/useflags/' . rawurlencode($flag);
}

function write_policy_template(string $path, array $extraFlags = []): void
{
    $flags = array_merge(DEFAULT_NOT_DISABLED_FLAGS, $extraFlags);
    ksort($flags, SORT_FLAG_CASE | SORT_STRING);
    @mkdir(dirname($path), 0777, true);
    $text = "# Gentoo USE-Flag Policy\n\n";
    $text .= "Diese Datei dokumentiert USE-Flags, die nicht automatisch global abgeschaltet werden sollen.\n";
    $text .= "Das Script liest die Backtick-Einträge in diesem Abschnitt. Ergänze hier Flags, wenn eine Frage dauerhaft entschieden ist.\n\n";
    $text .= "## Nicht Global Abschalten\n\n";
    foreach ($flags as $flag => $reason) {
        $text .= "- `{$flag}`: {$reason}\n";
    }
    file_put_contents($path, $text);
}

function classify_source(string $cp, string $flag, array $iuseDefaults, array $explicit, array $globalExplicit): array
{
    $details = $explicit[$cp . "\0" . $flag] ?? [];
    if ($details) {
        foreach ($details as $detail) {
            if (basename(explode(':', $detail, 2)[0]) === 'zzz-dynamic') {
                return ['package.use/zzz-dynamic', implode(', ', $details)];
            }
        }
        return ['package.use', implode(', ', $details)];
    }
    $globalDetails = $globalExplicit[$flag] ?? [];
    if ($globalDetails) {
        foreach ($globalDetails as $detail) {
            if (basename(explode(':', $detail, 2)[0]) === 'zzz-dynamic') {
                return ['package.use/zzz-dynamic', implode(', ', $globalDetails)];
            }
        }
        return ['package.use', implode(', ', $globalDetails)];
    }
    if (in_array($flag, $iuseDefaults, true)) {
        return ['+IUSE package default', ''];
    }
    return ['default/profile/implicit', ''];
}

function truncate_packages(array $items, int $maxItems): string
{
    $items = array_values($items);
    if (count($items) <= $maxItems) {
        return implode(', ', array_map(static fn(string $x): string => "`{$x}`", $items));
    }
    $shown = implode(', ', array_map(static fn(string $x): string => "`{$x}`", array_slice($items, 0, $maxItems)));
    return $shown . ' ... (+' . (count($items) - $maxItems) . ')';
}

function plain_package_list(array $items, int $maxItems = 12): string
{
    $items = array_values($items);
    if (count($items) <= $maxItems) {
        return implode(', ', $items);
    }
    return implode(', ', array_slice($items, 0, $maxItems)) . ', ... (+' . (count($items) - $maxItems) . ')';
}

function console_truncate(string $text, int $maxLength = 240): string
{
    $text = preg_replace('/\s+/', ' ', trim($text)) ?? trim($text);
    if (strlen($text) <= $maxLength) {
        return $text;
    }
    return rtrim(substr($text, 0, $maxLength - 3)) . '...';
}

function unique_sorted(array $items): array
{
    $items = array_values(array_unique(array_map('strval', $items)));
    usort($items, 'strcasecmp');
    return $items;
}

function build_console_removal_proposals(array $rows, array $policyFlags, array $gentooDescriptions, int $limit = 10): array
{
    $notDisabled = array_fill_keys(array_merge(array_keys(DEFAULT_NOT_DISABLED_FLAGS), array_keys(USE_FLAGS_WITHOUT_REMOVE_PROPOSALS), array_keys($policyFlags)), true);
    $byFlag = [];
    foreach ($rows as $row) {
        $flag = $row['use_flag'];
        if (isset($notDisabled[$flag]) || is_protected_flag($flag)) {
            continue;
        }
        $byFlag[$flag] ??= [];
        $byFlag[$flag][] = $row;
    }

    $proposals = [];
    foreach ($byFlag as $flag => $flagRows) {
        $lostRows = array_values(array_filter($flagRows, static fn(array $row): bool => $row['proposal'] === 'remove-candidate'));
        $keepRows = array_values(array_filter($flagRows, static fn(array $row): bool => $row['proposal'] === 'keep-or-package-enable'));
        if (!$lostRows) {
            continue;
        }
        $packagesLosing = unique_sorted(array_column($lostRows, 'package'));
        $packagesKeep = unique_sorted(array_column($keepRows, 'cp'));
        $explicitSources = unique_sorted(array_filter(array_column($keepRows, 'source'), static fn(string $source): bool => str_starts_with($source, 'package.use')));
        $proposals[] = [
            'flag' => $flag,
            'lost_count' => count($packagesLosing),
            'keep_count' => count($packagesKeep),
            'packages_losing' => $packagesLosing,
            'packages_keep' => $packagesKeep,
            'explicit_sources' => $explicitSources,
            'gentoo_description' => $gentooDescriptions[$flag] ?? null,
            'safe' => !$packagesKeep,
        ];
    }

    usort($proposals, static function (array $a, array $b): int {
        return [!$a['safe'], -$a['lost_count'], strcasecmp($a['flag'], $b['flag'])] <=> [!$b['safe'], -$b['lost_count'], 0];
    });
    $safe = array_values(array_filter($proposals, static fn(array $item): bool => $item['safe']));
    $mixed = array_values(array_filter($proposals, static fn(array $item): bool => !$item['safe']));
    $selected = array_slice($mixed, 0, max(0, $limit - 3));
    foreach (array_slice($safe, 0, 3) as $item) {
        if (!in_array($item, $selected, true)) {
            $selected[] = $item;
        }
    }
    foreach ($proposals as $item) {
        if (count($selected) >= $limit) {
            break;
        }
        if (!in_array($item, $selected, true)) {
            $selected[] = $item;
        }
    }
    usort($selected, static fn(array $a, array $b): int => [-$a['lost_count'], $a['keep_count'], strcasecmp($a['flag'], $b['flag'])] <=> [-$b['lost_count'], $b['keep_count'], 0]);
    return array_slice($selected, 0, $limit);
}

function print_console_summary(string $outDir, array $proposals): void
{
    echo "Report: {$outDir}\n\n";
    echo "Top removal proposals (max 10, analysis only):\n";
    if (!$proposals) {
        echo "- Keine passenden Removal-Proposals gefunden.\n";
        return;
    }
    foreach ($proposals as $index => $proposal) {
        $n = $index + 1;
        echo "{$n}. {$proposal['flag']}: {$proposal['lost_count']} Pakete verlieren das Flag; {$proposal['keep_count']} paketbasierte Aktivierungen nötig\n";
        if ($proposal['gentoo_description']) {
            echo "   Gentoo: " . console_truncate($proposal['gentoo_description']['description']) . "\n";
            if (!empty($proposal['gentoo_description']['local_samples'])) {
                echo "   Gentoo lokal: " . console_truncate($proposal['gentoo_description']['local_samples']) . "\n";
            }
            echo "   Gentoo Quelle: {$proposal['gentoo_description']['source']}; " . gentoo_useflag_url($proposal['flag']) . "\n";
        } else {
            echo "   Gentoo: keine lokale Beschreibung gefunden; " . gentoo_useflag_url($proposal['flag']) . "\n";
        }
        echo '   verliert: ' . plain_package_list($proposal['packages_losing']) . "\n";
        if ($proposal['packages_keep']) {
            echo '   paketbasiert behalten: ' . plain_package_list($proposal['packages_keep']) . "\n";
        } else {
            echo "   paketbasiert behalten: nichts nötig\n";
        }
        echo "\n";
    }
}

function increment_count(array &$map, string $key, int $by = 1): void
{
    $map[$key] = ($map[$key] ?? 0) + $by;
}

function csv_write_assoc(string $path, array $fieldnames, array $rows): void
{
    $fh = fopen($path, 'wb');
    if ($fh === false) {
        throw new RuntimeException("Cannot write {$path}");
    }
    fputcsv($fh, $fieldnames);
    foreach ($rows as $row) {
        fputcsv($fh, array_map(static fn(string $field): string => (string)($row[$field] ?? ''), $fieldnames));
    }
    fclose($fh);
}

function write_json(string $path, array $data): void
{
    file_put_contents($path, json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . "\n");
}

function main(array $argv): int
{
    if (in_array('-h', $argv, true) || in_array('--help', $argv, true)) {
        echo "Usage: gentoo_useflag_report.php\n\n";
        echo "Environment: GENTOO_USEFLAG_VAR_DB_PKG, GENTOO_USEFLAG_PORTAGE_DIR, GENTOO_USEFLAG_POLICY_MD, GENTOO_USEFLAG_OUT_BASE, GENTOO_USEFLAG_MAX_PACKAGES_PER_GROUP\n";
        return 0;
    }

    $now = new DateTimeImmutable('now');
    $scriptDir = __DIR__;
    $varDbPkg = getenv('GENTOO_USEFLAG_VAR_DB_PKG') ?: '/var/db/pkg';
    $portageDir = getenv('GENTOO_USEFLAG_PORTAGE_DIR') ?: '/etc/portage';
    $policyMd = getenv('GENTOO_USEFLAG_POLICY_MD') ?: $scriptDir . '/gentoo-useflag-policy.md';
    $maxPackagesPerGroup = (int)(getenv('GENTOO_USEFLAG_MAX_PACKAGES_PER_GROUP') ?: '80');
    $outBase = getenv('GENTOO_USEFLAG_OUT_BASE') ?: '/tmp';
    $outDir = rtrim($outBase, '/') . '/useflag-report-' . $now->format('Y-m-d-His');
    @mkdir($outDir, 0777, true);

    [$explicit, $globalExplicit] = parse_package_use($portageDir);
    $installedPackages = iter_installed($varDbPkg, true);
    $reverseUseDeps = build_reverse_use_dependencies($installedPackages);
    $policyFlagsForResolution = read_policy_flags($policyMd);
    $gentooDescriptions = read_gentoo_useflag_descriptions($portageDir);
    $rows = [];
    $byFlag = [];
    $byPackage = [];
    $packageCount = 0;

    foreach ($installedPackages as $pkg) {
        if (!$pkg['active_flags']) {
            continue;
        }
        $packageCount++;
        $flags = $pkg['active_flags'];
        sort($flags, SORT_NATURAL | SORT_FLAG_CASE);
        foreach ($flags as $flag) {
            [$source, $sourceDetail] = classify_source($pkg['cp'], $flag, $pkg['iuse_defaults'], $explicit, $globalExplicit);
            $depRefs = $reverseUseDeps[$pkg['cp'] . "\0" . $flag] ?? [];
            $reqUseMention = required_use_mentions((string)$pkg['required_use'], $flag);
            $row = [
                'use_flag' => $flag,
                'package' => $pkg['cpv'],
                'cp' => $pkg['cp'],
                'source' => $source,
                'source_detail' => $sourceDetail,
                'reverse_dependency_refs' => summarize_dep_refs($depRefs),
                'reverse_dependency_ref_count' => (string)count($depRefs),
                'required_use_mention' => $reqUseMention ? 'yes' : 'no',
            ];
            [$resolution, $resolutionDetail] = resolution_for_row($row, $depRefs, $reqUseMention);
            $row['resolution'] = $resolution;
            $row['resolution_detail'] = $resolutionDetail;
            $row['proposal'] = proposal_for_row($row, $resolution, is_protected_flag($flag), $policyFlagsForResolution);
            $rows[] = $row;
            $byFlag[$flag][$source][] = $pkg['cpv'];
            $byPackage[$pkg['cpv']][] = $row;
        }
    }

    usort($rows, static fn(array $a, array $b): int => [strtolower($a['use_flag']), $a['source'], $a['package']] <=> [strtolower($b['use_flag']), $b['source'], $b['package']]);
    $csvFieldnames = [
        'use_flag',
        'package',
        'cp',
        'source',
        'source_detail',
        'resolution',
        'resolution_detail',
        'proposal',
        'reverse_dependency_ref_count',
        'reverse_dependency_refs',
        'required_use_mention',
    ];
    csv_write_assoc($outDir . '/active-useflags-by-package.csv', $csvFieldnames, $rows);
    csv_write_assoc($outDir . '/useflag-resolution.csv', $csvFieldnames, $rows);

    $rowsBySource = [];
    $rowsByResolution = [];
    $rowsByProposal = [];
    foreach ($rows as $row) {
        increment_count($rowsBySource, $row['source']);
        increment_count($rowsByResolution, $row['resolution']);
        increment_count($rowsByProposal, $row['proposal']);
    }
    ksort($rowsBySource);
    ksort($rowsByResolution);
    ksort($rowsByProposal);

    $summary = [
        'generated_at' => $now->format(DateTimeInterface::ATOM),
        'var_db_pkg' => $varDbPkg,
        'portage_dir' => $portageDir,
        'packages_with_use_data' => $packageCount,
        'distinct_active_use_flags' => count($byFlag),
        'detail_rows' => count($rows),
        'rows_by_source' => $rowsBySource,
        'rows_by_resolution' => $rowsByResolution,
        'rows_by_proposal' => $rowsByProposal,
    ];
    write_json($outDir . '/active-useflags-summary.json', $summary);

    $dynRows = array_values(array_filter($rows, static fn(array $r): bool => $r['source'] === 'package.use/zzz-dynamic'));
    $f = fopen($outDir . '/zzz-dynamic.md', 'wb');
    fwrite($f, "# package.use/zzz-dynamic aktive USE-Flags\n\n");
    if (!$dynRows) {
        fwrite($f, "Keine aktiven Flags aus `package.use/zzz-dynamic` gefunden.\n");
    } else {
        $current = null;
        foreach ($dynRows as $row) {
            if ($row['use_flag'] !== $current) {
                $current = $row['use_flag'];
                fwrite($f, "\n## `{$current}`\n");
            }
            fwrite($f, "- `{$row['package']}` ({$row['source_detail']})\n");
        }
    }
    fclose($f);

    $f = fopen($outDir . '/overview.md', 'wb');
    fwrite($f, "# Gentoo USE-Flag Übersicht\n\n");
    fwrite($f, "Generiert: `{$summary['generated_at']}`\n\n");
    fwrite($f, "Quelle: `{$varDbPkg}` und `{$portageDir}`.\n\n");
    fwrite($f, "Einstufung:\n");
    fwrite($f, "- `package.use/zzz-dynamic`: aktiv und explizit in einer Datei namens `zzz-dynamic` angefordert.\n");
    fwrite($f, "- `package.use`: aktiv und explizit in anderer `package.use*`-Konfiguration angefordert.\n");
    fwrite($f, "- `+IUSE package default`: aktiv und als Paket-Default mit `+flag` in den installierten Paketmetadaten markiert.\n");
    fwrite($f, "- `default/profile/implicit`: aktiv, aber nicht eindeutig explizit in `package.use*` gefunden.\n\n");
    fwrite($f, "- Pakete mit USE-Daten: {$packageCount}\n");
    fwrite($f, "- Unterschiedliche aktive USE-Flags: " . count($byFlag) . "\n");
    fwrite($f, "- Detailzeilen: " . count($rows) . "\n\n");
    fwrite($f, "## Nach USE-Flag gruppiert\n");
    $flags = array_keys($byFlag);
    usort($flags, 'strcasecmp');
    foreach ($flags as $flag) {
        fwrite($f, "\n### `{$flag}`\n");
        foreach (['package.use/zzz-dynamic', 'package.use', '+IUSE package default', 'default/profile/implicit'] as $source) {
            $pkgs = unique_sorted($byFlag[$flag][$source] ?? []);
            if ($pkgs) {
                fwrite($f, $source . ': ' . truncate_packages($pkgs, $maxPackagesPerGroup) . "\n\n");
            }
        }
    }
    fclose($f);

    $f = fopen($outDir . '/all-useflags-evaluation.md', 'wb');
    fwrite($f, "# Alle USE-Flags mit Auswertung\n\n");
    fwrite($f, "Diese Liste fasst pro USE-Flag zusammen, warum es aktuell aktiv ist und ob es ein Entfernungskandidat ist.\n\n");
    foreach ($flags as $flag) {
        $flagRows = array_values(array_filter($rows, static fn(array $row): bool => $row['use_flag'] === $flag));
        $sources = [];
        $resolutions = [];
        $proposals = [];
        foreach ($flagRows as $row) {
            increment_count($sources, $row['source']);
            increment_count($resolutions, $row['resolution']);
            increment_count($proposals, $row['proposal']);
        }
        ksort($sources);
        ksort($resolutions);
        ksort($proposals);
        fwrite($f, "## `{$flag}`\n");
        $desc = $gentooDescriptions[$flag] ?? null;
        if ($desc) {
            fwrite($f, "- Gentoo-Beschreibung: {$desc['description']}\n");
            if (!empty($desc['local_samples'])) {
                fwrite($f, "- Lokale Gentoo-Beispiele: {$desc['local_samples']}\n");
            }
            fwrite($f, "- Gentoo-Quelle: `{$desc['source']}`; packages.gentoo.org: `" . gentoo_useflag_url($flag) . "`\n");
        } else {
            fwrite($f, "- Gentoo-Beschreibung: nicht im lokalen Gentoo-Profilcache gefunden; packages.gentoo.org: `" . gentoo_useflag_url($flag) . "`\n");
        }
        $affectedPackages = unique_sorted(array_column($flagRows, 'package'));
        fwrite($f, "- Betroffene installierte Pakete: " . count($affectedPackages) . "\n");
        fwrite($f, "- Pakete: " . truncate_packages($affectedPackages, $maxPackagesPerGroup) . "\n");
        fwrite($f, "- Quellen: " . implode(', ', array_map(static fn($k, $v): string => "`{$k}`={$v}", array_keys($sources), $sources)) . "\n");
        fwrite($f, "- Resolution: " . implode(', ', array_map(static fn($k, $v): string => "`{$k}`={$v}", array_keys($resolutions), $resolutions)) . "\n");
        fwrite($f, "- Proposal: " . implode(', ', array_map(static fn($k, $v): string => "`{$k}`={$v}", array_keys($proposals), $proposals)) . "\n\n");
    }
    fclose($f);

    $packages = array_keys($byPackage);
    usort($packages, 'strcasecmp');
    $f = fopen($outDir . '/packages-useflags.md', 'wb');
    fwrite($f, "# Installierte Pakete und aktive USE-Flags\n\n");
    fwrite($f, "Format je Flag: `flag` - Quelle; Resolution; Proposal.\n\n");
    foreach ($packages as $package) {
        fwrite($f, "## `{$package}`\n");
        usort($byPackage[$package], static fn(array $a, array $b): int => strcasecmp($a['use_flag'], $b['use_flag']));
        foreach ($byPackage[$package] as $row) {
            $detail = $row['source_detail'] ?: $row['resolution_detail'];
            fwrite($f, "- `{$row['use_flag']}` - {$row['source']}; {$row['resolution']}; {$row['proposal']}");
            if ($detail) {
                fwrite($f, " ({$detail})");
            }
            if ($row['reverse_dependency_refs']) {
                fwrite($f, "; reverse deps: {$row['reverse_dependency_refs']}");
            }
            fwrite($f, "\n");
        }
        fwrite($f, "\n");
    }
    fclose($f);

    $removableRows = array_values(array_filter($rows, static fn(array $row): bool => $row['proposal'] === 'remove-candidate'));
    $byRemoveFlag = [];
    foreach ($removableRows as $row) {
        $byRemoveFlag[$row['use_flag']][] = $row;
    }
    $removeFlags = array_keys($byRemoveFlag);
    usort($removeFlags, 'strcasecmp');
    $f = fopen($outDir . '/removal-proposals.md', 'wb');
    fwrite($f, "# USE-Flag Entfernungsvorschlaege\n\n");
    fwrite($f, "Diese Zeilen sind aktiv, aber weder explizit per package.use gesetzt noch in den installierten Reverse-USE-Dependencies gefunden.\n");
    fwrite($f, "Vor realer Entfernung trotzdem `emerge -pv --changed-use @world` ausfuehren.\n\n");
    foreach ($removeFlags as $flag) {
        $packagesForFlag = unique_sorted(array_column($byRemoveFlag[$flag], 'package'));
        fwrite($f, "## `{$flag}`\n");
        fwrite($f, "- Pakete: " . truncate_packages($packagesForFlag, $maxPackagesPerGroup) . "\n");
        fwrite($f, "- Anzahl: " . count($packagesForFlag) . "\n\n");
    }
    fclose($f);

    $f = fopen($outDir . '/global-disable-crosscheck.md', 'wb');
    fwrite($f, "# Cross-Check bei globalem Abschalten\n\n");
    fwrite($f, "Wenn ein USE-Flag global deaktiviert wird, betrifft das jedes installierte Paket, bei dem dieses Flag aktuell aktiv ist.\n");
    fwrite($f, "Pakete mit `keep-or-package-enable` brauchen voraussichtlich eine paketbasierte Aktivierung oder sind bereits explizit gesetzt.\n");
    fwrite($f, "Pakete mit `remove-candidate` sind die eigentlichen Entfernungskandidaten.\n\n");
    foreach ($flags as $flag) {
        $flagRows = array_values(array_filter($rows, static fn(array $row): bool => $row['use_flag'] === $flag));
        $keepRows = array_values(array_filter($flagRows, static fn(array $row): bool => $row['proposal'] === 'keep-or-package-enable'));
        $removeRows = array_values(array_filter($flagRows, static fn(array $row): bool => $row['proposal'] === 'remove-candidate'));
        $policyRows = array_values(array_filter($flagRows, static fn(array $row): bool => str_starts_with($row['proposal'], 'keep-') && $row['proposal'] !== 'keep-or-package-enable'));
        if (!$keepRows && !$removeRows) {
            continue;
        }
        fwrite($f, "## `{$flag}`\n");
        fwrite($f, "- Gesamt betroffen: " . count($flagRows) . " Pakete\n");
        fwrite($f, "- Paketweise behalten/aktivieren: " . count($keepRows) . "\n");
        fwrite($f, "- Entfernungskandidaten: " . count($removeRows) . "\n");
        fwrite($f, "- Policy/geschuetzt: " . count($policyRows) . "\n");
        if ($keepRows) {
            fwrite($f, "- Behalten/Aktivieren: " . truncate_packages(unique_sorted(array_column($keepRows, 'package')), $maxPackagesPerGroup) . "\n");
        }
        if ($removeRows) {
            fwrite($f, "- Optional entfernbar: " . truncate_packages(unique_sorted(array_column($removeRows, 'package')), $maxPackagesPerGroup) . "\n");
        }
        fwrite($f, "\n");
    }
    fclose($f);

    if (!is_file($policyMd)) {
        write_policy_template($policyMd, read_policy_flags($policyMd));
    }

    $policyFlags = read_policy_flags($policyMd);
    $notDisabledKeys = array_fill_keys(array_merge(array_keys(DEFAULT_NOT_DISABLED_FLAGS), array_keys(USE_FLAGS_WITHOUT_REMOVE_PROPOSALS), array_keys($policyFlags)), true);
    $maintainDepsFlags = [];
    $nonMaintainRows = [];
    $candidateFlags = [];
    foreach ($rows as $row) {
        if ($row['source'] === 'package.use/zzz-dynamic') {
            $maintainDepsFlags[$row['use_flag']] = true;
            continue;
        }
        $nonMaintainRows[] = $row;
        $flag = $row['use_flag'];
        if (!isset($notDisabledKeys[$flag]) && !is_protected_flag($flag)) {
            $candidateFlags[$flag] = true;
        }
    }
    $summary['tightening'] = [
        'policy_md' => $policyMd,
        'maintain_deps_distinct_flags' => count($maintainDepsFlags),
        'non_maintain_deps_rows' => count($nonMaintainRows),
        'global_disable_candidate_flags' => count($candidateFlags),
        'protected_or_policy_not_disabled_flags' => count($notDisabledKeys),
    ];
    write_json($outDir . '/active-useflags-summary.json', $summary);

    $byCandidateFlag = [];
    foreach ($nonMaintainRows as $row) {
        $byCandidateFlag[$row['use_flag']][$row['source']][] = $row['package'];
    }
    $candidateFlagNames = array_keys($candidateFlags);
    usort($candidateFlagNames, 'strcasecmp');
    $f = fopen($outDir . '/tightening-analysis.md', 'wb');
    fwrite($f, "# USE-Flag Tightening Analyse\n\n");
    fwrite($f, "Ziel: aktive USE-Flags identifizieren, die nicht über `package.use/zzz-dynamic` / maintain-deps hereinkommen.\n\n");
    fwrite($f, "- Policy-Datei: `{$policyMd}`\n");
    fwrite($f, "- Nicht-maintain-deps Detailzeilen: " . count($nonMaintainRows) . "\n");
    fwrite($f, "- Kandidaten für globales Abschalten: " . count($candidateFlags) . " Flags\n");
    fwrite($f, "- Maintain-deps/zzz-dynamic Flags: " . count($maintainDepsFlags) . " Flags\n\n");
    fwrite($f, "## Nicht Global Abgeschaltet\n\n");
    $notDisabledSorted = array_keys($notDisabledKeys);
    usort($notDisabledSorted, 'strcasecmp');
    foreach ($notDisabledSorted as $flag) {
        $reason = $policyFlags[$flag] ?? USE_FLAGS_WITHOUT_REMOVE_PROPOSALS[$flag] ?? DEFAULT_NOT_DISABLED_FLAGS[$flag] ?? 'protected';
        fwrite($f, "- `{$flag}`: {$reason}\n");
    }
    fwrite($f, "\n## Kandidaten\n\n");
    foreach ($candidateFlagNames as $flag) {
        fwrite($f, "### `{$flag}`\n");
        foreach (['package.use', '+IUSE package default', 'default/profile/implicit'] as $source) {
            $pkgs = unique_sorted($byCandidateFlag[$flag][$source] ?? []);
            if ($pkgs) {
                fwrite($f, $source . ': ' . truncate_packages($pkgs, $maxPackagesPerGroup) . "\n\n");
            }
        }
    }
    fclose($f);

    $consoleProposals = build_console_removal_proposals($rows, $policyFlags, $gentooDescriptions, 10);
    print_console_summary($outDir, $consoleProposals);
    return 0;
}

exit(main($argv));
