<?php

declare(strict_types=1);

namespace PpPhp\Core\Licensing;

use PpPhp\Core\Auth\OwnerCredentials;
use PpPhp\Core\PlatformSettings;

/**
 * Per-instance owner passwords, without a password store.
 *
 * The problem with "a different owner password on every customer install" is
 * usually: where do you keep them all? A table of per-machine passwords is a
 * single juicy target — breach it once, own every install.
 *
 * So we keep NOTHING. Each install's owner password is DERIVED:
 *
 *     password(instance) = KDF(master, instance_id, version)
 *
 * The master is the licence signing key you already guard and back up — nothing
 * new to protect. The server hands each client only the bcrypt HASH of its
 * derived password (inside the signed lease). When you need to log into a
 * particular install, the console re-derives the plaintext live from the key.
 *
 * What this buys:
 *   • Nothing at rest to steal — no password table exists.
 *   • Deterministic — you can always recompute any install's password, even if
 *     the licence database is wiped.
 *   • Isolated — compromising one install yields a bcrypt hash of one password;
 *     it reveals neither the master nor any other install's password.
 *   • Rotatable — bump the version and every install re-derives on its next
 *     daily lease, with nothing to redistribute by hand.
 *
 * SERVER-ONLY. This file (and the master it uses) never ship to a client — a
 * client that could derive passwords would defeat the whole point. It is on the
 * packager's server-only list, and the master lives in storage/license/ which is
 * excluded from every bundle.
 */
final class OwnerDerivation
{
    public const SETTING_VERSION = 'platform.owner_derive_version';
    private const BCRYPT_COST = 11;

    /** The label used for the pre-activation password a fresh install boots with. */
    public const BOOTSTRAP = 'bootstrap';

    private LicenseKeys $keys;

    public function __construct(?LicenseKeys $keys = null)
    {
        $this->keys = $keys ?? new LicenseKeys();
    }

    public function available(): bool
    {
        return $this->keys->exists();
    }

    /**
     * The derivation master, domain-separated from the Ed25519 signing secret.
     *
     * Reusing the signing key means there is exactly ONE secret to guard and back
     * up, not two — and it is one the owner already protects. HMAC with a fixed
     * label keeps this key cryptographically independent of the signatures the
     * same secret produces.
     */
    private function master(): string
    {
        return hash_hmac('sha256', 'ppphp-owner-derivation-v1', $this->keys->secretKey(), true);
    }

    // ── Version (rotation) ────────────────────────────────────────────

    public static function version(): int
    {
        $v = (int) (PlatformSettings::get(self::SETTING_VERSION)['value'] ?? 1);
        return $v >= 1 ? $v : 1;
    }

    /** Bump the version → every install re-derives on its next daily lease. */
    public static function bumpVersion(): int
    {
        $v = self::version() + 1;
        PlatformSettings::set(self::SETTING_VERSION, ['value' => $v, 'setAt' => date('c')]);
        return $v;
    }

    // ── Derivation ────────────────────────────────────────────────────

    /**
     * The plaintext owner password for one install. Deterministic — the same
     * inputs always yield the same password, so it can be revealed on demand and
     * never has to be stored.
     */
    public function instancePassword(string $instanceId, ?int $version = null): string
    {
        $version ??= self::version();
        $raw = hash_hmac('sha256', 'pw:' . $instanceId . ':v' . $version, $this->master(), true);
        return self::format(substr($raw, 0, 15));   // 120 bits, formatted for typing
    }

    /**
     * The bcrypt hash to hand a client. DETERMINISTIC (the salt is derived too),
     * so an unchanged password produces an unchanged hash — no daily write churn
     * on the client, and the fingerprint check stays a clean no-op.
     */
    public function instanceHash(string $instanceId, ?int $version = null): string
    {
        $version ??= self::version();
        $pw   = $this->instancePassword($instanceId, $version);
        $seed = substr(hash_hmac('sha256', 'salt:' . $instanceId . ':v' . $version, $this->master(), true), 0, 16);
        return self::deterministicBcrypt($pw, $seed);
    }

    /**
     * A deterministic hash whose plaintext is never revealed — used in
     * token-only mode to positively DISABLE password login on an install
     * (existing ones included) while token login still works. Stable, so no
     * renewal churn.
     */
    public function disabledHash(string $instanceId, ?int $version = null): string
    {
        $version ??= self::version();
        $pw   = $this->instancePassword('login-disabled:' . $instanceId, $version);
        $seed = substr(hash_hmac('sha256', 'salt:login-disabled:' . $instanceId . ':v' . $version, $this->master(), true), 0, 16);
        return self::deterministicBcrypt($pw, $seed);
    }

    // ── Seed file (mode-aware) ────────────────────────────────────────

    /**
     * The hash a fresh install should boot with, for the current mode:
     *   • unified      → the one vendor password's hash;
     *   • per-instance → a derived BOOTSTRAP password, so even a not-yet-activated
     *                    install has a password the owner can reveal (label
     *                    "bootstrap"), rotating to its real per-instance password
     *                    the moment it activates.
     */
    public function currentSeedHash(): string
    {
        $mode = OwnerCredentials::mode();
        if ($mode === OwnerCredentials::MODE_TOKEN && $this->available()) {
            // Fresh install boots with password login disabled — access is by
            // signed token only. Owner uses a "bootstrap" token until it activates.
            return $this->disabledHash(self::BOOTSTRAP);
        }
        if ($mode === OwnerCredentials::MODE_PER_INSTANCE && $this->available()) {
            return $this->instanceHash(self::BOOTSTRAP);
        }
        return OwnerCredentials::distributableHash();
    }

    /** Write config/owner-seed.json to match the current mode (build-time reconcile). */
    public function refreshSeedFile(): void
    {
        $h = $this->currentSeedHash();
        if ($h !== '') {
            OwnerCredentials::writeSeed($h);
        }
    }

    // ── Encoding helpers ──────────────────────────────────────────────

    /** Crockford base32 (no I/L/O/U), grouped in fours: AAAA-BBBB-… 120 bits → 24 chars. */
    private static function format(string $bytes): string
    {
        $alpha = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
        $bits = 0; $val = 0; $out = '';
        foreach (str_split($bytes) as $c) {
            $val = ($val << 8) | ord($c);
            $bits += 8;
            while ($bits >= 5) {
                $bits -= 5;
                $out .= $alpha[($val >> $bits) & 0x1f];
            }
        }
        if ($bits > 0) $out .= $alpha[($val << (5 - $bits)) & 0x1f];
        return implode('-', str_split($out, 4));
    }

    /** bcrypt with a caller-supplied salt → a stable hash for stable inputs. */
    private static function deterministicBcrypt(string $plain, string $salt16): string
    {
        $salt = substr(self::bcryptB64($salt16), 0, 22);
        $h = crypt($plain, sprintf('$2y$%02d$%s', self::BCRYPT_COST, $salt));
        return (is_string($h) && str_starts_with($h, '$2y$')) ? $h : '';
    }

    /** bcrypt's own base64 alphabet, for building a valid 22-char salt from bytes. */
    private static function bcryptB64(string $bytes): string
    {
        $alpha = './ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
        $bits = 0; $val = 0; $out = '';
        foreach (str_split($bytes) as $c) {
            $val = ($val << 8) | ord($c);
            $bits += 8;
            while ($bits >= 6) {
                $bits -= 6;
                $out .= $alpha[($val >> $bits) & 0x3f];
            }
        }
        if ($bits > 0) $out .= $alpha[($val << (6 - $bits)) & 0x3f];
        return $out;
    }
}
