"""
File Purpose: MetadataHaver and UuidHaver
"""
import os
import uuid

from ..errors import FileContentsMissingError
from ..tools import (
    alias, UNSET, ATTR_UNSET,
    datetime_now, read_python_params_file,
)

class MetadataHaver():
    '''class which tracks "metadata" in attrs implied by cls_metadata_attrs.
    E.g., cls_metadata_attrs == {'dirname', 'datetime_run'}.

    To add to cls_metadata_attrs, subclass should use:
        _new_cls_metadata_attrs = {...}

    "metadata" probably refers to "metadata about a simulation run / other data source"
    '''
    cls_metdata_attrs = set()

    def __init_subclass__(cls, **kw_super):
        '''called when subclassing MetadataHaver; sets cls_metadata_attrs,
        from parent's cls_metadata_attrs plus any _new_cls_metadata_attrs provided in cls.
        '''
        super().__init_subclass__(**kw_super)

        inherited = set()
        for parent in cls.__bases__:
            if hasattr(parent, 'cls_metdata_attrs'):
                inherited.update(parent.cls_metdata_attrs)
        new = getattr(cls, '_new_cls_metadata_attrs', set())
        cls.cls_metdata_attrs = inherited.union(new)

    metadata_attrs = alias('cls_metdata_attrs',
        doc='''alias to cls_metadata_attrs; set of all attrs which tell metadata,
        about this data source (simulation run / observation / other data source).
        E.g. metadata_attrs={'dirname', 'datetime_run'}.''')

    @property
    def metadata_all(self):
        '''dict of metadata attributes and their values (value=ATTR_UNSET if missing).
        See also: self.metadata.
        '''
        return {attr: getattr(self, attr, ATTR_UNSET) for attr in self.metadata_attrs}
        # for now, if getattr(self, attr, ATTR_UNSET) crashes, this will crash.
        # eventually, might catch and log crash somewhere instead of killing the whole thing.

    @property
    def metadata(self):
        '''dict of metadata attributes and their values, excluding missing and ATTR_UNSET.
        See also: self.metadata_all.
        '''
        return {attr: v for attr, v in self.metadata_all.items() if v is not ATTR_UNSET}


### --------------------- uuidHaver --------------------- ###

def pc_uuid_here(dir, *, generate=None, default=UNSET, return_meta=False):
    '''return the PlasmaCalcs uuid associated with this directory.

    The uuid is intended to be a universally unique identifier;
        it is highly unlikely that any other uuid will ever match this one.
        This is especially useful for the .register() method and the registry.

    The uuid will be stored in a file named _pc_uuid.txt inside dir, which contains:
        # comment lines starting with '#' explaining the purpose of the file
        # assignment lines below might also have # comments at end of line
        uuid = "uuidstringwithouthyphens"
        generated_at = "YYYY-MM-DDTHH:MM:SS.ssssss+HH:MM"  # when this uuid was generated
        # the file might also contain other assigments but likely will not;
        # other assignments will be ignored.

    dir: str
        path to directory.
    generate: None, True, or False
        whether to generate a new uuid (and create corresponding _pc_uuid.txt file)
        None --> as needed; only if _pc_uuid.txt does not already exist.
        True --> generate new uuid; crash if _pc_uuid.txt already exists.
        False --> return existing uuid; crash (or return default) if _pc_uuid.txt does not exist.
    default: UNSET or any object
        if not UNSET, return `default` if _pc_uuid.txt file does not exist and generate is False.
        Only used if generate is False.
    return_meta: bool
        whether to instead return a dict containing all info from _pc_uuid.txt,
        including the metadata. If True, dict contains 'uuid' and all other keys from file.
        (Values evaluated via ast.literal_eval if possible, else kept as string.)

    When sharing runs across machines, keep the _pc_uuid.txt file,
        to allow PlasmaCalcs to identify it as "the same run" in multiple places.
    When copy-pasting a run as a starting point but wanting PlasmaCalcs
        to treat the new copied run as a different run, delete its _pc_uuid.txt file.
    '''
    file = os.path.join(dir, '_pc_uuid.txt')
    exists = os.path.isfile(file)
    if generate is not None:
        if generate and exists:
            raise FileExistsError(f'generate=True but _pc_uuid.txt file already exists: {file!r}')
        if not generate and not exists:
            if default is not UNSET:
                return default
            raise FileNotFoundError(f'generate=False but _pc_uuid.txt file does not exist: {file!r}')
    if not exists:
        # generate new uuid and write to file
        new_uuid = uuid.uuid4().hex
        generated_at = datetime_now(tz='utc')  # as isoformat string in UTC timezone
        lines = [
            '# PlasmaCalcs-generated identifier for directory contents.',
            '# Delete this file to tell PlasmaCalcs registry to treat this as a new run.',
            f'uuid="{new_uuid}"',
            f'generated_at="{generated_at}"',
        ]
        with open(file, 'w') as f:
            f.write('\n'.join(lines) + '\n')
    # read file, even if just generated it, to ensure consistent behavior.
    params = read_python_params_file(file, eval=True)
    if 'uuid' not in params:
        raise FileContentsMissingError(f"'uuid' missing from _pc_uuid.txt file: {file!r}")
    if return_meta:
        return params
    else:
        return params['uuid']

class UuidHaver(MetadataHaver):
    '''class whose instances have a uuid, gettable via self.get_pc_uuid().
    This is a universally unique identifier for this data source.
    Different instances with the same data source should have the same uuid.

    One simple way to guarantee this behavior is to generate a _pc_uuid.txt file,
    and save it within the data source directory. See pc_uuid_here() for details.
    (However, this class does not assume _pc_uuid.txt exists;
        subclass can choose to implement get_pc_uuid() in a different way if desired.)

    self.pc_uuid should be ATTR_UNSET if it was never generated, else the uuid string.
    (This should be equivalent to self.get_pc_uuid(generate=False, default=ATTR_UNSET)

    To summarize, this class has:
        pc_uuid_here(): get/create uuid for a given directory
        get_pc_uuid(): get/create uuid for this instance's data source. Subclass must implement.
        pc_uuid: uuid if it was ever generated, else ATTR_UNSET.
    '''
    _new_cls_metadata_attrs = {'pc_uuid'}

    pc_uuid_here = staticmethod(pc_uuid_here)

    def get_pc_uuid(self, *, generate=None, default=UNSET):
        '''return the PlasmaCalcs uuid associated with self's data source.

        [Not implemented here; subclass should implement!]

        generate: None, True, or False
            whether to generate a new uuid.
            None --> generate if needed, do not generate if it already exists.
            True --> generate new uuid; crash if it already exists.
            False --> return existing uuid; crash (or return default) if it does not exist.
        default: UNSET or any object
            if not UNSET, return `default` if uuid does not exist and generate is False.
            Only used if generate is False.
        '''
        raise NotImplementedError(f'{type(self).__name__}.get_pc_uuid()')

    @property
    def pc_uuid(self):
        '''PlasmaCalcs uuid (universally unique identifier) associated with self.dirname.
        Returns value of existing uuid if possible, else ATTR_UNSET.

        See also: self.get_pc_uuid(), which will generate the uuid as needed.
        Equivalent to self.get_pc_uuid(generate=False, default=ATTR_UNSET).
        '''
        return self.get_pc_uuid(generate=False, default=ATTR_UNSET)
