import json
import os
import re
import sys
from abc import ABC, abstractmethod
from datetime import datetime


class RestClientGenerator(ABC):

    def __init__(self, rest_api_file, output_dir):
        f = open(rest_api_file, 'r')
        rest_api = json.load(f)
        f.close()

        self.rest_api = rest_api
        self.output_dir = output_dir
        self.version = rest_api['version']
        self.parameters = {}
        self.category = None
        self.subcategory = None
        self.action = None
        self.id1 = None
        self.id2 = None

        self.endpoints = {
            'users/{user}/filters/{filterId}/update': {'method_name': 'update_filter'},
            'ga4gh/reads/{study}/{file}': {'method_name': 'fetch_reads'},
            'analysis/clinical/{clinicalAnalysis}/interpretation/{interpretationId}/merge': {
                'method_name': 'merge_interpretation'},
            'analysis/clinical/{clinicalAnalysis}/interpretation/{interpretationId}/update': {
                'method_name': 'update_interpretation'},
            'analysis/clinical/{clinicalAnalysis}/interpretation/{interpretations}/delete': {
                'method_name': 'delete_interpretation'},
            'panels/import': {'method_name': 'import_panels'}
        }
        self.categories = {
            'Users': 'User',
            'Projects': 'Project',
            'Studies': 'Study',
            'Files': 'File',
            'Jobs': 'Job',
            'Samples': 'Sample',
            'Individuals': 'Individual',
            'Families': 'Family',
            'Cohorts': 'Cohort',
            'Disease Panels': 'DiseasePanel',
            'Analysis - Alignment': 'Alignment',
            'Analysis - Variant': 'Variant',
            'Analysis - Clinical': 'ClinicalAnalysis',
            'Operations - Variant Storage': 'VariantOperation',
            'Meta': 'Meta',
            'Cva': 'Cva',
            'GA4GH': 'GA4GH',
            'Admin': 'Admin'
        }

    @staticmethod
    def get_autogenerated_message():
        date_ = datetime.now().strftime("%Y-%m-%d")
        return [
            'WARNING: AUTOGENERATED CODE',
            '',
            'This code was generated by a tool.',
            'Autogenerated on: ' + date_,
            '',
            'Manual changes to this file may cause unexpected behavior in your application.',
            'Manual changes to this file will be overwritten if the code is regenerated.'
        ]

    @staticmethod
    def get_category_name(category):
        return category['name']

    @staticmethod
    def get_category_path(category):
        return category['path'].replace('/{apiVersion}/', '')

    @staticmethod
    def get_endpoint_path(endpoint):
        return endpoint['path'].replace('/{apiVersion}/', '')

    @staticmethod
    def get_endpoint_description(endpoint):
        return endpoint['description'] if endpoint['description'].endswith(".") else endpoint['description'] + "."

    @staticmethod
    def get_endpoint_response(endpoint):
        return endpoint['response']

    @staticmethod
    def get_endpoint_response_class(endpoint):
        return endpoint['responseClass']

    @staticmethod
    def get_endpoint_method(endpoint):
        return endpoint['method']

    def get_path_params(self, endpoint):
        return re.findall(r'{(.*?)}', self.get_endpoint_path(endpoint))

    def get_mandatory_query_params(self, endpoint):
        path_params = self.get_path_params(endpoint)
        params = []
        for parameter in endpoint['parameters']:
            if parameter['required'] and parameter['name'] not in path_params and parameter['name'] != 'body':
                params.append(parameter['name'])
        return params

    @staticmethod
    def has_optional_params(endpoint):
        return any([parameter['required'] is False for parameter in endpoint['parameters']])

    def is_required(self, parameter):
        return self.parameters[parameter]['required']

    def is_path_param(self, parameter):
        return self.parameters[parameter]['param'] == 'path'

    def get_parameter_type(self, parameter):
        return self.parameters[parameter]['type']

    def get_parameter_type_class(self, parameter):
        return self.parameters[parameter]['typeClass']

    @staticmethod
    def get_optional_parameters(endpoint):
        params = []
        for parameter in endpoint['parameters']:
            if parameter['required'] is False and parameter['name'] != 'body':
                params.append(parameter['name'])
        return params

    def get_parameter_description(self, parameter):
        if parameter not in self.parameters or 'description' not in self.parameters[parameter]:
            return ''
        if self.parameters[parameter]['description'].endswith("."):
            return self.parameters[parameter]['description']
        else:
            return self.parameters[parameter]['description'] + "."

    def get_parameter_allowed_values(self, parameter):
        return self.parameters[parameter]['allowedValues']

    def get_endpoint_category(self):
        return self.category

    def get_endpoint_subcategory(self):
        return self.subcategory

    def get_endpoint_action(self):
        return self.action

    def get_endpoint_id1(self):
        return self.id1

    def get_endpoint_id2(self):
        return self.id2

    @staticmethod
    def any_arg(items):
        return any([True if '{' in item and '}' in item else False for item in items])

    @staticmethod
    def all_arg(items):
        return all([True if '{' in item and '}' in item else False for item in items])

    def get_method_name(self, endpoint, category):
        if self.get_endpoint_path(endpoint) in self.endpoints:
            return self.endpoints[self.get_endpoint_path(endpoint)]['method_name']

        method_name = None
        subpath = self.get_endpoint_path(endpoint).replace(self.get_category_path(category) + '/', '')
        items = subpath.split('/')
        if len(items) == 1:
            method_name = items[0]
        elif len(items) == 2:
            # e.g. /{apiVersion}/ga4gh/reads/search
            if not self.any_arg(items):
                method_name = '_'.join(items[::-1])
            # e.g. /{apiVersion}/users/{user}/info
            elif self.any_arg([items[0]]) and not self.any_arg([items[1]]):
                method_name = items[1]
        elif len(items) == 3:
            # e.g. /{apiVersion}/analysis/variant/cohort/stats/run
            if not self.any_arg(items):
                method_name = '_'.join([items[2], items[0], items[1]])
            # e.g. /{apiVersion}/users/{user}/configs/filters
            elif self.any_arg([items[0]]) and not self.any_arg([items[1:]]):
                method_name = '_'.join([items[2], items[1]])
            # e.g. /{apiVersion}/studies/acl/{members}/update
            elif self.any_arg([items[1]]) and not self.any_arg([items[0], items[2]]):
                method_name = '_'.join([items[2], items[0]])
        elif len(items) == 4:
            # e.g. /{apiVersion}/operation/variant/sample/genotype/index
            if not self.any_arg(items):
                method_name = '_'.join([items[0], items[1], items[2], items[3]])
            # /{apiVersion}/analysis/clinical/{clinicalAnalysis}/interpretation/{interpretationId}/merge
            elif self.all_arg([items[0], items[2]]) and not self.any_arg([items[1], items[3]]):
                method_name = '_'.join([items[3], items[1]])
        elif len(items) == 5:
            # e.g. /{apiVersion}/files/{file}/annotationSets/{annotationSet}/annotations/update
            if self.all_arg([items[0], items[2]]) and not self.any_arg([items[1], items[3], items[4]]):
                method_name = '_'.join([items[4], items[1], items[3]])
            elif not self.any_arg(items):
                # e.g. /{apiVersion}/operation/variant/secondary/sample/index/configure
                method_name = '_'.join(
                    [items[4], items[0], items[1], items[2], items[3]])  # configure-variant-secondary-sample-index
        if not method_name:
            raise NotImplementedError('Case not implemented for PATH: "{}"'.format(self.get_endpoint_path(endpoint)))
        return re.sub(r'(?<!^)(?=[A-Z])', '_', method_name).lower()

    @abstractmethod
    def get_imports(self):
        pass

    @abstractmethod
    def get_class_definition(self, category):
        pass

    @abstractmethod
    def get_class_end(self):
        pass

    @abstractmethod
    def get_method_definition(self, category, endpoint):
        pass

    @abstractmethod
    def get_file_name(self, category):
        pass

    def create_rest_clients(self):
        for category in self.rest_api['categories']:
            text = []
            text.append(self.get_class_definition(category))

            for endpoint in category['endpoints']:
                # We update the dictionary of parameters of the endpoint
                self.parameters = {}
                for parameter in endpoint['parameters']:
                    self.parameters[parameter['name'] if parameter['name'] != 'body' else 'data'] = parameter

                # We extract the resources of the endpoint
                self.parse_resources(category, endpoint)

                text.append(self.get_method_definition(category, endpoint))

            text.append(self.get_class_end())
            # Now, we put in the first position the imports.
            text.insert(0, self.get_imports())

            # Choose the file name to be created
            file_name = self.get_file_name(category)
            sys.stderr.write('Creating ' + os.path.join(self.output_dir, file_name) + '...\n')
            with open(os.path.join(self.output_dir, file_name), 'w') as fhand:
                fhand.write('\n'.join(text))

    def parse_resources(self, category, endpoint):
        if endpoint['path'] == '/{apiVersion}/ga4gh/reads/{study}/{file}':
            self.category = 'ga4gh/reads'
            self.id1 = 'study'
            self.id2 = 'file'
            self.subcategory = ''
            self.action = ''
        else:
            subpath = endpoint['path'].replace('/{apiVersion}/', '')
            resources = re.findall(
                '([a-zA-Z0-9\/]+)(\/\{[a-zA-Z0-9]+\})?(\/[a-zA-Z0-9]+)?(\/\{[a-zA-Z0-9]+\})?(\/[a-zA-Z0-9\/]+)',
                subpath)
            if resources:
                [self.category, self.id1, self.subcategory, self.id2, self.action] = resources if type(
                    resources[0]) != tuple else list(resources[0])

            if self.id1.startswith("/"):
                self.id1 = self.id1[2:-1]
            if self.id2.startswith("/"):
                self.id2 = self.id2[2:-1]
            if self.subcategory.startswith("/"):
                self.subcategory = self.subcategory[1:]
            if self.action.startswith("/"):
                self.action = self.action[1:]

            if self.subcategory == '' and self.id1 == '' and '/' in self.category:
                self.subcategory = self.category.split('/', 1)[1]
                self.category = self.category.split('/', 1)[0]

            if self.subcategory == '' and self.id2 == '' and self.id1 != '' and '/' in self.category:
                # Move to subcategory and id2
                self.id2 = self.id1
                self.id1 = ''
                self.subcategory = self.category.split('/', 1)[1]
                self.category = self.category.split('/', 1)[0]
