# This file is part of Indico.
# Copyright (C) 2002 - 2026 CERN
#
# Indico is free software; you can redistribute it and/or
# modify it under the terms of the MIT License; see the
# LICENSE file for more details.

from flask import request, session
from marshmallow import fields
from werkzeug.exceptions import NotFound, Unauthorized

from indico.core.oauth.util import TOKEN_PREFIX_SERVICE
from indico.modules.events.contributions.controllers.display import RHContributionDisplayBase
from indico.modules.events.controllers.base import RHDisplayEventBase
from indico.modules.events.editing.fields import EditableList
from indico.modules.events.editing.models.editable import Editable, EditableType
from indico.modules.events.editing.settings import editing_settings
from indico.modules.events.management.controllers.base import RHManageEventBase
from indico.modules.events.models.events import Event
from indico.util.marshmallow import not_empty
from indico.web.args import parse_single_arg, use_kwargs
from indico.web.rh import RequireUserMixin


class TokenAccessMixin:
    SERVICE_ALLOWED = False
    is_service_call = False

    def _token_can_access(self):
        # we need to "fish" the event here because at this point _check_params
        # hasn't run yet
        event = Event.get_or_404(request.view_args['event_id'], is_deleted=False)
        if (
            not self.SERVICE_ALLOWED or
            not request.bearer_token or
            not request.bearer_token.startswith(TOKEN_PREFIX_SERVICE)
        ):
            return False

        event_token = editing_settings.get(event, 'service_token')
        if request.bearer_token != event_token:
            raise Unauthorized('Invalid bearer token')

        self.is_service_call = True
        return True

    def _check_csrf(self):
        # check CSRF if there is no bearer token or there's a session cookie
        if session.user or not request.bearer_token:
            super()._check_csrf()


class RHEditingBase(TokenAccessMixin, RequireUserMixin, RHDisplayEventBase):
    """Base class for editing RHs that don't reference an editable."""

    EVENT_FEATURE = 'editing'

    def _check_access(self):
        if not TokenAccessMixin._token_can_access(self):
            RHDisplayEventBase._check_access(self)
            RequireUserMixin._check_access(self)


class RHEditingManagementBase(TokenAccessMixin, RHManageEventBase):
    """Base class for editing RHs that don't reference an editable."""

    EVENT_FEATURE = 'editing'
    PERMISSION = 'editing_manager'

    def _check_access(self):
        if not TokenAccessMixin._token_can_access(self):
            super()._check_access()


class RHEditableTypeManagementBase(RHEditingManagementBase):
    """Base class for editable type RHs."""

    def _process_args(self):
        RHManageEventBase._process_args(self)
        self.editable_type = EditableType[request.view_args['type']]


class RHEditableTypeEditorBase(RHEditableTypeManagementBase):
    """Base class for editable type RHs accessible by editors."""

    def _check_access(self):
        if session.user and self.event.can_manage(session.user, self.editable_type.editor_permission):
            # editors have access here without the need for any management permission
            return
        RHEditableTypeManagementBase._check_access(self)


class RHContributionEditableBase(TokenAccessMixin, RequireUserMixin, RHContributionDisplayBase):
    """Base class for operations on an editable."""

    EVENT_FEATURE = 'editing'
    EDITABLE_REQUIRED = True

    normalize_url_spec = {
        'locators': {
            lambda self: self.contrib
        },
        'preserved_args': {'type'}
    }

    _editable_query_options = ()

    def _check_access(self):
        if not TokenAccessMixin._token_can_access(self):
            RequireUserMixin._check_access(self)
            RHContributionDisplayBase._check_access(self)

    def _can_view_unpublished(self):
        if super()._can_view_unpublished():
            return True
        return self.editable is not None and self.editable.can_see_timeline(session.user)

    @use_kwargs({
        'editable_type': fields.Enum(EditableType, data_key='type'),
    }, location='view_args')
    def _process_args(self, editable_type):
        RHContributionDisplayBase._process_args(self)
        self.editable_type = editable_type
        self.editable = (Editable.query
                         .with_parent(self.contrib)
                         .filter_by(type=self.editable_type)
                         .options(*self._editable_query_options)
                         .first())
        if self.editable is None and self.EDITABLE_REQUIRED:
            raise NotFound


class RHEditablesBase(RHEditingManagementBase):
    """Base class for operations on multiple editables."""

    @use_kwargs({
        'editable_type': fields.Enum(EditableType, data_key='type'),
    }, location='view_args')
    def _process_args(self, editable_type):
        RHEditingManagementBase._process_args(self)
        self.editable_type = editable_type
        self.editables = parse_single_arg(
            'editables', EditableList(self.event, self.editable_type, required=True, validate=not_empty)
        )
