"""Forgot-password service.

Password recovery is built **entirely** on the generic verification framework
(:mod:`app.auth.otp_service`) via
:data:`~app.models.enums.VerificationPurpose.PASSWORD_RESET`. Every OTP concern —
generation, HMAC hashing, expiry, single-use, rate limiting, email delivery and
the issued/verified/failed/expired audit lines — is handled there. This module
adds no OTP logic and no second password validator; it only orchestrates the
three recovery steps and applies the password change.
"""

from __future__ import annotations

from flask import current_app
from werkzeug.security import generate_password_hash

from app.auth.forms import password_meets_policy, _PASSWORD_POLICY_MESSAGE
from app.auth.otp_service import (
    OtpRateLimited,
    invalidate_codes,
    issue_verification_code,
    verify_code,
)
from app.extensions import db
from app.models import User, VerificationPurpose
from app.models.base import utcnow

PURPOSE = VerificationPurpose.PASSWORD_RESET


def _find_user(email: str) -> User | None:
    normalized = (email or "").strip().lower()
    return db.session.scalar(db.select(User).filter_by(email=normalized).limit(1))


def start_password_reset(email: str) -> None:
    """Issue a PASSWORD_RESET code when the account exists.

    Returns nothing and never signals whether the email is registered — the
    caller always shows the same generic confirmation (no account enumeration).
    Audits the request; a rate-limited request is logged and silently absorbed
    so the response stays uniform.
    """
    user = _find_user(email)
    if user is None:
        # No email address is logged (audit must not contain email contents).
        current_app.logger.info("password_reset.requested account=unknown")
        return
    try:
        issue_verification_code(user, PURPOSE)
        current_app.logger.info("password_reset.requested user=%s", user.id)
    except OtpRateLimited:
        current_app.logger.warning("password_reset.rate_limited user=%s", user.id)


def verify_password_reset(user: User, code: str) -> tuple[bool, str | None]:
    """Verify (and consume) a PASSWORD_RESET code — delegates to the framework.

    The verified/failed/expired audit line is emitted by the framework with
    ``purpose=password_reset``."""
    return verify_code(user, PURPOSE, code)


def complete_password_reset(user: User, new_password: str) -> tuple[bool, str | None]:
    """Set the new password and clear any remaining PASSWORD_RESET codes.

    Runs after the code was verified (single-use) in the previous step. Reuses
    the shared password policy and Werkzeug hashing — no duplicated validation.
    Only PASSWORD_RESET codes are invalidated, preserving purpose isolation for
    EMAIL_VERIFICATION / EMAIL_CHANGE / LOGIN_VERIFICATION / TWO_FACTOR.

    Security: a password reset **revokes every trusted device** on the account,
    so a device an attacker may have trusted can no longer bypass the login-OTP
    step. The browser cookie is cleared by the route (which has the response).
    """
    if not password_meets_policy(new_password):
        return False, _PASSWORD_POLICY_MESSAGE

    user.password_hash = generate_password_hash(new_password)
    user.last_password_change = utcnow()   # drives the Security timeline entry
    db.session.commit()

    invalidate_codes(user, PURPOSE)

    # Revoke trusted devices (reuses the trusted-device service; audited as
    # TRUSTED_DEVICES_REVOKED_AFTER_PASSWORD_RESET). Imported locally to keep the
    # module's import graph flat.
    from app.auth.services import AuthEvent, log_auth_event
    from app.auth.trusted_devices import revoke_all_for_user

    log_auth_event(AuthEvent.PASSWORD_RESET_COMPLETED, user=user)
    revoked = revoke_all_for_user(user, event=AuthEvent.TRUSTED_DEVICES_REVOKED_AFTER_PASSWORD_RESET)
    current_app.logger.info(
        "password_reset.completed user=%s trusted_devices_revoked=%s", user.id, revoked
    )
    return True, None
