# By: Riasat Ullah
# Handles on-call related query like preparing on-call calendar, finding current and next on-call roles, etc.

from utils import times, var_names
import datetime


def get_on_call_shift_details(records):
    '''
    Gets the on-call shift name and policy ref id based on the most recent data.
    :param records: list of historical policy and routine items
    :return: (str) on-call shift name
    '''
    item = records[0]
    pol_name = item[var_names.policy_name]
    pol_ref = item[var_names.policy_ref_id]
    level = item[var_names.assignee_level]
    disp_name = pol_name + ' (L' + str(level) + ')'
    details = {
        var_names.key: [disp_name, pol_ref, level],
        var_names.policy_name: pol_name,
        var_names.policy_ref_id: pol_ref,
        var_names.assignee_level: item[var_names.assignee_level],
        var_names.routine_ref_id: item[var_names.routine_ref_id],
        var_names.for_group: item[var_names.for_group]
    }
    return details


def prepare_on_call_calendar(historical_policy_data, assignee_pol_id, timezone, start_date, end_date):
    '''
    Get all the days and times a users is on-call during a certain period.
    :param historical_policy_data: (dict) of policy and associated routines to identify the on-call roles from
    :param assignee_pol_id: policy ID of the assignee
    :param timezone: timezone the dates and times should be converted to
    :param start_date: (datetime.date) start date
    :param end_date: (datetime.date) end date
    :return: (list of list) -> [[ [date, [time slot 1, time slot 2, ...], ..], ]]
            --> the first sub list represents the on-call shift
    '''
    start_date = datetime.datetime.combine(start_date, datetime.time(0, 0))
    end_date = datetime.datetime.combine(end_date, datetime.time(0, 0)) + datetime.timedelta(days=1)
    calendar_data = []
    for key in historical_policy_data:
        pol_cal = []
        records = historical_policy_data[key]
        on_call_shift_det = get_on_call_shift_details(records)

        for pol_item in records:
            pol_tz_start = times.utc_to_region_time(pol_item[var_names.start_timestamp], timezone)
            pol_tz_end = times.utc_to_region_time(pol_item[var_names.end_timestamp], timezone)

            if pol_tz_start > end_date or pol_tz_end < start_date:
                continue
            else:
                for rou_item in pol_item[var_names.routines]:
                    rou_tz_start = times.utc_to_region_time(rou_item[var_names.start_timestamp], timezone)
                    rou_tz_end = times.utc_to_region_time(rou_item[var_names.end_timestamp], timezone)
                    rou_obj = rou_item[var_names.routines]
                    if rou_tz_start > end_date or rou_tz_end < start_date:
                        continue
                    else:
                        period_start = rou_tz_start if rou_tz_start >= start_date else start_date
                        if pol_tz_start >= period_start:
                            period_start = pol_tz_start

                        period_end = rou_tz_end if rou_tz_end <= end_date else end_date
                        if pol_tz_end <= end_date:
                            period_end = pol_tz_end

                        period = (period_end.date() - period_start.date()).days
                        if (period_end - period_start).total_seconds() % (60 * 60 * 24) > 0:
                            period += 1

                        rou_schedule = rou_obj.prepare_schedule(period_start, period)
                        for item in rou_schedule:
                            assignee_is_on_call = False
                            for rot in item[var_names.on_call]:
                                if rot.assignee_policy_id == assignee_pol_id:
                                    assignee_is_on_call = True
                                    break

                            curr_tz_rot_start = item[var_names.rotation_start] if rou_obj.routine_timezone == timezone \
                                else times.switch_region_time(item[var_names.rotation_start], rou_obj.routine_timezone,
                                                              timezone)
                            curr_tz_rot_end = item[var_names.rotation_end] if rou_obj.routine_timezone == timezone \
                                else times.switch_region_time(item[var_names.rotation_end], rou_obj.routine_timezone,
                                                              timezone)

                            if assignee_is_on_call:
                                pol_cal.append({
                                    var_names.rotation_start: curr_tz_rot_start,
                                    var_names.rotation_end: curr_tz_rot_end,
                                    var_names.on_call: [on_call_shift_det[var_names.key]]
                                })
        calendar_data.append(pol_cal)

    return calendar_data


def get_on_call_roles(policy_data, assignee_pol_id, timezone, check_datetime, look_forward):
    '''
    Get the on-call shifts, current and upcoming on-call roles of a given user.
    :param policy_data: (dict) of policy and associated routines to identify the on-call roles from
    :param assignee_pol_id: policy ID of the user whose on-call roles are being requested
    :param timezone: timezone the data should be converted to
    :param check_datetime: (datetime.datetime) timestamp to check for the on-call roles
    :param look_forward: (int) number of days to look forward for the upcoming on-call roles
    :return: on-call shifts, current on-call roles, next on-call roles
    '''
    on_call_shifts = []
    current_roles = []
    next_roles = []
    for key in policy_data:
        records = policy_data[key]
        shift_details = get_on_call_shift_details(records)
        on_call_shifts.append(shift_details)
        for pol_item in records:
            if pol_item[var_names.start_timestamp] <= check_datetime < pol_item[var_names.end_timestamp]:
                for rou_item in pol_item[var_names.routines]:
                    rou_obj = rou_item[var_names.routines]
                    if rou_item[var_names.start_timestamp] <= check_datetime < rou_item[var_names.end_timestamp]:
                        on_call_period, next_on_call_period =\
                            rou_obj.get_assignee_on_call_period(assignee_pol_id, check_datetime, look_forward)

                        if on_call_period is not None:
                            data = {
                                **shift_details,
                                **{var_names.start_period: times.switch_region_time(on_call_period[0],
                                                                                    rou_obj.routine_timezone, timezone),
                                   var_names.end_period: times.switch_region_time(on_call_period[1],
                                                                                  rou_obj.routine_timezone, timezone)}
                            }
                            current_roles.append(data)

                        if next_on_call_period is not None:
                            data = {
                                **shift_details,
                                **{var_names.start_period: times.switch_region_time(next_on_call_period[0],
                                                                                    rou_obj.routine_timezone,
                                                                                    timezone),
                                   var_names.end_period: times.switch_region_time(next_on_call_period[1],
                                                                                  rou_obj.routine_timezone,
                                                                                  timezone)}
                            }
                            next_roles.append(data)

    return on_call_shifts, current_roles, next_roles


def prepare_routine_calendar(historical_routine_data, timezone, start_date, end_date):
    '''
    Get all the days and times a users is on-call during a certain period.
    :param historical_routine_data: (list) of versions of the same routine across a timeline
    :param timezone: timezone the dates and times should be converted to
    :param start_date: (datetime.date) start date
    :param end_date: (datetime.date) end date
    :return: (tuple) -> (list of list, list of list) ->
            --> first item -- [[ {rotation_start: , rotation_end: , on_call: }, ... ]]
            --> second item -- [[assignee display name, preferred username], ...]
            --> the first sub list represents the on-call shift
    '''
    start_date = datetime.datetime.combine(start_date, datetime.time(0, 0))
    end_date = datetime.datetime.combine(end_date, datetime.time(0, 0)) + datetime.timedelta(days=1)
    calendar_data = []
    on_call_list = []

    for rou_item in historical_routine_data:
        rou_tz_start = times.utc_to_region_time(rou_item[var_names.start_timestamp], timezone)
        rou_tz_end = times.utc_to_region_time(rou_item[var_names.end_timestamp], timezone)
        rou_obj = rou_item[var_names.routines]

        if rou_tz_start > end_date or rou_tz_end < start_date:
            continue
        else:
            period_start = rou_tz_start if rou_tz_start >= start_date else start_date
            period_end = rou_tz_end if rou_tz_end <= end_date else end_date
            period = (period_end.date() - period_start.date()).days
            if (period_end - period_start).total_seconds() % (60 * 60 * 24) > 0:
                period += 1

            rou_schedule = rou_obj.prepare_schedule(period_start, period)
            for item in rou_schedule:
                sch_on_call_fmt = [[rot.display_name, rot.assignee_name] for rot in item[var_names.on_call]]
                if sch_on_call_fmt not in on_call_list:
                    on_call_list.append(sch_on_call_fmt)

                curr_tz_rot_start = item[var_names.rotation_start] if rou_obj.routine_timezone == timezone \
                    else times.switch_region_time(item[var_names.rotation_start], rou_obj.routine_timezone, timezone)
                curr_tz_rot_end = item[var_names.rotation_end] if rou_obj.routine_timezone == timezone \
                    else times.switch_region_time(item[var_names.rotation_end], rou_obj.routine_timezone, timezone)

                calendar_data.append({
                    var_names.rotation_start: curr_tz_rot_start,
                    var_names.rotation_end: curr_tz_rot_end,
                    var_names.on_call: sch_on_call_fmt
                })

    return [calendar_data], on_call_list
