from datetime import datetime, date, time
from sqlalchemy import create_engine, event
from sqlalchemy.orm import scoped_session, sessionmaker, aliased, mapper, relationship
from sqlalchemy.dialects import mysql
from sqlalchemy import exists, desc, text, and_
from sqlalchemy.inspection import inspect
from scrapeanything.database.models import Model
import os
import traceback

from scrapeanything.utils.types import Types
from scrapeanything.utils.utils import Utils
from scrapeanything.utils.config import Config
from scrapeanything.utils.log import Log

class Repository:

    def __init__(self, config: Config) -> None:
        self.config = config

        is_debug = self.config.get(section='COMMANDS', key='debug')

        username = self.config.get(section='DATABASE', key='username')
        password = self.config.get(section='DATABASE', key='password')
        host = self.config.get(section='DATABASE', key='host')
        port = self.config.get(section='DATABASE', key='port')
        database = self.config.get(section='DATABASE', key='database')
        self.engine = create_engine(f'mysql+pymysql://{username}:{password}@{host}:{port}/{database}', echo=is_debug) # , isolation_level='READ UNCOMMITTED')
        Session = sessionmaker(bind=self.engine)
        self.session = scoped_session(Session)
  
    def save(self, entity: any) -> any:
        '''
        Description: save a collection of models or a single model
        Arguments: <data> - collection or single model
        Returns: last id inserted or updated
        '''
        try:
            data = entity

            model_from_database = self.load(entity=data)
            model = self.update(entity_from_database=model_from_database, entity=data)

            if model not in self.session:
                self.session.merge(model)
            # else:
            #     self.session.merge(model)

            # self.session.commit()

            entity_type = type(model)
            Log.trace('Saved entity {0} with id {1}', entity_type, model.id)

            return model
        except Exception as e:
            Log.error(f'An error ocurred: {e}')
            Log.error(traceback.format_exc())


    def update(self, entity_from_database: any, entity: any) -> any:
        '''
        Description: updates an entity from its version on the database
        Arguments: 
        - <entity_from_database> - entity loaded from the database
        - <entity> - entity built in memory
        Returns: updated entity (holds Id from the database saved entity)
        '''

        if entity_from_database is None:
            return entity

        mapper = inspect(entity)
        for attr in mapper.attrs:
            if attr.key == 'id' or attr.key == 'created_at' or attr.key == 'updated_at':
                continue
            # elif attr.key.endswith('s'):
            #     continue
            elif attr.key.endswith('id'):
                continue

            value = getattr(entity, attr.key)
            if Types.is_primitive(value=value) and getattr(entity_from_database, attr.key) != value:
                setattr(entity_from_database, attr.key, value)

        if entity_from_database is not None:
            setattr(entity_from_database, 'updated_at', datetime.now())

        return entity_from_database

    def load(self, entity: any):
        '''
        Description:
        Arguments:
        Returns:
        '''

        entity_type = type(entity)

        id = getattr(entity, 'id')
        if id is not None:
            condition = getattr(entity_type, 'id') == entity.id
            return self.session.query(entity_type).filter(condition).first()

        filters = self.get_keys(entity=entity)
        from_database = self.first(entity_type, filters)
        return from_database

    def first(self, entity_type: any, filters: list):
        '''
        Description: returns the first element found given some filters
        Arguments:
        Returns:
        '''

        query = self.session.query(entity_type)

        aliases = {}
        for attr, value in filters.items():
            # condition is of type object.subobject.property = value
            if attr.find('.') > -1:

                entities = attr.split('.')[:-1] # from object.subobject1.subobject2.property take object.subobject1.subobject2
                attribute = attr.split('.')[-1] # from object.subobject1.subobject2.property last attribute is property

                parent_entity_type = entity_type
                for i, sub_entity in enumerate(entities): # from object.subobject1.subobject2.property get only object.subobject1.subobject2
                    # subobject<i>
                    sub_entity_type = Types.to_class(module_name='database.models', class_name=self.get_sub_entity_type(entity=parent_entity_type, prop=sub_entity))
                    aliases[sub_entity] = aliased(sub_entity_type)
                    # join
                    query = query.join(aliases[sub_entity], Utils.find(parent_entity_type, sub_entity))

                    # joins have to be executed on [object, subobject1]; [suboject1, suboject2]; etc.
                    parent_entity_type = sub_entity_type

                # filter
                # load an object by similarity
                if self.config.get(section='DATABASE', key='load_filter', default='equal') == 'like':
                    condition = getattr(aliases[sub_entity], attribute).like(f'%{value}%')
                else:
                    condition = getattr(aliases[sub_entity], attribute) == value

                query = query.filter(condition)

            # condition is of type object.property = value
            elif attr.find('.') == -1:
                if self.config.get(section='DATABASE', key='load_filter', default='equal') == 'like':
                    condition = getattr(entity_type, attr).like(f'%{value}%')
                else:
                    condition = getattr(entity_type, attr) == value

                # add condition to the query 
                query = query.filter(condition)

        entity = query.first()
        return entity

    def all(self, entity_type):
        '''
        Description:
        Arguments:
        Returns:
        '''

        entity_type = Types.to_class(module_name='database.models', class_name=entity_type)
        entities = self.session.query(entity_type).all()
        return entities

    def get_keys(self, entity: any):
        '''
        Description: Returns list of columns and their values to determine uniquely a given entity (defined in models.py)
        Arguments:
        Returns:
        '''

        def normalize_keys(keys: list):
            normalized_keys = []

            for key in keys:
                if key.find('*') > -1:
                    entity = key.split('.')[-2] # take the last entity before *
                    entity_type = entity.split('_')[0].capitalize()
                    entity_type = Types.to_class(module_name='database.models', class_name=entity_type)

                    for token in entity_type.__keys__.split(','):
                        token = token.strip()
                        normalized_key = key.replace('*', token)
                        normalized_keys += normalize_keys([ normalized_key ])
                else:
                    normalized_keys.append(key)

            return normalized_keys

        def replace_star_in_key(keys: str):
            # supports __keys__ = match.*

            response = []

            for key in keys.split(','):
                key = key.strip()
                if key.find('*') > -1:
                    ref_entity = key.strip().split('.')[-2] # another_entity
                    ref_entity_type = ref_entity.split('_')[0].capitalize().strip() if ref_entity.find('_') != -1 else ref_entity.capitalize().strip() # extract entity name # TODO: use self.get_sub_entity_type()?
                    ref_entity_type = Types.to_class(module_name='database.models', class_name=ref_entity_type.capitalize().strip())
                    exploded_keys = ref_entity_type.__keys__ # __keys__ of another_entity

                    # exploded_keys are taken from child entity (i.e. match.* => championship.name, team_home.name, team_away.name).
                    # Prepende at each reference property the source object (i.e. match.championship.name, match.team_home.name, match.team_away.name)
                    for exploded_key in exploded_keys.split(','):
                        exploded_key = exploded_key.strip()
                        if exploded_key.find('*') > -1:
                            exploded_key = f'{ref_entity}.{exploded_key.strip()}'
                            response.append(replace_star_in_key(keys=exploded_key))
                        else:
                            response.append(f'{ref_entity}.{exploded_key.strip()}')
                else:
                    response.append(key)

            return response
        
        filters = {}

        # for <entity> there could be some columns that refer to __keys__ of another entity (i.e. other_entity.another_entity.*)
        __keys__ = normalize_keys(keys=[ key.strip() for key in entity.__keys__.split(',') ])

        # get all keys listed comma separated
        for key in __keys__:
            filters[key] = eval(f'entity.{key}') # Utils.find(entity, key) # { key: value }

        return filters

    def get_raw_query(self, q: any):
        '''
        Description: transforms SqlAlchemy query into t-sql query (It can be used for debugging purposes)
        Arguments: <q> - SqlAlchemy query (from self.session(<q>))
        Returns: t-sql query string version
        '''

        query = q.statement.compile(dialect=mysql.dialect())
        params = query.params # get query parameters and values

        query_string = str(q.as_scalar()) # get query in t-sql format. It contains constants as query parameters (i.e. SELECT * FROM table WHERE value = :value)
        for param, value in params.items():

             # replace query parameters with real values
            if type(value) is str:
                value = f"'{value}'"
            elif type(value) is date:
                value = f"'{datetime.strftime(value, '%Y-%m-%d')}'"
            elif type(value) is time:
                value = f"'{datetime.strftime(value, '%H-%M-%S')}'"
            elif type(value) is datetime:
                value = f"'{datetime.strftime(value, '%Y-%m-%d %H-%M-%S')}'"

            query_string = query_string.replace(f':{param}', str(value))

        query_string = query_string.replace('\n', '') # the query contains some "\n". Let's remove them
        query_string = query_string[1:-1] # remove initial "(" and last ")" form query_string

        return query_string

    def get_sub_entity_type(self, entity: any, prop: str):
        '''
        Description: <TODO>
        Arguments: <TODO>
        Returns: <TODO>
        '''

        try:
            return getattr(entity, prop).property.argument
        except:
            key = getattr(entity, prop).property.key
            if key == 'team_home' or 'team_away':
                return 'Team'
            if key == 'match':
                return 'Match'
            if key == 'bet':
                return 'Bet'

    def query(self, entity_type):
        return self.session.query(entity_type)

    def sql(self, query: str, parameters: dict=None, entity_type: any=None) -> any:
        '''
        Executes sql query. Make sure you use :parameter for parameters
        '''
        q = text(query)
        result = self.session.execute(q, parameters).mappings().all()
        return result

    def assign(self, entity, prop, value):
        loaded_value = self.load(entity=value)
        value = self.update(entity_from_database=loaded_value, entity=value)
        if type(value) is list:
            for v in value:
                if v not in self.session:
                    value.is_dirty = True
                setattr(entity, prop, value)
        else:
            if value not in self.session:
                value.is_dirty = True
            setattr(entity, prop, value)

        return entity

    def rollback(self):
        self.session.rollback()

    def commit(self):
        self.session.commit()

    def close(self):
        self.session.close()
        self.engine.dispose()