"""
Servizio autenticazione – login, JWT, TOTP 2FA, refresh token.
"""

import base64
import io
import uuid
from datetime import UTC, datetime, timedelta

import pyotp
import qrcode
from jose import JWTError
from sqlalchemy import select, update
from sqlalchemy.ext.asyncio import AsyncSession

from app.config import get_settings
from app.core.exceptions import (
    AccountDisabledError,
    AccountLockedError,
    InvalidCredentialsError,
    TOTPInvalidError,
    TOTPRequiredError,
    TokenInvalidError,
)
from app.core.security import (
    create_access_token,
    create_refresh_token,
    decode_token,
    encrypt_credential,
    decrypt_credential,
    hash_password,
    hash_token,
    verify_password,
)
from app.models.audit_log import AuditLog
from app.models.user import RefreshToken, User

settings = get_settings()

# Numero massimo di tentativi falliti prima del blocco
MAX_FAILED_ATTEMPTS = 5
LOCK_DURATION_MINUTES = 15


class AuthService:
    def __init__(self, db: AsyncSession) -> None:
        self.db = db

    async def login(
        self,
        email: str,
        password: str,
        totp_code: str | None,
        ip_address: str | None = None,
        user_agent: str | None = None,
    ) -> tuple[str, str]:
        """
        Autentica l'utente con email + password (+ TOTP se abilitato).
        Restituisce (access_token, refresh_token).
        """
        # 1. Trova utente per email (non filtrare per tenant: l'email è unica globalmente
        #    per ora, ma in futuro si potrebbe filtrare per subdomain)
        user = await self._get_user_by_email(email)

        if not user:
            await self._log_audit(None, None, "auth.login", "failure", ip_address, {"reason": "user_not_found"})
            raise InvalidCredentialsError()

        # 2. Verifica account attivo
        if not user.is_active:
            raise AccountDisabledError()

        # 3. Verifica blocco temporaneo
        if user.locked_until and user.locked_until > datetime.now(UTC):
            locked_str = user.locked_until.strftime("%H:%M")
            raise AccountLockedError(locked_until=locked_str)

        # 4. Verifica password
        if not verify_password(password, user.password_hash):
            await self._handle_failed_login(user)
            await self._log_audit(user.tenant_id, user.id, "auth.login", "failure", ip_address, {"reason": "wrong_password"})
            raise InvalidCredentialsError()

        # 5. Verifica TOTP (se abilitato)
        if user.totp_enabled:
            if not totp_code:
                raise TOTPRequiredError()
            if not self._verify_totp(user, totp_code):
                await self._log_audit(user.tenant_id, user.id, "auth.login", "failure", ip_address, {"reason": "invalid_totp"})
                raise TOTPInvalidError()

        # 6. Reset contatori falliti
        await self._reset_failed_login(user)

        # 7. Genera token
        access_token = create_access_token(
            subject=user.id,
            tenant_id=user.tenant_id,
            role=user.role,
        )
        refresh_token_raw = create_refresh_token(
            subject=user.id,
            tenant_id=user.tenant_id,
        )

        # 8. Salva refresh token in DB (hash)
        rt = RefreshToken(
            user_id=user.id,
            token_hash=hash_token(refresh_token_raw),
            expires_at=datetime.now(UTC) + timedelta(days=settings.refresh_token_expire_days),
            user_agent=user_agent,
            ip_address=ip_address,
        )
        self.db.add(rt)

        # 9. Aggiorna last_login_at
        await self.db.execute(
            update(User)
            .where(User.id == user.id)
            .values(last_login_at=datetime.now(UTC))
        )

        await self._log_audit(user.tenant_id, user.id, "auth.login", "success", ip_address, {})

        return access_token, refresh_token_raw

    async def refresh_tokens(self, refresh_token_raw: str) -> tuple[str, str]:
        """
        Valida il refresh token e restituisce nuova coppia di token.
        Implementa rotation: il vecchio refresh token viene revocato.
        """
        # Valida struttura JWT
        try:
            payload = decode_token(refresh_token_raw)
        except JWTError:
            raise TokenInvalidError()

        if payload.get("type") != "refresh":
            raise TokenInvalidError()

        # Cerca il token in DB
        token_hash = hash_token(refresh_token_raw)
        result = await self.db.execute(
            select(RefreshToken).where(RefreshToken.token_hash == token_hash)
        )
        rt = result.scalar_one_or_none()

        if not rt or not rt.is_valid:
            raise TokenInvalidError()

        # Carica l'utente
        user_result = await self.db.execute(
            select(User).where(User.id == rt.user_id)
        )
        user = user_result.scalar_one_or_none()

        if not user or not user.is_active:
            raise TokenInvalidError()

        # Revoca il vecchio refresh token (rotation)
        rt.revoked_at = datetime.now(UTC)

        # Genera nuovi token
        new_access = create_access_token(
            subject=user.id,
            tenant_id=user.tenant_id,
            role=user.role,
        )
        new_refresh_raw = create_refresh_token(
            subject=user.id,
            tenant_id=user.tenant_id,
        )

        # Salva nuovo refresh token
        new_rt = RefreshToken(
            user_id=user.id,
            token_hash=hash_token(new_refresh_raw),
            expires_at=datetime.now(UTC) + timedelta(days=settings.refresh_token_expire_days),
            ip_address=rt.ip_address,
        )
        self.db.add(new_rt)

        return new_access, new_refresh_raw

    async def logout(self, refresh_token_raw: str) -> None:
        """Revoca il refresh token (logout)."""
        token_hash = hash_token(refresh_token_raw)
        result = await self.db.execute(
            select(RefreshToken).where(RefreshToken.token_hash == token_hash)
        )
        rt = result.scalar_one_or_none()
        if rt:
            rt.revoked_at = datetime.now(UTC)

    async def setup_totp(self, user: User) -> dict:
        """
        Genera segreto TOTP e QR code per l'utente.
        Il segreto viene cifrato e salvato in DB ma TOTP non è ancora attivo
        (richiede verifica con totp_verify).
        """
        # Genera segreto base32
        secret = pyotp.random_base32()

        # Cifra il segreto prima di salvarlo
        encrypted_secret = encrypt_credential(secret)
        user.totp_secret = encrypted_secret
        # Non attivare ancora: richiede verifica
        user.totp_enabled = False

        # Genera URI otpauth://
        totp = pyotp.TOTP(secret)
        uri = totp.provisioning_uri(name=user.email, issuer_name="PecFlow")

        # Genera QR code
        qr = qrcode.QRCode(version=1, box_size=6, border=4)
        qr.add_data(uri)
        qr.make(fit=True)
        img = qr.make_image(fill_color="black", back_color="white")
        buffered = io.BytesIO()
        img.save(buffered, format="PNG")
        qr_b64 = base64.b64encode(buffered.getvalue()).decode("ascii")

        return {
            "secret": secret,
            "qr_uri": uri,
            "qr_image_base64": f"data:image/png;base64,{qr_b64}",
        }

    async def verify_and_enable_totp(self, user: User, totp_code: str) -> bool:
        """
        Verifica il codice TOTP e attiva il 2FA se corretto.
        """
        if not user.totp_secret:
            return False

        if not self._verify_totp(user, totp_code):
            raise TOTPInvalidError()

        user.totp_enabled = True
        return True

    async def disable_totp(self, user: User) -> None:
        """Disabilita il 2FA per l'utente."""
        user.totp_secret = None
        user.totp_enabled = False

    # ─── Private helpers ──────────────────────────────────────────────────────

    async def _get_user_by_email(self, email: str) -> User | None:
        result = await self.db.execute(
            select(User).where(User.email == email.lower())
        )
        return result.scalar_one_or_none()

    def _verify_totp(self, user: User, code: str) -> bool:
        """Verifica il codice TOTP (accetta ±1 intervallo per clock skew)."""
        if not user.totp_secret:
            return False
        try:
            secret = decrypt_credential(user.totp_secret)
            totp = pyotp.TOTP(secret)
            return totp.verify(code, valid_window=1)
        except Exception:
            return False

    async def _handle_failed_login(self, user: User) -> None:
        """Incrementa contatore fallimenti, blocca se necessario."""
        new_count = user.failed_login_count + 1
        updates: dict = {"failed_login_count": new_count}

        if new_count >= MAX_FAILED_ATTEMPTS:
            updates["locked_until"] = datetime.now(UTC) + timedelta(minutes=LOCK_DURATION_MINUTES)

        await self.db.execute(
            update(User).where(User.id == user.id).values(**updates)
        )

    async def _reset_failed_login(self, user: User) -> None:
        await self.db.execute(
            update(User)
            .where(User.id == user.id)
            .values(failed_login_count=0, locked_until=None)
        )

    async def _log_audit(
        self,
        tenant_id: uuid.UUID | None,
        user_id: uuid.UUID | None,
        action: str,
        outcome: str,
        ip_address: str | None,
        payload: dict,
    ) -> None:
        log = AuditLog(
            tenant_id=tenant_id,
            user_id=user_id,
            action=action,
            outcome=outcome,
            ip_address=ip_address,
            payload=payload,
        )
        self.db.add(log)
