'''
This file contains various pydantic models used in the runner. Primarily used
when parsing a config.yaml file, it helps keep validation fast and consistent.
'''

import json
from typing import Optional, Any
from enum import Enum
import logging
from pydantic import BaseModel, Field, field_validator
from . import utils
logger = logging.getLogger(__name__)


class VezaObjectType(str, Enum):
    '''The VezaObjectType represents teh object that this source maps to on the
    Veza side. There are templates in Veza (Custom appliction, HRIS, IDP, etc)
    that each take a different set of Object types.

    Use None to indicate that the objects in the stream are not to be mapped to
    Veza.
    '''
    Employee = 'Employee'
    LocalUser = 'LocalUser'
    LocalGroup = 'LocalGroup'
    LocalRole = 'LocalRole'
    Resource = 'Resource'
    Group = 'Group'
    User = 'User'
    none = 'None'


class VezaFieldType(str, Enum):
    BOOLEAN = 'BOOLEAN'
    NUMBER = 'NUMBER'
    STRING = 'STRING'
    STRING_LIST = 'STRING_LIST'
    TIMESTAMP = 'TIMESTAMP'
    TIMESTAMP_AD = 'TIMESTAMP_AD'


transform_options = list(utils.FUNCS.keys())
transform_enum_values = {k: k for k in transform_options}
transform_enum_values['none'] = 'none'
DynamicTranformOptions_Enum = Enum('DynamicTranformOptions_Enum',
                                   transform_enum_values)
TRUTHY_VALUES = ['y', 'yes', 'true']


def to_bool(value):
    '''convert value to a boolean'''

    return value.lower() in TRUTHY_VALUES


class Transform(BaseModel):
    name: Optional[str]
    options: Optional[Any] = None


class FieldMapping(BaseModel, use_enum_values=True):

    include: bool = True
    source_field: Optional[str] = None
    notes: Optional[str] = None
    # transform: str = Field(choices=transform_options)
    transforms: list[Transform] = Field(default_factory=lambda: [])
    transform: DynamicTranformOptions_Enum = DynamicTranformOptions_Enum.none
    # str = Field(choices=transform_options)
    field_options: Optional[dict] = None

    # veza_obj: Optional[VezaObjectType] = None
    is_custom: bool = False
    destination_field: str
    veza_field_type: Optional[VezaFieldType] = None
    as_list: bool = False
    custom_type: Optional[VezaFieldType] = None

    # @field_validator("veza_obj", mode="before")
    # @classmethod
    # def veza_obj_validator(cls, veza_obj: str,
    #                        info: dict[str, Any]) -> bool:

    #     # if destination_field == 'unique_id':
    #     #     import bpdb; bpdb.set_trace()  # noqa: E702

    #     if veza_obj == '':
    #         return None

    #     return veza_obj

    @field_validator("destination_field", mode="before")
    @classmethod
    def destination_field_validator(cls, destination_field: str,
                                    info: dict[str, Any]) -> bool:

        return destination_field

    @field_validator("transform", mode="before")
    @classmethod
    def transform_transform(cls, transform: str) -> bool:
        return str(transform).lower()


    @field_validator("custom_type", mode="after")
    @classmethod
    def custom_type_validator(cls, custom_type, values: dict[str, Any]) -> bool:
        """Setting custom_type: <SomeVezaFieldType>, implies that the field is
        a custom field. Set these other value accordingly"""
        values.data['is_custom'] = True
        values.data['veza_field_type'] = custom_type

        return custom_type 

    @field_validator("veza_field_type", mode="before")
    def veza_field_type_prevalidate(cls,
                                    field_type: str,
                                    info: dict[str, Any]) -> bool:

        logger.debug('source_field: %s', info.data['source_field'])

        if field_type:
            return field_type.upper()
        else:
            return VezaFieldType.STRING

    @field_validator("veza_field_type", mode="after")
    @classmethod
    def veza_field_type_validate(cls,
                                 field_type: str,
                                 info: dict[str, Any]) -> bool:

        if (info.data['include'] and info.data['is_custom']) and not field_type:  # noqa E501
            raise ValueError('field_type required when is_custom = True')

        return field_type

    @field_validator("field_options", mode="before")
    @classmethod
    def validate_field_options(cls, field_options: str) -> dict:
        try:
            options = json.loads(field_options)

            if isinstance(options, list):
                return {'options': options}

            if isinstance(options, dict):

                return options

            return options
        except Exception as err:  # noqa E501

            return {'options': field_options}

    @field_validator("field_options", mode="before")
    @classmethod
    def validate_transform_to_list(cls, field_options,
                                   info: dict[str, Any]):
        '''if field_options is set, take the transform and field_options
        as add it to transforms.'''

        if field_options:
            # import bpdb; bpdb.set_trace()  # noqa: E702
            info.data['transforms'].append(
                    Transform.model_validate({
                        'name': info.data['transform'],
                        'options': field_options
                    }))


# class FieldMappings(BaseModel):
#     mappings: dict[str, FieldMapping]


# I use pydantic for data validation. It's fast and saves a ton of work doing
# manual validation of data and payloads. Here we define several structures.

# TODO Rename these constants to use UPPER_CASE styling
class DestinationEnum(str, Enum):
    employee = 'employee'
    application = 'application'
    local_user = 'local_user'
    local_group = 'local_group'
    local_role = 'local_role'
    resource = 'resource'


# TODO Rename these constants to use UPPER_CASE styling
class FieldType(str, Enum):
    string = 'STRING'
    string_list = 'STRING_LIST'
    number = 'NUMBER'
    boolean = 'BOOLEAN'
    timestamp = 'TIMESTAMP'
    timestamp_windows_format = 'TIMESTAMP_AD_FORMAT'


class CustomProperty(BaseModel):
    type: str
    name: str
    lcm_unique_identifier: Optional[bool] = None


class ColumnMapping(BaseModel):
    column_name: str
    destination_property: Optional[str] = None
    destination_type: DestinationEnum = DestinationEnum.employee
    custom_property: Optional[dict] = None
    as_list: bool = Field(default=False)
    template: Optional[str] = None


class HRISConfig(BaseModel):
    hris_name: str  # "khansen - hris 2",
    hris_type: str  # = Field(choices=["hris"])
    hris_url: str  # "https://khansen-hris2.example.com",
    hris_provisioning_source: bool = Field(default=False)
    # hris_identity_mapping: Optional[HRISIdentityModel] = Field(default=None)


class AdvancedConfig(BaseModel):
    list_delimiter: str = Field(default=",")  # can this be null, or blank?


class MappingConfig(BaseModel):
    template_type: str = "hris"
    column_mappings: list[ColumnMapping]
    hris: Optional[HRISConfig] = None
    advanced: AdvancedConfig


class Provider(BaseModel):
    # "name": "khansen - csv 2",
    # "custom_templates": ["hris"],
    push_type: str = Field(default="CSV")
    csv_mapping_configuration: MappingConfig
