# Classes to represent metadata for command line tools.
from pathlib import Path
from collections import OrderedDict
from abc import abstractmethod
import re
from ruamel.yaml import safe_load
from ...classes.metadata.metadata_base import MetadataBase
from ...classes.metadata.shared_properties import CodeRepository, Person, WebSite, Keyword, ApplicationSuite
from ...helpers.get_metadata_from_biotools import make_tool_metadata_kwargs_from_biotools
from ...classes.metadata.common_functions import _mk_hashes, CommonPropsMixin


class ToolMetadataBase(MetadataBase):
    """Factor stuff out to here."""

    @abstractmethod
    def _mk_identifier(self, **kwargs):
        pass

    @abstractmethod
    def _check_identifier(self, identifier):
        pass

    @property
    def identifier(self):
        return self._identifier

    @identifier.setter
    def identifier(self, identifier=None, **kwargs):
        if identifier:
            identifier = self._check_identifier(identifier)
        else:
            identifier = self._mk_identifier(**kwargs)
        self._identifier = identifier

    @property
    def keywords(self):
        return self._keywords

    @keywords.setter
    def keywords(self, keywords_list):
        if keywords_list:
            keywords = []
            for keyword in keywords_list:
                if isinstance(keyword, Keyword):
                    keywords.append(keyword)
                else:
                    if isinstance(keyword, dict):
                        keywords.append(Keyword(**keyword))
                    else:
                        keywords.append(Keyword(keyword))
        else:
            keywords = [Keyword()]  # Why did I do this instead of None? It's so I can
        self._keywords = keywords


class ToolMetadata(CommonPropsMixin, ToolMetadataBase):
    """Class to represent metadata for a 'stand alone' command line tool."""

    @staticmethod
    def _init_metadata():
        return OrderedDict([
            ('name', None),
            ('softwareVersion', None),
            ('version', '0.1.0'),  # Set to something low if not provided.
            ('identifier', None),
            ('description', None),
            ('codeRepository', None),
            ('license', None),
            ('WebSite', None),
            ('contactPoint', None),
            ('publication', None),
            ('keywords', None),
            ('alternateName', None),
            ('creator', None),
            ('programmingLanguage', None),
            ('datePublished', None),
            ('downloadURL', None),
            ('extra', None),
        ])


    def _check_identifier(self, identifier):
        if not identifier[:3] == "TL_":
            raise ValueError(f"Tool identifiers must start with 'TL_' you provided {identifier}")
        else:
            hex_pattern = r'[0-9a-f]{6}\.[0-9a-f]{2}$'
            match_obj = re.match(hex_pattern, identifier[3:])
            if not match_obj:
                raise ValueError(f"Tool identifier not formatted correctly: {identifier}")

        return identifier

    def _mk_identifier(self, start=0):
        if not (self.name and self.softwareVersion):
            raise ValueError(f"Name and softwareVersion must be provided to make an identifier.")
        name_hash, version_hash = _mk_hashes(self.name, self.softwareVersion)
        identifier = f"TL_{name_hash[start:start + 6]}.{version_hash[:2]}"
        return identifier

    # Class factory methods

    @classmethod
    def load_from_file(cls, file_path):
        file_path = Path(file_path)
        with file_path.open('r') as file:
            file_dict = safe_load(file)
        return cls(**file_dict)


    @classmethod
    def create_from_biotools(cls, biotools_id, softwareVersion, version='0.1.1'):
        kwargs = make_tool_metadata_kwargs_from_biotools(biotools_id)
        kwargs['softwareVersion'] = softwareVersion
        kwargs['version'] = version
        return cls(**kwargs)

    def mk_instance(self):
        raise NotImplementedError


class ParentToolMetadata(CommonPropsMixin, ToolMetadataBase):

    @staticmethod
    def _init_metadata():
        """
        Can set default values here.
        :return:
        """
        return OrderedDict([
            ('name', None),
            ('softwareVersion', None),
            ('version', '0.1.0'),
            ('identifier', None),
            ('featureList', None),
            ('description', None),
            ('codeRepository', None),
            ('license', None),
            ('WebSite', None),
            ('contactPoint', None),
            ('publication', None),
            ('keywords', None),
            ('alternateName', None),
            ('creator', None),
            ('programmingLanguage', None),
            ('datePublished', None),
            ('downloadURL', None),
            ('extra', None)
        ])

    def _check_identifier(self, identifier):
        if not identifier[:3] == "TL_":
            raise ValueError(f"Tool identifiers must start with 'TL_' you provided {identifier}")
        else:
            hex_pattern = r'[0-9a-f]{6}\.[0-9a-f]{2}$'
            match_obj = re.match(hex_pattern, identifier[3:])
            if not match_obj:
                raise ValueError(f"Tool identifier not formatted correctly: {identifier}")

        return identifier

    def _mk_identifier(self, start=0):
        if not (self.name and self.softwareVersion):
            raise ValueError(f"Name and softwareVersion must be provided to make an identifier.")
        name_hash, version_hash = _mk_hashes(self.name, self.softwareVersion)
        identifier = f"TL_{name_hash[start:start + 6]}.{version_hash[:2]}"
        return identifier

    def make_subtool_metadata(self, subtool_name, parent_metadata_path):
        if not self.featureList:
            raise ValueError(f"Cannot create subtool. featureList of {self.name} is not populated.")
        if subtool_name not in self.featureList:
            raise ValueError(f"{subtool_name} must be in the parent featureList")
        subtool_metadata = SubtoolMetadata(name=subtool_name, _parentMetadata=self, parentMetadata=parent_metadata_path)
        return subtool_metadata

    @classmethod
    def load_from_file(cls, file_path):
        file_path = Path(file_path)
        with file_path.open('r') as file:
            file_dict = safe_load(file)
        return cls(**file_dict)

    @classmethod
    def create_from_biotools(cls, biotools_id, softwareVersion, subtools, version='0.1.1'):
        kwargs = make_tool_metadata_kwargs_from_biotools(biotools_id)
        kwargs['featureList'] = list(subtools)
        kwargs['softwareVersion'] = softwareVersion
        kwargs['version'] = version
        return cls(**kwargs)


class SubtoolMetadata(ToolMetadataBase):

    @staticmethod
    def _init_metadata():
        return OrderedDict([
            ('applicationSuite', ApplicationSuite()),
            ('name', None),
            ('version', '0.1'),
            ('identifier', None),
            ('description', None),
            ('keywords', None),
            ('alternateName', None),
            ('parentMetadata', None),  # relative path to parentMetadata
            ('_parentMetadata', None),  # ParentMetadata instance. Can be loaded from parentMetadata or set directly.
            ('_primary_file_attrs', None),
        ])

    def __init__(self, file_path=None, **kwargs):
        # file_path only used if class is initiated using 'load_from_file'. file_path is path that SubtoolMetadata is loaded from.
        self._parentMetadata = kwargs.get('_parentMetadata')
        if not self._parentMetadata:
            self.parentMetadata = kwargs['parentMetadata']  # must have a path if it isn't set directly.
            self._load_parent_metadata(file_path)  # sets self._parentMetadata
        self._primary_file_attrs = []
        for k, value in kwargs.items():
            if value:
                self._primary_file_attrs.append(k)  # keep track of kwargs supplied.
        self._load_attrs_from_parent()
        super().__init__(**kwargs)


    @property
    def name(self):
        return self._name

    @name.setter
    def name(self, value):
        if not value in self._parentMetadata.featureList:
            raise ValueError(f"{value} is not in {self._parentMetadata.name} metadata featureList")
        self._name = value


    @classmethod
    def load_from_file(cls, file_path):
        file_path = Path(file_path)
        with file_path.open('r') as file:
            file_dict = safe_load(file)
        return cls(**file_dict, file_path=file_path)

    def _load_parent_metadata(self, subtool_metadata_file_path):
        # subtool_metadata_file_path of subtool metadata. ParentMetadata path should be relative to this.
        dir_name = subtool_metadata_file_path.parent
        full_path = dir_name / self.parentMetadata
        with full_path.resolve().open('r') as f:
            parent_metadata_dict = safe_load(f)
        self._parentMetadata = ParentToolMetadata(**parent_metadata_dict)

    def _load_attrs_from_parent(self):
        # initialize everything from parent. Will be overwritten anything supplied in kwargs.
        parent_meta = self._parentMetadata
        self.applicationSuite = ApplicationSuite(name=parent_meta.name, softwareVersion=parent_meta.softwareVersion,
                                                 identifier=parent_meta.identifier)
        # self.identifier = self._mk_identifier()
        self.keywords = parent_meta.keywords
        return

    def _mk_identifier(self):
        identifier_str, version_str = self._parentMetadata.identifier.split('.', 1)
        subtool_hash = _mk_hashes(self.name)
        identifier = f"{identifier_str}_{subtool_hash}.{version_str}"
        return identifier

    def _check_identifier(self, identifier):
        parent_identifier = self._parentMetadata.identifier
        if not identifier.startswith(parent_identifier[:9]):
            raise ValueError(f"Subtool identifier {identifier} does not properly correspond to parent identifier {parent_identifier}")
        if not identifier.endswith(parent_identifier[-3:]):  # should be '.xx'
            raise ValueError(
                f"Subtool identifier {identifier} does not properly correspond to parent identifier {parent_identifier}")
        hex_pattern = r'[0-9a-f]{6}_[0-9a-f]{2}\.[0-9a-f]{2}$'
        match_obj = re.match(hex_pattern, identifier[3:])
        if not match_obj:
            raise ValueError(f"Tool identifier not formatted correctly: {identifier}")

        return identifier

    @classmethod
    def initialize_from_parent(cls, parent_metadata, subtool_name):
        subtool_dict = {}
        subtool_dict['applicationSuite'] = {'name': parent_metadata.name,
                                            'softwareVersion': parent_metadata.softwareVersion,
                                            'identifier': parent_metadata.identifier}
        if not subtool_name in parent_metadata.featureList:
            raise ValueError(
                f"Cannot create subtool metadata. {subtool_name} is not in featureList of {parent_metadata.name}")
        subtool_dict['name'] = subtool_name
        return cls(**subtool_dict)

    def mk_completed_file(self):
        raise NotImplementedError

    def mk_instance(self):
        raise NotImplementedError

    def mk_file(self, file_path, keys=None):
        parent_path = Path(self.parentMetadata)
        subtool_path = file_path
        rel_path = parent_path.relative_to(subtool_path.parents[1])
        rel_path_str = '../' + str(rel_path)
        self.parentMetadata = rel_path_str
        super().mk_file(file_path)
