#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""
 Main functions for obsinfo-validate.

 obsinfo strongly follows the hierarchy of StationXML files.

 Module contains both the class ValidateObsinfo and a main entry point for obsinfo-validate
"""
import os
from pathlib import Path
import glob
import re
import sys
from argparse import ArgumentParser
import logging
from logging.handlers import RotatingFileHandler

from obsinfo.misc.discoveryfiles import Datapath
import obsinfo
from obsinfo.obsMetadata.obsmetadata import ObsMetadata
from obsinfo.misc.const import EXIT_SUCCESS


class ValidateObsinfo():
    """
    Methods to validate each level of information files.

    Attributes:
        datapath (:class:`/.Datapath`): Store datapath list(s)
        verbose (bool): Prints progress messages
        remote (bool): Indicates file in command line argument is to be found
            using datapath
        debug (bool): Print more detailed messages.
    """
    # SHOULD THIS BE INIT (looks like Luis took from unittest)
    def setUp(self, verbose=True, remote=False, debug=False):
        """
        Set up status variables according to .obsinforc and command line arguments.

        Args:
            verbose (bool): Print several progress messages
            remote (bool): Find file in command line argument using datapath
            debug (bool): Print more detailed messages and enable traceback
                of exceptions
        Returns: self
        """
        dp = Datapath()
        self.datapath = dp
        self.datapath.infofiles_path = dp.datapath_list
        self.datapath.validate_path = Path(obsinfo.__file__).parent.joinpath('data', 'schemas')

        self.verbose = verbose
        self.remote = remote
        self.debug = debug

        return self

    def validate_single_file(self, info_file, filetype, file_format='yaml',
                             quiet=False, check_schema=False):
        """
        Validate a single obsinfo file.

        Args:
            info_file (str): info file to validate. No assumptions made about
                path.
            filetype (str): the information file type
            file_format (str): the information file format ('json' or 'yaml')
            check_schema (bool): validate schema file as well
        Returns:
            (bool): True if validated, False if not
        """
        if file_format not in ('json', 'yaml'):
            logging.error('file_format ("{file_format}") not in ("json", "yaml")')
            raise ValueError('file_format ("{file_format}") not in ("json", "yaml")')
        try:
            ret = ObsMetadata().validate(info_file,
                                         self.datapath.validate_path,
                                         self.remote,
                                         file_format,
                                         filetype,
                                         self.verbose,
                                         filetype + '.schema.json',
                                         quiet,
                                         check_schema=check_schema)
        except Exception as e:
            print(str(e))
            return False
        if self.verbose:
            self._print_passed(f'{filetype} test for: {info_file}', ret)
        return ret

    def validate_directory(self, dir, forced_filetype=None, drilldown=False,
                           continue_on_fail=False, check_schema=False):
        """
        Validate all information files in a directory.

        Args:
            dir (:class:`Path`): directory where files reside
            forced_filetype (str): file type.  If None, determine it.
            drilldown (bool): drill down through directories beneath the specified one
            continue_on_fail (bool): continue testing if a validation fails
            check_schema (bool): check schema on first use
        Returns:
            false if any file failed
        """
        suffixes = ['.yaml', '.yml']
        checked_schemas = []
        if not dir.is_dir():
            raise ValueError(f'"{dir}" is not a directory!')
        if drilldown is True:
            print(f'Validating files in and below directory {dir}')
            if forced_filetype is not None:
                raise ValueError('Cannot specify filetype when drilling down '
                                 'directories')
            files = dir.glob('**/*.*')
        else:
            print(f'Validating files in directory {dir}')
            files = dir.glob('*.*')
        print(f'Will skip files whose suffix is not in "{suffixes}"')
        self_verbose = self.verbose
        filetype = forced_filetype
        any_false = False
        self.verbose = False
        check_this_schema = False
        results = {'passed': 0, 'failed': 0, 'skipped': 0}
        for file in files:
            if file.suffix.lower() not in suffixes:
                print(f'Skipping {str(file.relative_to(dir))}')
                results['skipped'] += 1
                continue
            if forced_filetype==None:
                try:
                    filetype = ObsMetadata.get_information_file_type(str(file))
                except Exception:
                    print(f"Could not find a schema for {str(file.relative_to(dir))}, skipping...")
                    results['skipped'] += 1
                    continue
            print(f"Validating {str(file.relative_to(dir))} against "
                  f"{filetype}.schema.json ... ", end='', flush=True)
            if check_schema is True:
                if filetype in checked_schemas: 
                    check_this_schema = False  
                else:
                    check_this_schema = True   
                    checked_schemas.append(filetype)
            validated = self.validate_single_file(str(file), filetype,
                                                  check_schema=check_this_schema)
            if validated is not True:
                results['failed'] += 1
                print("FAILED")
                any_false = True
                if continue_on_fail is False:
                    print('Quitting on first error')
                    self.verbose = self_verbose
                    return False
            else:
                print("PASSED")
                results['passed'] += 1
        print(80 * '=')
        print('{:d} passed, {:d} failed, {:d} skipped'.format(
            results['passed'], results['failed'], results['skipped']))
        self.verbose = self_verbose
        if any_false is True:
            return False
        return True

    def validate_filters_in_directory(self, dir):
        """
        Validate all information files in filter directory.

        Args:
            dir (str): directory where filter files reside
        """
        if dir.is_dir():
            for file in dir.iterdir():
                self.validate_single_file(file, "filter")

    def validate_stages_in_directory(self, dir):
        """
        Validate all information files in stage directory as given by datapath.

        Args:
            1dir (str): directory where stage files reside
        """
        datalogger_dir_re = re.compile(".*/dataloggers")
        sensor_dir_re = re.compile(".*/sensors")
        exception_re = re.compile(".*/test-with")
        if re.match(datalogger_dir_re, str(dir)) or re.match(sensor_dir_re, str(dir)):
            for file in (Path(dir).joinpath("responses")).iterdir():
                if not file.is_dir() and re.match(exception_re, str(file)) is None:
                    self.validate_single_file(file, "stage")

    def validate_all_components(self):
        """
        Validate all information files in each components
        
        (sensor, preamplifier, datalogger) subdirectory as given by datapath
        """
        components_list = ("sensors", "preamplifiers", "dataloggers")
        for dir in self.datapath.infofiles_path:
            for comp in components_list:
                # includes sensors, preamplifiers and dataloggers
                files_in_validate_dir = Path(dir).joinpath(comp, "*.yaml")
                self.validate_files(files_in_validate_dir, comp)

    def validate_all_filters(self):
        """
        Validate all filter files in datapath/<component>/responses/filters/"
        """
        for dir in self.datapath.infofiles_path:
            # "*rs includes sensors, preamplifiers and dataloggers"
            files_in_validate_dir = Path(dir).joinpath("*rs", "responses",
                                                       "filters", "*.yaml")
            self.validate_files(files_in_validate_dir, "filter")

    def validate_all_stages(self):
        """
        Validate all stage files in datapath/<component>/responses/
        """
        for dir in self.datapath.infofiles_path:
            # "*rs includes sensors, preamplifiers and dataloggers"
            files_in_validate_dir = Path(dir).joinpath("*rs", "responses",
                                                       "*.yaml")
            self.validate_files(files_in_validate_dir, "stage")

    def validate_all_instrumentations(self):
        """
        Validate all instrumentation files in datapath/instrumentation/
        """
        for dir in self.datapath.infofiles_path:
            # includes sensors, preamplifiers and dataloggers
            files_in_validate_dir = Path(dir).joinpath(
                "instrumentation/*.yaml")
            self.validate_files(files_in_validate_dir, "instrumentation")

    def validate_all_networks(self):
        """
        Validate all network files in datapath/network/
        """
        for dir in self.datapath.infofiles_path:
            # includes sensors, preamplifiers and dataloggers
            files_in_validate_dir = Path(dir).joinpath("network/*.yaml")
            self.validate_files(files_in_validate_dir, "network")

    def validate_files(self, files, filetype):
        """
        Validate all files of a given type

        Args:
            files (:class:`Path`): Full paths of files (including wildcards)
            filetype (str): information file type
        """
        assert ObsMetadata.is_valid_type(filetype)

        filelist = glob.glob(str(files))
        for file in filelist:
            self.validate_single_file(file, filetype)

    @staticmethod
    def _print_passed(text, passed):
        if passed:
            print(f'{text}: PASSED')
        else:
            print(f'{text}: FAILED')


def main():
    """
    Entry point for obsinfo-validate.

     1) Sets up status variables from command line arguments.
     2) Validates file according to file type contained in name
     3) Manages all uncaught exceptions
    """
    args = retrieve_arguments()
    # logger = init_logging()

    val = ValidateObsinfo()
    val.setUp(verbose=args.verbose, remote=args.remote, debug=args.debug)

    filetype = None
    if args.schema:
        if ObsMetadata.is_valid_type(args.schema):
            filetype = args.schema
        else:
            raise ValueError(f'Unknown schema type: {args.schema}')

    if args.input.is_file():
        if filetype is None:
            filetype = ObsMetadata.get_information_file_type(str(args.input))
        if args.verbose:
            print(f'Validating {filetype} file')
        val.validate_single_file(str(args.input), filetype,
                                 check_schema=args.check_schema)
    elif args.input.is_dir():
        if args.schema and args.drilldown:
            raise ValueError("Cannot specify schema for a drill-down "
                             "directory validation")
        val.validate_directory(args.input, filetype, args.drilldown,
                               args.continue_on_fail, check_schema=args.check_schema)

#     try:
#         val.validate_single_file(input_filename, filetype)
#     except TypeError as e:
#         print("TypeError:" + str(e))
#         logger.error("TypeError: Illegal format: fields may be missing or "
#                      "with wrong format in input file, or there is a "
#                      "programming error")
#         if args.debug:
#             raise
#         sys.exit(EXIT_DATAERR)
#     except (KeyError, IndexError) as e:
#         print("Illegal value in dictionary key or list index:" + str(e))
#         logger.error("KeyError, IndexError: Illegal value in dictionary key "
#                      "or list index")
#         if args.debug:
#             raise
#         sys.exit(EXIT_SOFTWARE)
#     except ValueError as e:
#         print("ValueError:" + str(e))
#         logger.error("ValueError: An illegal value was detected")
#         if args.debug:
#             raise
#         sys.exit(EXIT_DATAERR)
#     except FileNotFoundError as e:
#         if args.debug:
#             raise
#         print("File not found:" + str(e))
#         logger.error(f"FileNotFoundError: {str(e)}")
#         sys.exit(EXIT_NOINPUT)
#     except JSONDecodeError as e:
#         print("JSONDecodeError:" + str(e))
#         logger.error("JSONDecodeError: File and/or subfiles have an illegal "
#                      "format. Probably indentation or missing "
#                      "quotes/parentheses/brackets")
#         if args.debug:
#             raise
#         sys.exit(EXIT_DATAERR)
#     except (IOError, OSError, LookupError) as e:
#         print("File could not be opened or read:" + str(e))
#         logger.error("IOError, OSError, LookupError: File could not be "
#                      "opened or read")
#         if args.debug:
#             raise
#         sys.exit(EXIT_UNAVAILABLE)
#     except AttributeError as e:
#         print("Attribute error: " + str(e))
#         logger.debug("AttributeError: Programming error: an object in code "
#                      "had a wrong attribute")
#         if args.debug:
#             raise
#         sys.exit(EXIT_SOFTWARE)
#     except:
#         print("General exception")
#         logger.debug("General exception:")
#         if args.debug:
#             raise
#         sys.exit(EXIT_FAILURE)
#
    sys.exit(EXIT_SUCCESS)


def retrieve_arguments():
    """
    Retrieve command line arguments.

    Returns:
        dictionary object with all status variables and information file name.
    """
    # Parse the arguments
    parser = ArgumentParser(
        prog="obsinfo-validate",
        description="Validates obsinfo information files against schema.")

    # flags
    parser.add_argument(
        "-q", "--quiet", action='store_true', default=False,
        help="Quiet operation. Don't print informative messages")
    parser.add_argument(
        "-s", "--schema",
        help="Force validation against the given schema file (sets --check_schema)")
    parser.add_argument(
        "-r", "--remote", action='store_true', default=False,
        help="Search for the input_filename in the DATAPATH repositories")
    parser.add_argument(
        "-d", "--debug", action='store_true', default=False,
        help="Print traceback for exceptions")
    parser.add_argument(
        "--drilldown", action='store_true', default=False,
        help="Drill down through all subdirectories (if a directory "
             "was specified)")
    parser.add_argument(
        "--continue_on_fail", action='store_true', default=False,
        help="Continue validating if a file fails (if a directory was specified)")
    parser.add_argument(
        "--check_schema", action='store_true', default=False,
        help="Check the schema before validating")
    # positional arguments
    parser.add_argument(
        "input", type=str,
        help="Information file or directory to be validated. If a directory, "
             "tests all files in the directory.  If 'DATAPATH', "
             "will test all files in the DATAPATH (sets --drilldown)")

    args = parser.parse_args()
    args.verbose = not args.quiet

    if args.input == 'DATAPATH':
        args.input = Datapath().datapath_list[0]
        args.drilldown = True
        print(f'Validating first DATAPATH dir')

    if args.schema is not None:
        args.check_schema = True
    
    args.input = Path(args.input)
    if not args.input.is_absolute():
        args.input = Path(os.getcwd()).joinpath(args.input).resolve()

    return args


def init_logging():
    """
    Create or open a rotating logging file and add it to ObsinfoConfiguration

    :returns: object of Logger class
    """
    logfile = Path.home().joinpath('.obsinfo', 'obsinfolog-validate')

    logger = logging.getLogger("obsinfo")

    logger.setLevel(logging.DEBUG)
    # add a rotating handler with 200K (approx) files and just two files
    handler = RotatingFileHandler(logfile, maxBytes=200000,
                                  backupCount=2)
    frmt = logging.Formatter(fmt='%(asctime)s %(levelname)-8s %(message)s',
                             datefmt='%Y-%m-%d %H:%M:%S')
    handler.setFormatter(frmt)
    logger.addHandler(handler)

    return logger


if __name__ == '__main__':
    main()
