import os
from abc import ABCMeta, abstractmethod
from jsonschema import validate
from six import add_metaclass

from endpoints.building import PreparedBuild
from data import model
from buildtrigger.triggerutil import get_trigger_config, InvalidServiceException

NAMESPACES_SCHEMA = {
  'type': 'array',
  'items': {
    'type': 'object',
    'properties': {
      'personal': {
        'type': 'boolean',
        'description': 'True if the namespace is the user\'s personal namespace',
      },
      'score': {
        'type': 'number',
        'description': 'Score of the relevance of the namespace',
      },
      'avatar_url': {
        'type': 'string',
        'description': 'URL of the avatar for this namespace',
      },
      'url': {
        'type': 'string',
        'description': 'URL of the website to view the namespace',
      },
      'id': {
        'type': 'string',
        'description': 'Trigger-internal ID of the namespace',
      },
      'title': {
        'type': 'string',
        'description': 'Human-readable title of the namespace',
      },
    },
    'required': ['personal', 'score', 'avatar_url', 'url', 'id', 'title'],
  },
}

BUILD_SOURCES_SCHEMA = {
  'type': 'array',
  'items': {
    'type': 'object',
    'properties': {
      'name': {
        'type': 'string',
        'description': 'The name of the repository, without its namespace',
      },
      'full_name': {
        'type': 'string',
        'description': 'The name of the repository, with its namespace',
      },
      'description': {
        'type': 'string',
        'description': 'The description of the repository. May be an empty string',
      },
      'last_updated': {
        'type': 'number',
        'description': 'The date/time when the repository was last updated, since epoch in UTC',
      },
      'url': {
        'type': 'string',
        'description': 'The URL at which to view the repository in the browser',
      },
      'has_admin_permissions': {
        'type': 'boolean',
        'description': 'True if the current user has admin permissions on the repository',
      },
      'private': {
        'type': 'boolean',
        'description': 'True if the repository is private',
      },
    },
    'required': ['name', 'full_name', 'description', 'last_updated', 'url',
                 'has_admin_permissions', 'private'],
  },
}

METADATA_SCHEMA = {
  'type': 'object',
  'properties': {
    'commit': {
      'type': 'string',
      'description': 'first 7 characters of the SHA-1 identifier for a git commit',
      'pattern': '^([A-Fa-f0-9]{7,})$',
    },
    'git_url': {
      'type': 'string',
      'description': 'The GIT url to use for the checkout',
    },
    'ref': {
      'type': 'string',
      'description': 'git reference for a git commit',
      'pattern': r'^refs\/(heads|tags|remotes)\/(.+)$',
    },
    'default_branch': {
      'type': 'string',
      'description': 'default branch of the git repository',
    },
    'commit_info': {
      'type': 'object',
      'description': 'metadata about a git commit',
      'properties': {
        'url': {
          'type': 'string',
          'description': 'URL to view a git commit',
        },
        'message': {
          'type': 'string',
          'description': 'git commit message',
        },
        'date': {
          'type': 'string',
          'description': 'timestamp for a git commit'
        },
        'author': {
          'type': 'object',
          'description': 'metadata about the author of a git commit',
          'properties': {
            'username': {
              'type': 'string',
              'description': 'username of the author',
            },
            'url': {
              'type': 'string',
              'description': 'URL to view the profile of the author',
            },
            'avatar_url': {
              'type': 'string',
              'description': 'URL to view the avatar of the author',
            },
          },
          'required': ['username'],
        },
        'committer': {
          'type': 'object',
          'description': 'metadata about the committer of a git commit',
          'properties': {
            'username': {
              'type': 'string',
              'description': 'username of the committer',
            },
            'url': {
              'type': 'string',
              'description': 'URL to view the profile of the committer',
            },
            'avatar_url': {
              'type': 'string',
              'description': 'URL to view the avatar of the committer',
            },
          },
          'required': ['username'],
        },
      },
      'required': ['url', 'message', 'date'],
    },
  },
  'required': ['commit', 'git_url'],
}


@add_metaclass(ABCMeta)
class BuildTriggerHandler(object):
  def __init__(self, trigger, override_config=None):
    self.trigger = trigger
    self.config = override_config or get_trigger_config(trigger)

  @property
  def auth_token(self):
    """ Returns the auth token for the trigger. """
    return self.trigger.auth_token

  @abstractmethod
  def load_dockerfile_contents(self):
    """
    Loads the Dockerfile found for the trigger's config and returns them or None if none could
    be found/loaded.
    """
    pass

  @abstractmethod
  def list_build_source_namespaces(self):
    """
    Take the auth information for the specific trigger type and load the
    list of namespaces that can contain build sources.
    """
    pass

  @abstractmethod
  def list_build_sources_for_namespace(self, namespace):
    """
    Take the auth information for the specific trigger type and load the
    list of repositories under the given namespace.
    """
    pass

  @abstractmethod
  def list_build_subdirs(self):
    """
    Take the auth information and the specified config so far and list all of
    the possible subdirs containing dockerfiles.
    """
    pass

  @abstractmethod
  def handle_trigger_request(self, request):
    """
    Transform the incoming request data into a set of actions. Returns a PreparedBuild.
    """
    pass

  @abstractmethod
  def is_active(self):
    """
    Returns True if the current build trigger is active. Inactive means further
    setup is needed.
    """
    pass

  @abstractmethod
  def activate(self, standard_webhook_url):
    """
    Activates the trigger for the service, with the given new configuration.
    Returns new public and private config that should be stored if successful.
    """
    pass

  @abstractmethod
  def deactivate(self):
    """
    Deactivates the trigger for the service, removing any hooks installed in
    the remote service. Returns the new config that should be stored if this
    trigger is going to be re-activated.
    """
    pass

  @abstractmethod
  def manual_start(self, run_parameters=None):
    """
    Manually creates a repository build for this trigger. Returns a PreparedBuild.
    """
    pass

  @abstractmethod
  def list_field_values(self, field_name, limit=None):
    """
    Lists all values for the given custom trigger field. For example, a trigger might have a
    field named "branches", and this method would return all branches.
    """
    pass

  @abstractmethod
  def get_repository_url(self):
    """ Returns the URL of the current trigger's repository. Note that this operation
        can be called in a loop, so it should be as fast as possible. """
    pass

  @classmethod
  def filename_is_dockerfile(cls, file_name):
    """ Returns whether the file is named Dockerfile or follows the convention <name>.Dockerfile"""
    return file_name.endswith(".Dockerfile") or u"Dockerfile" == file_name

  @classmethod
  def service_name(cls):
    """
    Particular service implemented by subclasses.
    """
    raise NotImplementedError

  @classmethod
  def get_handler(cls, trigger, override_config=None):
    for subc in cls.__subclasses__():
      if subc.service_name() == trigger.service.name:
        return subc(trigger, override_config)

    raise InvalidServiceException('Unable to find service: %s' % trigger.service.name)

  def put_config_key(self, key, value):
    """ Updates a config key in the trigger, saving it to the DB. """
    self.config[key] = value
    model.build.update_build_trigger(self.trigger, self.config)

  def set_auth_token(self, auth_token):
    """ Sets the auth token for the trigger, saving it to the DB. """
    model.build.update_build_trigger(self.trigger, self.config, auth_token=auth_token)

  def get_dockerfile_path(self):
    """ Returns the normalized path to the Dockerfile found in the subdirectory
        in the config. """
    dockerfile_path = self.config.get('dockerfile_path') or 'Dockerfile'
    if dockerfile_path[0] == '/':
      dockerfile_path = dockerfile_path[1:]
    return dockerfile_path

  def prepare_build(self, metadata, is_manual=False):
    # Ensure that the metadata meets the scheme.
    validate(metadata, METADATA_SCHEMA)

    config = self.config
    ref = metadata.get('ref', None)
    commit_sha = metadata['commit']
    default_branch = metadata.get('default_branch', None)
    prepared = PreparedBuild(self.trigger)
    prepared.name_from_sha(commit_sha)
    # TODO: Charlie Tuesday, March 28, 2017 come back and clean up subdirectory.
    prepared.subdirectory = config.get('dockerfile_path', None)
    prepared.context = config.get('context', None)
    prepared.is_manual = is_manual
    prepared.metadata = metadata

    if ref is not None:
      prepared.tags_from_ref(ref, default_branch)
    else:
      prepared.tags = [commit_sha[:7]]

    return prepared

  @classmethod
  def build_sources_response(cls, sources):
    validate(sources, BUILD_SOURCES_SCHEMA)
    return sources

  @classmethod
  def build_namespaces_response(cls, namespaces_dict):
    namespaces = list(namespaces_dict.values())
    validate(namespaces, NAMESPACES_SCHEMA)
    return namespaces

  @classmethod
  def get_parent_directory_mappings(cls, dockerfile_path, current_paths=None):
    """ Returns a map of dockerfile_paths to it's possible contexts. """
    if dockerfile_path == "":
      return {}

    if dockerfile_path[0] != os.path.sep:
      dockerfile_path = os.path.sep + dockerfile_path

    dockerfile_path = os.path.normpath(dockerfile_path)
    all_paths = set()
    path, _ = os.path.split(dockerfile_path)
    if path == "":
      path = os.path.sep

    all_paths.add(path)
    for i in range(1, len(path.split(os.path.sep))):
      path, _ = os.path.split(path)
      all_paths.add(path)

    if current_paths:
      return dict({dockerfile_path: list(all_paths)}, **current_paths)

    return {dockerfile_path: list(all_paths)}