#!python
"""Validator for dlite datamodels.
"""
import sys
import argparse
import json
import re
from pathlib import Path


def parse(url, driver=None, options="mode=r", id=None):
    """Loads an instance from storage.

    Arguments:
        url: URL of storage to load from.
        driver: The name of storage plugin to use.
        options: Options passed to the storage plugin.
        id: ID of instance to load if the storage contains multiple instances.
            The default is to load all instances.

    Returns:
        List of new instances.
    """
    import dlite
    loc = url.split("#", 1)[0].split("?",1)[0]
    if driver is None:
        driver = Path(loc).suffix.lstrip('.').lower()
    

    # Validate all instances if `id` is None
    if id is None:
        try:
            uuids = dlite.Instance.get_uuids(driver, url, options=options)
        #except dlite.DLiteError:
        except dlite.DLiteUnsupportedError:
            pass
        else:
            insts = []
            for uuid in uuids:
                insts.extend(parse(url, driver=driver, options=options, id=uuid))
            return insts

    # Validate all instances if `id` is None
    if id is None:
        try:
            uuids = dlite.Instance.get_uuids(driver, url, options=options)
        #except dlite.DLiteError:
        except dlite.DLiteUnsupportedError:
            pass
        else:
            insts = []
            for uuid in uuids:
                insts.extend(parse(url, driver=driver, options=options, id=uuid))
            return insts

    match = re.match(r'^([a-zA-Z][a-zA-Z0-9+.-]*)://', url)
    if match and match.groups()[0].lower() != 'file':
        if options:
            url += f"?{options}"
        if id:
            url += f"#{id}"
        return [dlite.Instance.from_url(url)]
    else:
        path = Path(loc.split(":", 1)[1] if match else loc).resolve()

        # For json, check syntax with the json module since it has better
        # error reporting
        if driver == "json":
            with open(path, 'rt') as f:
                json.load(f)

        return [dlite.Instance.from_location(
            driver, path, options=options, id=id,
        )]


def check_dimensions(url, meta):
    """Check wheter all dimensions in metadata are referred to in shapes."""
    shape_dims = set()
    for prop in meta.properties['properties']:
        shape_dims.update(prop.shape)
    unused = set(meta.dimnames()).difference(shape_dims)
    if unused:
        print(f'{url}: unused dimensions: {", ".join(unused)}')
        sys.exit(1)


def main():
    """Datamodel validator"""
    parser = argparse.ArgumentParser(
        description='DLite datamodel validator.',
    )
    parser.add_argument(
        'url',
        help='URL to datamodel to validate.  Relative file paths are allowed.'
    )
    parser.add_argument(
        '--driver', '--plugin', '-d',
        help='Storage plugin to use when loading the instance.  If `url` '
        'is a file path, the default is to infer the driver from the file '
        'extension.',
    )
    parser.add_argument(
        '--options', '-o',
        help='Semicolon-separated list of options parsed on to the dlite '
        'storage plugin.  For instance, to validate an entity in a '
        '"mongodb" storage, you may want to provide '
        '`--options="database=DBNAME;collection=dlite_coll;user=myname"`.'
    )
    parser.add_argument(
        '--id', '-i',
        help='Id of instance to load if the storage contains multiple '
        'instances.'
    )
    parser.add_argument(
        '--storage-path', '-p', action='append', default=[],
        help='URL or path to storage path to look up metadata. '
        'Only needed when validating data instances, or datamodels '
        'referring to other datamodels as properties. '
        'This option can be given multiple times.' 
    )
    parser.add_argument(
        '--show', '-s', action='store_true',
        help='Print the json-serialisation of the instance to standard output.'
    )
    parser.add_argument(
        '--debug', '-D', action='store_true',
        help='Whether to print traceback on error.'
    )

    args = parser.parse_args()

    if not args.debug:
        # Turn off DLite warnings about behavior changes
        import __main__
        __main__.DLITE_BEHAVIOR = True

    # Delay importing dlite to after options are processed such that it is
    # possible to configure DLite by settings variables in __main__.
    import dlite

    for path in args.storage_path:
        dlite.storage_path.append(Path(path).resolve())

    try:
        insts = parse(args.url, args.driver, args.options, args.id)
    except Exception as exc:
        if args.debug:
            raise
        else:
            print(f'{args.url}: {exc.__class__.__name__}: {exc}')
            sys.exit(1)

    for inst in insts:
        if inst.is_meta:
            check_dimensions(args.url, inst)
            # Check that datamodels for properties that refer to
            # a specific datamodel can be loaded.
            for prop in inst.properties['properties']:
                if prop.type == 'ref':
                    if prop.ref == 'ref':
                        print(f'Property "{prop.name}" is of type "ref", the value '
                                'is ref and not a uri. Check that the property '
                                '"$ref" is given, the "$" is a strict requirement.')
                        sys.exit(1)
                    # Add the path of the original url to storage_path
                    # since it is likely that referenced data models are 
                    # in the same folder.
                    dlite.storage_path.append(Path(args.url).resolve().parent)
                    propmeta = dlite.get_instance(prop.ref)
                    print(f"Datamodel for property '{prop.name}' was loaded successfully, and "
                        " not further validated.")


    print(f'{args.url}: validated')

    if (args.show):
        for inst in insts:
            if len(insts) > 1:
                print("="*79)
            print(inst)


if __name__ == '__main__':
    main()
