#!/usr/bin/php
<?php

/******************************************************************************
*
* relcheck
* part of lfs-ryco
*
* Copyright (c) 2020-2025 Ryan Coe <bluemrp9@gmail.com>
*
* The MIT License (MIT)
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
******************************************************************************/

declare(strict_types=1);

if (@file_exists('./vendor/_autoload.php')) {
    require_once('./vendor/_autoload.php');
} else {
    require_once((isset($_SERVER['LFS_ROOTFS']) ? $_SERVER['LFS_ROOTFS'] : '').'/usr/share/php/vendor/_autoload.php');
}

use LFS\LFS;
use LFS\Repo;
use Ryco\Error\Exception;
use Ryco\Log;
use Ryco\Package\RPM;
use Ryco\Pushover\PushMessage;
use Ryco\Pushover\Pushover;
use Ryco\Utils\ArrayHelper;
use Ryco\Utils\System;

LFS::_init();

$program = "relcheck";
$version = "2025.07.09";

$db_update = false;
$home = System::get_home_dir();
$db_file = $home."/.relcheck.json";
$db_ver = 1;
$repo_file = LFS::get_prefix().'/rpmbuild/REPO/lfsrepo.json.gz';
$repo_ini_file = LFS::get_prefix().'/rpmbuild/TOOLS/repo.ini';
$pushover_enable = (array_key_exists('PUSHOVER', $_SERVER) ? boolval($_SERVER['PUSHOVER']) : false);
$db = ['dbver'=>$db_ver, 'last_checked'=>0, 'packages'=>[]];
$pkg_skel = [
    'branch'=>'',
    'homepage'=>'',
    'id'=>0,
    'ignore'=>false,
    'latest'=>'',
    'match'=>[],
    'notify'=>false,
    'prefix'=>'',
    'project'=>'',
    'remove_decimals'=>false,
    'replace_underscores'=>false,
    'skip_plus'=>false,
    'scanned'=>'never',
    'skip'=>[],
    'sort'=>false,
    'suffix'=>'',
    'updated'=>'never',
    'version'=>'',
    'eol'=>'',
];

// Parse command line
$srpm_scan = ($argc >= 2 && $argv[1] === "scan" ? true : false);
$update = ($argc >= 2 && $argv[1] === "update" ? true : false);
$show_branch = ($argc >= 2 && $argv[1] === "branch" ? true : false);
$show_missing = ($argc >= 2 && $argv[1] === "missing" ? true : false);
$show_failed = ($argc >= 2 && $argv[1] === "failed" ? true : false);
$show_last = ($argc >= 2 && $argv[1] === "last" ? true : false);
$set_project_id = ($argc >= 2 && $argv[1] === "project" ? true : false);
$show_usage = ($argc >= 2 && ($argv[1] === "help" || $argv[1] === "usage" || $argv[1] === "-h" || $argv[1] === "--help") ? true : false);
$pkg_set = ($argc >= 2 && $argv[1] === "set" ? true : false);
$pkg_name = ($argc >= 3 ? $argv[2] : "");
$pkg_ver = ($argc == 4 ? $argv[3] : "");

// Program name and version
Log::info(sprintf('%s %s version %s', LFS::DISTRO, $program, $version));

// Show usage
if ($show_usage) {
    printf("Usage:\n\n");
    printf("%s scan                 : scan srpms for new packages and version info\n", $program);
    printf("%s update package       : update package from release-monitoring (all for everything)\n", $program);
    printf("%s branch               : show packages with specific branches\n", $program);
    printf("%s missing              : show packages with missing project ids\n", $program);
    printf("%s failed               : show packages with failed update checks\n", $program);
    printf("%s last                 : show package last scanned\n", $program);
    printf("%s project package id   : set project id for package\n", $program);
    printf("%s set package version  : set local package version\n", $program);
    exit(0);
}

// Load local repo and build version table
$local_repo = Repo::createFromRepoFile($repo_file)->buildVersionTableByArch('source', false);

$config = $local_repo->getConfig($repo_ini_file);
$pushover = Pushover::fromConfig($config);
$pushover_enable = ($pushover_enable && $pushover->isValid());

// Check database file exists
if (System::fileExists($db_file)) {
    Log::info("Reading {$db_file}");
    $db_str = System::readFile($db_file);
    $db = ArrayHelper::jsonDecode($db_str);
    Log::info('{yellow}Loaded database{reset}');
} else {
    // Empty database
    Log::info('{yellow}Using empty database{reset}');
}
// TODO: use sanitize array
$db['dbver'] = intval($db['dbver']);
if ($db['dbver'] !== $db_ver) {
    Log::error(sprintf('{red}DB version mismatch (need %d, found %d){reset}', $db_ver, $db['dbver']));
    exit(1);
}
if (!array_key_exists('last_checked', $db)) {
    Log::error('{cyan}Missing array key last_checked{reset}');
    $db['last_checked'] = 0;
}
if (!array_key_exists('packages', $db)) {
    Log::error('{red}Missing array key packages{reset}');
    exit(1);
}
if (!is_array($db['packages'])) {
    Log::error('{red}Packages is not an array{reset}');
    exit(1);
}
foreach ($db['packages'] as $key=>&$package) {
    // Required
    if (!array_key_exists('id', $package)) {
        Log::error("{red}Package {$key} missing array key ID{reset}");
        exit(1);
    }
    $package['id'] = intval($package['id']);
    if (!array_key_exists('project', $package)) {
        Log::error("{cyan}Package {$key} missing array key project{reset}");
        $package['project'] = "";
    }
    if (!array_key_exists('homepage', $package)) {
        Log::error("{cyan}Package {$key} missing array key homepage{reset}");
        $package['homepage'] = "";
    }
    if (!array_key_exists('version', $package)) {
        Log::error("{cyan}Package {$key} missing array key version{reset}");
        $package['version'] = "";
    }
    if (!array_key_exists('latest', $package)) {
        Log::error("{cyan}Package {$key} missing array key latest");
        $package['latest'] = "";
    }
    if (!array_key_exists('skip', $package) || !is_array($package['skip'])) {
        Log::error("Package {$key} missing array key skip");
        $package['skip'] = array();
    }
    if (!array_key_exists('match', $package) || !is_array($package['match'])) {
        Log::error("Package {$key} missing array key match");
        $package['match'] = array();
    }
    // Optional
    if (!array_key_exists('branch', $package)) {
        $package['branch'] = "";
    }
    if (!array_key_exists('sort', $package)) {
        $package['sort'] = false;
    }
    if (!array_key_exists('prefix', $package)) {
        $package['prefix'] = "";
    }
    if (!array_key_exists('suffix', $package)) {
        $package['suffix'] = "";
    }
    if (!array_key_exists('remove_decimals', $package)) {
        $package['remove_decimals'] = false;
    }
    if (!array_key_exists('replace_underscores', $package)) {
        $package['replace_underscores'] = false;
    }
    if (!array_key_exists('skip_plus', $package)) {
        $package['skip_plus'] = false;
    }
    if (!array_key_exists('ignore', $package)) {
        $package['ignore'] = false;
    }
    if (!array_key_exists('eol', $package)) {
        $package['eol'] = '';
    }
    // Notification
    if (!array_key_exists('notify', $package)) {
        $package['notify'] = false;
    }
    // Last updated
    if (!array_key_exists('updated', $package)) {
        $package['updated'] = 'never';
    }
    // Last scanned update
    if (!array_key_exists('scanned', $package)) {
        $package['scanned'] = 'never';
    }
    // Key sort
    ksort($package);
}
unset($package);

// Current timestamp for comparison
$stamp = time();

// Generate timestemp for eol comparison
$today = mktime(0, 0, 0, intval(date('n', $stamp)), intval(date('j', $stamp)), intval(date('Y', $stamp)));

// Show packages with specific branches
if ($show_branch) {
    foreach ($db['packages'] as $name => $package) {
        if (strlen($package['branch']) > 0) {
            Log::info($name.' '.$package['branch']);
        }
    }
    exit(0);
}

// Show packages with missing project IDs
if ($show_missing) {
    foreach ($db['packages'] as $name => $package) {
        if ($package['id'] === 0 && !$package['ignore']) {
            Log::info($name);
        }
    }
    exit(0);
}

// Show packages with failed update checks
if ($show_failed) {
    $failed_arr = [];
    $now = $stamp-(86400*5);  // 5 days
    $len = 0;
    foreach ($db['packages'] as $name => $package) {
        if ($package['id'] === 0) {
            continue;
        }
        $add = false;
        $updated = $package['updated'];
        if ($updated === 'never') {
            $add = true;
        } else {
            $updated = strtotime($updated);
            if ($updated !== false) {
                if ($updated < $now) {
                    $add = true;
                }
            }
        }
        if ($add) {
            $failed_arr[] = [$name, $updated];
            if (strlen($name) > $len) {
                $len = strlen($name);
            }
        }
    }
    if (count($failed_arr) > 0) {
        foreach ($failed_arr as $failed) {
            Log::info(sprintf('%s : %s', str_pad($failed[0], $len), $failed[1]));
        }
    }
    exit(0);
}

if ($show_last) {
    $scanned_arr = [];
    $len = 0;
    foreach ($db['packages'] as $name => $package) {
        $scanned_arr[] = array($name, $package['scanned']);
        if (strlen($name) > $len) {
            $len = strlen($name);
        }
    }
    if (count($scanned_arr) > 0) {
        foreach ($scanned_arr as $scanned) {
            Log::info(sprintf('%s : %s', str_pad($scanned[0], $len), $scanned[1]));
        }
    }
    exit(0);
}

// Set project ID
if ($set_project_id) {
    if (!array_key_exists($pkg_name, $db['packages'])) {
        Log::error("Cannot find project '{$pkg_name}' in database");
        exit(1);
    }
    Log::info("Setting project id for {$pkg_name} to {$pkg_ver}}");
    $db['packages'][$pkg_name]['id'] = intval($pkg_ver);
    if ($db['packages'][$pkg_name]['id'] > 0) {
        $update = true;
    }
    $db_update = true;
}

// Check source rpms
if ($srpm_scan) {
    // Check srpm against db
    foreach ($local_repo->getKeysFromVersionTable() as $key) {
        $package = $local_repo->getPackage($key);
        $name = $package->name;
        if (!array_key_exists($name, $db['packages'])) {
            // Create new package entry
            Log::warning("Package {$name} not found in DB");
            $db['packages'][$name] = $pkg_skel;
            $db['packages'][$name]['version'] = $package->version;
            $db['packages'][$name]['scanned'] = date("Y-m-d H:i:s", $stamp);
            $db_update = true;
        } else {
            // Check and update version
            if (RPM::version_compare($package->version, $db['packages'][$name]['version']) > 0) {
                Log::info(sprintf("Updating package %s from version %s to version %s",
                    $name, $db['packages'][$name]['version'], $package->version));
                if (strlen($db['packages'][$name]['branch']) > 0 &&
                    !str_starts_with($package->version, $db['packages'][$name]['branch'])) {
                    Log::error(sprintf(' - {red}Branch %s mismatch with version %s{reset}',
                        $db['packages'][$name]['branch'], $package->version));
                }
                $db['packages'][$name]['version'] = $package->version;
                $db['packages'][$name]['scanned'] = date('Y-m-d H:i:s', $stamp);
                $db_update = true;
            }
        }
    }
}

// Check upstream package versions
if ($update) {
    $db_update = true;
    $db['last_checked'] = $stamp;
    Log::info('{yellow}Checking package versions{reset}');

    // Initialize curl
    $ch = curl_init();
    if ($ch === false) {
        Log::error('{red}Failed to initialize curl{reset}');
        exit(1);
    }

    // Get curl version
    $cv = curl_version();

    // Set curl options
    curl_setopt($ch, CURLOPT_HEADER, 0);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
    curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
    curl_setopt($ch, CURLOPT_TIMEOUT, 30);
    curl_setopt($ch, CURLOPT_USERAGENT, 'curl/'.$cv['version']);

    foreach ($db['packages'] as $key => &$package) {
        if ($package['ignore'] || (strlen($pkg_name) > 0 && $pkg_name !== $key)) {
            continue;
        }
        if (strlen($package['version']) === 0 || $package['id'] === 0) {
            Log::warning("{cyan}Skipping {$key}{reset}");
            continue;
        }

        Log::info("Checking {$key}");

        $project = (strlen($package['project']) > 0 ? $package['project'] : $key);
        $url = sprintf("https://release-monitoring.org/api/v2/projects/?name=%s", urlencode($project));
        curl_setopt($ch, CURLOPT_URL, $url);
        $output = curl_exec($ch);

        // Try again on zero length data
        if ($output === false || strlen($output) == 0) {
            $output = curl_exec($ch);
        }

        if ($output === false || strlen($output) == 0) {
            Log::error("{red}Zero length data received for {$key}{reset}");
            continue;
        }
        try {
            $json = ArrayHelper::jsonDecode($output);
        } catch (Exception $e) {
            Log::error(sprintf('{red}%s: %s{reset}', $key, $e->getMessage()));
            continue;
        }
        if (!array_key_exists('items', $json)) {
            Log::error("{red} Missing array key 'items' for {$key}{reset}");
            continue;
        }

        $found = false;
        foreach($json['items'] as $item) {
            // Project id must match exactly or skip
            if (intval($item['id']) !== intval($package['id'])) {
                continue;
            }

            // Set package found status
            $found = true;

            // Find latest version
            $package['latest'] = '';
            if (array_key_exists("sort", $package) && $package['sort']) {
                RPM::sort_version_arr_desc($item['versions']);
            }
            foreach ($item['versions'] as $version) {
                //printf("%s\n", $version);
                if (in_array($version, $package['skip'])) {
                    continue;
                }
                if (strlen($package['branch']) > 0 && substr_compare($version, $package['branch'], 0, strlen($package['branch'])) != 0) {
                    continue;
                }
                $match_found = false;
                foreach ($package['match'] as $match) {
                    if (strpos($version, $match) !== false) {
                        $match_found = true;
                    }
                }
                if ($match_found) {
                    continue;
                }
                $package['latest'] = $version;
                break;
            }
            unset($version);

            // Update project homepage
            $package['homepage'] = (strlen($item['homepage']) > 0 ? $item['homepage'] : "");
            // Update stamp
            $package['updated'] = date("Y-m-d H:i:s", $stamp);
            break;
        }
        if (!$found) {
            Log::error("{red}Failed to match project ID for {$key}{reset}");
            continue;
        }
    }
    unset($package);
    unset($ch);
}

if ($db['last_checked']+LFS::STALE_TIME < $stamp) {
    Log::warning('{cyan}Using stale data{reset}');
}

// Set package version
if ($pkg_set && strlen($pkg_name) > 0 && strlen($pkg_ver) > 0) {
    if (array_key_exists($pkg_name, $db['packages'])) {
        $db['packages'][$pkg_name]['version'] = $pkg_ver;
        $db_update = true;
    } else {
        Log::error("Package '{$pkg_name}' does not exist");
    }
}

// Compare package versions
$count = 0;
Log::info('{yellow}Showing packages with updates{reset}');
echo "\n";
$title = LFS::DISTRO.' Release Check';
$msg_arr = [];
foreach ($db['packages'] as $key => &$package) {
    if (!array_key_exists('version', $package) || strlen($package['version']) === 0) {
        printf("Package name    : %s\n", $key);
        Log::error('{red}Missing version string{reset}');
        continue;
    }
    if (!array_key_exists('id', $package) || $package['id'] <= 0) {
        continue;
    }

    // Version compare
    $ver1 = $package['version'];
    $ver2 = $package['latest'];
    if ($package['remove_decimals']) {
        $ver1 = str_replace(".", "", $ver1);
    }
    if ($package['replace_underscores']) {
        $ver2 = str_replace("_", ".", $ver2);
    }
    if ($package['skip_plus']) {
        $pos = strpos($ver2, '+');
        if ($pos !== false) {
            $ver2 = substr($ver2, $pos);
        }
    }
    if (strlen($package['prefix']) > 0) {
        $ver2 = ltrim($ver2, $package['prefix']);
    }
    if (strlen($package['suffix']) > 0) {
        $ver2 = rtrim($ver2, $package['suffix']);
    }
    $ver2 = str_replace("-", ".", $ver2);
    if (strlen($ver1) > 0 && strlen($ver2) > 0) {
        $result = RPM::version_compare($ver1, $ver2);
    } else {
        $result = 0;
    }

    if (($result === 0 || $result === 1) && $package['notify']) {
        $package['notify'] = false;
        $db_update = true;
    }

    $eol = false;
    if (strlen($package['eol']) > 0) {
        $_eol = strtotime($package['eol']);
        if ($_eol === false) {
            Log::error('Failed to convert EOL time for '.$package['name']);
        } elseif ($_eol <= $today) {
            $eol = true;
        }
    }

    // Skip packages with result 0 or 1
    if (!$eol && ($result === 0 || $result === 1)) {
        continue;
    }

    if ($result === -1) {
        $count++;
    }

    $msg = "";
    $_name = (strlen($package['project']) > 0 ? ' ('.$package['project'].')' : '');
    $msg .= sprintf("Package name    : %s%s\n", $key, $_name);
    $msg .= sprintf("Homepage        : %s\n", $package['homepage']);
    $msg .= sprintf("Current version : %s\n", $package['version']);
    $msg .= sprintf("Latest version  : %s\n", $package['latest']);
    if ($eol) {
        $msg .= sprintf("End of Life     : %s\n", $package['eol']);
    }
    echo $msg;

    // Send pushover notifications
    if ($pushover_enable && (($result === -1 && $package['notify'] === false) || $eol)) {
        $package['notify'] = true;
        $_msg = PushMessage::fromParts($title, $msg);
        $msg_arr[] = $_msg;
        $db_update = true;
    }
    echo "\n";
}
if (count($msg_arr) > 0) {
    $pushover->sendMessage($msg_arr);
}
unset($package);

if ($count > 0) {
    Log::info(sprintf('%d package(s) with newer versions', $count));
}

if ($db_update) {
    ksort($db['packages']);
    $db_str = ArrayHelper::jsonEncode($db, true)."\n";
    System::atomicWriteFile($db_file, $db_str, 0644);
    Log::info('{yellow}Wrote database{reset}');
}

exit(0);
