from collections import defaultdict, namedtuple
import copy
import itertools

from typing import Dict, Iterable, Iterator, List, Sequence, Set, Type, Union, TYPE_CHECKING

from django.utils.text import slugify

if TYPE_CHECKING:
    from opentech.apply.users.models import User  # NOQA


class Permission:
    def can_edit(self, user: 'User') -> bool:
        return False

    def can_staff_review(self, user: 'User') -> bool:
        return False

    def can_reviewer_review(self, user: 'User') -> bool:
        return False

    def can_review(self, user: 'User') -> bool:
        return self.can_staff_review(user) or self.can_reviewer_review(user)


class StaffReviewPermission(Permission):
    def can_staff_review(self, user: 'User') -> bool:
        return user.is_apply_staff


class ReviewerReviewPermission(Permission):
    def can_reviewer_review(self, user: 'User') -> bool:
        return user.is_reviewer


class CanEditPermission(Permission):
    def can_edit(self, user: 'User') -> bool:
        return True



Stage = namedtuple('Stage', ['name', 'has_external_review'])

Request = Stage('Request', False)

INITAL_STATE = 'in_discussion'

SingleStage = {
    'in_discussion' : {
        'transitions': {
            'internal_review' : 'Open Review',
            'rejected' : 'Reject',
        },
        'display': 'Under Discussion',
        'stage': Request,
        'permissions': Permission(),
        'step': 0,
    },
    'internal_review' : {
        'transitions': {
            'in_discussion_2' : 'Close Review',
        },
        'display': 'Internal Review',
        'stage': Request,
        'permissions': StaffReviewPermission(),
        'step': 1,
    },
    'post_review_discussion': {
        'transitions': {
            'accepted': 'Accept',
            'rejected': 'Reject',
        },
        'display': 'Under Discussion',
        'stage': Request,
        'permissions': Permission(),
        'step': 2,
    },
    'accepted': {
        'display': 'Accepted',
        'stage': Request,
        'permissions': Permission(),
        'step': 3,
    },
    'rejected': {
        'display': 'Rejected',
        'stage': Request,
        'permissions': Permission(),
        'step': 3,
    },
}

DoubleStage = {
    'in_discussion' : {
        'transitions': {
            'internal_review' : 'Open Review',
            'rejected' : 'Reject',
        },
        'display': 'Under Discussion',
        'stage': Request,
        'permissions': Permission(),
        'step': 0,
    },
}


status_options = [(key, value['display']) for key, value in SingleStage.items()]

"""
This file defines classes which allow you to compose workflows based on the following structure:

Workflow -> Stage -> Phase -> Action

These classes are designed such that they can be mapped to a wagtail streamfield to allow admins
to build/adjust workflows as required.

Current limitations:
* Changing the name of a phase will mean that any object which references it cannot progress. [will
be fixed when streamfield, may require intermediate fix prior to launch]
* Outcomes are a special case of phase and perhaps should be handled separately. [will look at when
progressing stages]
"""


def phase_name(stage: 'Stage', phase: Union['Phase', str], step: int) -> str:
    # Build the identifiable name for a phase
    if not isinstance(phase, str):
        phase_name = phase._internal
    else:
        phase_name = phase

    return '__'.join([stage._internal, phase_name, str(step)])


class Workflow(Iterable):
    """
    A Workflow is a collection of Stages an application goes through. When a Stage is complete,
    it will return the next Stage in the list or `None` if no such Stage exists.
    """
    name: str = ''
    stage_classes: Sequence[Type['Stage']] = list()

    def __init__(self) -> None:
        self.stages = [stage(self) for stage in self.stage_classes]

    def __iter__(self) -> Iterator['Phase']:
        for stage in self.stages:
            yield from stage

    def current(self, current_phase: Union[str, 'Phase']) -> Union['Phase', None]:
        if isinstance(current_phase, Phase):
            return current_phase

        if not current_phase:
            return self.first()

        for stage in self.stages:
            phase = stage.get_phase(current_phase)
            if phase:
                return phase

        stage_name, _, _ = current_phase.split('__')
        for stage in self.stages:
            if stage.name == stage_name:
                # Fall back to the first phase of the stage
                return stage.first()

        return None

    def first(self) -> 'Phase':
        return self.stages[0].next()

    def process(self, current_phase: str, action: str) -> Union['Phase', None]:
        phase = self.current(current_phase)
        new_phase = phase.process(action)
        if not new_phase:
            new_stage = self.next_stage(phase.stage)
            if new_stage:
                return new_stage.first()
        return new_phase

    def next_stage(self, current_stage: 'Stage') -> 'Stage':
        for i, stage in enumerate(self.stages):
            if stage == current_stage:
                try:
                    return self.stages[i + 1]
                except IndexError:
                    pass

        return None

    def next(self, current_phase: Union[str, 'Phase']=None) -> Union['Phase', None]:
        if not current_phase:
            return self.first()

        phase = self.current(current_phase)

        for stage in self.stages:
            if stage == phase.stage:
                next_phase = stage.next(phase)
                if not next_phase:
                    continue
                return next_phase

        next_stage = self.next_stage(phase.stage)
        if next_stage:
            return stage.next()
        return None

    def __str__(self) -> str:
        return self.name


class Stage(Iterable):
    """
    Holds the Phases that are progressed through as part of the workflow process
    """
    name: str = 'Stage'
    phases: list = list()

    def __init__(self, workflow: 'Workflow', name: str='') -> None:
        if name:
            self.name = name

        self._internal = self.name.split()[0]

        self.workflow = workflow
        self.steps = len(self.phases)
        # Make the phases new instances to prevent errors with mutability
        self.phases = self.copy_phases(self.phases)

    def __eq__(self, other: object) -> bool:
        if isinstance(other, Stage):
            return self.name == other.name

        return super().__eq__(other)

    def __lt__(self, other: object) -> bool:
        if isinstance(other, Stage):
            return self.workflow.stages.index(self) < self.workflow.stages.index(other)
        return False

    def copy_phases(self, phases: List['Phase']) -> List['Phase']:
        new_phases = list()
        for step, phase in enumerate(self.phases):
            try:
                new_phases.append(self.copy_phase(phase, step))
            except AttributeError:
                # We have a step with multiple equivalent phases
                for sub_phase in phase:
                    new_phases.append(self.copy_phase(sub_phase, step))
        return new_phases

    def copy_phase(self, phase: 'Phase', step: int) -> 'Phase':
        phase.stage = self
        phase.step = step
        return copy.copy(phase)

    def __iter__(self) -> 'PhaseIterator':
        return PhaseIterator(self.phases, self.steps)

    def __str__(self) -> str:
        return self.name

    def get_phase(self, phase_name: str) -> 'Phase':
        for phase in self.phases:
            if phase == phase_name:
                return phase

        # We don't have the exact name
        for phase in self.phases:
            if phase._internal == phase_name:
                # Grab the first phase to match the name
                return phase

        return None

    def first(self) -> 'Phase':
        return self.phases[0]

    def next(self, current_phase: 'Phase'=None) -> 'Phase':
        if not current_phase:
            return self.first()

        for i, phase in enumerate(self.phases):
            if phase == current_phase:
                try:
                    next_phase = self.phases[i + 1]
                except IndexError:
                    pass
                else:
                    if next_phase.step != phase.step:
                        return next_phase
        return None


class PhaseIterator(Iterator):
    class Step:
        """Allow handling phases which are equivalent e.g. outcomes (accepted/rejected)
        Delegates to the underlying phases except where naming is concerned
        """
        def __init__(self, phases: List['Phase']) -> None:
            self.phases = phases

        def __lt__(self, other: object) -> bool:
            return all(phase < other for phase in self.phases)

        @property
        def step(self) -> int:
            return self.phases[0].step

        @property
        def name(self) -> str:
            # Hardcode a name for multi-phased step - always outcome at the moment
            if len(self.phases) > 1:
                return 'Outcome'
            return self.phases[0].name

        def __eq__(self, other: object) -> bool:
            return any(phase == other for phase in self.phases)

    def __init__(self, phases: List['Phase'], steps: int) -> None:
        self.current = 0
        self.phases: Dict[int, List['Phase']] = defaultdict(list)
        for phase in phases:
            self.phases[phase.step].append(phase)
        self.steps = steps

    def __iter__(self) -> 'PhaseIterator':
        return self

    def __next__(self) -> 'Step':
        self.current += 1
        if self.current > self.steps:
            raise StopIteration
        return self.Step(self.phases[self.current - 1])


class Phase:
    """
    Holds the Actions which a user can perform at each stage. A Phase with no actions is
    essentially locked
    """
    actions: Sequence['Action'] = list()
    name: str = ''
    public_name: str = ''
    permissions: 'Permission' = Permission()

    def __init__(self, name: str='', public_name: str ='', active: bool=True, can_proceed: bool=False, permissions: Permission=None) -> None:
        if name:
            self.name = name

        if permissions:
            self.permissions = permissions

        self.active = active
        self.can_proceed = can_proceed

        if public_name:
            self.public_name = public_name
        elif not self.public_name:
            self.public_name = self.name

        self._internal = slugify(self.name)
        self.stage: Union['Stage', None] = None
        self._actions = {action.name: action for action in self.actions}
        self.step: int = 0

    def __eq__(self, other: Union[object, str]) -> bool:
        if isinstance(other, str):
            return str(self) == other
        to_match = ['stage', 'name', 'step']
        return all(getattr(self, attr) == getattr(other, attr) for attr in to_match)

    def __lt__(self, other: object) -> bool:
        if isinstance(other, Phase):
            if self.stage < other.stage:
                return True
            return self.step < other.step and self.stage == other.stage
        return False

    @property
    def action_names(self) -> List[str]:
        return list(self._actions.keys())

    def __str__(self) -> str:
        return phase_name(self.stage, self, self.step)

    def __getitem__(self, value: str) -> 'Action':
        return self._actions[value]

    def process(self, action: str) -> Union['Phase', None]:
        return self[action].process(self)

    def has_perm(self, user: 'User', perm: str) -> bool:
        perm_method = getattr(self.permissions, f'can_{perm}', lambda x: False)
        return perm_method(user)


class Action:
    """
    Base Action class.

    Actions return the Phase within the current Stage which the workflow should progress to next.
    A value of `None` will allow the Stage to progress.
    """
    def __init__(self, name: str) -> None:
        self.name = name

    def process(self, phase: 'Phase') -> Union['Phase', None]:
        # Use this to define the behaviour of the action
        raise NotImplementedError


class ChangePhaseAction(Action):
    # Change to a specific Phase
    def __init__(self, phase: Union['Phase', str], *args: str, **kwargs: str) -> None:
        self.target_phase = phase
        super().__init__(*args, **kwargs)

    def process(self, phase: 'Phase') -> Union['Phase', None]:
        if isinstance(self.target_phase, str):
            return phase.stage.get_phase(self.target_phase)
        return self.target_phase


class NextPhaseAction(Action):
    # Change to the next action in the current Stage
    def process(self, phase: 'Phase') -> Union['Phase', None]:
        return phase.stage.next(phase)


# --- OTF Workflow ---


reject_action = ChangePhaseAction('rejected', 'Reject')

accept_action = ChangePhaseAction('accepted', 'Accept')

progress_stage = ChangePhaseAction('invited-to-proposal', 'Invite to Proposal')

next_phase = NextPhaseAction('Progress')


class InDraft(Phase):
    name = 'Invited for Proposal'
    public_name = 'In draft'
    actions = [NextPhaseAction('Submit')]
    permissions = CanEditPermission()


class ReviewPhase(Phase):
    name = 'Internal Review'
    public_name = 'In review'
    actions = [NextPhaseAction('Close Review')]
    permissions = StaffReviewPermission()


class DiscussionWithProgressionPhase(Phase):
    name = 'Under Discussion'
    public_name = 'In review'
    actions = [progress_stage, reject_action]


class DiscussionPhase(Phase):
    name = 'Under Discussion'
    public_name = 'In review'
    actions = [accept_action, reject_action]


class DiscussionWithNextPhase(Phase):
    name = 'Under Discussion'
    public_name = 'In review'
    actions = [NextPhaseAction('Open Review'), reject_action]


rejected = Phase(name='Rejected', active=False)

accepted = Phase(name='Accepted', active=False)

progressed = Phase(name='Invited to Proposal', active=False, can_proceed=True)


class RequestStage(Stage):
    name = 'Request'
    has_external_review = False
    phases = [
        DiscussionWithNextPhase(),
        ReviewPhase(),
        DiscussionPhase(),
        [accepted, rejected]
    ]


class ConceptStage(Stage):
    name = 'Concept Note'
    has_external_review = False
    phases = [
        DiscussionWithNextPhase(),
        ReviewPhase(),
        DiscussionWithProgressionPhase(),
        [progressed, rejected],
    ]


class ProposalStage(Stage):
    name = 'Proposal'
    has_external_review = True
    phases = [
        InDraft(),
        DiscussionWithNextPhase(),
        ReviewPhase(),
        DiscussionWithNextPhase(),
        ReviewPhase('AC Review', public_name='In AC review', permissions=ReviewerReviewPermission()),
        DiscussionPhase(public_name='In AC review'),
        [accepted, rejected]
    ]


# class SingleStage(Workflow):
#     name = 'Single Stage'
#     stage_classes = [RequestStage]


# class DoubleStage(Workflow):
#     name = 'Two Stage'
#     stage_classes = [ConceptStage, ProposalStage]


# statuses = set(phase.name for phase in Phase.__subclasses__())
# status_options = [(slugify(opt), opt) for opt in statuses]


# def get_active_statuses() -> Set[str]:
#     active = set()

#     def add_if_active(phase: 'Phase') -> None:
#         if phase.active:
#             active.add(str(phase))

#     for phase in itertools.chain(SingleStage(), DoubleStage()):
#         try:
#             add_if_active(phase)
#         except AttributeError:
#             # it is actually a step
#             step = phase
#             for phase in step.phases:
#                 add_if_active(phase)
#     return active


active_statuses = [] #get_active_statuses()


def get_review_statuses(user: Union[None, 'User']=None) -> Set[str]:
    return []
#     reviews = set()

#     for step in itertools.chain(SingleStage(), DoubleStage()):
#         for phase in step.phases:
#             if isinstance(phase, ReviewPhase):
#                 if user is None:
#                     reviews.add(str(phase))
#                 elif phase.has_perm(user, 'review'):
#                     reviews.add(str(phase))

#     return reviews


review_statuses = [] #get_review_statuses()