import argparse
import collections
import itertools
import json
import logging
import re
import types
import os
import sys
from typing import Set

import plugin_sdk.sdk_version
from urllib import parse
from functools import lru_cache
from os import path

import jsonschema
from jsonschema.exceptions import ValidationError

logging.basicConfig(stream=sys.stderr, format="%(message)s", level=logging.INFO)
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 is_remote(self)->bool:
        return re.match(r'^custom.remote', self.plugin_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)->Set[str]:
        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 _print_json_warings(meta:PluginMetadata):
    process_type_keys = {'processTypeNames', 'processTypes'}
    process_key = process_type_keys.intersection(meta.raw_json.keys())
    if len(process_key) > 0:
        log.warning(f'"{next(iter(process_key))}" is deprecated in custom extension JSON definition file. '
        'Please use "technologies" instead.')


def _check_duplicates(meta:PluginMetadata):
    duplicates = [ (meta.timeseries, "plugin timeseries") ,
                   (meta.properties, "plugin properties"),
                   (meta.configui_properties, "plugin configUI properties")]
    for duplicate in duplicates:
        _validate_duplicates((tm["key"] for tm in duplicate[0]), duplicate[1])


def _validate_plugin_json(plugin_json_path:str):
    json_data = _read_plugin_json(plugin_json_path)
    meta = PluginMetadata(json_data)
    try:
        _validate_meta(meta)
    except ValidationError as ve:
        if len(ve.path) > 0 and ve.path[0] == "technologies":
            raise Exception (f"The technology {ve.instance} is not valid.\n"
                            "Please refer to https://dynatrace.github.io/plugin-sdk/api/known_technologies.html") from ve
        else:
            raise

    # semantic checks
    _check_duplicates(meta)

    #UI properties
    props = meta.properties_by_key
    ui_props = {p["key"] for p in meta.configui_properties}
    if not ui_props.issubset(props):
        raise Exception(f"Validating plugin.json failed, configUI properties {ui_props.difference(props)} "
                        "refers to non existing property")

    # we are considering all types of timeseries
    all_timeseries_by_key = {**meta.timeseries_by_key, **meta.statetimeseries_by_key}
    key_metrics = {m["key"] for m in meta.key_metrics}
    if not key_metrics.issubset(all_timeseries_by_key):
        raise Exception(f"Validating plugin.json failed, key metrics {key_metrics.difference(all_timeseries_by_key)} "
                        " refers to non existing metric")

    for chart in itertools.chain(meta.key_charts, meta.charts):
        chart_metrics = {c["key"] for c in chart["series"] }
        if not chart_metrics.issubset(all_timeseries_by_key):
            raise Exception("Validating plugin.json failed, "
                            f"chart group: {chart['group']} "
                            f"title:{chart['title']} "
                            f"series:{chart_metrics.difference(all_timeseries_by_key)} "
                            "refers to non existing metric")

        #dimension check
        for chart_series in chart["series"]:
            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))
                )
    _print_json_warings(meta)
    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 json.JSONDecodeError as error:
            raise Exception("Unable to decode plugin.json file. %s", error) from error
        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:PluginMetadata):

    def file_resolver(*kwargs):
        p = parse.urlparse(kwargs[1])
        split = parse.urlsplit(kwargs[0])
        if p.path.endswith(".json"):
            t = path.dirname(kwargs[0])
            k = path.join(t, kwargs[1])
            return "file:" + k

        m = parse.urljoin(split.path, kwargs[1])
        if split.scheme and split.scheme != "file":
            m = os.path.join(split.scheme + ":", m)
        return "file:" + m

    def push_scope_path_only(self, *kwargs):
        split = parse.urlsplit(kwargs[0])
        if split.scheme and split.scheme != "file":
            result = os.path.join(split.scheme + ":", split.path)
        else:
            result = split.path
        self._scopes_stack.append(result)
        return

    def get_schema_by_plugin_name(meta):
        current_path = path.dirname(path.abspath(__file__))
        if meta.is_remote:
            return path.join(current_path, "json_schema", "schema_activegate.json")
        return path.join(current_path, "json_schema", "schema_local.json")

    schema_location = get_schema_by_plugin_name(meta)
    with open(schema_location, 'r') as json_schema:

        schema = json.load(json_schema)
        root_validator = jsonschema.Draft4Validator(schema)
        root_validator.resolver.push_scope = types.MethodType(push_scope_path_only, root_validator.resolver)
        root_validator.resolver._urljoin_cache = lru_cache(1024)(file_resolver)
        root_validator.resolver.push_scope(path.dirname(schema_location + "/"))
        log.info("Validating plugin.json against schema")
        root_validator.validate(meta.raw_json)


def _parse_args(cmd_args):
    parser = argparse.ArgumentParser()
    parser.add_argument("-p", "--plugin_dir", default=os.getcwd())
    parser.add_argument(
        "-v", "--verbose", help="increase verbosity", action="store_true"
    )

    __version__ = plugin_sdk.sdk_version.get_version()
    parser.add_argument(
        "--version",
        action="version",
        help="show program's version and exit",
        version=("{name} {version}".format(name=__package__, version=__version__)),
    )
    args = parser.parse_args(cmd_args)
    log.info("Arguments=%s", args)
    if args.verbose:
        log.setLevel(logging.DEBUG)
    return args


def main():
    parsed_args = None
    try:
        parsed_args = _parse_args(sys.argv[1:])
        _main(parsed_args)
    except Exception as ex:
        log.error("Error occured: %s", ex)
        if parsed_args and parsed_args.verbose:
            log.exception("Error details:")
        sys.exit(1)


def _main(args):
    log.info("Starting oneagent_verify_plugin")
    source_dir = os.path.abspath(args.plugin_dir)
    plugin_json_path = os.path.join(source_dir, "plugin.json")
    meta = _validate_plugin_json(plugin_json_path)
    plugin_name = meta.plugin_name
    log.info("Plugin %s is valid", plugin_name)


if __name__ == "__main__":
    main()
