#!/usr/bin/env python
# pylint: disable=invalid-name
# *****************************************************************************
# MLZ library of Tango servers
# Copyright (c) 2015-present by the authors, see LICENSE
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation; either version 2 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
# Module authors:
#   Georg Brandl <g.brandl@fz-juelich.de>
#   Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
#
# *****************************************************************************

"""Check current server configuration."""

import argparse
import logging
import sys
from pathlib import Path

# Add import path for inplace usage
sys.path.insert(0, str(Path(__file__).absolute().parents[1]))

# pylint: disable=wrong-import-position
import entangle.device
from entangle.lib.loggers import ColoredConsoleHandler
from entangle.lib.util import import_devcls
from entangle.server import get_global_config
from entangle.server.config import read_resfile


class Checker:
    def __init__(self, log, resdir, recursive):
        self.main_log = log
        self._errors = 0
        global_cfg = get_global_config()
        self._resdir = resdir if resdir is not None else global_cfg.resdir
        self._recursive = recursive
        # apply additional device directories
        entangle.device.__path__[:0] = global_cfg.devicedirs or []

    def _all_servers(self):
        if self._recursive:
            res_files = sorted(self._resdir.rglob('*.res'))
        else:
            res_files = sorted(self._resdir.glob('*.res'))
        return [(entry, entry.stem) for entry in res_files
                if 'disabled' not in entry.name]

    def error(self, msg, *args):
        self._errors += 1
        self.log.error(msg, *args)

    def check(self, servers):
        if not servers:
            servers = self._all_servers()

        seen_devs = {}

        for (filename, server) in servers:  # pylint: disable=too-many-nested-blocks
            filename = filename or self._resdir / (server + '.res')
            subdir = filename.parent
            self.main_log.debug('checking resfile: %s', filename)
            relpath = str(filename.relative_to(self._resdir))
            self.log = logging.getLogger(relpath)

            try:
                _desc, devices, _ = read_resfile(filename, tango_format=False)
            except Exception as e:
                self.error('could not read resources: %s', e)
                continue

            for device in devices:
                self.log = logging.getLogger(relpath + ' -> ' + device)
                self.log.debug('checking device')

                seen_dict = seen_devs.setdefault(subdir, {})
                if device in seen_dict:
                    self.error('duplicate device name, also in server "%s"',
                               seen_dict[device])
                seen_dict[device] = server

                props = devices[device]
                try:
                    clsname = props['type']
                    cls = import_devcls(str(clsname))
                except Exception as e:
                    self.error('cannot import device class: %s', e)
                    continue

                cls_properties = cls.properties
                cls_attributes = cls.attributes
                if 'wrapper' in props:
                    try:
                        wname = props['wrapper']
                        wcls = import_devcls(str(wname), False)
                    except Exception as e:
                        self.error('cannot import wrapper class: %s', e)
                        continue

                    cls_properties = cls_properties.copy()
                    cls_properties.update(getattr(wcls, 'properties', {}))
                    cls_attributes = cls_attributes.copy()
                    cls_attributes.update(getattr(wcls, 'attributes', {}))

                for (propname, propinfo) in cls_properties.items():
                    if propname not in props:
                        if propinfo.default is None:
                            self.error('mandatory property "%s" not in config',
                                       propname)
                        continue
                    value = props[propname]
                    try:
                        propinfo.validator(value)
                    except Exception as e:
                        self.error('value %r for property "%s" is not valid: '
                                   '%s', value, propname, e)

                for (attrname, attrinfo) in cls_attributes.items():
                    if 'default-' + attrname in props:
                        value = props['default-' + attrname]
                        try:
                            if attrinfo.dims == 0:
                                attrinfo.validator(value)
                            else:
                                for v in value:
                                    attrinfo.validator(v)
                        except Exception as e:
                            self.error('value %r for attribute "%s" default '
                                       'is not valid: %s', value, attrname, e)

                for propname in props:
                    if propname != 'type' and propname not in cls_properties:
                        if propname.startswith('default-'):
                            if propname[8:] not in cls_attributes:
                                self.error('property "%s" exists in resfile '
                                           'but no attribute "%s" exists in '
                                           'the device class', propname,
                                           propname[8:])
                            continue
                        self.error('property "%s" exists in resfile but not in'
                                   ' the device class', propname)

        if self._errors:
            self.main_log.error('%s file(s) checked, total %d error(s) '
                                'reported', len(servers), self._errors)
        else:
            self.main_log.info('%s file(s) checked, no errors reported',
                               len(servers))

        return 1 if self._errors else 0


def parse_args(argv):
    parser = argparse.ArgumentParser(description='Check entangle server '
                                     'configuration')
    parser.add_argument('-v', '--verbose', action='store_true',
                        help='Verbose logging', default=False)
    parser.add_argument('-r', '--resdir', action='store', type=Path,
                        help='Use a different resource file directory')
    parser.add_argument('-R', '--recursive', action='store_true',
                        help='Go recursively through all resource files in '
                        'subdirectories of resdir')
    parser.add_argument('server', type=str, nargs='*',
                        help='One or more server names, default all',
                        default=[])
    return parser.parse_args(argv[1:])


def main(argv):
    args = parse_args(argv)

    loglevel = logging.DEBUG if args.verbose else logging.INFO
    logging.basicConfig(level=loglevel)
    root_log = logging.getLogger()
    root_log.removeHandler(root_log.handlers[0])
    root_log.addHandler(ColoredConsoleHandler())
    log = logging.getLogger('checkconfig')
    servers = [(None, srv) for srv in args.server if 'disabled' not in srv]

    return Checker(log, args.resdir, args.recursive).check(servers)


if __name__ == '__main__':
    sys.exit(main(sys.argv))
