# -*- coding: utf-8 -*-
# Copyright 2021 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License")
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.


"""Role-based access control and permissions checking.

Provides functions to verify if users have the necessary privileges to
view, edit, or administer features and other resources.
"""

import flask
from google.cloud import ndb  # type: ignore

import settings
from framework.users import User
from internals import feature_helpers
from internals.core_models import FeatureEntry
from internals.review_models import Gate
from internals.user_models import AppUser


def can_admin_site(user: User) -> bool:
    """Return True if the current user is allowed to administer the site."""
    # A user is an admin if they have an AppUser entity that has is_admin set.
    if user:
        app_user = AppUser.get_app_user(user.email())
        if app_user is not None:
            return app_user.is_admin

    return False


def is_google_or_chromium_account(user: User) -> bool:
    """Return True if the current uses a @chromium.org or @google.com email."""
    # A user is an admin if they have an AppUser entity that has is_admin set.
    if user:
        return user.email().endswith(('@chromium.org', '@google.com'))
    return False


def can_view_feature_formatted(user: User, feature: dict) -> bool:
    """Return True if the user is allowed to view the given feature."""
    if not feature:
        return False

    return not feature[
        'confidential'
    ] or _can_view_confidential_feature_formatted(user, feature)  # noqa: E501


def _can_view_confidential_feature_formatted(user: User, feature: dict):
    """Check if the user is an owner, editor, spec mentor, or creator
    for this feature or has a google.com or chromium.org account.
    If yes, they feature can be viewed, otherwise they cannot view
    confidential features.
    """  # noqa: D205
    if not user:
        return False

    if is_google_or_chromium_account(user) or can_admin_site(user):
        return True

    email = user.email()
    is_participant = (
        # Basic JSON format
        email in feature.get('owners', [])
        or email in feature.get('editors', [])
        or email == feature.get('creator')
        or
        # Verbose or tiny JSON formats
        email in feature.get('owner_emails', [])
        or email in feature.get('editor_emails', [])
        or email in feature.get('cc_emails', [])
    )
    if is_participant:
        return True

    return False


def can_view_feature(user: User, feature: FeatureEntry) -> bool:
    """Return True if the user is allowed to view the given feature."""
    if not feature:
        return False

    if feature.deleted and not can_edit_feature(user, feature):
        return False

    return not feature.confidential or _can_view_confidential_feature(
        user, feature
    )  # noqa: E501


def _can_view_confidential_feature(user: User, feature: FeatureEntry):
    """Check if the user is an owner, editor, spec mentor, or creator
    for this feature or has a google.com or chromium.org account.
    If yes, they feature can be viewed, otherwise they cannot view
    confidential features.
    """  # noqa: D205
    if not user or not feature:
        return False

    if is_google_or_chromium_account(user) or can_admin_site(user):
        return True

    email = user.email()
    if (
        email in feature.owner_emails
        or email in feature.editor_emails
        or email == feature.creator_email
    ):
        return True

    return False


# Note: Once an organization has over 10 registered users and there
# are no concerns, we can register the entire organization for
# streamlining.  Other folks, and anyone needing to be a site editor,
# should email a request to become a registered user.
REGISTERED_USER_ORGANIZATIONS = (
    '@chromium.org',
    '@google.com',
    '@microsoft.com',
    '@igalia.com',
)


def can_create_feature(user: User) -> bool:
    """Return True if the user is allowed to create features."""
    if not user:
        return False

    if can_admin_site(user):
        return True

    if user.email().endswith(REGISTERED_USER_ORGANIZATIONS):
        return True

    app_user = AppUser.get_app_user(user.email())
    if app_user:
        return True

    return False


def can_comment(user: User) -> bool:
    """Return true if the user is allowed to post review comments."""
    return can_create_feature(user)


def can_edit_any_feature(user: User) -> bool:
    """Return True if the user is allowed to edit all features."""
    if not user:
        return False
    app_user = AppUser.get_app_user(user.email())
    if not app_user:
        return False

    # Site editors or admins should be able to edit any feature.
    return app_user.is_admin or app_user.is_site_editor


RELEASE_NOTE_REVIEWERS = [
    'elmirakalali@google.com',
    'siobhankeating@google.com',
    'mhoste@google.com',
    'kimreardon@google.com',
    'aaudi@google.com',
    'crushworth@google.com',
    'atarrant@google.com',
    'jrobbins@google.com',
]


def can_review_release_notes(user: User) -> bool:
    """Return True if the user is allowed to mark release notes final."""
    if not user:
        return False
    app_user = AppUser.get_app_user(user.email())

    return (
        app_user and app_user.is_admin
    ) or user.email() in RELEASE_NOTE_REVIEWERS


def feature_edit_list(user: User) -> list[int]:
    """Return a list of features the current user can edit"""  # noqa: D415
    if not user:
        return []

    # If the user can edit any feature, we don't need the full list.
    # We can just assume they will have edit access to all features.
    if can_edit_any_feature(user):
        return []

    # Query features to find which can be edited.
    editable_feature_keys: list[ndb.Key] = feature_helpers.get_by_participant(
        user.email()
    )
    # Return a list of unique ids of features that can be edited.
    return list(set([fk.integer_id() for fk in editable_feature_keys]))


def can_edit_feature(user: User, feature: FeatureEntry) -> bool:
    """Return True if the user is allowed to edit the given feature."""
    # If the user can edit any feature, they can edit this feature.
    if can_edit_any_feature(user):
        return True

    if not feature or not user:
        return False

    email = user.email()
    # Check if the user is an owner, editor, spec mentor, or creator
    # for this feature. If yes, the feature can be edited.
    return (
        email in feature.owner_emails
        or email in feature.editor_emails
        or (feature.spec_mentor_emails and email in feature.spec_mentor_emails)
        or email == feature.creator_email
    )


def can_review_gate(
    user: User, feature: FeatureEntry, gate: Gate | None, approvers: list[str]
) -> bool:
    """Return True if the user is allowed to review the given gate."""
    if not can_view_feature(user, feature):
        return False
    if can_admin_site(user):
        return True
    is_approver = user is not None and user.email() in approvers
    is_assigned = (
        user is not None
        and gate is not None
        and user.email() in gate.assignee_emails
    )
    return is_approver or is_assigned


def _maybe_redirect_to_login(handler_obj) -> flask.Response | dict:
    # Don't redirect if the handler is not an UI page (it is an API handler).
    if not hasattr(handler_obj, 'get_common_data'):
        return {}

    # Don't redirect if this is a UI page and we already redirected.
    common_data = handler_obj.get_common_data()
    if (
        'current_path' in common_data
        and 'loginStatus=False' in common_data['current_path']
    ):
        return {}

    return handler_obj.redirect(settings.LOGIN_PAGE_URL)


def _reject_or_proceed(
    handler_obj, handler_method, handler_args, handler_kwargs, perm_function
):
    """Redirect, abort(403), or call handler_method."""
    user = handler_obj.get_current_user()
    req = handler_obj.request

    # Give the user a chance to sign in
    if not user and req.method == 'GET':
        redirect = _maybe_redirect_to_login(handler_obj)
        if redirect:
            return redirect

    if not perm_function(user):
        handler_obj.abort(403)
    else:
        return handler_method(handler_obj, *handler_args, **handler_kwargs)


def require_admin_site(handler):
    """Handler decorator to require the user can admin the site."""

    def check_login(self, *args, **kwargs):
        return _reject_or_proceed(self, handler, args, kwargs, can_admin_site)

    return check_login


def require_view_feature(handler):
    """Handler decorator to require the user can view the current feature."""

    # TODO(jrobbins): make this per-feature
    def check_login(self, *args, **kwargs):
        return _reject_or_proceed(self, handler, args, kwargs, can_view_feature)

    return check_login


def require_create_feature(handler):
    """Handler decorator to require the user can create a feature."""

    def check_login(self, *args, **kwargs):
        return _reject_or_proceed(
            self, handler, args, kwargs, can_create_feature
        )

    return check_login


def validate_feature_create_permission(handler_obj):
    """Check if user has permission to create feature and abort if not."""
    user = handler_obj.get_current_user()
    req = handler_obj.request

    # Give the user a chance to sign in
    if not user and req.method == 'GET':
        redirect = _maybe_redirect_to_login(handler_obj)
        if redirect:
            return redirect

    # Respond with 403 if user does not have create permission for feature.
    if not can_create_feature(user):
        handler_obj.abort(403)


def validate_feature_edit_permission(
    handler_obj, fe: FeatureEntry
) -> flask.Response | dict:
    """Check if user has permission to edit feature and abort if not."""
    user = handler_obj.get_current_user()
    req = handler_obj.request

    # Give the user a chance to sign in
    if not user and req.method == 'GET':
        redirect = _maybe_redirect_to_login(handler_obj)
        if redirect:
            return redirect

    # Respond with 404 if feature was not found.
    # Load feature directly from NDB so as to never get a stale cached copy.
    if not fe:
        handler_obj.abort(404, msg='Feature not found')

    # Respond with 403 if user does not have edit permission for feature.
    feature_id = fe.key.integer_id()
    if not can_edit_feature(user, fe):
        handler_obj.abort(403, msg='User cannot edit feature %r' % feature_id)

    return {}
