"""TOTP code generation (like Google Authenticator) with no external deps.

RFC 6238 / RFC 4226. Defaults: SHA1, 6 digits, 30-second step.
"""
from __future__ import annotations

import base64
import hashlib
import hmac
import re
import struct
import time

# A line inside an item block we pull the secret from: "2fa:XXXX", "totp = XXXX", etc.
_SECRET_LINE = re.compile(r"(?:2fa|totp|otp|secret)\s*[:=]\s*(.+)", re.IGNORECASE)


def normalize_secret(secret: str) -> str:
    """Normalize a base32 secret (no spaces, upper case, padding)."""
    cleaned = re.sub(r"\s+", "", secret or "").upper()
    # strip an otpauth link prefix if the whole link was pasted
    if cleaned.startswith("OTPAUTH://"):
        match = re.search(r"[?&]SECRET=([A-Z2-7]+)", cleaned)
        cleaned = match.group(1) if match else ""
    cleaned = cleaned.replace("=", "")
    if not cleaned:
        return ""
    pad = (-len(cleaned)) % 8
    return cleaned + "=" * pad


def extract_secret(block: str) -> str:
    """Find a 2FA-secret line in the block text. Returns '' if not found/invalid."""
    for line in (block or "").splitlines():
        match = _SECRET_LINE.match(line.strip())
        if match:
            candidate = normalize_secret(match.group(1))
            if is_valid_secret(candidate):
                return candidate
    return ""


def is_valid_secret(secret: str) -> bool:
    if not secret:
        return False
    try:
        base64.b32decode(normalize_secret(secret), casefold=True)
        return True
    except Exception:
        return False


def generate(
    secret: str, *, digits: int = 6, period: int = 30, at: float | None = None
) -> tuple[str, int]:
    """Returns (code, how many seconds the code is still valid)."""
    key = base64.b32decode(normalize_secret(secret), casefold=True)
    now = int(at if at is not None else time.time())
    counter = now // period
    msg = struct.pack(">Q", counter)
    digest = hmac.new(key, msg, hashlib.sha1).digest()
    offset = digest[-1] & 0x0F
    code_int = struct.unpack(">I", digest[offset : offset + 4])[0] & 0x7FFFFFFF
    code = str(code_int % (10 ** digits)).zfill(digits)
    remaining = period - (now % period)
    return code, remaining
