"""WordPress password-hash verifiers — migrate owners without a reset.

The single hardest migration requirement: owners keep their existing passwords.
These verify-only hashers let Django authenticate a stored WordPress hash, and —
because they sit LAST in ``PASSWORD_HASHERS`` (below the preferred Argon2) —
Django transparently re-hashes the password to Argon2 on the next successful
login. Legacy hashes age out with zero user friction. We never CREATE these
formats.

Storage format: a migrated WordPress ``user_pass`` is stored in Django as
``<algorithm>$<original wp hash>`` so Django's ``identify_hasher`` routes it here:
  - ``wp_phpass$$P$B....``   (phpass portable, pre-WP-6.8)
  - ``wp_bcrypt$$wp$2y$...`` (WordPress 6.8+ bcrypt), or ``wp_bcrypt$$2y$...``
    (vanilla bcrypt from the wp-password-bcrypt plugin).

Correctness: verification mirrors WordPress ``wp_check_password()``, which does
NOT trim the password (only ``wp_hash_password()`` trims — an asymmetry confirmed
empirically). The WP 6.8 pre-hash is bcrypt over base64(HMAC-SHA-384(password,
b"wp-sha384")) — an HMAC with that exact static key, NOT plain SHA-384. Both
paths are verified against real WordPress 6.8.5 output in tests/test_hashers.py.
"""
import base64
import hashlib
import hmac

import bcrypt
from django.contrib.auth.hashers import BasePasswordHasher, mask_hash
from django.utils.crypto import constant_time_compare
from django.utils.translation import gettext_noop as _

# phpass base64 alphabet (its own ordering; not standard base64).
_ITOA64 = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"


def _phpass_encode64(data: bytes, count: int) -> str:
    """Port of phpass PasswordHash::encode64 (bit-exact with the PHP original)."""
    out = []
    i = 0
    while i < count:
        value = data[i]
        i += 1
        out.append(_ITOA64[value & 0x3F])
        if i < count:
            value |= data[i] << 8
        out.append(_ITOA64[(value >> 6) & 0x3F])
        if i >= count:
            break
        i += 1
        if i < count:
            value |= data[i] << 16
        out.append(_ITOA64[(value >> 12) & 0x3F])
        if i >= count:
            break
        i += 1
        out.append(_ITOA64[(value >> 18) & 0x3F])
    return "".join(out)


def phpass_crypt(password: bytes, setting: str) -> str:
    """Port of phpass PasswordHash::crypt_private for portable $P$/$H$ hashes.

    Returns the full hash string, or "*" on a malformed setting (never matches).
    """
    if len(setting) < 12 or setting[:3] not in ("$P$", "$H$"):
        return "*"
    count_log2 = _ITOA64.find(setting[3])
    if count_log2 < 7 or count_log2 > 30:
        return "*"
    count = 1 << count_log2
    salt = setting[4:12].encode("ascii")
    if len(salt) != 8:
        return "*"
    digest = hashlib.md5(salt + password).digest()
    for _iteration in range(count):
        digest = hashlib.md5(digest + password).digest()
    return setting[:12] + _phpass_encode64(digest, 16)


def _strip(encoded: str) -> str:
    """Drop the Django ``<algorithm>$`` prefix, returning the raw WordPress hash."""
    return encoded.split("$", 1)[1]


class WordPressPhpassHasher(BasePasswordHasher):
    """Verify legacy WordPress phpass ($P$/$H$) hashes. Verify-only."""

    algorithm = "wp_phpass"

    def verify(self, password, encoded):
        wp_hash = _strip(encoded)
        computed = phpass_crypt(password.encode("utf-8"), wp_hash)
        return constant_time_compare(computed, wp_hash)

    def encode(self, password, salt):
        raise NotImplementedError(
            "wp_phpass is verify-only; new passwords hash with the preferred hasher."
        )

    def safe_summary(self, encoded):
        wp_hash = _strip(encoded)
        return {
            _("algorithm"): self.algorithm,
            _("iterations"): 1 << _ITOA64.find(wp_hash[3]),
            _("salt"): mask_hash(wp_hash[4:12], show=2),
            _("hash"): mask_hash(wp_hash[12:]),
        }

    def harden_runtime(self, password, encoded):
        pass


class WordPressBcryptHasher(BasePasswordHasher):
    """Verify WordPress 6.8+ ($wp$...) and vanilla ($2y$) bcrypt hashes. Verify-only."""

    algorithm = "wp_bcrypt"

    def verify(self, password, encoded):
        wp_hash = _strip(encoded)
        if wp_hash.startswith("$wp"):
            # WP 6.8+: bcrypt over base64(HMAC-SHA-384(password, "wp-sha384")).
            inner = wp_hash[3:].encode("ascii")
            data = base64.b64encode(
                hmac.new(b"wp-sha384", password.encode("utf-8"), hashlib.sha384).digest()
            )
        elif wp_hash.startswith(("$2a$", "$2b$", "$2y$")):
            # Vanilla bcrypt (wp-password-bcrypt plugin): password straight in.
            inner = wp_hash.encode("ascii")
            data = password.encode("utf-8")
        else:
            return False
        try:
            return bcrypt.checkpw(data, inner)
        except (ValueError, TypeError):
            return False

    def encode(self, password, salt):
        raise NotImplementedError(
            "wp_bcrypt is verify-only; new passwords hash with the preferred hasher."
        )

    def safe_summary(self, encoded):
        wp_hash = _strip(encoded)
        variant = "wp6.8" if wp_hash.startswith("$wp") else "bcrypt"
        return {
            _("algorithm"): self.algorithm,
            _("variant"): variant,
            _("hash"): mask_hash(wp_hash),
        }

    def harden_runtime(self, password, encoded):
        pass
