import logging
import re
import uuid as uuid_mod
from datetime import datetime, timedelta, timezone
from uuid import UUID

from fastapi import HTTPException, Request, status
from sqlalchemy import delete, select
from sqlalchemy.ext.asyncio import AsyncSession

from app.core.config import settings
from app.core.security import (
    create_access_token,
    create_refresh_token,
    generate_invite_token,
    generate_reset_token,
    hash_password,
    hash_token,
    verify_password,
)
from app.models import Company, PasswordReset, Session, User, UserRole
from app.schemas.auth import LoginRequest, RegisterRequest, TokenResponse
from app.services.email_service import email_service

logger = logging.getLogger(__name__)

# Login-Lockout-Konfiguration
FAILED_LOGIN_MAX = 10           # nach 10 Fehlversuchen → Lockout
FAILED_LOGIN_LOCKOUT_SEC = 900  # 15 Minuten gesperrt


def _get_client_ip(request: "Request | None") -> str | None:
    """Gibt die echte Client-IP zurück (berücksichtigt X-Real-IP / X-Forwarded-For hinter nginx-Proxy)."""
    if not request:
        return None
    real_ip = request.headers.get("X-Real-IP")
    if real_ip:
        return real_ip.strip()
    forwarded = request.headers.get("X-Forwarded-For")
    if forwarded:
        # Erstes Element = Original-Client-IP
        return forwarded.split(",")[0].strip()
    return request.client.host if request.client else None


def _slugify(name: str) -> str:
    slug = re.sub(r"[^a-z0-9]+", "-", name.lower()).strip("-")
    return slug[:80]


class AuthService:

    async def _check_login_lockout(self, email: str, redis) -> None:
        """Wirft HTTP 429 wenn Account wegen zu vieler Fehlversuche gesperrt ist."""
        lockout_key = f"login_lockout:{email.lower()}"
        if await redis.exists(lockout_key):
            ttl = await redis.ttl(lockout_key)
            wait_min = ttl // 60 + 1
            raise HTTPException(
                status_code=429,
                detail=f"Account temporär gesperrt. Bitte {wait_min} Minute(n) warten.",
            )

    async def _record_login_failure(self, email: str, redis) -> None:
        """Zählt Fehlversuch und setzt Lockout nach FAILED_LOGIN_MAX Fehlversuchen."""
        fail_key = f"login_fails:{email.lower()}"
        lockout_key = f"login_lockout:{email.lower()}"
        fails = await redis.incr(fail_key)
        await redis.expire(fail_key, FAILED_LOGIN_LOCKOUT_SEC)
        if fails >= FAILED_LOGIN_MAX:
            await redis.set(lockout_key, "1", ex=FAILED_LOGIN_LOCKOUT_SEC)
            await redis.delete(fail_key)

    async def _clear_login_failures(self, email: str, redis) -> None:
        """Löscht Fehlversuche nach erfolgreichem Login."""
        await redis.delete(f"login_fails:{email.lower()}")
        await redis.delete(f"login_lockout:{email.lower()}")

    async def register(self, data: RegisterRequest, db: AsyncSession) -> TokenResponse:
        existing = await db.scalar(select(User).where(User.email == data.email))
        if existing:
            raise HTTPException(status_code=400, detail="Email already registered")

        base_slug = _slugify(data.company_name)
        slug = base_slug
        counter = 1
        while await db.scalar(select(Company).where(Company.slug == slug)):
            slug = f"{base_slug}-{counter}"
            counter += 1

        company = Company(name=data.company_name, slug=slug)
        db.add(company)
        await db.flush()

        user = User(
            company_id=company.id,
            email=data.email,
            password_hash=hash_password(data.password),
            first_name=data.first_name,
            last_name=data.last_name,
            role=UserRole.COMPANY_ADMIN,
        )
        db.add(user)
        await db.flush()

        from app.services.absence_service import absence_service
        await absence_service.create_defaults_for_company(company.id, db)

        tokens = await self._create_session(user, db)
        await email_service.send_welcome(user, db)
        return tokens

    async def login(self, data: LoginRequest, db: AsyncSession, request: Request) -> TokenResponse:
        import redis.asyncio as aioredis
        from app.models.audit_log import AuditLog
        from app.models.user import AuthProvider
        from app.services.ldap_service import ldap_service

        client_ip = _get_client_ip(request)

        redis_client = aioredis.from_url(settings.redis_url, decode_responses=True)
        try:
            # Lockout-Check vor DB-Abfrage (verhindert auch User-Enumeration via Timing)
            await self._check_login_lockout(data.email, redis_client)

            user = await db.scalar(select(User).where(User.email == data.email))
            if not user:
                # Fehlversuch zählen auch bei unbekannter E-Mail (kein User-ID-Leak)
                await self._record_login_failure(data.email, redis_client)
                db.add(AuditLog(
                    company_id=None,
                    user_id=None,
                    action="login_failed",
                    entity_type="user",
                    entity_id=None,
                    new_value={"email": data.email},
                    ip=client_ip,
                ))
                await db.commit()
                raise HTTPException(
                    status_code=status.HTTP_401_UNAUTHORIZED,
                    detail="Invalid email or password",
                )
            if not user.is_active:
                raise HTTPException(status_code=403, detail="Account is deactivated")

            # Mandanten-Sperre: deaktivierte Firma → kein Login (Reseller/SUPER_ADMIN
            # haben company_id IS NULL und sind davon nicht betroffen).
            if user.company_id is not None:
                company = await db.get(Company, user.company_id)
                if company is not None and not company.is_active:
                    raise HTTPException(status_code=403, detail="Dieser Mandant ist deaktiviert.")

            auth_ok = False
            if user.auth_provider == AuthProvider.LDAP:
                ldap_cfg = await ldap_service.get_config(user.company_id, db)
                if not ldap_cfg or not ldap_cfg.enabled:
                    raise HTTPException(
                        status_code=status.HTTP_401_UNAUTHORIZED,
                        detail="LDAP authentication not available",
                    )
                auth_ok = ldap_service.authenticate_ldap(ldap_cfg, data.email, data.password)
            else:
                auth_ok = bool(user.password_hash and verify_password(data.password, user.password_hash))

            if not auth_ok:
                await self._record_login_failure(data.email, redis_client)
                db.add(AuditLog(
                    company_id=user.company_id,
                    user_id=user.id,
                    action="login_failed",
                    entity_type="user",
                    entity_id=user.id,
                    new_value={"email": data.email},
                    ip=client_ip,
                ))
                await db.commit()
                raise HTTPException(
                    status_code=status.HTTP_401_UNAUTHORIZED,
                    detail="Invalid email or password",
                )

            # Erfolgreicher Login: Fehlversuche zurücksetzen
            await self._clear_login_failures(data.email, redis_client)
        finally:
            await redis_client.aclose()

        # AuditLog: Erfolgreicher Login
        db.add(AuditLog(
            company_id=user.company_id,
            user_id=user.id,
            action="login_success",
            entity_type="user",
            entity_id=user.id,
            ip=client_ip,
        ))

        # TOTP: wenn aktiviert → partial token zurückgeben statt vollem Login
        if user.totp_enabled:
            from app.core.security import create_partial_token
            from app.schemas.auth import TokenResponse
            partial = create_partial_token(str(user.id))
            await db.commit()
            return TokenResponse(
                access_token="",
                refresh_token="",
                totp_required=True,
                partial_token=partial,
            )

        user.last_login = datetime.now(timezone.utc)
        return await self._create_session(user, db, request=request)

    async def refresh(self, raw_token: str, db: AsyncSession) -> TokenResponse:
        import redis.asyncio as aioredis
        from app.models.audit_log import AuditLog

        token_hash = hash_token(raw_token)
        burned_key = f"burned_token:{token_hash}"

        redis_client = aioredis.from_url(settings.redis_url, decode_responses=True)
        try:
            # Re-Use-Detection: prüfe ob Token bereits verbrannt wurde
            burned_user_id = await redis_client.get(burned_key)
            if burned_user_id:
                # Token wurde bereits einmal genutzt — möglicher Token-Diebstahl!
                logger.warning(
                    "Replay-Angriff erkannt: verbrannter Refresh-Token für User %s",
                    burned_user_id,
                )
                # Alle Sessions des betroffenen Users invalidieren
                try:
                    uid = uuid_mod.UUID(burned_user_id)
                    await db.execute(delete(Session).where(Session.user_id == uid))
                    db.add(AuditLog(
                        company_id=None,
                        user_id=uid,
                        action="refresh_token_reuse",
                        entity_type="session",
                        entity_id=uid,
                        new_value={"token_hash_prefix": token_hash[:8], "action": "all_sessions_invalidated"},
                        ip=None,
                    ))
                    await db.commit()
                except Exception:
                    pass
                raise HTTPException(
                    status_code=401,
                    detail="Sicherheitsvorfall: Alle Sessions wurden invalidiert. Bitte erneut anmelden.",
                )

            session = await db.scalar(
                select(Session).where(Session.refresh_token_hash == token_hash)
            )
            if not session or session.expires_at < datetime.now(timezone.utc):
                raise HTTPException(status_code=401, detail="Invalid or expired refresh token")

            user = await db.get(User, session.user_id)
            if not user or not user.is_active:
                raise HTTPException(status_code=401, detail="User not found or inactive")

            user_id_str = str(session.user_id)

            # Session löschen (Token "verbrennen")
            await db.delete(session)

            # Verbrannten Token-Hash 48h in Redis merken
            await redis_client.set(burned_key, user_id_str, ex=48 * 3600)
        finally:
            await redis_client.aclose()

        return await self._create_session(user, db)

    async def logout(self, raw_token: str, db: AsyncSession) -> None:
        token_hash = hash_token(raw_token)
        session = await db.scalar(
            select(Session).where(Session.refresh_token_hash == token_hash)
        )
        if session:
            await db.delete(session)

    async def request_password_reset(self, email: str, db: AsyncSession) -> str | None:
        """
        Gibt None zurück (lokale User) oder 'ldap' wenn der User LDAP-Auth nutzt.
        Die aufrufende Route entscheidet, was dem Client mitgeteilt wird.
        """
        from app.models.user import AuthProvider
        user = await db.scalar(select(User).where(User.email == email))
        if not user:
            return None  # Security: kein Hinweis ob E-Mail existiert

        if user.auth_provider == AuthProvider.LDAP:
            return "ldap"

        old_resets = await db.scalars(
            select(PasswordReset).where(
                PasswordReset.user_id == user.id,
                PasswordReset.used_at.is_(None),
            )
        )
        for r in old_resets:
            await db.delete(r)

        raw, hashed = generate_reset_token()
        reset = PasswordReset(
            user_id=user.id,
            token_hash=hashed,
            expires_at=datetime.now(timezone.utc) + timedelta(hours=1),
        )
        db.add(reset)
        await email_service.send_password_reset(user, raw, db)
        return None

    async def confirm_password_reset(self, token: str, new_password: str, db: AsyncSession) -> None:
        token_hash = hash_token(token)
        reset = await db.scalar(
            select(PasswordReset).where(
                PasswordReset.token_hash == token_hash,
                PasswordReset.used_at.is_(None),
            )
        )
        if not reset or reset.expires_at < datetime.now(timezone.utc):
            raise HTTPException(status_code=400, detail="Invalid or expired reset token")

        user = await db.get(User, reset.user_id)
        if not user:
            raise HTTPException(status_code=400, detail="User not found")

        user.password_hash = hash_password(new_password)
        reset.used_at = datetime.now(timezone.utc)

        sessions = await db.scalars(select(Session).where(Session.user_id == user.id))
        for s in sessions:
            await db.delete(s)

    async def _create_session(
        self,
        user: User,
        db: AsyncSession,
        request: Request | None = None,
    ) -> TokenResponse:
        raw_refresh, hashed_refresh = create_refresh_token()
        session = Session(
            user_id=user.id,
            refresh_token_hash=hashed_refresh,
            expires_at=datetime.now(timezone.utc) + timedelta(days=settings.refresh_token_expire_days),
            device=request.headers.get("User-Agent", "")[:255] if request else None,
            ip=_get_client_ip(request),
        )
        db.add(session)
        access_token = create_access_token(
            str(user.id),
            extra={"role": user.role, "company_id": str(user.company_id)},
        )
        return TokenResponse(access_token=access_token, refresh_token=raw_refresh)


auth_service = AuthService()
