"""Backup code service - single-use recovery codes (InTalk-style)."""
from __future__ import annotations
import hashlib
import secrets
from datetime import datetime, timezone
from typing import Iterable
from uuid import UUID

from sqlalchemy import delete, func, select, update
from sqlalchemy.ext.asyncio import AsyncSession

from src.config import get_settings
from src.domain.auth.mfa_models import UserBackupCode

_ALPHABET: Iterable[str] = "23456789ABCDEFGHJKLMNPQRSTUVWXYZ"


class BackupCodeService:
    """Issue and consume single-use backup codes for TOTP recovery."""

    def __init__(self, db: AsyncSession):
        self._db = db

    @staticmethod
    def normalize(code: str) -> str:
        return "".join(ch for ch in str(code).strip().upper() if ch.isalnum())

    @classmethod
    def hash_code(cls, code: str) -> str:
        pepper = get_settings().secret_key
        payload = f"{pepper}:{cls.normalize(code)}".encode("utf-8")
        return hashlib.sha256(payload).hexdigest()

    def generate_codes(self, count: int = 10, length: int = 10) -> list[str]:
        codes: set[str] = set()
        alphabet = list(_ALPHABET)
        while len(codes) < count:
            raw = "".join(secrets.choice(alphabet) for _ in range(length))
            codes.add(raw)
        return sorted(codes)

    async def regenerate_for_user(self, user_id: UUID, count: int = 10) -> list[str]:
        """Rotate backup codes (invalidate existing)."""
        await self._db.execute(delete(UserBackupCode).where(UserBackupCode.user_id == user_id))
        codes = self.generate_codes(count=count)
        for code in codes:
            self._db.add(UserBackupCode(user_id=user_id, code_hash=self.hash_code(code)))
        await self._db.flush()
        return codes

    async def consume(self, user_id: UUID, code: str) -> bool:
        """Return True and mark used if valid unused code."""
        h = self.hash_code(code)
        result = await self._db.execute(
            select(UserBackupCode).where(
                UserBackupCode.user_id == user_id,
                UserBackupCode.code_hash == h,
                UserBackupCode.used_at.is_(None),
            )
        )
        row = result.scalar_one_or_none()
        if not row:
            return False
        row.used_at = datetime.now(timezone.utc)
        await self._db.flush()
        return True

    async def remaining_count(self, user_id: UUID) -> int:
        result = await self._db.execute(
            select(func.count()).select_from(UserBackupCode).where(
                UserBackupCode.user_id == user_id,
                UserBackupCode.used_at.is_(None),
            )
        )
        return int(result.scalar_one())
