import secrets
from datetime import datetime, timedelta

from fastapi import APIRouter, Depends, HTTPException, BackgroundTasks, status, Request
from sqlalchemy.orm import Session

from app.database import get_db
from app.models.user import User
from app.models.email_verification import EmailVerification
from app.models.password_reset import PasswordReset
from app.schemas.auth import (
    UserCreate, UserResponse, Token, LoginRequest,
    UserUpdateMe, ForgotPasswordRequest, ResetPasswordRequest,
)
from app.services import email_service
from app.utils.auth import (
    get_password_hash, verify_password, create_access_token, get_current_user,
)

router = APIRouter()


def _check_login_rate_limit(client_ip: str):
    """Rate limit: max 10 login attempts per IP per 15 min, persisted in Redis."""
    try:
        import redis as redis_lib
        from app.config import settings
        r = redis_lib.from_url(settings.REDIS_URL, decode_responses=True, socket_connect_timeout=1)
        key = f"login_attempts:{client_ip}"
        count = r.incr(key)
        if count == 1:
            r.expire(key, 15 * 60)
        if count > 10:
            raise HTTPException(status_code=429, detail="Too many login attempts. Try again in 15 minutes.")
    except HTTPException:
        raise
    except Exception as e:
        import logging; logging.getLogger(__name__).warning(f"Redis rate limit unavailable (failing open): {e}")


# Rate limit: max 3 reset requests per email per hour
RESET_LIMIT = 3
RESET_WINDOW_HOURS = 1


def _check_reset_rate_limit(db: Session, email: str):
    email_normalized = email.lower().strip()
    user = db.query(User).filter(User.email == email_normalized).first()
    if not user:
        return  # silently ignore unknown emails
    window_start = datetime.utcnow() - timedelta(hours=RESET_WINDOW_HOURS)
    count = db.query(PasswordReset).filter(
        PasswordReset.user_id == user.id,
        PasswordReset.created_at >= window_start,
    ).count()
    if count >= RESET_LIMIT:
        raise HTTPException(
            status_code=status.HTTP_429_TOO_MANY_REQUESTS,
            detail=f"Too many reset requests. Please wait before trying again.",
        )


async def _verify_turnstile(token: str) -> bool:
    """Verify Cloudflare Turnstile token. Returns True if valid or if not configured."""
    from app.config import settings as cfg
    secret = cfg.TURNSTILE_SECRET_KEY
    if not secret:
        return True
    try:
        import httpx
        resp = httpx.post(
            "https://challenges.cloudflare.com/turnstile/v0/siteverify",
            data={"secret": secret, "response": token},
            timeout=10,
        )
        return resp.json().get("success", False)
    except Exception as e:
        import logging; logging.getLogger(__name__).warning(f"Turnstile verification failed (failing open): {e}")
        return True


@router.post("/register")
async def register(user_data: UserCreate, background_tasks: BackgroundTasks, db: Session = Depends(get_db)):
    # Verify Turnstile if configured
    from app.config import settings as cfg
    if cfg.TURNSTILE_SECRET_KEY:
        if not user_data.turnstile_token:
            raise HTTPException(status_code=400, detail="Bot verification required")
        if not await _verify_turnstile(user_data.turnstile_token):
            raise HTTPException(status_code=400, detail="Bot verification failed — please try again")

    # Check if registration is enabled (unless this is the first user - always allow admin creation)
    is_first_user = db.query(User).count() == 0
    if not is_first_user:
        try:
            import redis as redis_lib
            from app.config import settings
            r = redis_lib.from_url(settings.REDIS_URL, decode_responses=True, socket_connect_timeout=1)
            registration_enabled = r.get("settings:registration_enabled")
            # Default to enabled if not set, disabled if explicitly set to "false"
            if registration_enabled == "false":
                raise HTTPException(status_code=403, detail="Registration is currently disabled by administrator")
        except HTTPException:
            raise
        except Exception as e:
            import logging; logging.getLogger(__name__).warning(f"Redis registration check failed (failing open): {e}")

    if len(user_data.password) < 8:
        raise HTTPException(status_code=400, detail="Password must be at least 8 characters")

    email_normalized = user_data.email.lower().strip()
    existing = db.query(User).filter(User.email == email_normalized).first()
    if existing:
        raise HTTPException(status_code=400, detail="Email already registered")
    user = User(
        email=email_normalized,
        hashed_password=get_password_hash(user_data.password),
        name=user_data.name,
        role="admin" if is_first_user else "user",
    )
    db.add(user)
    db.flush()

    # Create email verification record
    token = secrets.token_urlsafe(32)
    verification = EmailVerification(
        user_id=user.id,
        token=token,
        expires_at=datetime.utcnow() + timedelta(hours=24),
        verified_at=datetime.utcnow() if is_first_user else None,
    )
    db.add(verification)
    db.commit()
    db.refresh(user)

    if is_first_user:
        # Auto-verified admin — log them in immediately
        access_token = create_access_token(data={"sub": user.email})
        return {"access_token": access_token, "token_type": "bearer"}

    # Regular user — must verify email before logging in
    background_tasks.add_task(email_service.send_verification_email, user.email, user.name, token)
    return {"requires_verification": True, "message": "Account created. Please check your email to verify your account before logging in."}


@router.post("/login", response_model=Token)
async def login(login_data: LoginRequest, db: Session = Depends(get_db), request: Request = None):
    # Block password login if SSO-only mode
    sso_settings = _get_sso_settings()
    if sso_settings["sso_only"]:
        raise HTTPException(status_code=403, detail="Password login is disabled. Please use SSO.")

    # Verify Turnstile if configured
    from app.config import settings as cfg
    if cfg.TURNSTILE_SECRET_KEY:
        if not login_data.turnstile_token:
            raise HTTPException(status_code=400, detail="Bot verification required")
        if not await _verify_turnstile(login_data.turnstile_token):
            raise HTTPException(status_code=400, detail="Bot verification failed — please try again")

    if request:
        client_ip = request.client.host if request.client else "unknown"
        _check_login_rate_limit(client_ip)

    email_normalized = login_data.email.lower().strip()
    user = db.query(User).filter(User.email == email_normalized).first()
    if not user or not verify_password(login_data.password, user.hashed_password):
        raise HTTPException(status_code=401, detail="Invalid email or password")

    # Check email verification — skip for users without any verification record (legacy/seeded)
    verification = db.query(EmailVerification).filter(EmailVerification.user_id == user.id).first()
    if verification and verification.verified_at is None:
        raise HTTPException(
            status_code=403,
            detail="Email not verified. Please check your inbox and verify your email before logging in.",
        )

    access_token = create_access_token(data={"sub": user.email})
    return Token(access_token=access_token)


@router.get("/verify-email")
def verify_email(token: str, db: Session = Depends(get_db)):
    record = db.query(EmailVerification).filter(EmailVerification.token == token).first()
    if not record:
        raise HTTPException(status_code=400, detail="Invalid verification token")
    if record.verified_at is not None:
        return {"message": "Email already verified. You can log in."}
    if datetime.utcnow() > record.expires_at:
        raise HTTPException(status_code=400, detail="Verification link has expired. Please register again or request a new link.")

    record.verified_at = datetime.utcnow()
    db.commit()
    return {"message": "Email verified successfully! You can now log in."}


@router.post("/resend-verification")
async def resend_verification(data: ForgotPasswordRequest, background_tasks: BackgroundTasks, db: Session = Depends(get_db)):
    email_normalized = data.email.lower().strip()
    user = db.query(User).filter(User.email == email_normalized).first()
    if not user:
        return {"message": "If that email exists, a verification link has been sent."}

    record = db.query(EmailVerification).filter(EmailVerification.user_id == user.id).first()
    if record and record.verified_at is not None:
        return {"message": "Email already verified."}

    token = secrets.token_urlsafe(32)
    if record:
        record.token = token
        record.expires_at = datetime.utcnow() + timedelta(hours=24)
    else:
        db.add(EmailVerification(user_id=user.id, token=token, expires_at=datetime.utcnow() + timedelta(hours=24)))
    db.commit()

    background_tasks.add_task(email_service.send_verification_email, user.email, user.name, token)
    return {"message": "If that email exists, a verification link has been sent."}


@router.post("/forgot-password")
async def forgot_password(data: ForgotPasswordRequest, background_tasks: BackgroundTasks, db: Session = Depends(get_db)):
    email_normalized = data.email.lower().strip()
    _check_reset_rate_limit(db, email_normalized)

    user = db.query(User).filter(User.email == email_normalized).first()
    # Always return same message to avoid email enumeration
    if not user:
        return {"message": "If that email is registered, a reset link has been sent."}

    token = secrets.token_urlsafe(32)
    reset = PasswordReset(
        user_id=user.id,
        token=token,
        expires_at=datetime.utcnow() + timedelta(hours=1),
    )
    db.add(reset)
    db.commit()

    background_tasks.add_task(email_service.send_password_reset_email, user.email, user.name, token)
    return {"message": "If that email is registered, a reset link has been sent."}


@router.post("/reset-password")
def reset_password(data: ResetPasswordRequest, db: Session = Depends(get_db)):
    if len(data.new_password) < 8:
        raise HTTPException(status_code=400, detail="Password must be at least 8 characters")

    record = db.query(PasswordReset).filter(
        PasswordReset.token == data.token,
        PasswordReset.used == False,
    ).first()
    if not record:
        raise HTTPException(status_code=400, detail="Invalid or already used reset token")
    if datetime.utcnow() > record.expires_at:
        raise HTTPException(status_code=400, detail="Reset link has expired. Please request a new one.")

    user = db.query(User).filter(User.id == record.user_id).first()
    if not user:
        raise HTTPException(status_code=404, detail="User not found")

    user.hashed_password = get_password_hash(data.new_password)
    record.used = True
    db.commit()
    return {"message": "Password reset successfully. You can now log in."}


@router.get("/me/settings")
def get_user_settings(
    current_user: User = Depends(get_current_user),
    db: Session = Depends(get_db),
):
    """Get user settings. Most settings live in Redis (Nextcloud config etc.);
    reminders_disabled is canonical in Postgres and overrides Redis."""
    data = {}
    try:
        import redis as redis_lib, json
        from app.config import settings as cfg
        r = redis_lib.from_url(cfg.REDIS_URL, decode_responses=True)
        raw = r.get(f"user_settings:{current_user.id}")
        if raw:
            data = json.loads(raw)
    except Exception as e:
        import logging; logging.getLogger(__name__).warning(f"Failed to load user settings: {e}")

    # Canonical opt-out preference comes from the DB
    data["reminders_disabled"] = bool(current_user.reminders_disabled)
    return data


@router.put("/me/settings")
def save_user_settings(
    settings_data: dict,
    current_user: User = Depends(get_current_user),
    db: Session = Depends(get_db),
):
    """Save user settings. reminders_disabled is persisted to Postgres;
    other keys go to Redis."""
    # Persist opt-out preference to DB (canonical source for the scheduler)
    if "reminders_disabled" in settings_data:
        current_user.reminders_disabled = bool(settings_data.get("reminders_disabled"))
        db.add(current_user)
        db.commit()

    # Keep the full blob in Redis so other fields (Nextcloud config etc.) persist
    try:
        import redis as redis_lib, json
        from app.config import settings as cfg
        r = redis_lib.from_url(cfg.REDIS_URL, decode_responses=True)
        r.set(f"user_settings:{current_user.id}", json.dumps(settings_data))
    except Exception as e:
        import logging; logging.getLogger(__name__).warning(f"Failed to save user settings to Redis: {e}")
    return {"saved": True}


@router.get("/me", response_model=UserResponse)
def get_me(current_user: User = Depends(get_current_user)):
    return current_user


@router.put("/me")
def update_me(data: UserUpdateMe, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
    if data.new_password:
        if not data.current_password:
            raise HTTPException(status_code=400, detail="Current password required to set a new one")
        if not verify_password(data.current_password, current_user.hashed_password):
            raise HTTPException(status_code=400, detail="Current password is incorrect")
        if len(data.new_password) < 8:
            raise HTTPException(status_code=400, detail="New password must be at least 8 characters")
        current_user.hashed_password = get_password_hash(data.new_password)

    if data.name:
        current_user.name = data.name

    db.commit()
    db.refresh(current_user)
    return {
        "id": current_user.id, "email": current_user.email,
        "name": current_user.name, "role": current_user.role,
    }


# ── SSO / OIDC ─────────────────────────────────────────────────────

def _get_sso_settings():
    """Return SSO admin settings from Redis."""
    try:
        import redis as redis_lib
        from app.config import settings as cfg
        r = redis_lib.from_url(cfg.REDIS_URL, decode_responses=True, socket_connect_timeout=1)
        return {
            "sso_only": r.get("settings:sso_only") == "true",
        }
    except Exception:
        return {"sso_only": False}


@router.get("/sso/config")
def sso_config():
    """Public endpoint — tells frontend whether SSO is available and login mode."""
    from app.config import settings as cfg
    sso_enabled = bool(cfg.OIDC_PROVIDER_URL and cfg.OIDC_CLIENT_ID)
    sso_settings = _get_sso_settings()
    return {
        "sso_enabled": sso_enabled,
        "sso_only": sso_settings["sso_only"],
        "provider_name": cfg.OIDC_PROVIDER_NAME if sso_enabled else None,
    }


@router.get("/sso/login")
def sso_login(request: Request):
    """Redirect user to the OIDC provider for login."""
    from authlib.integrations.starlette_client import OAuth
    from starlette.responses import RedirectResponse
    from app.config import settings as cfg

    if not cfg.OIDC_PROVIDER_URL or not cfg.OIDC_CLIENT_ID:
        raise HTTPException(status_code=400, detail="SSO is not configured")

    oauth = OAuth()
    oauth.register(
        name="oidc",
        server_metadata_url=f"{cfg.OIDC_PROVIDER_URL.rstrip('/')}/.well-known/openid-configuration",
        client_id=cfg.OIDC_CLIENT_ID,
        client_secret=cfg.OIDC_CLIENT_SECRET,
        client_kwargs={"scope": cfg.OIDC_SCOPES},
    )

    redirect_uri = f"{cfg.APP_URL}/api/auth/sso/callback"
    return oauth.oidc.authorize_redirect(request, redirect_uri)


@router.get("/sso/callback")
async def sso_callback(request: Request, db: Session = Depends(get_db)):
    """Handle OIDC provider callback — create or login user."""
    from authlib.integrations.starlette_client import OAuth
    from starlette.responses import RedirectResponse
    from app.config import settings as cfg

    if not cfg.OIDC_PROVIDER_URL or not cfg.OIDC_CLIENT_ID:
        raise HTTPException(status_code=400, detail="SSO is not configured")

    oauth = OAuth()
    oauth.register(
        name="oidc",
        server_metadata_url=f"{cfg.OIDC_PROVIDER_URL.rstrip('/')}/.well-known/openid-configuration",
        client_id=cfg.OIDC_CLIENT_ID,
        client_secret=cfg.OIDC_CLIENT_SECRET,
        client_kwargs={"scope": cfg.OIDC_SCOPES},
    )

    try:
        token = await oauth.oidc.authorize_access_token(request)
    except Exception:
        return RedirectResponse(url=f"{cfg.APP_URL}/login?error=sso_failed")

    userinfo = token.get("userinfo") or {}
    email = userinfo.get("email", "").lower().strip()
    name = userinfo.get("name") or userinfo.get("preferred_username") or email.split("@")[0]

    if not email:
        return RedirectResponse(url=f"{cfg.APP_URL}/login?error=no_email")

    # Find or create user
    user = db.query(User).filter(User.email == email).first()
    if not user:
        user = User(
            email=email,
            hashed_password=get_password_hash(secrets.token_urlsafe(32)),  # random password — SSO users don't use it
            name=name,
            role="user",
        )
        db.add(user)
        db.flush()
        # Auto-verify SSO users
        verification = EmailVerification(
            user_id=user.id,
            token=secrets.token_urlsafe(32),
            expires_at=datetime.utcnow() + timedelta(hours=1),
            verified_at=datetime.utcnow(),
        )
        db.add(verification)
        db.commit()
        db.refresh(user)

    access_token = create_access_token(data={"sub": user.email})
    return RedirectResponse(url=f"{cfg.APP_URL}/sso-callback?token={access_token}")
