from datetime import datetime, timedelta

import bcrypt
from fastapi import Depends, HTTPException, Request
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from fastapi_login import LoginManager
from ldap import INVALID_CREDENTIALS  # pylint: disable-msg=no-name-in-module  # ty: ignore[unresolved-import]
from ldap.ldapobject import ReconnectLDAPObject


from argos.logging import logger
from argos.schemas import Config, General
from argos.server.exceptions import NotAuthenticatedException
from argos.server.models import BlockedToken
from argos.server.queries import get_user

auth_scheme = HTTPBearer()


def get_db(request: Request):
    db = request.app.state.SessionLocal()
    try:
        yield db
    finally:
        db.close()


def get_config(request: Request):
    return request.app.state.config


async def get_manager(request: Request) -> LoginManager:
    if request.app.state.config.general.unauthenticated_access is not None:
        return await request.app.state.manager.optional(request)

    token = await request.app.state.manager._get_token(request)  # pylint: disable-msg=protected-access
    db = request.app.state.SessionLocal()
    if db.query(BlockedToken).filter(BlockedToken.token == token).count() > 0:
        raise NotAuthenticatedException

    return await request.app.state.manager(request)


async def verify_token(
    request: Request, token: HTTPAuthorizationCredentials = Depends(auth_scheme)
):
    """Verify agent token"""
    if token.credentials not in request.app.state.config.service.secrets:
        raise HTTPException(status_code=401, detail="Unauthorized")
    return token


async def good_user_credentials(
    config: Config, request: Request, username: str, password: str
):
    if config.general.ldap is not None:
        return await good_ldap_user_credentials(
            config, request.app.state.ldap, username, password
        )

    return await good_internal_user_credentials(
        request.app.state.SessionLocal(), username, password
    )


async def good_ldap_user_credentials(
    config: Config, ldapobj: ReconnectLDAPObject, username: str, password: str
) -> bool:
    ldap_dn = await find_ldap_user(config, ldapobj, username)
    if ldap_dn is None:
        return False
    try:
        ldapobj.simple_bind_s(ldap_dn, password)
    except INVALID_CREDENTIALS:
        return False

    return True


async def find_ldap_user(config, ldapobj, user: str) -> str | None:
    """Do a LDAP search for user and return its dn"""
    import ldap
    import ldap.filter as ldap_filter
    from ldapurl import LDAP_SCOPE_SUBTREE

    try:
        ldapobj.simple_bind_s(config.general.ldap.bind_dn, config.general.ldap.bind_pwd)
    except ldap.LDAPError as err:  # pylint: disable-msg=no-member  # ty: ignore[unresolved-attribute]
        logger.error("LDAP error: %s", err)
        return None

    result = ldapobj.search_s(
        config.general.ldap.user_tree,
        LDAP_SCOPE_SUBTREE,
        filterstr=ldap_filter.filter_format(
            f"(&(%s=%s){config.general.ldap.user_filter})",
            [
                config.general.ldap.user_attr,
                user,
            ],
        ),
        attrlist=[config.general.ldap.user_attr],
    )

    # If there is a result, there should, logically, be only one entry
    if len(result) > 0:
        return result[0][0]

    return None


async def good_internal_user_credentials(db, username, password) -> bool:
    user = await get_user(db, username)
    if user is None:
        return False

    if not bcrypt.checkpw(password.encode("utf-8"), user.password.encode("utf-8")):
        return False

    user.last_login_at = datetime.now()
    db.commit()

    return True


async def create_user_token(
    manager, config: General, username: str, rememberme: str | None = None
):
    session_duration = config.session_duration
    if config.remember_me_duration is not None and rememberme == "on":
        session_duration = config.remember_me_duration

    delta = timedelta(minutes=session_duration)
    return {
        "token": manager.create_access_token(data={"sub": username}, expires=delta),
        "delta": delta,
    }
