<?php

declare(strict_types=1);

namespace PpPhp\Core\Licensing;

/**
 * A FUZZY machine fingerprint.
 *
 * Hard binding (one licence ↔ one IP, or ↔ one exact hash) is the classic
 * licensing mistake: a customer moves host, their provider rotates an IP, PHP
 * gets patched — and a paying user is locked out of software that was working
 * fine a minute ago. Support cost goes up, security does not.
 *
 * So this collects several INDEPENDENT traits and compares them by weight. The
 * instance's secret remains the real identity; the fingerprint is a signal:
 *   • small drift  → normal (upgrade, migration)  → recorded, allowed
 *   • total change → the credentials moved somewhere else → flagged for the
 *     licence holder to look at, still allowed (a restore-from-backup looks
 *     exactly like a clone, and we refuse to break real customers over a guess)
 *
 * Nothing personally identifying leaves the machine: paths, DB names and hosts
 * are hashed before they are sent.
 */
final class Fingerprint
{
    /** Trait weights — must total 100. */
    private const WEIGHTS = [
        'host'   => 30,   // public hostname the app answers on
        'root'   => 25,   // hash of the install path
        'os'     => 15,   // operating system family
        'php'    => 10,   // PHP major.minor
        'srv'    => 10,   // server software family
        'db'     => 10,   // hash of the database name
    ];

    /** Below this score the two fingerprints are treated as different machines. */
    public const CLONE_THRESHOLD = 40;

    /** Collect this installation's traits. */
    public static function collect(?string $basePath = null): array
    {
        $basePath ??= defined('BASE_PATH') ? constant('BASE_PATH') : dirname(__DIR__, 2);

        $host = (string) ($_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_NAME'] ?? php_uname('n'));
        $host = strtolower(preg_replace('/:\d+$/', '', $host) ?? '');

        $srv = (string) ($_SERVER['SERVER_SOFTWARE'] ?? 'cli');
        $srv = strtolower((string) preg_replace('/[\/ ].*$/', '', $srv));   // "Apache/2.4.62 (Unix)" → "apache"

        return [
            'host' => $host,
            'root' => substr(hash('sha256', $basePath), 0, 16),
            'os'   => strtolower(php_uname('s')),
            'php'  => PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION,
            'srv'  => $srv,
            'db'   => substr(hash('sha256', self::dbName()), 0, 16),
        ];
    }

    private static function dbName(): string
    {
        try {
            $cfg = @include (defined('BASE_PATH') ? constant('BASE_PATH') : dirname(__DIR__, 2)) . '/config/database.php';
            if (is_array($cfg)) {
                return (string) ($cfg['database'] ?? $cfg['dbname'] ?? $cfg['name'] ?? '');
            }
        } catch (\Throwable $e) {
            // fall through — an unreadable config just means one weak trait
        }
        return '';
    }

    /**
     * Similarity of two fingerprints, 0–100.
     * Missing traits on either side simply do not contribute.
     */
    public static function score(array $a, array $b): int
    {
        $possible = 0;
        $matched  = 0;
        foreach (self::WEIGHTS as $k => $w) {
            $av = trim((string) ($a[$k] ?? ''));
            $bv = trim((string) ($b[$k] ?? ''));
            if ($av === '' || $bv === '') continue;
            $possible += $w;
            if (hash_equals($av, $bv)) $matched += $w;
        }
        if ($possible === 0) return 0;
        return (int) round($matched * 100 / $possible);
    }

    /** Which traits changed between two fingerprints (for the audit trail). */
    public static function drift(array $a, array $b): array
    {
        $changed = [];
        foreach (array_keys(self::WEIGHTS) as $k) {
            $av = (string) ($a[$k] ?? '');
            $bv = (string) ($b[$k] ?? '');
            if ($av !== '' && $bv !== '' && $av !== $bv) $changed[] = $k;
        }
        return $changed;
    }
}
