import logging
import json
import traceback
import unittest
import datetime
import os
import re
from collections import namedtuple
import copy
import yaml
from jsonpath_rw import parse

logger = logging.getLogger("util")
logger.setLevel(logging.WARNING)

class LoggingUtil(object):
    """ Logging utility controlling format and setting initial logging level """
    @staticmethod
    def init_logging (name, level=logging.INFO, format='short'):
        logger = logging.getLogger(__name__)
        if not logger.parent.name == 'root':
            return logger

        FORMAT = {
            "short" : '%(funcName)s: %(message)s',
            "medium" : '%(funcName)s: %(asctime)-15s %(message)s',
            "long"  : '%(asctime)-15s %(filename)s %(funcName)s %(levelname)s: %(message)s'
        }[format]
        handler = logging.StreamHandler()
        formatter = logging.Formatter(FORMAT)
        handler.setFormatter(formatter)
        logger = logging.getLogger (name)
        logger.addHandler(handler)
        logger.setLevel(level)
        return logger

    @staticmethod
    def setup_logging(
            default_path=os.path.join(os.path.dirname(__file__), 'logging.yaml'),
            default_level=logging.INFO,
            env_key='LOG_CFG'):
        """Setup logging configuration
        
        """
        path = default_path
        value = os.getenv(env_key, None)
        if value:
            path = value
        if os.path.exists(path):
            with open(path, 'rt') as f:
                config = yaml.safe_load(f.read())
            logging.config.dictConfig(config)
        else:
            logging.basicConfig(level=default_level)

class Resource:
    @staticmethod
    def get_resource_path(resource_name):
       # Given a string resolve it to a module relative file path unless it is already an absolute path.
        resource_path = resource_name
        if not resource_path.startswith (os.sep):
            resource_path = os.path.join (os.path.dirname (__file__), resource_path)
        return resource_path
    
    @staticmethod
    def load_json (path):
        result = None
        with open (path, 'r') as stream:
            result = json.loads (stream.read ())
        return result

    @staticmethod
    def load_yaml (path):
        result = None
        with open (path, 'r') as stream:
            result = yaml.load (stream.read ())
        return result
    
    def get_resource_obj (resource_name, format=None):
        result = None
        if not format:
            if resource_name.endswith ('.yaml'):
                format = 'yaml'
            else:
                format = 'json'
        path = Resource.get_resource_path (resource_name)
        if os.path.exists (path):
            m = {
                'json' : Resource.load_json,
                'yaml' : Resource.load_yaml
            }
            if format in m:
                result = m[format](path)
        return result

    @staticmethod
    # Modified from:
    # Copyright Ferry Boender, released under the MIT license.
    def deepupdate(target, src, overwrite_keys = [], skip = []):
        """Deep update target dict with src
        For each k,v in src: if k doesn't exist in target, it is deep copied from
        src to target. Otherwise, if v is a list, target[k] is extended with
        src[k]. If v is a set, target[k] is updated with v, If v is a dict,
        recursively deep-update it.

        Updated to deal with yaml structure: if you have a list of yaml dicts,
        want to merge them by "name"

        If there are particular keys you want to overwrite instead of merge, send in overwrite_keys
        """
        if type(src) == dict:
            for k, v in src.items():
                if k in overwrite_keys:
                    target[k] = copy.deepcopy(v)
                elif type(v) == list:
                    if not k in target:
                        target[k] = copy.deepcopy(v)
                    elif type(v[0]) == dict:
                        Resource.deepupdate(target[k],v,overwrite_keys)
                    else:
                        target[k].extend(v)
                elif type(v) == dict:
                    if not k in target:
                        target[k] = copy.deepcopy(v)
                    else:
                        Resource.deepupdate(target[k], v,overwrite_keys)
                elif type(v) == set:
                    if not k in target:
                        target[k] = v.copy()
                    else:
                        target[k].update(v.copy())
                else:
                    if not k in skip:
                        target[k] = copy.copy(v)
        else:
            #src is a list of dicts, target is a list of dicts, want to merge by name (yikes)
            src_elements = { x['name']: x for x in src }
            target_elements = { x['name']: x for x in target }
            for name in src_elements:
                if name in target_elements:
                    Resource.deepupdate(target_elements[name], src_elements[name],overwrite_keys)
                else:
                    target.append( src_elements[name] )

class JSONKit:
    """ Generic kit for sql like selects on JSON object hierarchies. """
    def select (self, query, graph, field="type", target=None):
        """ Query nodes by some field, matching a list of target values """
        jsonpath_query = parse (query)
        values = [ match.value for match in jsonpath_query.find (graph) ]
        return [ val for val in values if target is None or val[field] in target ]

class Syfur:
    """
    An intentionall bad implementation of a query language reminiscent of cypher.
    Workflow's must be secure even posted from non-secure clients.
    So we prevent them from executing arbitrary cppher.
    Some of this can be achived with parameters but not all.
    Hopefully, will be replaced by a cypher parser in the future.
    In the meantime, we provide a basic graph query capability enabling automated validation and other features.

    TODO: edges.
    """
    def __init__(self):
        self.queries = {
            "match"            : "match (obj) return {field}",
            "match_type"       : "match (obj:{type}) return {field}",
            "match_params"     : "match (obj{props}) return {field}",
        }
        self.id_pat = re.compile ("^([a-zA-Z_\.0-9]+)$")
        self.value_pat = re.compile ("^([a-zA-Z_:\.0-9]+)$")
        
    def _field(self, val):
        return val if val == "*" else f"obj.{val}"
    def _gen(self, template, parameters):
        logger.debug (f"syfur template: {template}, parameters: {json.dumps(parameters, indent=2)}")
        return self.queries[template].format (**parameters)
    
    def parse (self, query):
        tokens = query.split ()
        syntax_message = "Invalid syntax. 'Query := match <filter> return <field>. Filter := arg=value. Field := <str>."
        assert len(tokens) >= 3, syntax_message
        assert tokens[0].lower() == 'match' and tokens[-2].lower() == 'return', syntax_message

        parameters = {}
        for t in tokens:
            if t == 'match':
                continue
            if t == 'return':
                break
            assert '=' in t, syntax_message
            assert not 'delete' in t and not 'detach' in t, syntax_message
            
            k, v = t.split ('=')
            assert self.id_pat.match (k).groups(), syntax_message
            assert self.value_pat.match (k).groups(), syntax_message

            parameters[k] = v

        field = self._field (tokens[-1])
        assert self.id_pat.match (field), syntax_message

        query_template = "match_params"

        props = ",".join([f"""{k}:'{v}'""" for k, v in parameters.items() ])
        parameters['props'] = f"{{ {props} }}" if len(props) > 0 else props
        parameters['field'] = field
        
        return self._gen (query_template, parameters)
