import base64
import hashlib
import io
import os
import secrets
from datetime import datetime, timedelta, timezone

import pyotp

from ..models.mfa import MFACredential
from ..models.mfa_fields import MFAFields


def _get_fernet():
    key = os.environ.get('MFA_ENCRYPTION_KEY')
    if key is None:
        raise ValueError('MFA_ENCRYPTION_KEY environment variable is not set')
    from cryptography.fernet import Fernet
    return Fernet(key.encode() if isinstance(key, str) else key)


def _encrypt_secret(secret: str) -> str:
    f = _get_fernet()
    return f.encrypt(secret.encode()).decode()


def _decrypt_secret(encrypted: str) -> str:
    f = _get_fernet()
    return f.decrypt(encrypted.encode()).decode()


def _hash_backup_code(code: str) -> str:
    return hashlib.sha256(code.encode()).hexdigest()


class MFAService:

    def generate_totp_enrollment(self, user_uid: str, issuer: str = None) -> dict:
        """
        Generate a TOTP enrollment secret (stored temporarily in Firestore).
        Returns {'uri': ..., 'qr_b64': ..., 'secret': ...}
        """
        if issuer is None:
            issuer = os.environ.get('OAUTH2_ISSUER', 'Adafri')
        secret = pyotp.random_base32()
        encrypted_secret = _encrypt_secret(secret)

        # Store as pending enrollment doc (10-min TTL handled by caller or Firestore TTL policy)
        from ....base.firebase_collection import getTimestamp
        from ..models.mfa import MFACredential
        pending_ref = MFACredential().collection('mfa_pending_enrollment').document(user_uid)
        expires_at = (datetime.now(timezone.utc) + timedelta(minutes=10)).isoformat()
        pending_ref.set({
            'user_uid': user_uid,
            'totp_secret': encrypted_secret,
            'expires_at': expires_at,
            'createdAt': getTimestamp(),
        })

        totp = pyotp.TOTP(secret)
        uri = totp.provisioning_uri(name=user_uid, issuer_name=issuer)

        try:
            import qrcode
            qr = qrcode.QRCode(box_size=6, border=2)
            qr.add_data(uri)
            qr.make(fit=True)
            img = qr.make_image(fill_color='black', back_color='white')
            buf = io.BytesIO()
            img.save(buf, format='PNG')
            qr_b64 = base64.b64encode(buf.getvalue()).decode()
        except ImportError:
            qr_b64 = ''

        return {'uri': uri, 'qr_b64': qr_b64, 'secret': secret}

    def confirm_enrollment(self, user_uid: str, code: str) -> list:
        """
        Verify TOTP code against pending enrollment secret, save MFACredential.
        Returns list of 10 plaintext backup codes (hashed copies stored in Firestore).
        """
        pending_ref = MFACredential().collection('mfa_pending_enrollment').document(user_uid)
        pending_doc = pending_ref.get()
        if not pending_doc.exists:
            raise ValueError('No pending enrollment found for this user')

        pending = pending_doc.to_dict()
        expires_at = pending.get('expires_at', '')
        if expires_at:
            try:
                if datetime.fromisoformat(expires_at) < datetime.now(timezone.utc):
                    raise ValueError('Enrollment expired — please restart enrollment')
            except ValueError:
                pass

        encrypted_secret = pending.get('totp_secret', '')
        secret = _decrypt_secret(encrypted_secret)

        totp = pyotp.TOTP(secret)
        if not totp.verify(code, valid_window=1):
            raise ValueError('Invalid TOTP code')

        # Generate 10 backup codes
        raw_codes = [secrets.token_hex(4).upper() + '-' + secrets.token_hex(4).upper() for _ in range(10)]
        hashed_codes = [_hash_backup_code(c) for c in raw_codes]

        # Save credential
        cred = MFACredential()
        existing = cred.get_by_user(user_uid)
        if existing:
            existing.document_reference().set({
                'totp_secret': encrypted_secret,
                'backup_codes': hashed_codes,
                'is_enabled': True,
                'enrolled_at': datetime.now(timezone.utc).isoformat(),
                'method': 'totp',
            }, merge=True)
        else:
            cred.create(
                user_uid=user_uid,
                totp_secret=encrypted_secret,
                backup_codes=hashed_codes,
                is_enabled=True,
                method='totp',
            )

        # Clean up pending doc
        pending_ref.delete()
        return raw_codes

    def verify_totp(self, user_uid: str, code: str) -> bool:
        """Verify a TOTP code with replay protection."""
        cred = MFACredential().get_by_user(user_uid)
        if cred is None or not cred.is_enabled:
            return False
        try:
            secret = _decrypt_secret(cred.totp_secret)
        except Exception:
            return False

        totp = pyotp.TOTP(secret)
        if not totp.verify(code, valid_window=1):
            return False

        # Replay protection: check if this code was already used
        used_ref = MFACredential().collection('mfa_used_codes').document(f"{user_uid}_{code}")
        used_doc = used_ref.get()
        if used_doc.exists:
            return False

        # Mark code as used (expire after 90 seconds — 1 TOTP window)
        from ....base.firebase_collection import getTimestamp
        used_ref.set({
            'user_uid': user_uid,
            'code': code,
            'used_at': datetime.now(timezone.utc).isoformat(),
            'createdAt': getTimestamp(),
        })
        # Update last_used_at
        cred.document_reference().set({'last_used_at': datetime.now(timezone.utc).isoformat()}, merge=True)
        return True

    def verify_backup_code(self, user_uid: str, code: str) -> bool:
        """Verify and consume a backup code."""
        cred = MFACredential().get_by_user(user_uid)
        if cred is None or not cred.is_enabled:
            return False

        hashed = _hash_backup_code(code.strip().upper())
        backup_codes = list(cred.backup_codes or [])
        if hashed not in backup_codes:
            return False

        backup_codes.remove(hashed)
        cred.document_reference().set({'backup_codes': backup_codes}, merge=True)
        return True

    def disable_mfa(self, user_uid: str):
        """Disable MFA for a user."""
        cred = MFACredential().get_by_user(user_uid)
        if cred is None:
            return
        cred.document_reference().set({'is_enabled': False}, merge=True)

    def is_enabled(self, user_uid: str) -> bool:
        """Return True if MFA is enabled for this user."""
        cred = MFACredential().get_by_user(user_uid)
        return cred is not None and bool(getattr(cred, 'is_enabled', False))


mfa_service = MFAService()
