"""
Two-Factor Authentication logic for YOUR COMPANY CMS.

Methods supported:
  - TOTP  : Google Authenticator / Microsoft Authenticator (RFC 6238)
  - EMAIL : 6-digit OTP sent to user's email
  - SMS   : 6-digit OTP sent via Esendex (formerly VoodooSMS / Commify)

reCAPTCHA:
  - v3 hidden on all forms (score-based, invisible to user)
  - v2 visual challenge escalated automatically when v3 score < threshold
    (same pattern as Binance)
"""

import pyotp
import qrcode
import qrcode.image.svg
import base64
import secrets
import hashlib
import random
import string
from io import BytesIO
from datetime import timedelta

from django.conf import settings
from django.core.cache import cache
from django.core.mail import send_mail
from django.utils import timezone


# ─── TOTP (Authenticator App) ────────────────────────────────────────────────

def generate_totp_secret():
    """Generate a new base32 secret for TOTP setup."""
    return pyotp.random_base32()


from apps.pages.branding import company_name


def get_totp_uri(secret, user_email, issuer='The Panel'):
    """Return the otpauth:// URI for QR code generation."""
    totp = pyotp.TOTP(secret)
    return totp.provisioning_uri(name=user_email, issuer_name=issuer)


def get_totp_qr_base64(secret, user_email):
    """Generate a QR code PNG and return as base64 string for inline <img>."""
    uri = get_totp_uri(secret, user_email)
    qr = qrcode.make(uri)
    buffer = BytesIO()
    qr.save(buffer, format='PNG')
    return base64.b64encode(buffer.getvalue()).decode()


def verify_totp(secret, code):
    """Verify a TOTP code. Allows 1 step tolerance (30s window each side)."""
    totp = pyotp.TOTP(secret)
    return totp.verify(code, valid_window=1)


# ─── Email OTP ───────────────────────────────────────────────────────────────

def _otp_cache_key(user_id, method):
    return f'2fa_otp_{method}_{user_id}'


def send_email_otp(user, session=None):
    """Generate an 8-digit code, store in session (preferred) or cache, then email it."""
    code = ''.join(random.choices(string.digits, k=8))
    if session is not None:
        session['2fa_email_otp'] = code
        session['2fa_email_otp_uid'] = user.pk
    else:
        cache.set(_otp_cache_key(user.pk, 'email'), code, timeout=600)

    send_mail(
        subject='Your verification code',
        message=(
            f'Hi {user.first_name or user.username},\n\n'
            f'Your verification code is: {code}\n\n'
            f'This code expires in 10 minutes. Do not share it with anyone.\n\n'
            f'If you did not request this, please contact support@example.com immediately.\n\n'
            f'{company_name()} Security'
        ),
        from_email=_outgoing(),
        recipient_list=[user.email],
        fail_silently=False,
    )
    return True


def verify_email_otp(user, code, session=None):
    """Verify email OTP from session (preferred) or cache."""
    code = code.strip()
    if session is not None:
        stored = session.get('2fa_email_otp')
        uid    = session.get('2fa_email_otp_uid')
        if stored and stored == code and uid == user.pk:
            session.pop('2fa_email_otp', None)
            session.pop('2fa_email_otp_uid', None)
            return True
        return False
    # fallback: cache
    cache_key = _otp_cache_key(user.pk, 'email')
    stored = cache.get(cache_key)
    if stored and stored == code:
        cache.delete(cache_key)
        return True
    return False


# ─── SMS OTP ─────────────────────────────────────────────────────────────────

def send_sms_otp(user, session=None):
    """Generate an 8-digit OTP, store in session (preferred) or cache, send via SMS."""
    if not user.phone:
        return False

    code = ''.join(random.choices(string.digits, k=8))
    if session is not None:
        session['2fa_sms_otp'] = code
        session['2fa_sms_otp_uid'] = user.pk
    else:
        cache.set(_otp_cache_key(user.pk, 'sms'), code, timeout=600)

    from apps.pages.sms_utils import send_sms
    message = (
        f'Your {company_name()} code: {code}. '
        f'Expires in 10 minutes. Do not share it.'
    )
    return send_sms(user.phone, message)


def verify_sms_otp(user, code, session=None):
    """Verify SMS OTP from session (preferred) or cache."""
    code = code.strip()
    if session is not None:
        stored = session.get('2fa_sms_otp')
        uid    = session.get('2fa_sms_otp_uid')
        if stored and stored == code and uid == user.pk:
            session.pop('2fa_sms_otp', None)
            session.pop('2fa_sms_otp_uid', None)
            return True
        return False
    cache_key = _otp_cache_key(user.pk, 'sms')
    stored = cache.get(cache_key)
    if stored and stored == code:
        cache.delete(cache_key)
        return True
    return False


# ─── Backup Codes ────────────────────────────────────────────────────────────

def generate_backup_codes(count=8):
    """
    Generate one-time backup codes.
    Returns list of plain codes. Store hashed versions in DB.
    """
    codes = []
    for _ in range(count):
        code = '-'.join([
            ''.join(random.choices(string.ascii_uppercase + string.digits, k=4))
            for _ in range(3)
        ])
        codes.append(code)
    return codes


def hash_backup_code(code):
    return hashlib.sha256(code.strip().upper().encode()).hexdigest()


def verify_backup_code(two_fa_obj, code):
    """
    Check if code matches any stored hash. If yes, mark it used.
    Returns True/False.
    """
    hashed = hash_backup_code(code)
    for bc in two_fa_obj.backup_codes_list:
        if bc.get('hash') == hashed and not bc.get('used'):
            bc['used'] = True
            two_fa_obj.save(update_fields=['backup_codes'])
            return True
    return False


# ─── reCAPTCHA ───────────────────────────────────────────────────────────────

import requests as http_requests


def verify_recaptcha_v3(token, action=None):
    """
    Verify reCAPTCHA v3 token.
    Returns (success: bool, score: float)
    score >= 0.5 = human, < 0.5 = suspicious
    """
    from apps.pages.api_keys import get_recaptcha_secret_key
    secret = get_recaptcha_secret_key()
    if not secret or not token:
        return True, 1.0  # Skip in dev / if not configured

    try:
        resp = http_requests.post(
            'https://www.google.com/recaptcha/api/siteverify',
            data={'secret': secret, 'response': token},
            timeout=5,
        )
        data = resp.json()
        score = data.get('score', 0.0)
        success = data.get('success', False)
        # Optionally check action
        if action and data.get('action') != action:
            return False, score
        threshold = getattr(settings, 'RECAPTCHA_MINIMUM_SCORE', 0.5)
        return success and score >= threshold, score
    except Exception:
        return False, 0.0


def verify_recaptcha_v2(token):
    """Verify reCAPTCHA v2 (checkbox / visual challenge) token."""
    from apps.pages.api_keys import get_recaptcha_v2_secret_key
    secret = get_recaptcha_v2_secret_key()
    if not secret or not token:
        return False

    try:
        resp = http_requests.post(
            'https://www.google.com/recaptcha/api/siteverify',
            data={'secret': secret, 'response': token},
            timeout=5,
        )
        return resp.json().get('success', False)
    except Exception:
        return False


# ─── 2FA session helpers ─────────────────────────────────────────────────────

def mark_2fa_pending(request, user_id):
    """After password check passes, mark session as pending 2FA."""
    request.session['2fa_pending_user_id'] = user_id
    request.session['2fa_pending_at'] = timezone.now().isoformat()


def get_2fa_pending_user(request):
    """Return user_id if 2FA is pending and not expired (5 min window)."""
    user_id = request.session.get('2fa_pending_user_id')
    pending_at = request.session.get('2fa_pending_at')
    if not user_id or not pending_at:
        return None
    from datetime import datetime
    dt = datetime.fromisoformat(pending_at)
    if timezone.is_naive(dt):
        dt = timezone.make_aware(dt)
    if timezone.now() - dt > timedelta(minutes=5):
        # Expired — clear it
        request.session.pop('2fa_pending_user_id', None)
        request.session.pop('2fa_pending_at', None)
        return None
    return user_id


def clear_2fa_pending(request):
    request.session.pop('2fa_pending_user_id', None)
    request.session.pop('2fa_pending_at', None)


def _outgoing():
    """The sending address, from Settings. Never .env."""
    from apps.pages.email_backend import outgoing_address
    return outgoing_address()
