import json
import logging

from app import app
from cachetools import lru_cache
from notifications import spawn_notification
from data import model
from data.registry_model import registry_model
from data.registry_model.datatypes import RepositoryReference
from data.database import UseThenDisconnect
from util.morecollections import AttrDict

logger = logging.getLogger(__name__)


class BuildJobLoadException(Exception):
  """ Exception raised if a build job could not be instantiated for some reason. """
  pass


class BuildJob(object):
  """ Represents a single in-progress build job. """
  def __init__(self, job_item):
    self.job_item = job_item

    try:
      self.job_details = json.loads(job_item.body)
      self.build_notifier = BuildJobNotifier(self.build_uuid)
    except ValueError:
      raise BuildJobLoadException(
        'Could not parse build queue item config with ID %s' % self.job_details['build_uuid']
      )

  @property
  def retries_remaining(self):
    return self.job_item.retries_remaining

  def has_retries_remaining(self):
    return self.job_item.retries_remaining > 0

  def send_notification(self, kind, error_message=None, image_id=None, manifest_digests=None):
    self.build_notifier.send_notification(kind, error_message, image_id, manifest_digests)

  @lru_cache(maxsize=1)
  def _load_repo_build(self):
    with UseThenDisconnect(app.config):
      try:
        return model.build.get_repository_build(self.build_uuid)
      except model.InvalidRepositoryBuildException:
        raise BuildJobLoadException(
            'Could not load repository build with ID %s' % self.build_uuid)

  @property
  def build_uuid(self):
    """ Returns the unique UUID for this build job. """
    return self.job_details['build_uuid']

  @property
  def namespace(self):
    """ Returns the namespace under which this build is running. """
    return self.repo_build.repository.namespace_user.username

  @property
  def repo_name(self):
    """ Returns the name of the repository under which this build is running. """
    return self.repo_build.repository.name

  @property
  def repo_build(self):
    return self._load_repo_build()

  def get_build_package_url(self, user_files):
    """ Returns the URL of the build package for this build, if any or empty string if none. """
    archive_url = self.build_config.get('archive_url', None)
    if archive_url:
      return archive_url

    if not self.repo_build.resource_key:
      return ''

    return user_files.get_file_url(self.repo_build.resource_key, '127.0.0.1', requires_cors=False)

  @property
  def pull_credentials(self):
    """ Returns the pull credentials for this job, or None if none. """
    return self.job_details.get('pull_credentials')

  @property
  def build_config(self):
    try:
      return json.loads(self.repo_build.job_config)
    except ValueError:
      raise BuildJobLoadException(
        'Could not parse repository build job config with ID %s' % self.job_details['build_uuid']
      )

  def determine_cached_tag(self, base_image_id=None, cache_comments=None):
    """ Returns the tag to pull to prime the cache or None if none. """
    cached_tag = self._determine_cached_tag_by_tag()
    logger.debug('Determined cached tag %s for %s: %s', cached_tag, base_image_id, cache_comments)
    return cached_tag

  def _determine_cached_tag_by_tag(self):
    """ Determines the cached tag by looking for one of the tags being built, and seeing if it
        exists in the repository. This is a fallback for when no comment information is available.
    """
    with UseThenDisconnect(app.config):
      tags = self.build_config.get('docker_tags', ['latest'])
      repository = RepositoryReference.for_repo_obj(self.repo_build.repository)
      matching_tag = registry_model.find_matching_tag(repository, tags)
      if matching_tag is not None:
        return matching_tag.name

      most_recent_tag = registry_model.get_most_recent_tag(repository)
      if most_recent_tag is not None:
        return most_recent_tag.name

      return None


class BuildJobNotifier(object):
  """ A class for sending notifications to a job that only relies on the build_uuid """

  def __init__(self, build_uuid):
    self.build_uuid = build_uuid

  @property
  def repo_build(self):
    return self._load_repo_build()

  @lru_cache(maxsize=1)
  def _load_repo_build(self):
    try:
      return model.build.get_repository_build(self.build_uuid)
    except model.InvalidRepositoryBuildException:
      raise BuildJobLoadException(
        'Could not load repository build with ID %s' % self.build_uuid)

  @property
  def build_config(self):
    try:
      return json.loads(self.repo_build.job_config)
    except ValueError:
      raise BuildJobLoadException(
        'Could not parse repository build job config with ID %s' % self.repo_build.uuid
      )

  def send_notification(self, kind, error_message=None, image_id=None, manifest_digests=None):
    with UseThenDisconnect(app.config):
      tags = self.build_config.get('docker_tags', ['latest'])
      trigger = self.repo_build.trigger

      # TODO(bison): This is weird hack.  Figure this out.
      if trigger is not None and trigger.id is not None:
        trigger_kind = trigger.service.name
      else:
        trigger_kind = None

      event_data = {
        'build_id': self.repo_build.uuid,
        'build_name': self.repo_build.display_name,
        'docker_tags': tags,
        'trigger_id': trigger.uuid if trigger is not None else None,
        'trigger_kind': trigger_kind,
        'trigger_metadata': self.build_config.get('trigger_metadata', {})
      }

      if image_id is not None:
        event_data['image_id'] = image_id

      if manifest_digests:
        event_data['manifest_digests'] = manifest_digests

      if error_message is not None:
        event_data['error_message'] = error_message

      # TODO(jzelinskie): remove when more endpoints have been converted to using
      # interfaces
      repo = AttrDict({
        'namespace_name': self.repo_build.repository.namespace_user.username,
        'name': self.repo_build.repository.name,
      })
      spawn_notification(repo, kind, event_data,
                         subpage='build/%s' % self.repo_build.uuid,
                         pathargs=['build', self.repo_build.uuid])