<?php

declare(strict_types=1);

namespace Instinctor\Core\Auth;

/**
 * PhpassCompat — verify phpass portable hashes (WordPress password format).
 *
 * WordPress uses phpass (Portable PHP Password Hashing Framework) with:
 *   - Iteration count: 2^8 = 256 (log2 cost encoded as 'S' = 8th char in itoa64)
 *   - Hash prefix: '$P$'
 *
 * This class only implements CheckPassword() — we never create phpass hashes,
 * only verify them on first login after WP→Standalone migration, then
 * immediately re-hash to bcrypt.
 *
 * Algorithm mirrors Openwall phpass 0.3 (the exact version WP ships).
 *
 * @since 3.20.0
 */

if (!defined('ABSPATH')) exit;
final class PhpassCompat
{
    private const ITOA64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';

    /**
     * Verify a password against a phpass hash.
     *
     * @param string $password   Plaintext password
     * @param string $storedHash Hash from instinctor_users.password_hash
     * @return bool
     */
    public static function check(string $password, string $storedHash): bool
    {
        // If it looks like bcrypt, let PHP's password_verify handle it
        if (str_starts_with($storedHash, '$2y$') || str_starts_with($storedHash, '$2a$')) {
            return password_verify($password, $storedHash);
        }

        // phpass portable hash: $P$XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX (34 chars)
        if (!str_starts_with($storedHash, '$P$') && !str_starts_with($storedHash, '$H$')) {
            return false;
        }

        $hash = self::cryptPrivate($password, $storedHash);
        return hash_equals($storedHash, $hash);
    }

    private static function cryptPrivate(string $password, string $setting): string
    {
        $output = '*0';

        if (substr($setting, 0, 3) !== '$P$' && substr($setting, 0, 3) !== '$H$') {
            return $output;
        }

        $count_log2 = strpos(self::ITOA64, $setting[3]);
        if ($count_log2 < 7 || $count_log2 > 30) {
            return $output;
        }

        $count = 1 << $count_log2;
        $salt  = substr($setting, 4, 8);

        if (strlen($salt) !== 8) {
            return $output;
        }

        $hash = md5($salt . $password, true);
        do {
            $hash = md5($hash . $password, true);
        } while (--$count);

        $output  = substr($setting, 0, 12);
        $output .= self::encode64($hash, 16);

        return $output;
    }

    private static function encode64(string $input, int $count): string
    {
        $output = '';
        $i = 0;

        do {
            $value = ord($input[$i++]);
            $output .= self::ITOA64[$value & 0x3f];
            if ($i < $count) {
                $value |= ord($input[$i]) << 8;
            }
            $output .= self::ITOA64[($value >> 6) & 0x3f];
            if ($i++ >= $count) {
                break;
            }
            if ($i < $count) {
                $value |= ord($input[$i]) << 16;
            }
            $output .= self::ITOA64[($value >> 12) & 0x3f];
            if ($i++ >= $count) {
                break;
            }
            $output .= self::ITOA64[($value >> 18) & 0x3f];
        } while ($i < $count);

        return $output;
    }
}
