# (c) cavaliba.com - data - aaa.py

"""
Authentication/authorization context ("aaa" dict) for regular (session-based)
views and ajax/command entry points — the counterpart of app_data.api.aaa for
the browser/CLI side. get_aaa() builds the aaa dict from the configured
CAVALIBA_AUTH_MODE (basic/oauth2/local/unittest/forced/impersonate); start_view/
start_ajax/start_command wrap it with caching and redirect/permission handling.

Method inventory:
    get_username, update_last_login, start_command, start_ajax, start_view,
    get_aaa
"""

import base64
import datetime
import re

from django.conf import settings

import app_home.cache as cache
from app_data.ip import get_user_ip, is_trusted_ip
from app_data.telemetry import request_id, telemetry, telemetry_bump, telemetry_flush
from app_data.user import User
from app_home.configuration import get_configuration
from app_home.log import log2


def get_username(aaa=None):
    """Return aaa["username"] if aaa is set, else "auto" (for background/system actions)."""

    if aaa:
        return aaa.get("username", "auto")
    else:
        return "auto"


def update_last_login(aaa):
    """Update the user's last_login timestamp, throttled to at most once per
    CAVALIBA_LAST_LOGIN_MINUTES to avoid a DB write on every request."""

    if aaa["userid"]:
        last_login = aaa.get("last_login")
        if last_login:
            try:
                last_dt = datetime.datetime.strptime(last_login, "%Y-%m-%d %H:%M:%S")
                delta = datetime.datetime.utcnow() - last_dt
                if delta.total_seconds() < settings.CAVALIBA_LAST_LOGIN_MINUTES * 60:
                    return
            except Exception:
                pass
        user = User.from_id(id=aaa["userid"])
        user.update_last_login()


def start_command(command="n/a", action="n/a"):
    """Init the request-local cache and log the start of a management command."""

    cache.init()
    log2.bind_command(command, action)

    log2.ok("cli.start")

    return


def start_ajax(request):
    """Build the ajax view context: aaa dict (from session cache or freshly computed
    via get_aaa()) plus cache.cache_aaa/context["aaa"] populated for the request."""

    cache.init()
    log2.enter(channel="web", origin=request.path, client_ip=get_user_ip(request))

    endpoint = getattr(request.resolver_match, "url_name", "") or ""
    telemetry_flush()  # safety net, see start_view
    telemetry_bump("ajax.request", id=endpoint)
    request_id(endpoint)

    if get_configuration(appname="env", keyname="CAVALIBA_AAA_NOCACHE") == "yes":
        aaa = get_aaa(request)
        request.session["aaa"] = aaa
        log2.debug("start_ajax - aaa not cached: %s", aaa)
    else:
        if "aaa" in request.session:
            aaa = request.session["aaa"]
        else:
            aaa = get_aaa(request)
            request.session["aaa"] = aaa

    cache.cache_aaa = aaa
    log2.bind_aaa(aaa)

    context = {}
    context["aaa"] = aaa

    return context


def start_view(
    request, app="na", view="na", auth_required=True, perm=None, noauthz=None, visitor=False
):
    """Build the view context: aaa dict (session-cached per CACHE_SESSION config),
    plus context["redirect"] set to `noauth`/`noauthz` if authentication/permission
    (`perm`) requirements aren't met. Also triggers update_last_login()."""

    cache.init()
    log2.enter(channel="web", origin=view, domain=app, client_ip=get_user_ip(request))

    telemetry_flush()  # safety net: a previous request whose request_finished never fired
    telemetry_bump("view.request", id=f"{app}.{view}")
    request_id(f"{app}.{view}")

    cache_session = get_configuration(appname="user", keyname="CACHE_SESSION")

    if cache_session == "no":
        aaa = get_aaa(request)
        telemetry_bump("cache.session", id="nocache")
        log2.debug("start_view %s.%s - cache_session=no ; get_aaa() called", app, view)
    else:
        if "aaa" in request.session:
            aaa = request.session["aaa"]
            telemetry_bump("cache.session", id="hit")
            log2.debug("start_view %s.%s - cache_session=yes ; cache hit", app, view)
        else:
            telemetry_bump("cache.session", id="miss")
            aaa = get_aaa(request)
            log2.debug("start_view %s.%s - cache_session=yes ; cache miss ; get_aaa()", app, view)
            if aaa["is_authenticated"]:
                request.session["aaa"] = aaa
                log2.debug("start_view - cache miss + authenticated ; set request.session")
                # new federated session; local form logins are counted by the login signal
                if aaa["auth_mode"] != "local":
                    telemetry("auth.login", id=aaa["auth_mode"])
            else:
                request.session.flush()
                log2.debug("start_view - cache miss + not authenticated ; flush session")

    cache.cache_aaa = aaa
    log2.bind_aaa(aaa)

    log2.debug("start_view - aaa: %s", aaa)

    context = {}
    context["aaa"] = aaa
    context["redirect"] = None

    update_last_login(aaa)
    if aaa["is_authenticated"] and aaa.get("username"):
        telemetry("users.active", value=aaa["username"])

    if auth_required:
        if aaa["auth_mode"] == "local":
            if not (aaa["is_authenticated"] or (visitor and aaa["is_visitor"])):
                context["redirect"] = f"{settings.LOGIN_URL}?next={request.path}"
                log2.debug("start_view - auth required, local mode, not authenticated")
                return context

        else:
            if not (aaa["is_authenticated"] or (visitor and aaa["is_visitor"])):
                log2.debug("start_view - auth required, not authenticated (other mode)")
                context["redirect"] = settings.CAVALIBA_PUBLIC_REDIRECT
                return context

    if perm:
        if perm not in aaa["perms"]:
            log2.deny(f"{view}.access", reason="permission_denied", extra={"perm": perm})
            if noauthz:
                context["redirect"] = noauthz
            else:
                context["redirect"] = settings.CAVALIBA_PUBLIC_REDIRECT
            return context

    log2.debug("start_view %s.%s", app, view)

    return context


def get_aaa(request, auth_mode=None, impersonate=None, impersonate_by=None):
    """Build the aaa dict from scratch for this request: resolve the login name per
    CAVALIBA_AUTH_MODE (basic/oauth2/local/unittest/forced/impersonate), look up or
    JIT-provision the User, then populate perms/groups/roles/is_admin. Returns
    cache.cache_aaa unchanged if already populated this request."""

    if len(cache.cache_aaa) > 0:
        log2.debug("get_aaa - from cache.cache_aaa")
        return cache.cache_aaa

    aaa = {}

    uname = ""
    email = ""

    aaa["is_trusted_ip"] = False
    aaa["is_anonymous"] = True
    aaa["is_authenticated"] = False
    aaa["is_visitor"] = False
    aaa["is_admin"] = False

    aaa["username"] = "unknown"
    aaa["user"] = None
    aaa["userid"] = None

    aaa["groups"] = []
    aaa["groups_direct"] = []
    aaa["groups_computed"] = []
    aaa["groups_indirect"] = []
    aaa["perms"] = []
    aaa["roles"] = []

    aaa["user_ip"] = get_user_ip(request)
    aaa["is_trusted_ip"] = is_trusted_ip(aaa["user_ip"])

    aaa["impersonate_by"] = ""
    aaa["last_login"] = None

    aaa["sync_mode"] = False  # this user is not syncing data by default (no update to last_sync)

    if not auth_mode:
        auth_mode = get_configuration(appname="env", keyname="CAVALIBA_AUTH_MODE")

    aaa["auth_mode"] = auth_mode

    log2.debug("get_aaa - auth_mode: %s", auth_mode)

    if auth_mode == "basic":
        header = request.META.get("HTTP_AUTHORIZATION")
        try:
            auth = header.split()
        except Exception:
            auth = ""
            uname = ""
        if len(auth) == 2:
            if auth[0].lower() == "basic":
                myenc = auth[1]
                myenc = bytes(myenc, encoding="utf-8")
                uname, pwd = str(base64.b64decode(myenc)).split(":")
                uname = uname[2:]

    elif auth_mode == "oauth2":
        login_field = get_configuration(appname="user", keyname="AUTH_FEDERATED_LOGIN_FIELD")
        uname = request.headers.get(login_field)
        email_field = get_configuration(appname="user", keyname="AUTH_FEDERATED_EMAIL_FIELD")
        email = request.headers.get(email_field)

        log2.debug("get_aaa - oauth2 login_field %s = %s, email_field %s = %s",
                   login_field, uname, email_field, email)  # fmt: skip

    elif auth_mode == "local":
        if request.user.is_authenticated:
            uname = request.user.username
            aaa["is_anonymous"] = False
            log2.debug("get_aaa - local - anonymous=False, request.user.username %s", uname)
        else:
            log2.debug("get_aaa - local - request.user not authenticated, no username")

    elif auth_mode == "unittest":
        uname = "unittest"
        aaa["is_anonymous"] = False

    elif auth_mode == "forced":
        force_user = get_configuration(appname="env", keyname="CAVALIBA_FORCE_LOGIN")
        log2.debug("get_aaa - force_user %s", force_user)
        if len(force_user) > 0:
            uname = force_user
            aaa["is_anonymous"] = False
        else:
            aaa["is_anonymous"] = True
            log2.debug("get_aaa - no force_user ; anonymous. DONE")
            return aaa

    elif auth_mode == "impersonate":
        uname = impersonate
        aaa["impersonate_by"] = impersonate_by

    if not uname:
        aaa["is_anonymous"] = True
        log2.debug("get_aaa - no uname ; anonymous. DONE")
        return aaa

    if len(uname) == 0:
        aaa["is_anonymous"] = True
        log2.debug("get_aaa - empty uname ; anonymous. DONE")
        return aaa

    aaa["is_anonymous"] = False
    log2.debug("get_aaa - uname %s ; not anonymous", uname)

    truncate_login = get_configuration(appname="user", keyname="AUTH_LOGIN_REMOVE_DOMAIN")
    if truncate_login == "yes":
        if "@" in uname:
            uname = re.sub("@(.*)$", "", uname)
            log2.debug("get_aaa - domain removed from login ; uname %s", uname)

    aaa["username"] = uname

    aaa["is_visitor"] = True
    aaa["is_authenticated"] = False

    user = User.from_keyname(keyname=uname)

    jit = get_configuration(appname="user", keyname="AUTH_PROVISIONING")
    log2.debug("get_aaa - user %s ; jit config %s", user, jit)

    if user:
        aaa["is_visitor"] = False
        aaa["is_authenticated"] = True
        log2.debug("get_aaa - user found in DB ; authenticated")

        if not user.is_enabled:
            aaa["is_authenticated"] = False
            log2.debug("get_aaa - user disabled ; not authenticated. DONE")
            return aaa

    else:
        log2.debug("get_aaa - user not in DB ; visitor for now")

        if jit == "visitor":
            aaa["is_visitor"] = True
            aaa["is_authenticated"] = False
            log2.debug("get_aaa - jit %s ; visitor, not authenticated", jit)

        elif jit == "create" or jit == "sync":
            user = User(keyname=uname)
            if email:
                user.set_field_value_single(fieldname="email", value=email)
            user.save(aaa=aaa, action="create")

            if user:
                log2.ok("user.jit_create", target=("user", uname), extra={"mode": jit})
                aaa["is_visitor"] = False
                aaa["is_authenticated"] = True
                log2.debug("get_aaa - jit %s ; user created, authenticated", jit)
            else:
                log2.fail("user.jit_create", reason="save_failed", target=("user", uname),
                          extra={"mode": jit})  # fmt: skip
                log2.debug("get_aaa - jit %s ; user not created", jit)
                aaa["is_visitor"] = True
                aaa["is_authenticated"] = True
                return aaa

        else:
            aaa["is_visitor"] = False
            aaa["is_authenticated"] = False
            log2.debug("get_aaa - not in DB, jit %s ; not authenticated. DONE", jit)
            return aaa

    if user:
        aaa["userid"] = user.id
        aaa["user"] = user.to_dict()
        aaa["last_login"] = user.last_login

    if user:
        try:
            user.compute_aaa()
        except Exception:
            pass

        aaa["perms"] = [p.keyname for p in user.permissions]
        aaa["groups_direct"] = [g.keyname for g in user.direct_groups]
        aaa["groups_computed"] = [g.keyname for g in user.auto_groups]
        aaa["groups_indirect"] = [g.keyname for g in user.parent_groups]
        aaa["groups"] = [g.keyname for g in user.groups]
        aaa["roles"] = [r.keyname for r in user.roles]

    if aaa["username"] == "admin" or "role_admin" in aaa["roles"]:
        aaa["is_admin"] = True

    log2.debug("get_aaa - done ; is_admin %s groups %s roles %s perms %s",
               aaa["is_admin"], aaa["groups"], aaa["roles"], aaa["perms"])  # fmt: skip

    return aaa
