from collections import defaultdict
from enum import Enum
import itertools

from django.utils.text import slugify

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

Workflow -> Stage -> Phase -> Action

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]
"""


class UserPermissions(Enum):
    STAFF = 1
    ADMIN = 2
    LEAD = 3
    APPLICANT = 4


class Workflow(dict):
    def __init__(self, name, admin_name, **data):
        self.name = name
        self.admin_name = admin_name
        super().__init__(**data)

    def __str__(self):
        return self.name

    @property
    def stages(self):
        stages = []
        for phase in self.values():
            if phase.stage not in stages:
                stages.append(phase.stage)
        return stages

    @property
    def stepped_phases(self):
        phases = defaultdict(list)
        for phase in list(self.values()):
            phases[phase.step].append(phase)
        return phases

    def phases_for(self, user=None):
        # Grab the first phase for each step - visible only, the display phase
        return [
            phase for phase, *_ in self.stepped_phases.values()
            if not user or phase.permissions.can_view(user)
        ]

    def previous_visible(self, current, user):
        """Find the latest phase that the user has view permissions for"""
        display_phase = self.stepped_phases[current.step][0]
        phases = self.phases_for()
        index = phases.index(display_phase)
        for phase in phases[index - 1::-1]:
            if phase.permissions.can_view(user):
                return phase


class Phase:
    """
    Phase Names:
    display_name = phase name displayed to staff members in the system
    public_name = phase name displayed to applicants in the system
    future_name = phase_name displayed to applicants if they haven't passed this stage
    """
    def __init__(self, name, display, stage, permissions, step, public=None, future=None, transitions=dict()):
        self.name = name
        self.display_name = display
        if public and future:
            raise ValueError("Cant provide both a future and a public name")

        self.public_name = public or self.display_name
        self.future_name_staff = future or self.display_name
        self.future_name_public = future or self.public_name
        self.stage = stage
        self.permissions = Permissions(permissions)
        self.step = step

        # For building transition methods on the parent
        self.transitions = {}

        default_permissions = {UserPermissions.STAFF, UserPermissions.LEAD, UserPermissions.ADMIN}

        for transition_target, action in transitions.items():
            transition = dict()
            try:
                transition['display'] = action.get('display')
            except AttributeError:
                transition['display'] = action
                transition['permissions'] = default_permissions
            else:
                transition['method'] = action.get('method')
                conditions = action.get('conditions', '')
                transition['conditions'] = conditions.split(',') if conditions else []
                transition['permissions'] = action.get('permissions', default_permissions)
            self.transitions[transition_target] = transition

    def __str__(self):
        return self.display_name

    def __repr__(self):
        return f'<Phase {self.display_name} ({self.public_name})>'


class Stage:
    def __init__(self, name, has_external_review=False):
        self.name = name
        self.has_external_review = has_external_review

    def __str__(self):
        return self.name


class Permissions:
    def __init__(self, permissions):
        self.permissions = permissions

    def can_do(self, user, action):
        checks = self.permissions.get(action, list())
        return any(check(user) for check in checks)

    def can_edit(self, user):
        return self.can_do(user, 'edit')

    def can_review(self, user):
        return self.can_do(user, 'review')

    def can_view(self, user):
        return self.can_do(user, 'view')


staff_can = lambda user: user.is_apply_staff  # NOQA

applicant_can = lambda user: user.is_applicant  # NOQA

reviewer_can = lambda user: user.is_reviewer  # NOQA


def make_permissions(edit=list(), review=list(), view=[staff_can, applicant_can, reviewer_can]):
    return {
        'edit': edit,
        'review': review,
        'view': view,
    }


no_permissions = make_permissions()

default_permissions = make_permissions(edit=[staff_can], review=[staff_can])

hidden_from_applicant_permissions = make_permissions(edit=[staff_can], review=[staff_can], view=[staff_can, reviewer_can])

reviewer_review_permissions = make_permissions(edit=[staff_can], review=[staff_can, reviewer_can])

applicant_edit_permissions = make_permissions(edit=[applicant_can], review=[staff_can])

staff_applicant_edit_permissions = make_permissions(edit=[staff_can, applicant_can])

staff_edit_permissions = make_permissions(edit=[staff_can])


Request = Stage('Request', False)

RequestExt = Stage('RequestExt', True)

Concept = Stage('Concept', False)

Proposal = Stage('Proposal', True)


INITIAL_STATE = 'in_discussion'

SingleStageDefinition = [
    {
        INITIAL_STATE: {
            'transitions': {
                'internal_review': 'Open Review',
                'rejected': 'Dismiss',
                'more_info': 'Request More Information',
                'accepted': 'Accept',
                'determination': 'Ready For Determination',
            },
            'display': 'Screening',
            'public': 'Application Received',
            'stage': Request,
            'permissions': default_permissions,
        },
        'more_info': {
            'transitions': {
                INITIAL_STATE: {
                    'display': 'Submit',
                    'permissions': {UserPermissions.APPLICANT, UserPermissions.STAFF, UserPermissions.LEAD, UserPermissions.ADMIN},
                    'method': 'create_revision',
                },
                'accepted': 'Accept',
                'rejected': 'Dismiss',
                'determination': 'Ready For Determination',
            },
            'display': 'More information required',
            'stage': Request,
            'permissions': applicant_edit_permissions,
        },
    },
    {
        'internal_review': {
            'transitions': {
                'post_review_discussion': 'Close Review',
            },
            'display': 'Internal Review',
            'public': 'OTF Review',
            'stage': Request,
            'permissions': default_permissions,
        },
    },
    {
        'post_review_discussion': {
            'transitions': {
                'accepted': 'Accept',
                'rejected': 'Dismiss',
                'determination': 'Ready For Determination',
                'post_review_more_info': 'Request More Information',
            },
            'display': 'Ready For Discussion',
            'stage': Request,
            'permissions': hidden_from_applicant_permissions,
        },
        'post_review_more_info': {
            'transitions': {
                'post_review_discussion': {
                    'display': 'Submit',
                    'permissions': {UserPermissions.APPLICANT, UserPermissions.STAFF, UserPermissions.LEAD, UserPermissions.ADMIN},
                    'method': 'create_revision',
                },
                'accepted': 'Accept',
                'rejected': 'Dismiss',
                'determination': 'Ready For Determination',
            },
            'display': 'More information required',
            'stage': Request,
            'permissions': applicant_edit_permissions,
        },
    },
    {
        'determination': {
            'transitions': {
                'accepted': 'Accept',
                'rejected': 'Dismiss',
            },
            'display': 'Ready for Determination',
            'permissions': hidden_from_applicant_permissions,
            'stage': Request,
        },
    },
    {
        'accepted': {
            'display': 'Accepted',
            'future': 'Application Outcome',
            'stage': Request,
            'permissions': staff_applicant_edit_permissions,
        },
        'rejected': {
            'display': 'Dismissed',
            'stage': Request,
            'permissions': no_permissions,
        },
    },
]

SingleStageExternalDefinition = [
    {
        INITIAL_STATE: {
            'transitions': {
                'ext_internal_review': 'Open Review',
                'ext_rejected': 'Dismiss',
                'ext_more_info': 'Request More Information',
                'ext_determination': 'Ready For Determination',
            },
            'display': 'Screening',
            'public': 'Application Received',
            'stage': RequestExt,
            'permissions': default_permissions,
        },
        'ext_more_info': {
            'transitions': {
                INITIAL_STATE: {
                    'display': 'Submit',
                    'permissions': {UserPermissions.APPLICANT, UserPermissions.STAFF, UserPermissions.LEAD, UserPermissions.ADMIN},
                    'method': 'create_revision',
                },
            },
            'display': 'More information required',
            'stage': RequestExt,
            'permissions': applicant_edit_permissions,
        },
    },
    {
        'ext_internal_review': {
            'transitions': {
                'ext_post_review_discussion': 'Close Review',
            },
            'display': 'Internal Review',
            'public': 'OTF Review',
            'stage': RequestExt,
            'permissions': default_permissions,
        },
    },
    {
        'ext_post_review_discussion': {
            'transitions': {
                'ext_external_review': 'Open AC review',
                'ext_rejected': 'Dismiss',
                'ext_post_review_more_info': 'Request More Information',
                'ext_determination': 'Ready For Determination',
            },
            'display': 'Ready For Discussion',
            'stage': RequestExt,
            'permissions': hidden_from_applicant_permissions,
        },
        'ext_post_review_more_info': {
            'transitions': {
                'ext_post_review_discussion': {
                    'display': 'Submit',
                    'permissions': {UserPermissions.APPLICANT, UserPermissions.STAFF, UserPermissions.LEAD, UserPermissions.ADMIN},
                    'method': 'create_revision',
                },
            },
            'display': 'More information required',
            'stage': RequestExt,
            'permissions': applicant_edit_permissions,
        },
    },
    {
        'ext_external_review': {
            'transitions': {
                'ext_post_external_review_discussion': 'Close Review',
            },
            'display': 'Advisory Council Review',
            'stage': RequestExt,
            'permissions': reviewer_review_permissions,
        },
    },
    {
        'ext_post_external_review_discussion': {
            'transitions': {
                'ext_accepted': 'Accept',
                'ext_rejected': 'Dismiss',
                'ext_post_external_review_more_info': 'Request More Information',
                'ext_determination': 'Ready For Determination',
            },
            'display': 'Ready For Discussion',
            'stage': RequestExt,
            'permissions': hidden_from_applicant_permissions,
        },
        'ext_post_external_review_more_info': {
            'transitions': {
                'ext_post_external_review_discussion': {
                    'display': 'Submit',
                    'permissions': {UserPermissions.APPLICANT, UserPermissions.STAFF, UserPermissions.LEAD, UserPermissions.ADMIN},
                    'method': 'create_revision',
                },
            },
            'display': 'More information required',
            'stage': RequestExt,
            'permissions': applicant_edit_permissions,
        },
    },
    {
        'ext_determination': {
            'transitions': {
                'ext_accepted': 'Accept',
                'ext_rejected': 'Dismiss',
            },
            'display': 'Ready for Determination',
            'permissions': hidden_from_applicant_permissions,
            'stage': RequestExt,
        },
    },
    {
        'ext_accepted': {
            'display': 'Accepted',
            'future': 'Application Outcome',
            'stage': RequestExt,
            'permissions': staff_applicant_edit_permissions,
        },
        'ext_rejected': {
            'display': 'Dismissed',
            'stage': RequestExt,
            'permissions': no_permissions,
        },
    },
]


DoubleStageDefinition = [
    {
        INITIAL_STATE: {
            'transitions': {
                'concept_internal_review': 'Open Review',
                'concept_rejected': 'Dismiss',
                'concept_more_info': 'Request More Information',
                'invited_to_proposal': 'Invite to Proposal',
                'concept_determination': 'Ready For Preliminary Determination',
            },
            'display': 'Screening',
            'public': 'Concept Note Received',
            'stage': Concept,
            'permissions': default_permissions,
        },
        'concept_more_info': {
            'transitions': {
                INITIAL_STATE: {
                    'display': 'Submit',
                    'permissions': {UserPermissions.APPLICANT, UserPermissions.STAFF, UserPermissions.LEAD, UserPermissions.ADMIN},
                    'method': 'create_revision',
                },
                'concept_rejected': 'Dismiss',
                'invited_to_proposal': 'Invite to Proposal',
                'concept_determination': 'Ready For Preliminary Determination',
            },
            'display': 'More information required',
            'stage': Concept,
            'permissions': applicant_edit_permissions,
        },
    },
    {
        'concept_internal_review': {
            'transitions': {
                'concept_review_discussion': 'Close Review',
                'invited_to_proposal': 'Invite to Proposal',
            },
            'display': 'Internal Review',
            'public': 'OTF Review',
            'stage': Concept,
            'permissions': default_permissions,
        },
    },
    {
        'concept_review_discussion': {
            'transitions': {
                'invited_to_proposal': 'Invite to Proposal',
                'concept_rejected': 'Dismiss',
                'concept_review_more_info': 'Request More Information',
                'concept_determination': 'Ready For Preliminary Determination',
            },
            'display': 'Ready For Discussion',
            'stage': Concept,
            'permissions': hidden_from_applicant_permissions,
        },
        'concept_review_more_info': {
            'transitions': {
                'concept_review_discussion': {
                    'display': 'Submit',
                    'permissions': {UserPermissions.APPLICANT, UserPermissions.STAFF, UserPermissions.LEAD, UserPermissions.ADMIN},
                    'method': 'create_revision',
                },
                'invited_to_proposal': 'Invite to Proposal',
            },
            'display': 'More information required',
            'stage': Concept,
            'permissions': applicant_edit_permissions,
        },
    },
    {
        'concept_determination': {
            'transitions': {
                'invited_to_proposal': 'Invite to Proposal',
                'concept_rejected': 'Dismiss',
            },
            'display': 'Ready for Preliminary Determination',
            'permissions': hidden_from_applicant_permissions,
            'stage': Concept,
        },
    },
    {
        'invited_to_proposal': {
            'display': 'Concept Accepted',
            'future': 'Preliminary Determination',
            'transitions': {
                'draft_proposal': {
                    'display': 'Progress',
                    'method': 'progress_application',
                    'permissions': {UserPermissions.STAFF, UserPermissions.LEAD, UserPermissions.ADMIN},
                    'conditions': 'not_progressed',
                },
            },
            'stage': Concept,
            'permissions': no_permissions,
        },
        'concept_rejected': {
            'display': 'Dismissed',
            'stage': Concept,
            'permissions': no_permissions,
        },
    },
    {
        'draft_proposal': {
            'transitions': {
                'proposal_discussion': {'display': 'Submit', 'permissions': {UserPermissions.APPLICANT}, 'method': 'create_revision'},
                'proposal_rejected': 'Dismiss',
                'external_review': 'Open AC review',
                'proposal_determination': 'Ready For Final Determination',
            },
            'display': 'Invited for Proposal',
            'stage': Proposal,
            'permissions': applicant_edit_permissions,
        },
    },
    {
        'proposal_discussion': {
            'transitions': {
                'proposal_internal_review': 'Open Review',
                'proposal_rejected': 'Dismiss',
                'proposal_more_info': 'Request More Information',
                'proposal_determination': 'Ready For Final Determination',
                'external_review': 'Open AC review',
            },
            'display': 'Proposal Received',
            'stage': Proposal,
            'permissions': default_permissions,
        },
        'proposal_more_info': {
            'transitions': {
                'proposal_discussion': {
                    'display': 'Submit',
                    'permissions': {UserPermissions.APPLICANT, UserPermissions.STAFF, UserPermissions.LEAD, UserPermissions.ADMIN},
                    'method': 'create_revision',
                },
                'proposal_rejected': 'Dismiss',
                'proposal_determination': 'Ready For Final Determination',
                'external_review': 'Open AC review',
            },
            'display': 'More information required',
            'stage': Proposal,
            'permissions': applicant_edit_permissions,
        },
    },
    {
        'proposal_internal_review': {
            'transitions': {
                'post_proposal_review_discussion': 'Close Review',
            },
            'display': 'Internal Review',
            'public': 'OTF Review',
            'stage': Proposal,
            'permissions': default_permissions,
        },
    },
    {
        'post_proposal_review_discussion': {
            'transitions': {
                'external_review': 'Open AC review',
                'proposal_determination': 'Ready For Final Determination',
                'proposal_rejected': 'Dismiss',
                'post_proposal_review_more_info': 'Request More Information',
            },
            'display': 'Ready For Discussion',
            'stage': Proposal,
            'permissions': hidden_from_applicant_permissions,
        },
        'post_proposal_review_more_info': {
            'transitions': {
                'post_proposal_review_discussion': {
                    'display': 'Submit',
                    'permissions': {UserPermissions.APPLICANT, UserPermissions.STAFF, UserPermissions.LEAD, UserPermissions.ADMIN},
                    'method': 'create_revision',
                },
                'external_review': 'Open AC review',
            },
            'display': 'More information required',
            'stage': Proposal,
            'permissions': applicant_edit_permissions,
        },
    },
    {
        'external_review': {
            'transitions': {
                'post_external_review_discussion': 'Close Review',
            },
            'display': 'Advisory Council Review',
            'stage': Proposal,
            'permissions': reviewer_review_permissions,
        },
    },
    {
        'post_external_review_discussion': {
            'transitions': {
                'proposal_accepted': 'Accept',
                'proposal_rejected': 'Dismiss',
                'proposal_determination': 'Ready For Final Determination',
                'post_external_review_more_info': 'Request More Information',
            },
            'display': 'Ready For Discussion',
            'stage': Proposal,
            'permissions': hidden_from_applicant_permissions,
        },
        'post_external_review_more_info': {
            'transitions': {
                'post_external_review_discussion': {
                    'display': 'Submit',
                    'permissions': {UserPermissions.APPLICANT, UserPermissions.STAFF, UserPermissions.LEAD, UserPermissions.ADMIN},
                    'method': 'create_revision',
                },
            },
            'display': 'More information required',
            'stage': Proposal,
            'permissions': applicant_edit_permissions,
        },
    },
    {
        'proposal_determination': {
            'transitions': {
                'proposal_accepted': 'Accept',
                'proposal_rejected': 'Dismiss',
            },
            'display': 'Ready for Final Determination',
            'permissions': hidden_from_applicant_permissions,
            'stage': Proposal,
        },
    },
    {
        'proposal_accepted': {
            'display': 'Accepted',
            'future': 'Final Determination',
            'stage': Proposal,
            'permissions': staff_applicant_edit_permissions,
        },
        'proposal_rejected': {
            'display': 'Dismissed',
            'stage': Proposal,
            'permissions': no_permissions,
        },
    },
]


def unpack_phases(phases):
    for step, step_data in enumerate(phases):
        for name, phase_data in step_data.items():
            yield step, name, phase_data


def phase_data(phases):
    return {
        phase_name: Phase(phase_name, step=step, **phase_data)
        for step, phase_name, phase_data in unpack_phases(phases)
    }


Request = Workflow('Request', 'single', **phase_data(SingleStageDefinition))

RequestExternal = Workflow('Request with external review', 'single_ext', **phase_data(SingleStageExternalDefinition))

ConceptProposal = Workflow('Concept & Proposal', 'double', **phase_data(DoubleStageDefinition))


WORKFLOWS = {
    Request.admin_name: Request,
    RequestExternal.admin_name: RequestExternal,
    ConceptProposal.admin_name: ConceptProposal,
}


# This is not a dictionary as the keys will clash for the first phase of each workflow
# We cannot find the transitions for the first stage in this instance
PHASES = list(itertools.chain.from_iterable(workflow.items() for workflow in WORKFLOWS.values()))


def get_stage_change_actions():
    changes = set()
    for workflow in WORKFLOWS.values():
        stage = None
        for phase in workflow.values():
            if phase.stage != stage and stage:
                changes.add(phase.name)
            stage = phase.stage

    return changes


STAGE_CHANGE_ACTIONS = get_stage_change_actions()


STATUSES = defaultdict(set)

for key, value in PHASES:
    STATUSES[value.display_name].add(key)

active_statuses = [
    status for status, _ in PHASES
    if 'accepted' not in status and 'rejected' not in status and 'invited' not in status
]


def get_review_active_statuses(user=None):
    reviews = set()

    for phase_name, phase in PHASES:
        if phase_name in active_statuses:
            if user is None:
                reviews.add(phase_name)
            elif phase.permissions.can_review(user):
                reviews.add(phase_name)
    return reviews


def get_review_statuses(user=None):
    reviews = set()

    for phase_name, phase in PHASES:
        if 'review' in phase_name and 'discussion' not in phase_name:
            if user is None:
                reviews.add(phase_name)
            elif phase.permissions.can_review(user):
                reviews.add(phase_name)
    return reviews


review_statuses = get_review_statuses()

DETERMINATION_PHASES = list(phase_name for phase_name, _ in PHASES if '_discussion' in phase_name)
DETERMINATION_RESPONSE_PHASES = [
    'post_review_discussion',
    'concept_review_discussion',
    'post_external_review_discussion',
    'ext_post_external_review_discussion',
]


def get_determination_transitions():
    transitions = {}
    for phase_name, phase in PHASES:
        for transition_name in phase.transitions:
            if 'accepted' in transition_name:
                transitions[transition_name] = 'accepted'
            elif 'rejected' in transition_name:
                transitions[transition_name] = 'rejected'
            elif 'more_info' in transition_name:
                transitions[transition_name] = 'more_info'
            elif 'invited_to_proposal' in transition_name:
                transitions[transition_name] = 'accepted'

    return transitions


def get_action_mapping(workflow):
    # Maps action names to the phase they originate from
    transitions = defaultdict(lambda: {'display': '', 'transitions': []})
    if workflow:
        phases = workflow.items()
    else:
        phases = PHASES
    for phase_name, phase in phases:
        for transition_name, transition in phase.transitions.items():
            transition_display = transition['display']
            transition_key = slugify(transition_display)
            transitions[transition_key]['transitions'].append(transition_name)
            transitions[transition_key]['display'] = transition_display

    return transitions


DETERMINATION_OUTCOMES = get_determination_transitions()


def phases_matching(phrase, exclude=list()):
    return [
        status for status, _ in PHASES
        if status.endswith(phrase) and status not in exclude
    ]


PHASES_MAPPING = {
    'received': {
        'name': 'Received',
        'statuses': [INITIAL_STATE, 'proposal_discussion'],
    },
    'internal-review': {
        'name': 'Internal Review',
        'statuses': phases_matching('internal_review'),
    },
    'in-discussion': {
        'name': 'Ready for Discussion',
        'statuses': phases_matching('discussion', exclude=[INITIAL_STATE, 'proposal_discussion']),
    },
    'more-information': {
        'name': 'More Information Requested',
        'statuses': phases_matching('more_info'),
    },
    'invited-for-proposal': {
        'name': 'Invited for Proposal',
        'statuses': ['draft_proposal'],
    },
    'external-review': {
        'name': 'AC Review',
        'statuses': phases_matching('external_review'),
    },
    'ready-for-determination': {
        'name': 'Ready for Determination',
        'statuses': phases_matching('determination'),
    },
    'accepted': {
        'name': 'Accepted',
        'statuses': phases_matching('accepted'),
    },
    'dismissed': {
        'name': 'Dismissed',
        'statuses': phases_matching('rejected'),
    },
}