from fastapi import APIRouter, Depends, HTTPException, Request, Response
from pydantic import BaseModel, Field
from sqlalchemy.ext.asyncio import AsyncSession

from app.core.crypto import decrypt_value, encrypt_value
from app.core.database import get_db
from app.core.dependencies import CurrentUser
from app.core.limiter import limiter
from app.core.security import hash_password, verify_password
from app.models.audit_log import AuditLog
from app.schemas.auth import (
    LoginRequest,
    MessageResponse,
    PasswordResetConfirm,
    PasswordResetRequest,
    RefreshRequest,
    RegisterRequest,
    TokenResponse,
    TotpConfirmRequest,
    TotpDisableRequest,
    TotpLoginRequest,
    TotpSetupResponse,
)
from app.schemas.user import InviteAccept, UserOut
from app.services.auth_service import auth_service


class ChangePasswordRequest(BaseModel):
    current_password: str
    new_password: str = Field(min_length=8)

router = APIRouter(prefix="/auth", tags=["Auth"])

_COOKIE_NAME = "refresh_token"
_COOKIE_PATH = "/"
_COOKIE_MAX_AGE = 30 * 24 * 60 * 60  # 30 Tage


def _set_refresh_cookie(response: Response, token: str) -> None:
    """Setzt den Refresh-Token als HttpOnly+SameSite=Strict Cookie."""
    from app.core.config import settings
    response.set_cookie(
        key=_COOKIE_NAME,
        value=token,
        httponly=True,
        secure=settings.is_production,
        samesite="strict",
        max_age=_COOKIE_MAX_AGE,
        path=_COOKIE_PATH,
    )


def _delete_refresh_cookie(response: Response) -> None:
    """Löscht den Refresh-Token-Cookie."""
    response.delete_cookie(key=_COOKIE_NAME, path=_COOKIE_PATH)


@router.post("/register", response_model=TokenResponse, status_code=201)
@limiter.limit("3/hour")
async def register(request: Request, data: RegisterRequest, db: AsyncSession = Depends(get_db)):
    """Create a new company + admin account."""
    return await auth_service.register(data, db)


@router.post("/login", response_model=TokenResponse)
@limiter.limit("10/minute")
async def login(request: Request, response: Response, data: LoginRequest, db: AsyncSession = Depends(get_db)):
    result = await auth_service.login(data, db, request)
    if result.refresh_token:
        _set_refresh_cookie(response, result.refresh_token)
        result.refresh_token = None  # Nicht im JSON-Body zurückgeben
    return result


@router.post("/refresh", response_model=TokenResponse)
@limiter.limit("30/minute")
async def refresh(request: Request, response: Response, data: RefreshRequest | None = None, db: AsyncSession = Depends(get_db)):
    # Body-Token hat Vorrang wenn explizit angegeben (API-Clients, Tests, Replay-Detection)
    # Cookie als Fallback für Browser-Clients
    token = (data.refresh_token if data and data.refresh_token else None) or request.cookies.get(_COOKIE_NAME)
    if not token:
        raise HTTPException(status_code=401, detail="Kein Refresh-Token")
    result = await auth_service.refresh(token, db)
    if result.refresh_token:
        _set_refresh_cookie(response, result.refresh_token)
        result.refresh_token = None  # Nicht im JSON-Body zurückgeben
    return result


@router.post("/logout", response_model=MessageResponse)
@limiter.limit("60/minute")
async def logout(request: Request, response: Response, data: RefreshRequest | None = None, db: AsyncSession = Depends(get_db)):
    # Body-Token hat Vorrang wenn explizit angegeben, Cookie als Fallback
    token = (data.refresh_token if data and data.refresh_token else None) or request.cookies.get(_COOKIE_NAME)
    if token:
        await auth_service.logout(token, db)
    _delete_refresh_cookie(response)
    return MessageResponse(message="Logged out successfully")


@router.post("/password-reset", response_model=MessageResponse)
@limiter.limit("3/hour")
async def request_password_reset(request: Request, data: PasswordResetRequest, db: AsyncSession = Depends(get_db)):
    result = await auth_service.request_password_reset(data.email, db)
    if result == "ldap":
        from fastapi import HTTPException
        raise HTTPException(
            status_code=400,
            detail="Dein Konto wird über LDAP verwaltet. Bitte setze dein Passwort direkt beim LDAP-Administrator zurück.",
        )
    return MessageResponse(message="Falls diese E-Mail-Adresse registriert ist, wurde ein Reset-Link verschickt.")


@router.post("/password-reset/confirm", response_model=MessageResponse)
@limiter.limit("5/hour")
async def confirm_password_reset(request: Request, data: PasswordResetConfirm, db: AsyncSession = Depends(get_db)):
    await auth_service.confirm_password_reset(data.token, data.new_password, db)
    return MessageResponse(message="Password updated successfully")


@router.post("/invite/accept", response_model=UserOut)
@limiter.limit("10/hour")
async def accept_invite(request: Request, data: InviteAccept, db: AsyncSession = Depends(get_db)):
    from app.services.user_service import user_service
    user = await user_service.accept_invite(data, db)
    return UserOut.model_validate(user)


@router.get("/me", response_model=UserOut)
async def me(current_user: CurrentUser):
    return UserOut.model_validate(current_user)


@router.post("/change-password", response_model=MessageResponse)
async def change_password(
    data: ChangePasswordRequest,
    current_user: CurrentUser,
    db: AsyncSession = Depends(get_db),
):
    """Passwort ändern (eingeloggter User, benötigt aktuelles Passwort)."""
    if not verify_password(data.current_password, current_user.password_hash):
        raise HTTPException(status_code=400, detail="Aktuelles Passwort ist falsch")
    import re
    if not re.search(r'[A-Z]', data.new_password) or not re.search(r'[0-9]', data.new_password):
        raise HTTPException(
            status_code=400,
            detail="Neues Passwort muss mindestens 1 Großbuchstaben und 1 Zahl enthalten"
        )
    current_user.password_hash = hash_password(data.new_password)
    db.add(AuditLog(
        company_id=current_user.company_id,
        user_id=current_user.id,
        action="password_changed",
        entity_type="user",
        entity_id=current_user.id,
    ))
    await db.commit()
    return MessageResponse(message="Passwort erfolgreich geändert")


# ── TOTP / 2FA ────────────────────────────────────────────────────────────────

def _totp_plain(user) -> str | None:
    """Gibt das entschlüsselte TOTP-Secret zurück, oder None."""
    if not user.totp_secret:
        return None
    try:
        return decrypt_value(user.totp_secret)
    except ValueError:
        # Fallback: Secret war noch im Klartext (Legacy-Daten vor 0026-Migration)
        return user.totp_secret


@router.post("/totp/setup", response_model=TotpSetupResponse)
async def totp_setup(current_user: CurrentUser):
    """Generiert ein neues TOTP-Secret und gibt die otpauth-URI zurück. Noch nicht aktiviert."""
    # Fix K-4: Verhindere Überschreiben eines aktiven TOTP-Secrets mit gestohlenem Access-Token
    if current_user.totp_enabled:
        raise HTTPException(
            status_code=400,
            detail="TOTP ist bereits aktiv. Bitte zuerst deaktivieren (POST /auth/totp/disable).",
        )
    import pyotp
    secret = pyotp.random_base32()
    issuer = "TimeMaster"
    label = current_user.email
    uri = pyotp.totp.TOTP(secret).provisioning_uri(name=label, issuer_name=issuer)
    # Secret Fernet-verschlüsselt speichern (noch nicht totp_enabled)
    current_user.totp_secret = encrypt_value(secret)
    # Hinweis: DB-Commit passiert NICHT hier – erst nach verify in /totp/confirm
    # Damit das Secret nicht verloren geht, sofort speichern
    return TotpSetupResponse(secret=secret, otpauth_uri=uri)


@router.post("/totp/setup/save", response_model=MessageResponse)
async def totp_setup_save(
    current_user: CurrentUser,
    db: AsyncSession = Depends(get_db),
):
    """Speichert das generierte Secret temporär (ohne Aktivierung)."""
    import pyotp
    if not current_user.totp_secret:
        secret = pyotp.random_base32()
        current_user.totp_secret = encrypt_value(secret)
    await db.commit()
    return MessageResponse(message="Secret gespeichert")


@router.post("/totp/confirm", response_model=MessageResponse)
async def totp_confirm(
    data: TotpConfirmRequest,
    current_user: CurrentUser,
    db: AsyncSession = Depends(get_db),
):
    """Bestätigt den ersten TOTP-Code und aktiviert 2FA."""
    import pyotp
    plain_secret = _totp_plain(current_user)
    if not plain_secret:
        raise HTTPException(400, "Kein TOTP-Secret vorhanden. Zuerst /totp/setup aufrufen.")
    totp = pyotp.TOTP(plain_secret)
    if not totp.verify(data.code, valid_window=1):
        raise HTTPException(400, "Ungültiger Code")
    current_user.totp_enabled = True
    db.add(AuditLog(
        company_id=current_user.company_id,
        user_id=current_user.id,
        action="totp_enabled",
        entity_type="user",
        entity_id=current_user.id,
    ))
    await db.commit()
    return MessageResponse(message="Zwei-Faktor-Authentifizierung aktiviert")


@router.post("/totp/disable", response_model=MessageResponse)
async def totp_disable(
    data: TotpDisableRequest,
    current_user: CurrentUser,
    db: AsyncSession = Depends(get_db),
):
    """Deaktiviert TOTP. Benötigt aktuelles Passwort + gültigen TOTP-Code."""
    import pyotp
    if not verify_password(data.password, current_user.password_hash or ""):
        raise HTTPException(400, "Passwort falsch")
    if not current_user.totp_enabled or not current_user.totp_secret:
        raise HTTPException(400, "2FA ist nicht aktiv")
    plain_secret = _totp_plain(current_user)
    totp = pyotp.TOTP(plain_secret or "")
    if not totp.verify(data.code, valid_window=1):
        raise HTTPException(400, "Ungültiger TOTP-Code")
    current_user.totp_enabled = False
    current_user.totp_secret = None
    db.add(AuditLog(
        company_id=current_user.company_id,
        user_id=current_user.id,
        action="totp_disabled",
        entity_type="user",
        entity_id=current_user.id,
    ))
    await db.commit()
    return MessageResponse(message="Zwei-Faktor-Authentifizierung deaktiviert")


TOTP_MAX_ATTEMPTS = 5
TOTP_LOCKOUT_SECONDS = 900  # 15 Minuten


async def _check_totp_lockout(user_id: str, redis) -> None:
    """Wirft HTTP 429 wenn TOTP-Login wegen zu vieler Fehlversuche gesperrt ist."""
    key = f"totp_lockout:{user_id}"
    if await redis.exists(key):
        ttl = await redis.ttl(key)
        wait_min = ttl // 60 + 1
        raise HTTPException(429, detail=f"TOTP gesperrt. Bitte {wait_min} Minute(n) warten.")


async def _record_totp_failure(user_id: str, redis) -> None:
    """Zählt TOTP-Fehlversuch und setzt Lockout nach TOTP_MAX_ATTEMPTS Fehlversuchen."""
    fail_key = f"totp_fails:{user_id}"
    lock_key = f"totp_lockout:{user_id}"
    fails = await redis.incr(fail_key)
    await redis.expire(fail_key, TOTP_LOCKOUT_SECONDS)
    if fails >= TOTP_MAX_ATTEMPTS:
        await redis.set(lock_key, "1", ex=TOTP_LOCKOUT_SECONDS)
        await redis.delete(fail_key)


async def _clear_totp_failures(user_id: str, redis) -> None:
    """Löscht TOTP-Fehlversuche nach erfolgreichem Login."""
    await redis.delete(f"totp_fails:{user_id}")
    await redis.delete(f"totp_lockout:{user_id}")


@router.post("/totp/login", response_model=TokenResponse)
@limiter.limit("10/minute")
async def totp_login(
    request: Request,
    response: Response,
    data: TotpLoginRequest,
    db: AsyncSession = Depends(get_db),
):
    """Zweiter Login-Schritt: partial_token + TOTP-Code → volle Tokens."""
    import pyotp
    from uuid import UUID
    from app.core.redis import get_async_redis
    from app.core.security import decode_partial_token
    from app.models.user import User
    from jwt import PyJWTError as JWTError

    try:
        user_id = decode_partial_token(data.partial_token)
    except JWTError:
        raise HTTPException(401, "Ungültiger oder abgelaufener Token")

    user = await db.get(User, UUID(user_id))
    if not user or not user.is_active:
        raise HTTPException(401, "Benutzer nicht gefunden")
    if not user.totp_enabled or not user.totp_secret:
        raise HTTPException(400, "2FA nicht aktiv")

    redis_client = get_async_redis()
    # M-5: Lockout-Check vor TOTP-Verifikation
    await _check_totp_lockout(user_id, redis_client)

    plain_secret = _totp_plain(user)
    totp = pyotp.TOTP(plain_secret or "")
    if not totp.verify(data.code, valid_window=1):
        # M-5: Fehlversuch zählen
        await _record_totp_failure(user_id, redis_client)
        raise HTTPException(400, "Ungültiger Code")

    # M-5: Erfolg → Fehlversuche zurücksetzen
    await _clear_totp_failures(user_id, redis_client)

    from datetime import datetime, timezone
    user.last_login = datetime.now(timezone.utc)
    result = await auth_service._create_session(user, db, request=request)
    if result.refresh_token:
        _set_refresh_cookie(response, result.refresh_token)
        result.refresh_token = None  # Nicht im JSON-Body zurückgeben
    return result
