#
# Copyright (C) 2024 CESNET z.s.p.o.
#
# oarepo-workflows is free software; you can redistribute it and/or
# modify it under the terms of the MIT License; see LICENSE file for more
# details.
#
"""Permission generators usable in workflow configurations."""

from __future__ import annotations

import operator
from functools import reduce
from itertools import chain
from typing import TYPE_CHECKING, Any, Optional

from invenio_records_permissions.generators import ConditionalGenerator, Generator
from invenio_search.engine import dsl

from oarepo_workflows.errors import InvalidWorkflowError, MissingWorkflowError
from oarepo_workflows.proxies import current_oarepo_workflows

if TYPE_CHECKING:
    from collections.abc import Iterable

    from flask_principal import Need
    from invenio_records_permissions import RecordPermissionPolicy
    from invenio_records_resources.records import Record


# invenio_records_permissions.generators.ConditionalGenerator._make_query
def _make_query(generators: Iterable[Generator], **context: Any) -> dict | None:
    queries = [g.query_filter(**context) for g in generators]
    queries = [q for q in queries if q]
    return reduce(operator.or_, queries) if queries else dsl.Q("match_none")


class FromRecordWorkflow(Generator):
    """Permission delegating check to workflow.

    The implementation of the permission gets the workflow id from the passed context
    (record or data) and then looks up the workflow definition in the configuration.

    The workflow definition must contain a permissions policy that is then used to
    determine the permissions for the action.
    """

    _action: str

    def __init__(self, action: str) -> None:
        """Initialize the permission."""
        # might not be needed in subclasses
        super().__init__()
        self._action = action

    # noinspection PyMethodMayBeStatic
    def _get_workflow_id(self, record: Optional[Record] = None, **context: Any) -> str:
        """Get the workflow id from the context.

        If the record is passed, the workflow is determined from the record.
        If the record is not passed, the workflow is determined from the input data.

        If the workflow is not found, an error is raised.

        :param record: Record to get the workflow from.
        :param context: Context to get the workflow from.
        :return: Workflow id.
        :raises MissingWorkflowError: If the workflow is not found on the record/data.
        """
        if record:
            workflow_id = current_oarepo_workflows.get_workflow_from_record(record)
            if not workflow_id:
                raise MissingWorkflowError(
                    "Workflow not defined on record.", record=record
                )
        else:
            data = context.get("data", {})
            workflow_id = data.get("parent", {}).get("workflow", {})
            if not workflow_id:
                raise MissingWorkflowError(
                    "Workflow not defined in input.", record=data
                )
        return workflow_id

    def _get_permissions_from_workflow(
        self,
        action_name: str,
        record: Optional[Record] = None,
        **context: Any,
    ) -> RecordPermissionPolicy:
        """Get the permissions policy from the workflow.

        At first the workflow id is determined from the context.
        Then the permissions policy is determined from the workflow configuration,
        is instantiated with the action name and the context and the permissions
        for the action are returned.
        """
        workflow_id = self._get_workflow_id(record, **context)
        if workflow_id not in current_oarepo_workflows.record_workflows:
            raise InvalidWorkflowError(
                f"Workflow {workflow_id} does not exist in the configuration.",
                record=record or context.get("data", {}),
            )
        policy = current_oarepo_workflows.record_workflows[workflow_id].permissions
        ret = policy(action_name, record=record, **context)
        # debug point
        return ret

    def needs(self, record: Optional[Record] = None, **context: Any) -> list[Need]:
        """Return needs that are generated by the workflow permission."""
        return self._get_permissions_from_workflow(
            self._action, record, **context
        ).needs

    def excludes(self, **context: Any) -> list[Need]:
        """Return excludes that are generated by the workflow permission."""
        return self._get_permissions_from_workflow(self._action, **context).excludes

    def query_filter(self, record: Optional[Record] = None, **context: Any) -> dict:
        """Return query filters that are generated by the workflow permission.

        Note: this implementation in fact will be called from WorkflowRecordPermissionPolicy.query_filters
        for each registered workflow type. The query_filters are then combined into a single query.
        """
        return self._get_permissions_from_workflow(
            self._action, record, **context
        ).query_filters


class WorkflowPermission(FromRecordWorkflow):
    """Deprecated alias for FromRecordWorkflow."""

    def __init__(self, action: str) -> None:
        """Initialize the generator."""
        import warnings

        warnings.warn(
            "WorkflowPermission is deprecated. Use FromRecordWorkflow instead.",
            DeprecationWarning,
            stacklevel=2,
        )
        super().__init__(action)


class IfInState(ConditionalGenerator):
    """Generator that checks if the record is in a specific state.

    If it is in the state, the then_ generators are used, otherwise the else_ generators are used.

    Example:
        .. code-block:: python

            can_edit = [ IfInState("draft", [RecordOwners()]) ]

    """

    def __init__(
        self,
        state: str | list[str] | tuple[str, ...],
        then_: list[Generator] | tuple[Generator] | Generator | None = None,
        else_: list[Generator] | tuple[Generator] | Generator | None = None,
    ) -> None:
        """Initialize the generator."""
        if isinstance(then_, Generator):
            then_ = [then_]
        if isinstance(else_, Generator):
            else_ = [else_]
        super().__init__(then_ or [], else_=else_ or [])
        if isinstance(state, str):
            state = [state]
        if not isinstance(state, (list, tuple)):
            raise TypeError(
                f"State must be a string, list or tuple. Got {type(state)}."
            )
        self.state = state

    def _condition(self, record: Record, **context: Any) -> bool:
        """Check if the record is in the state."""
        try:
            return record.state in self.state  # noqa as AttributeError is caught
        except AttributeError:
            return False

    def query_filter(self, **context: Any) -> dsl.Q:
        """Apply then or else filter."""
        field = "state"

        q_instate = dsl.Q("terms", **{field: self.state})
        if self.then_:
            then_query = self._make_query(self.then_, **context)
        else:
            then_query = dsl.Q("match_none")

        if self.else_:
            else_query = self._make_query(self.else_, **context)
        else:
            else_query = dsl.Q("match_none")

        return (q_instate & then_query) | (~q_instate & else_query)

    def __repr__(self) -> str:
        """Representation of the generator."""
        return (
            f"IfInState({self.state}, then={repr(self.then_)}, else={repr(self.else_)})"
        )

    def __str__(self) -> str:
        """String representation of the generator."""
        return repr(self)


class SameAs(Generator):
    """Generator that delegates the permissions to another action.

    Example:
        .. code-block:: python
            class Perms:
                can_create_files = [SameAs("edit_files")]
                can_edit_files = [RecordOwners()]

    would mean that the permissions for creating files are the same as for editing files.
    This works even if you inherit from the class and override the can_edit_files.

    """

    def __init__(self, permission_name: str) -> None:
        """Initialize the generator.

        :param permission_name: Name of the permission to delegate to. In most cases,
        it will look like "can_<action>". A property with this name must exist on the policy
        and its value must be a list of generators.
        """
        self.delegated_permission_name = permission_name

    # noinspection PyUnusedLocal
    def _generators(
        self, *, policy: RecordPermissionPolicy, **context: Any
    ) -> list[Generator]:
        """Get the generators from the policy."""
        return getattr(policy, self.delegated_permission_name)

    def needs(
        self, *, policy: RecordPermissionPolicy | None = None, **context: Any
    ) -> list[Need]:
        """Get the needs from the policy."""
        if policy is None:
            raise ValueError(
                f"SameAs: Policy must be passed to the generator. Got the following context: {context.keys()}"
            )
        needs = [
            generator.needs(policy=policy, **context)
            for generator in self._generators(policy=policy, **context)
        ]
        return list(chain.from_iterable(needs))

    def excludes(
        self, *, policy: RecordPermissionPolicy | None = None, **context: Any
    ) -> list[Need]:
        """Get the excludes from the policy."""
        if policy is None:
            raise ValueError(
                f"SameAs: Policy must be passed to the generator. Got the following context: {context.keys()}"
            )
        excludes = [
            generator.excludes(policy=policy, **context)
            for generator in self._generators(policy=policy, **context)
        ]
        return list(chain.from_iterable(excludes))

    def query_filter(
        self, *, policy: RecordPermissionPolicy, **context: Any
    ) -> dict | None:
        """Search filters."""
        return _make_query(self._generators(policy=policy, **context), **context)

    def __repr__(self) -> str:
        """Representation of the generator."""
        return f"SameAs({self.delegated_permission_name})"

    def __str__(self) -> str:
        """String representation of the generator."""
        return repr(self)
