import logging
import io
import os.path
import zipfile

from github import Github, UnknownObjectException, GithubException
from tempfile import SpooledTemporaryFile

from app import app


user_files = app.config['USERFILES']
client = app.config['HTTPCLIENT']


logger = logging.getLogger(__name__)


ZIPBALL = 'application/zip'
CHUNK_SIZE = 512 * 1024


class BuildArchiveException(Exception):
  pass

class InvalidServiceException(Exception):
  pass

class TriggerActivationException(Exception):
  pass

class TriggerDeactivationException(Exception):
  pass

class ValidationRequestException(Exception):
  pass

class EmptyRepositoryException(Exception):
  pass


class BuildTrigger(object):
  def __init__(self):
    pass

  def list_build_sources(self, auth_token):
    """
    Take the auth information for the specific trigger type and load the
    list of build sources(repositories).
    """
    raise NotImplementedError

  def list_build_subdirs(self, auth_token, config):
    """
    Take the auth information and the specified config so far and list all of
    the possible subdirs containing dockerfiles.
    """
    raise NotImplementedError

  def handle_trigger_request(self, request, auth_token, config):
    """
    Transform the incoming request data into a set of actions. Returns a tuple
    of usefiles resource id, docker tags, build name, and resource subdir.
    """
    raise NotImplementedError

  def is_active(self, config):
    """
    Returns True if the current build trigger is active. Inactive means further
    setup is needed.
    """
    raise NotImplementedError

  def activate(self, trigger_uuid, standard_webhook_url, auth_token, config):
    """ 
    Activates the trigger for the service, with the given new configuration.
    Returns new configuration that should be stored if successful.
    """
    raise NotImplementedError

  def deactivate(self, auth_token, config):
    """
    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.
    """
    raise NotImplementedError

  def manual_start(self, auth_token, config):
    """
    Manually creates a repository build for this trigger.
    """
    raise NotImplementedError

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

  @classmethod
  def get_trigger_for_service(cls, service):
    for subc in cls.__subclasses__():
      if subc.service_name() == service:
        return subc()

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


def raise_unsupported():
  raise io.UnsupportedOperation


class GithubBuildTrigger(BuildTrigger):
  @staticmethod
  def _get_client(auth_token):
    return Github(auth_token, client_id=app.config['GITHUB_CLIENT_ID'],
                  client_secret=app.config['GITHUB_CLIENT_SECRET'])

  @classmethod
  def service_name(cls):
    return 'github'

  def is_active(self, config):
    return 'hook_id' in config

  def activate(self, trigger_uuid, standard_webhook_url, auth_token, config):
    new_build_source = config['build_source']
    gh_client = self._get_client(auth_token)

    try:
      to_add_webhook = gh_client.get_repo(new_build_source)
    except UnknownObjectException:
      msg = 'Unable to find GitHub repository for source: %s'
      raise TriggerActivationException(msg % new_build_source)

    webhook_config = {
      'url': standard_webhook_url,
      'content_type': 'json',
    }

    try:
      hook = to_add_webhook.create_hook('web', webhook_config)
      config['hook_id'] = hook.id
      config['master_branch'] = to_add_webhook.master_branch
    except GithubException:
      msg = 'Unable to create webhook on repository: %s'
      raise TriggerActivationException(msg % new_build_source)

    return config

  def deactivate(self, auth_token, config):
    gh_client = self._get_client(auth_token)

    try:
      repo = gh_client.get_repo(config['build_source'])
      to_delete = repo.get_hook(config['hook_id'])
      to_delete.delete()
    except GithubException:
      msg = 'Unable to remove hook: %s' % config['hook_id']
      raise TriggerDeactivationException(msg)

    config.pop('hook_id', None)

    return config


  def list_build_sources(self, auth_token):
    gh_client = self._get_client(auth_token)
    usr = gh_client.get_user()

    personal = {
      'personal': True,
      'repos': [repo.full_name for repo in usr.get_repos()],
      'info': {
        'name': usr.login,
        'avatar_url': usr.avatar_url,
      }
    }

    repos_by_org = [personal]

    for org in usr.get_orgs():
      repo_list = []
      for repo in org.get_repos(type='member'):
        repo_list.append(repo.full_name)

      repos_by_org.append({
        'personal': False,
        'repos': repo_list,
        'info': {
          'name': org.name,
          'avatar_url': org.avatar_url
        }
      })

    return repos_by_org

  def list_build_subdirs(self, auth_token, config):
    gh_client = self._get_client(auth_token)
    source = config['build_source']

    try:
      repo = gh_client.get_repo(source)
      default_commit = repo.get_branch(repo.master_branch).commit
      commit_tree = repo.get_git_tree(default_commit.sha, recursive=True)

      return [os.path.dirname(elem.path) for elem in commit_tree.tree
              if (elem.type == u'blob' and
                  os.path.basename(elem.path) == u'Dockerfile')]
    except GithubException:
      msg = 'Unable to list contents of repository: %s' % source
      raise EmptyRepositoryException(msg)

  @staticmethod
  def _prepare_build(config, repo, commit_sha, build_name, ref):
    # Prepare the download and upload URLs
    archive_link = repo.get_archive_link('zipball', commit_sha)
    download_archive = client.get(archive_link, stream=True)

    zipball_subdir = ''
    with SpooledTemporaryFile(CHUNK_SIZE) as zipball:
      for chunk in download_archive.iter_content(CHUNK_SIZE):
        zipball.write(chunk)

      # Pull out the name of the subdir that GitHub generated
      with zipfile.ZipFile(zipball) as archive:
        zipball_subdir = archive.namelist()[0]

      dockerfile_id = user_files.store_file(zipball, ZIPBALL)

    logger.debug('Successfully prepared job')

    # compute the tag(s)
    branch = ref.split('/')[-1]
    tags = {branch}
    if branch == repo.master_branch:
      tags.add('latest')
    logger.debug('Pushing to tags: %s' % tags)

    # compute the subdir
    repo_subdir = config['subdir']
    joined_subdir = os.path.join(zipball_subdir, repo_subdir)
    logger.debug('Final subdir: %s' % joined_subdir)

    return dockerfile_id, list(tags), build_name, joined_subdir

  @staticmethod
  def get_display_name(sha):
    return sha[0:7]

  def handle_trigger_request(self, request, auth_token, config):
    payload = request.get_json()

    if 'zen' in payload:
      raise ValidationRequestException()

    logger.debug('Payload %s', payload)
    ref = payload['ref']
    commit_sha = payload['head_commit']['id']
    short_sha = GithubBuildTrigger.get_display_name(commit_sha)

    gh_client = self._get_client(auth_token)

    repo_full_name = '%s/%s' % (payload['repository']['owner']['name'],
                                payload['repository']['name'])
    repo = gh_client.get_repo(repo_full_name)

    logger.debug('Github repo: %s', repo)

    return GithubBuildTrigger._prepare_build(config, repo, commit_sha,
                                             short_sha, ref)

  def manual_start(self, auth_token, config):
    source = config['build_source']
    subdir = config['subdir']

    gh_client = self._get_client(auth_token)
    repo = gh_client.get_repo(source)
    master = repo.get_branch(repo.master_branch)
    master_sha = master.commit.sha
    short_sha = GithubBuildTrigger.get_display_name(master_sha)
    ref = 'refs/heads/%s' % repo.master_branch

    return self._prepare_build(config, repo, master_sha, short_sha, ref)