from typing import Callable, Dict, Optional, Type
import json
import jsonschema
import logging
from threading import Lock
from functools import reduce

from formant.sdk.agent.v1.client import Client as AgentClient

logging.basicConfig()

JSONDecodeError = Exception  # type: Type[Exception]
try:
    JSONDecodeError = json.decoder.JSONDecodeError
except AttributeError:
    # python2 doesn't have JSON Decode Error
    pass


class JsonSchemaValidator:
    def __init__(
        self,
        client,  # type: AgentClient
        adapter_name,  # type: str
        update_adapter_config_callback,  # type: Callable[[Dict], None]
        validate=True,  # type: bool
        logger=None,  # type: Optional[logging.Logger]
        logger_level=logging.INFO,  # type: int
    ):
        self._lock = Lock()
        self._client = client
        self._adapter_name = adapter_name
        self._update_adapter_config_callback = update_adapter_config_callback
        self._validate = validate
        if logger is None:
            logger = logging.getLogger(adapter_name)
            logger.setLevel(logger_level)
        self.logger = logger
        self._client.register_config_update_callback(self._update_adapter_config)
        if self._client.ignore_unavailable:
            self._update_adapter_config()

    def _update_adapter_config(self):
        # Consumer might not be threadsafe
        with self._lock:
            try:
                configs = [
                    self._get_config_from_adapter_configuration(),
                    self._get_config_from_blob_data(),
                    self._get_config_from_json(),
                ]
                config = reduce(lambda s1, s2: s1 or s2, configs)
                if config is None:
                    raise Exception(
                        "Could not get configuration for '%s'" % self._adapter_name
                    )
                if self._validate:
                    self._validate_schema(config)
                try:
                    self._update_adapter_config_callback(config)
                except Exception as e:
                    self.logger.error(
                        "Error calling update config callback %s" % str(e)
                    )
            except Exception as e:
                self.logger.warn("Failed to load config: %s" % str(e))
                self._client.create_event(
                    "%s configuration loading failed: %s."
                    % (self._adapter_name, str(e)),
                    notify=False,
                    severity="warning",
                )

    def _get_config_from_adapter_configuration(self):
        self.logger.info("Loading config from adapter configuration")
        try:
            adapters = self._client.get_agent_configuration().document.adapters
            for adapter in adapters:
                config = json.loads(adapter.configuration)
                if self._adapter_name in config:
                    return config
        except Exception as e:
            self.logger.warn("Error loading config from adapter config: %s" % str(e))
        return None

    def _get_config_from_blob_data(self):
        self.logger.info("Loading config from blob data")
        try:
            config = json.loads(self._client.get_config_blob_data())
            if self._adapter_name in config:
                return config
        except Exception as e:
            self.logger.info("Error loading config from blob data: %s" % str(e))
        return None

    def _get_config_from_json(self):
        # Try to get config from config.json
        try:
            with open("config.json") as f:
                config = json.loads(f.read())
                if self._adapter_name in config:
                    return config
        except Exception as e:
            self.logger.info("Could not get config from config.json: %s" % str(e))
        return None

    def _validate_schema(self, config_blob):
        # Validate configuration based on schema
        with open("config_schema.json") as f:
            try:
                self.config_schema = json.load(f)
                self.logger.info("Loaded config schema from config_schema.json file")
            except JSONDecodeError as e:
                self.logger.warn(
                    "Could not load config schema. Is the file valid json?"
                )
                raise Exception("config schema error: %s" % str(e))
        self.logger.info("Validating config...")
        try:
            jsonschema.validate(config_blob, self.config_schema)
            self.logger.info("Validation succeeded")
        except (
            jsonschema.ValidationError,
            jsonschema.SchemaError,
            jsonschema.RefResolutionError,
        ) as e:
            self.logger.warn("Validation failed %s: %s", type(e).__name__, str(e))
        except Exception as e:
            self.logger.warn(
                "Validation failed with unkown error %s: %s", type(e).__name__, str(e)
            )
