from mojo.helpers import dates, crypto
from mojo import errors as merrors
from mojo.helpers.settings import settings
from mojo.apps.account.models.user import User

PASSWORD_RESET_TOKEN_TTL = settings.get("PASSWORD_RESET_TOKEN_TTL", 3600)
PASSWORD_RESET_CODE_TTL = settings.get("PASSWORD_RESET_CODE_TTL", 600)
MAGIC_LOGIN_TOKEN_TTL = settings.get("MAGIC_LOGIN_TOKEN_TTL", 3600)
EMAIL_VERIFY_TOKEN_TTL = settings.get("EMAIL_VERIFY_TOKEN_TTL", 86400)
PHONE_VERIFY_CODE_TTL = settings.get("PHONE_VERIFY_CODE_TTL", 600)

# Token kind prefixes — visible to the webapp before any decoding
KIND_PASSWORD_RESET = "pr"
KIND_MAGIC_LOGIN = "ml"
KIND_EMAIL_VERIFY = "ev"

# Secrets keys per kind — kept separate so they don't invalidate each other
_JTI_KEYS = {
    KIND_PASSWORD_RESET: "password_reset_jti",
    KIND_MAGIC_LOGIN: "magic_login_jti",
    KIND_EMAIL_VERIFY: "email_verify_jti",
}
_TS_KEYS = {
    KIND_PASSWORD_RESET: "password_reset_ts",
    KIND_MAGIC_LOGIN: "magic_login_ts",
    KIND_EMAIL_VERIFY: "email_verify_ts",
}
_TTL = {
    KIND_PASSWORD_RESET: PASSWORD_RESET_TOKEN_TTL,
    KIND_MAGIC_LOGIN: MAGIC_LOGIN_TOKEN_TTL,
    KIND_EMAIL_VERIFY: EMAIL_VERIFY_TOKEN_TTL,
}


# -----------------------------------------------------------------
# Internal helpers
# -----------------------------------------------------------------

def _generate(user, kind):
    """
    Generate a signed token for the given kind.

    Returned string format:  <kind>:<hex_encoded_payload><6-char-sig>
    The kind prefix is readable by the webapp without any decoding.
    """
    now_ts = int(dates.utcnow().timestamp())
    jti = crypto.random_string(12, True, True, False)
    token = crypto.b64_encode({"uid": user.pk, "ts": now_ts, "jti": jti, "kind": kind})
    sig = crypto.sign(token, user.get_auth_key())
    user.set_secret(_JTI_KEYS[kind], jti)
    user.set_secret(_TS_KEYS[kind], now_ts)
    user.save(update_fields=["mojo_secrets", "modified"])
    hex_token = token.encode("utf-8").hex() + sig[-6:]
    return f"{kind}:{hex_token}"


def _verify(raw_token, expected_kind=None):
    """
    Verify a token generated by _generate().
    Returns the User on success, raises ValueException on failure.

    expected_kind: if provided, the token's kind must match exactly.
    """
    orig_token = raw_token
    user = None
    try:
        # Parse kind prefix
        if ":" not in raw_token:
            raise merrors.ValueException("Invalid token format")
        kind, hex_token = raw_token.split(":", 1)
        if kind not in _JTI_KEYS:
            raise merrors.ValueException("Unknown token kind")
        if expected_kind is not None and kind != expected_kind:
            raise merrors.ValueException("Invalid token kind")

        tsig = hex_token[-6:]
        hex_token = hex_token[:-6]
        token = bytes.fromhex(hex_token).decode("utf-8")
        obj = crypto.b64_decode(token)
        if not isinstance(obj, dict) or "uid" not in obj or "ts" not in obj or "jti" not in obj:
            raise merrors.ValueException("Invalid token")
        if obj.get("kind") != kind:
            raise merrors.ValueException("Token kind mismatch")

        user = User.objects.get(pk=obj["uid"])
        sig = crypto.sign(token, user.get_auth_key())
        if sig[-6:] != tsig:
            user.report_incident(
                details=f"{user.username} invalid token signature (kind={kind})",
                event_type="invalid_token")
            raise merrors.ValueException("Invalid token")

        now_ts = int(dates.utcnow().timestamp())
        ttl = _TTL[kind]
        if now_ts - int(obj["ts"]) > int(ttl):
            user.report_incident(
                details=f"{user.username} expired token (kind={kind})",
                event_type="expired_token")
            raise merrors.ValueException("Expired token")

        expected_jti = user.get_secret(_JTI_KEYS[kind])
        if not expected_jti or expected_jti != obj["jti"]:
            user.report_incident(
                details=f"{user.username} token jti mismatch or reused (kind={kind})",
                event_type="invalid_token")
            raise merrors.ValueException("Invalid token")

        # Consume — single use
        user.set_secret(_JTI_KEYS[kind], None)
        user.save(update_fields=["mojo_secrets", "modified"])
        return user

    except merrors.ValueException:
        raise
    except Exception as err:
        if user:
            user.report_incident(
                details=f"{user.username} invalid token",
                event_type="token:unknown",
                error=str(err),
                level=8, token=orig_token)
        else:
            User.class_report_incident(
                details="token error",
                event_type="token:unknown",
                error=str(err),
                level=8, token=orig_token)
        raise merrors.ValueException("Invalid token")


# -----------------------------------------------------------------
# Public API
# -----------------------------------------------------------------

def generate_password_reset_token(user):
    """Generate a password-reset token (kind=pr)."""
    return _generate(user, KIND_PASSWORD_RESET)


def verify_password_reset_token(token):
    """Verify a password-reset token and return the User."""
    return _verify(token, expected_kind=KIND_PASSWORD_RESET)


def generate_magic_login_token(user):
    """Generate a magic-login token (kind=ml)."""
    return _generate(user, KIND_MAGIC_LOGIN)


def verify_magic_login_token(token):
    """Verify a magic-login token and return the User."""
    return _verify(token, expected_kind=KIND_MAGIC_LOGIN)


def generate_email_verify_token(user):
    """Generate an email verification token (kind=ev). TTL defaults to 24h."""
    return _generate(user, KIND_EMAIL_VERIFY)


def verify_email_verify_token(token):
    """Verify an email verification token and return the User."""
    return _verify(token, expected_kind=KIND_EMAIL_VERIFY)


def generate_phone_verify_code(user):
    """
    Generate a 6-digit SMS verification code and store it in user secrets.
    Returns the code string so the caller can pass it to phonehub.send_sms().
    """
    code = crypto.random_string(6, True, False, False)
    user.set_secret("phone_verify_code", code)
    user.set_secret("phone_verify_ts", int(dates.utcnow().timestamp()))
    user.save(update_fields=["mojo_secrets", "modified"])
    return code


def verify_phone_verify_code(user, code):
    """
    Verify the 6-digit code for a user.
    Raises ValueException on mismatch or expiry.
    Consumes the code on success (single-use).
    """
    stored = user.get_secret("phone_verify_code")
    stored_ts = int(user.get_secret("phone_verify_ts") or 0)
    now_ts = int(dates.utcnow().timestamp())
    if not stored or code != stored:
        user.report_incident(
            details=f"{user.username} invalid phone verify code",
            event_type="phone_verify:invalid")
        raise merrors.ValueException("Invalid code")
    if now_ts - stored_ts > int(PHONE_VERIFY_CODE_TTL):
        user.report_incident(
            details=f"{user.username} expired phone verify code",
            event_type="phone_verify:expired")
        raise merrors.ValueException("Expired code")
    # Consume — single use
    user.set_secret("phone_verify_code", None)
    user.set_secret("phone_verify_ts", None)
    user.save(update_fields=["mojo_secrets", "modified"])


# Legacy aliases — kept so existing call sites don't break
def generate_token(user):
    return generate_password_reset_token(user)


def validate_token(hex_token):
    return verify_password_reset_token(hex_token)
