from __future__ import annotations
import hashlib, hmac, struct, secrets

class TOTPVerifier:
    def __init__(self, secret: bytes, time_step=30, digits=8, window=1, max_attempts=3, replay_protection=True):
        self.secret=secret; self.time_step=time_step; self.digits=digits; self.window=window; self.max_attempts=max_attempts
        self.replay_protection=replay_protection; self.failed_attempts=0; self.used_counters=set(); self.events=[]
    def code_for_counter(self,counter:int)->str:
        msg=struct.pack('>Q',counter)
        digest=hmac.new(self.secret,msg,hashlib.sha1).digest(); off=digest[-1]&0x0f
        binary=((digest[off]&0x7f)<<24)|((digest[off+1]&0xff)<<16)|((digest[off+2]&0xff)<<8)|(digest[off+3]&0xff)
        return str(binary%(10**self.digits)).zfill(self.digits)
    def code_at(self,unix_time:int)->str:
        return self.code_for_counter(int(unix_time)//self.time_step)
    def verify(self,candidate:str,unix_time:int):
        if self.failed_attempts>=self.max_attempts:
            self.events.append({'event':'ATTEMPT_LIMIT_REACHED'}); return False,'ATTEMPT_LIMIT_REACHED'
        center=int(unix_time)//self.time_step
        for delta in range(-self.window,self.window+1):
            ctr=center+delta
            if ctr<0: continue
            if hmac.compare_digest(self.code_for_counter(ctr),str(candidate)):
                if self.replay_protection and ctr in self.used_counters:
                    self.events.append({'event':'TOTP_REPLAY_REJECTED','counter':ctr}); return False,'TOTP_REPLAY_REJECTED'
                self.used_counters.add(ctr); self.failed_attempts=0
                self.events.append({'event':'TOTP_ACCEPTED','counter':ctr,'delta':delta}); return True,'TOTP_ACCEPTED'
        self.failed_attempts+=1; self.events.append({'event':'TOTP_REJECTED','failed_attempts':self.failed_attempts}); return False,'TOTP_REJECTED'

class RecoveryCodeStore:
    def __init__(self, pepper=b'SEC005_SYNTHETIC_LOCAL_PEPPER'):
        self.pepper=pepper; self.generation=0; self._digests={}; self.events=[]
    def _digest(self,code:str)->str:
        return hmac.new(self.pepper,code.encode(),hashlib.sha256).hexdigest()
    def issue(self,codes:list[str]):
        self.generation+=1
        self._digests={self._digest(c):False for c in codes}
        self.events.append({'event':'RECOVERY_SET_ISSUED','generation':self.generation,'count':len(codes)})
        return list(codes)
    def consume(self,code:str):
        d=self._digest(code)
        if d not in self._digests or self._digests[d]:
            self.events.append({'event':'RECOVERY_CODE_REUSED_OR_INVALID','generation':self.generation}); return False,'RECOVERY_CODE_REUSED_OR_INVALID'
        self._digests[d]=True
        self.events.append({'event':'RECOVERY_ACCEPTED','generation':self.generation}); return True,'RECOVERY_ACCEPTED'
    def regenerate(self,codes:list[str]):
        old_generation=self.generation
        self._digests={}
        issued=self.issue(codes)
        self.events.append({'event':'RECOVERY_SET_REGENERATED','old_generation':old_generation,'new_generation':self.generation})
        return issued
    def state_contains_plaintext(self,codes:list[str])->bool:
        state=repr(self._digests)
        return any(c in state for c in codes)

def rfc_vectors_sha1():
    return [(59,'94287082'),(1111111109,'07081804'),(1111111111,'14050471'),(1234567890,'89005924'),(2000000000,'69279037'),(20000000000,'65353130')]
