from .runner_base import BaseRunner
from oaa.settings import config  # , DBConfig  # , load_config
from oaa.models import DynamicTranformOptions_Enum
import petl
import logging
from oaa import oaa_utils
from oaa import utils
from oaaclient.templates import HRISProvider  # , IdPProviderType
logger = logging.getLogger(__name__)


class HRISRunner(BaseRunner):
    source_type = 'db'

    """
    A Runner for doing Custom pushes of HRIS data to Veza.

    If you want to omit custom attributes, even if mapped in the file, pass
    in the argument --do-custom-attributes=False.

    Include --dry-run=True to make this do all of the parsing but not send the
    payload to Veza.


    """

    def _post_init(self,
                   *args, **kwargs,
                   ):

        if not config.valid():
            logger.error('configuration is not valid')
            exit(1)
        else:
            self._provider_name = config.HRIS_VENDOR_NAME

            self._datasource_name = f"{config.HRIS_VENDOR_NAME} Datasource"  # noqa E501

        self._set('hris', None)

    def preflight(self):
        '''
        Run preflight checks.

        This function will run several preflight checks on the configuration.
        It should be a pretty good indication of the readiness of the script to
        run.

        '''
        problems = []

        logger.debug('#### Configuration')
        logger.debug(self.show_config())

        for pf in oaa_utils.preflights:
            problems.extend(pf(self))

        if problems:
            for problem in problems:
                print(f'⚠️  {problem}')
        else:
            print('🥳 It appears you are all set to go')

    def run(self):
        '''
        Run the Integration.

        This will pull information from the DB, do some rudimentary
        transformation to cleanup the data then send the payload to OAA.

        '''
        # self.preflight()
        table = self._get_stream(query=config.QUERY)

        if not table:
            logger.error('unable to fetch any data')

        fields_to_cutout = set()

        null_to_blank_func = utils.transformers.get('null_to_blank',
                                                    None)['func']
        null_to_blank_conv = null_to_blank_func(options={})

        for fieldname, field in self.field_mappings().items():

            if not field['include']:
                continue

            # standardize the function name
            # standard_func_name = field['transform'].strip().lower()
            func_obj = utils.transformers.get(field['transform'], None)

            destination = field['destination_field']

            # if destination == '':
            #     logger.error('destination cannot be blank %s', field)
            #     exit()

            if func_obj:
                func = func_obj['func']
                # some functions require special treatment. This usually means
                # passing in to config value(s) to the function then returning
                # the inner function to the petl convert/addfield method.
                # TODO I'd like the extract this out. This is cumbersome and
                # not super readable.

                mode = func_obj['mode']

                func = func(options=field['field_options'])

                if mode == 'convert':
                    table = table.convert(fieldname, func, pass_row=True)

                    # if destination == 'termination_date':
                    #     table = table.cut(fieldname)
                    #     print(table)
                    #     import bpdb; bpdb.set_trace()  # noqa: E702
                elif mode == 'add':
                    table = table.addfield(fieldname, func)
                elif mode == 'select':
                    table = table.select(fieldname, func)

                # every field needs to be converted from null to blanks

            elif field['transform'] == 'ignore':
                fields_to_cutout.add(fieldname)

            # we can ignore fields for downstream mapping

            if field['transform'] != DynamicTranformOptions_Enum.none:

                logger.debug('adding field %s', destination)

                # if field['is_custom'].lower() == 'y':

                if field['is_custom']:
                    # It's a custom field

                    self._custom_attrs_map[destination] = field  # fieldname
                else:
                    self._attributes[destination] = fieldname

                # table = table.convert(fieldname, utils.util_null_to_blank)
                table = table.convert(fieldname, null_to_blank_conv)

        # cutout fields we don't want anymore. They aren't removed before
        # this moment because they may be needed in join_fields
        cur_fieldnames = table.fieldnames()
        filtered_fieldnames = filter(lambda f: f in cur_fieldnames, fields_to_cutout)  # noqa E501

        for fieldname in filtered_fieldnames:
            table = table.cutout(fieldname)

        hris = self._get_or_create_provider(name=config.HRIS_VENDOR_NAME,
                                            hris_type=config.HRIS_VENDOR_TYPE, # noqa E501
                                            url=config.HRIS_VENDOR_URL)
        self._set('hris', hris)
        self._create_oaa_objects(table)
        self._push_to_oaa()

    def _get_provider_type(self):
        return HRISProvider

    def _custom_provider_actions(self, provider_object=None):
        # Multiple types can be added by running `add_idp_type` multiple times
        # with different IdPProviderType enums
        provider_object.system.add_idp_type(config.IDP_PROVIDER_TYPE)

    def _get_or_create_provider_orig(self):
        # Configure the Custom IdP
        # TODO move this to oaa_utils
        logger.info(f"Creating HRIS for {config.HRIS_VENDOR_NAME}")
        hris = HRISProvider(name=config.HRIS_VENDOR_NAME,
                            hris_type=config.HRIS_VENDOR_TYPE,
                            url=config.HRIS_VENDOR_URL)

        # HRIS employees can be automatically linked to IdP users, setting the
        # IdP provider type to use for matching

        # Multiple types can be added by running `add_idp_type` multiple times
        # with different IdPProviderType enums
        hris.system.add_idp_type(config.IDP_PROVIDER_TYPE)

        veza_con = oaa_utils.get_oaa_client(runner=self)
        provider = veza_con.get_provider(self._provider_name)
        # create a new provider if the provider doesn't already exist

        if not provider:
            logger.info(f"Creating new provider {self._provider_name}")
            provider = veza_con.create_provider(self._provider_name,
                                                hris.TEMPLATE)

        if config.HRIS_ICON_B64_PATH:
            veza_con.update_provider_icon(hris['id'],
                                          encode_icon_file(config.HRIS_ICON_B64_PATH)) # noqa E501

        return hris

    @property
    def hris(self):
        return self._get('hris')

    def _create_oaa_objects_custom(self, table):

        logger.info("Loading users from the data streama")

        # for row_number, r in enumerate(petl.records(table)):
        table = table.addrownumbers()

        for r in petl.records(table):
            logger.debug('working on row %s', r.row)

            for f_name in r.flds:
                assert not callable(r[f_name])

            employee_number = r[config.HRIS_UNIQUE_ID_FIELD]

            if employee_number in self.hris.employees:
                logger.error("Employee entry with employee number already"
                             " processed, employee numbers must be unique."
                             f" employee_number {employee_number}")  # noqa E501
                logger.error("skipping row")

                continue

            # Required properties to create an employee record:
            # REQUIRED_VEZA_FIELDS
            object_with_required_fields = {}

            for req_field in oaa_utils.REQUIRED_VEZA_FIELDS:
                req_field_to_map = req_field

                if isinstance(req_field, tuple):
                    # one of these fields is required, not both
                    found_in_mapping = 0

                    for f in req_field:
                        if f in self._attributes and self._attributes[f] in r.flds:  # noqa E501
                            found_in_mapping += 1
                            req_field_to_map = f

                    if found_in_mapping > len(req_field):
                        logger.error('only one of %s can be mapped', req_field)
                        exit()

                    if found_in_mapping == 0:
                        logger.error('one of %s must be mapped', req_field)
                        exit()

                else:

                    if not self._attributes:
                        logger.error('something is wrong, self._attributes is empty')  # noqa E501

                    if not all((self._attributes.get(req_field), self._attributes[req_field] in r.flds)):  # noqa E501
                        logger.error('required field %s is not mapped',
                                     req_field)
                        logger.error('%s', self._attributes.keys())
                        logger.error('%s', r.flds)
                        exit()
                object_with_required_fields[req_field_to_map] = r[self._attributes[req_field_to_map]]  # noqa E501

            employee = self.hris.add_employee(**object_with_required_fields)

            # Add other attributes

            # These are those not already added as required
            filtered_fields = filter(lambda f: f not in oaa_utils.REQUIRED_VEZA_FIELDS, self._attributes)  # noqa E501

            for update_field in filtered_fields:
                attr_source = self._attributes[update_field]

                # Note: Special treatment for some column types

                if attr_source == 'group':
                    # employee departments must be a group, check that the
                    # group exists first then assign
                    department = r.get(attr_source)

                    if department and department not in self.hris.groups:
                        self.hris.add_group(unique_id=department,
                                            name=department,
                                            group_type=config.HRIS_GROUP_TYPE)  # what should this be  # noqa E501

                    if department:
                        employee.department = department

                elif update_field == 'manager':
                    manager_id = r.get(attr_source)

                    if manager_id:
                        # logger.debug('adding manager [%s] to user',
                        #              manager_id)
                        employee.add_manager(manager_id)
                elif update_field == 'employment_type':
                    logger.debug('adding employment type')
                    employee.employment_types.append(r[attr_source])

                else:
                    # logger.debug('adding %s=%s to employee',
                    #              attr_source, r[attr_source])

                    if update_field == 'email':
                        logger.debug('email %s', r[attr_source])

                    # setattr(employee,
                    #         update_field,
                    #         utils.util_null_to_blank(r[attr_source]))
                    setattr(employee, update_field, r[attr_source])

            # custom properties are set using the `set_property` method
            # with the name of the property and value

            for property_name, f in self._custom_attrs_map.items():
                source_field = f['source_field'] or f['destination_field']

                if r.get(source_field) in [None, '']:
                    logger.debug('field [%s] is blank on %s', source_field,
                                 r.unique_id)
                else:
                    logger.debug('adding %s=%s to employee [%s]', source_field,
                                 r[source_field],
                                 r.unique_id)

                    # maybe needs to be lowercase?
                    employee.set_property(property_name.lower(),
                                          r[source_field])  # ignore_none=True

        logger.info('Finished loading users. Users loaded: %s',
                    len(self.hris.employees))
