"""
Password hashing and credential upgrade utilities.

This module provides helper functions for verifying and upgrading user passwords stored in the database. It supports
multiple historical storage formats and ensures a gradual migration toward a modern PBKDF2-based format.

Supported password formats:

1. PBKDF2 (current format)
2. Legacy salted SHA-256
3. Legacy cleartext

During authentication, older formats can be verified and optionally upgraded to the PBKDF2 format.

The PBKDF2 format stored in the database follows this structure::

    pbkdf2_sha256$<iterations>$<salt_b64>$<derived_key_b64>

"""

import base64
import hashlib
import hmac
import secrets
import sqlite3

PBKDF2_ALG = "pbkdf2_sha256"
PBKDF2_ITERATIONS = 310_000
SALT_BYTES = 32
DK_BYTES = 32  # 256-bit

LOGIN_FAILURE_DELAY = 3.0


def verifyPasswordPbkdf2(password: bytes, stored: str) -> bool:
    """
    Verify a password against a stored PBKDF2 hash.

    :param password: The plaintext password as bytes.
    :param stored: The stored PBKDF2 password string.

    :return: ``True`` if the password matches the stored hash, otherwise ``False``.
    """
    try:
        algorithm, iter_s, salt_b64, dk_b64 = stored.split("$", 3)
        if algorithm != PBKDF2_ALG:
            return False
        iterations = int(iter_s)

        # restore padding
        def _b64d(s: str) -> bytes:
            pad = "=" * ((4 - (len(s) % 4)) % 4)
            return base64.urlsafe_b64decode(s + pad)

        salt = _b64d(salt_b64)
        dk_expected = _b64d(dk_b64)
        dk = hashlib.pbkdf2_hmac("sha256", password, salt, iterations, dklen=len(dk_expected))
        return hmac.compare_digest(dk, dk_expected)
    except Exception:
        return False


def hashPasswordPbkdf2(password: bytes, salt: bytes, iterations: int = PBKDF2_ITERATIONS) -> str:
    """
    Generate a PBKDF2 password hash.

    :param password: The plaintext password as bytes.
    :param salt: Cryptographic salt used for the PBKDF2 derivation.
    :param iterations: Number of PBKDF2 iterations. Defaults to :data:`PBKDF2_ITERATIONS`.

    :return: Encoded PBKDF2 password string suitable for database storage.
    """
    dk = hashlib.pbkdf2_hmac("sha256", password, salt, iterations, dklen=DK_BYTES)
    salt_b64 = base64.urlsafe_b64encode(salt).decode("ascii").rstrip("=")
    dk_b64 = base64.urlsafe_b64encode(dk).decode("ascii").rstrip("=")
    return f"{PBKDF2_ALG}${iterations}${salt_b64}${dk_b64}"


def generateSalt() -> bytes:
    """
    Generate a cryptographically secure random salt.

    :return: Random salt of length `SALT_BYTES`.
    """
    return secrets.token_bytes(SALT_BYTES)


def ensurePasswordSeedSchema(connection: sqlite3.Connection) -> None:
    """
    Ensure the database schema supports the legacy ``seed`` column.

    This function performs two tasks:

    1. Ensures the ``passwords`` table contains the ``seed`` column.
    2. Recreates the ``view_users_passwords`` view so that the ``seed`` column is exposed.

    This allows the system to maintain compatibility with legacy password formats during migration.

    :param connection: Active SQLite database connection.
    """
    cursor = connection.cursor()

    columns = cursor.execute("PRAGMA table_info(passwords)").fetchall()
    hasSeedColumn = any(column[1] == "seed" for column in columns)

    if not hasSeedColumn:
        cursor.execute("ALTER TABLE passwords ADD COLUMN seed TEXT")

    cursor.execute("DROP VIEW IF EXISTS view_users_passwords")
    cursor.execute(
        """
        CREATE VIEW view_users_passwords AS
        SELECT
            users.userId,
            users.userName,
            users.name,
            users.surName,
            passwords.passwordId,
            passwords.password,
            passwords.seed
        FROM
            passwords
            JOIN users ON users.userId = passwords.users_userId
        """
    )


def applyCredentialUpgrades(connection: sqlite3.Connection) -> None:
    """
    Upgrade legacy passwords stored in cleartext to PBKDF2. This function performs an **offline credential migration**:

    * Cleartext passwords are converted to PBKDF2.
    * PBKDF2 passwords are left unchanged.
    * Legacy salted SHA-256 passwords remain unchanged because the original plaintext password is not available.

    Upgrading salted SHA-256 credentials must instead occur during successful authentication.

    NOTE: Does not commit the transaction. This needs to be done from outside.

    :param connection: Active SQLite database connection.
    """
    cursor = connection.cursor()

    ensurePasswordSeedSchema(connection)

    passwordRows = cursor.execute(
        """
        SELECT passwordId, users_userId, password, seed
        FROM passwords
        """
    ).fetchall()

    for passwordId, userId, storedPassword, seed in passwordRows:
        if storedPassword is None:
            continue

        if isinstance(storedPassword, str) and storedPassword.startswith(f"{PBKDF2_ALG}$"):
            continue

        # Legacy salted+hashed row: cannot be upgraded offline
        if seed is not None and str(seed).strip() != "":
            continue

        # Legacy cleartext row: upgrade directly to PBKDF2
        salt = generateSalt()
        pbkdf2Hash = hashPasswordPbkdf2(storedPassword.encode("utf-8"), salt)

        cursor.execute(
            """
            UPDATE passwords
            SET password = ?, seed = NULL
            WHERE passwordId = ? AND users_userId = ?
            """,
            (pbkdf2Hash, passwordId, userId),
        )


def verifyStoredPassword(password: bytes, storedPassword: str, seed: str | None) -> bool:
    """
    Verify a password against any supported storage format.

    Supported formats:

    * PBKDF2 (current format)
    * Legacy salted SHA-256
    * Legacy cleartext

    :param password: Plaintext password as bytes.
    :param stored_password: Password value stored in the database.
    :param seed: Legacy salt used by the historical SHA-256 format.

    :return: ``True`` if the password is valid.
    """
    if storedPassword is None:
        return False

    # Current format
    if storedPassword.startswith(f"{PBKDF2_ALG}$"):
        return verifyPasswordPbkdf2(password, storedPassword)

    # Legacy salted+hashed format:
    # finalHash = sha256(sha256(password) + seed_bytes)
    if seed is not None and str(seed).strip() != "":
        try:
            seedBytes = bytes.fromhex(seed)
            passwordHashed = hashlib.sha256(password).hexdigest()
            dataToHash = passwordHashed.encode("utf-8") + seedBytes
            finalHash = hashlib.sha256(dataToHash).hexdigest()
            return hmac.compare_digest(finalHash, storedPassword)
        except Exception:
            return False

    # Legacy cleartext
    try:
        return hmac.compare_digest(storedPassword.encode("utf-8"), password)
    except Exception:
        return False


def passwordNeedsUpgrade(storedPassword: str) -> bool:
    """
    Determine whether a stored password should be upgraded.

    A password requires upgrading when it is not stored in the current PBKDF2 format.

    :param stored_password: Password value stored in the database.

    :return: ``True`` if the password should be upgraded.
    """
    if storedPassword is None:
        return False

    return not storedPassword.startswith(f"{PBKDF2_ALG}$")
