import collections
import itertools
import json
import logging
import re

import jsonschema

import ruxit.tools.plugin_schema as schema

log = logging.getLogger(__name__)


class PluginMetadata:
    def __init__(self, raw_json):
        self.raw_json = raw_json

    def _query_raw_json_param(self, param_name, query):
        try:
            return query(self.raw_json)
        except KeyError as ex:
            raise Exception("Unable to access %s in plugin.json" % param_name) from ex

    @property
    def plugin_name(self):
        return self._query_raw_json_param("name", lambda js: js['name'])

    @property
    def package_name(self):
        return self._query_raw_json_param("package name", lambda js: js['source']['package'])

    @property
    def plugin_version(self):
        version = self._query_raw_json_param("plugin version", lambda js: js['version'])
        package_version_re = r'^\d+\.\d+(.\d+)?$'
        if not re.match(package_version_re, version):
            raise Exception(
                "Expected plugin version to be in format MAJOR.MINOR or MAJOR.MINOR.REVISION, found %s" % version)
        return version

    @property
    def install_requires(self):
        return self.raw_json.get('source', {}).get('install_requires', [])

    @property
    def package_data(self):
        return self.raw_json.get('source', {}).get('package_data', {})

    @property
    def packages(self):
        return self.raw_json.get('source', {}).get('packages', [])

    @property
    def pymodules(self):
        _pymodules = self.raw_json.get('source', {}).get('modules', [])
        return _pymodules or [self.package_name]

    @property
    def timeseries(self):
        return [m['timeseries'] for m in self.raw_json["metrics"] if "timeseries" in m]

    @property
    def timeseries_by_key(self):
        return {tm['key']: tm for tm in self.timeseries}

    @property
    def statetimeseries(self):
        return [m["statetimeseries"] for m in self.raw_json["metrics"] if "statetimeseries" in m]

    @property
    def statetimeseries_by_key(self):
        return {tm['key']: tm for tm in self.statetimeseries}

    @property
    def properties(self):
        return [p for p in self.raw_json.get("properties", [])]

    @property
    def properties_by_key(self):
        return {p['key']: p for p in self.properties}

    @property
    def configui_properties(self):
        return [p for p in self.raw_json.get("configUI", {}).get("properties", [])]

    @property
    def charts(self):
        return self.raw_json.get("ui", {}).get("charts", [])

    @property
    def key_charts(self):
        return self.raw_json.get("ui", {}).get("keycharts", self.raw_json.get("ui", {}).get("keyCharts", []))

    @property
    def key_metrics(self):
        return self.raw_json.get("ui", {}).get("keyMetrics", [])

    def __str__(self):
        return "Plugin name=%s, version=%s" % (self.plugin_name, self.plugin_version)


def _find_duplicates(items):
    cn = collections.Counter(items)
    return [item for item, count in cn.items() if count > 1]


def _validate_duplicates(items, where):
    dup = _find_duplicates(items)
    if dup:
        raise Exception("Validating plugin.json failed, duplicate keys found in %s : %s" % (where, dup))


def _validate_plugin_json(plugin_json_path):
    json_data = _read_plugin_json(plugin_json_path)
    meta = PluginMetadata(json_data)
    try:
        _validate_meta(meta)
    except jsonschema.exceptions.ValidationError as ex:
        log.error("Validating plugin.json failed, details: %s", ex)
        raise
    # semantic checks
    _validate_duplicates((tm["key"] for tm in meta.timeseries), "plugin timeseries")
    _validate_duplicates((prop["key"] for prop in meta.properties), "plugin properties")
    _validate_duplicates((prop["key"] for prop in meta.configui_properties), "plugin configUI properties")
    props = meta.properties_by_key
    for configui_prop in meta.configui_properties:
        if configui_prop["key"] not in props:
            raise Exception(
                "Validating plugin.json failed, configUI property %s refers to non existing property" % configui_prop[
                    "key"])

    # we are considering all types of timeseries
    all_timeseries_by_key = {**meta.timeseries_by_key, **meta.statetimeseries_by_key}
    for key_metric in meta.key_metrics:
        if key_metric["key"] not in all_timeseries_by_key:
            raise Exception(
                "Validating plugin.json failed, key metric %s refers to non existing metric" %
                (key_metric["key"])
            )
    for chart in itertools.chain(meta.key_charts, meta.charts):
        for chart_series in chart["series"]:
            if chart_series["key"] not in all_timeseries_by_key:
                raise Exception(
                    "Validating plugin.json failed, chart group:%s title:%s series:%s refers to non existing metric" %
                    (chart["group"], chart["title"], chart_series["key"])
                )
            chart_referenced_ts = all_timeseries_by_key[chart_series["key"]]
            timeseries_dimensions = set(chart_referenced_ts.get("dimensions", []))
            chart_dimensions = set(chart_series.get("dimensions", []))
            if not chart_dimensions.issubset(timeseries_dimensions):
                raise Exception(
                    "Validating plugin.json failed, chart group:%s title:%s series:%s refers to invalid dimensions,"
                    " chart dimensions: %s, timeseries_dimensions: %s" %
                    (chart["group"], chart["title"], chart_series["key"], list(chart_dimensions),
                     list(timeseries_dimensions))
                )
    return meta


def _read_plugin_json(plugin_json_path):
    log.info("Checking plugin metadata: %s", plugin_json_path)
    with open(plugin_json_path, 'r') as fp:
        try:
            json_data = json.load(fp)
        except Exception as ex:
            raise Exception("Error when parsing plugin.json file: %s" % plugin_json_path) from ex
    return json_data


def _validate_meta(meta):
    meta.plugin_name and meta.package_name and meta.plugin_version
    root_validator = jsonschema.Draft4Validator(schema.plugin_json_schema)
    log.info("Validating plugin.json against schema")
    root_validator.validate(meta.raw_json)
