# Copyright (c) 2026 Thomas Goirand <zigo@debian.org>
#
# 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.

"""RequestContext: context for requests that persist through all of vigie."""

import copy
from typing import Any, Optional

from oslo_context import context
from oslo_utils import timeutils

# Import the centralized config getter to ensure consistent initialization
from vigie.config import get_config

# Ensure configuration is loaded and CONF is available
CONF = get_config()


class RequestContext(context.RequestContext):
    """Security context and request information.

    Represents the user taking a given action within the system.
    This class extends oslo_context.RequestContext to add Vigie-specific
    attributes while maintaining compatibility with the oslo_policy enforcer.
    """

    def __init__(self,
                 user_id: Optional[str] = None,
                 project_id: Optional[str] = None,
                 is_admin: Optional[bool] = None,
                 read_deleted: Optional[str] = "no",
                 project_name: Optional[str] = None,
                 remote_address: Optional[str] = None,
                 timestamp=None,
                 quota_class=None,
                 service_catalog: Optional[list] = None,
                 **kwargs):
        """Initialize RequestContext.

        :param read_deleted: 'no' indicates deleted records are hidden, 'yes'
            indicates deleted records are visible, 'only' indicates that
            *only* deleted records are visible.
        """
        # Ensure user_id and project_id are passed to the parent constructor
        # This is crucial for policy enforcement
        kwargs.setdefault('user_id', user_id)
        kwargs.setdefault('project_id', project_id)

        super(RequestContext, self).__init__(is_admin=is_admin, **kwargs)

        self.project_name = project_name
        self.read_deleted = read_deleted
        self.remote_address = remote_address
        
        if not timestamp:
            timestamp = timeutils.utcnow()
        elif isinstance(timestamp, str):
            timestamp = timeutils.parse_isotime(timestamp)
        self.timestamp = timestamp
        
        self.quota_class = quota_class
        self.service_catalog = service_catalog or []

        # Ensure admin role is present if is_admin is True
        # This matches the behavior expected by your policy rules (e.g., role:admin)
        if self.is_admin and 'admin' not in self.roles:
            self.roles.append('admin')

    @property
    def read_deleted(self):
        return self._read_deleted

    @read_deleted.setter
    def read_deleted(self, read_deleted: str):
        if read_deleted not in ('no', 'yes', 'only'):
            raise ValueError("read_deleted can only be one of 'no', "
                             "'yes' or 'only', not %r" % read_deleted)
        self._read_deleted = read_deleted

    def to_dict(self) -> dict[str, Any]:
        """Serialize the context to a dictionary."""
        result = super(RequestContext, self).to_dict()
        result.update({
            'user_id': self.user_id,
            'project_id': self.project_id,
            'project_name': self.project_name,
            'read_deleted': self.read_deleted,
            'remote_address': self.remote_address,
            'timestamp': self.timestamp.isoformat(),
            'quota_class': self.quota_class,
            'service_catalog': self.service_catalog,
        })
        return result

    @classmethod
    def from_dict(cls, values: dict) -> 'RequestContext':
        """Deserialize the context from a dictionary."""
        return cls(
            user_id=values.get('user_id'),
            project_id=values.get('project_id'),
            project_name=values.get('project_name'),
            read_deleted=values.get('read_deleted', 'no'),
            remote_address=values.get('remote_address'),
            timestamp=values.get('timestamp'),
            quota_class=values.get('quota_class'),
            service_catalog=values.get('service_catalog'),
            request_id=values.get('request_id'),
            global_request_id=values.get('global_request_id'),
            is_admin=values.get('is_admin'),
            roles=values.get('roles'),
            auth_token=values.get('auth_token'),
            user_domain_id=values.get('user_domain_id'),
            project_domain_id=values.get('project_domain_id'),
        )

    def authorize(self, action: str, target: Optional[dict] = None, fatal: bool = True):
        """Verify that the given action is valid on the target in this context.

        This method is a convenience wrapper around the global policy enforcer.
        It allows objects or managers to check policies directly using the
        context they carry.

        :param action: string representing the action to be checked (e.g., 'vigie:tasks_list').
        :param target: dictionary representing the object of the action.
        :param fatal: if False, will return False when authorization fails.

        :raises oslo_policy.PolicyNotAuthorized: if verification fails and fatal
            is True.
        """
        # Import here to avoid circular imports
        from vigie.policy.enforcer import get_enforcer
        
        if target is None:
            target = {'project_id': self.project_id, 'user_id': self.user_id}
        
        # Use the global CONF obtained via get_config()
        enforcer = get_enforcer(CONF)
        creds = self.to_policy_values()
        
        try:
            return enforcer.enforce(action, target, creds, do_raise=fatal)
        except Exception as e:
            if fatal:
                raise
            return False

    def elevated(self, read_deleted: Optional[str] = None, overwrite: bool = False) -> 'RequestContext':
        """Return a version of this context with admin flag set.
        
        This is essential for background tasks (like those in service.py)
        to perform administrative actions without user credentials.
        """
        context_copy = self.deepcopy()
        context_copy.is_admin = True

        if 'admin' not in context_copy.roles:
            context_copy.roles.append('admin')

        if read_deleted is not None:
            context_copy.read_deleted = read_deleted

        return context_copy

    def deepcopy(self) -> 'RequestContext':
        return copy.deepcopy(self)


def get_admin_context(read_deleted: Optional[str] = "no") -> RequestContext:
    """Return an administrative context.
    
    This is used by background services (vigie-worker) to perform
    system-level operations that require admin privileges.
    """
    return RequestContext(
        user_id=None,
        project_id=None,
        is_admin=True,
        read_deleted=read_deleted,
        overwrite=False,
        roles=['admin']  # Explicitly set roles to ensure policy matches
    )
