import json

from abc import ABCMeta, abstractmethod
from collections import namedtuple

from six import add_metaclass

from data import model
from image.docker.v1 import DockerV1Metadata


class DerivedImage(namedtuple('DerivedImage', ['ref', 'blob', 'internal_source_image_db_id'])):
  """
  DerivedImage represents a user-facing alias for an image which was derived from another image.
  """

class RepositoryReference(namedtuple('RepositoryReference', ['id', 'name', 'namespace_name'])):
  """
  RepositoryReference represents a reference to a Repository, without its full metadata.
  """

class ImageWithBlob(namedtuple('Image', ['image_id', 'blob', 'compat_metadata', 'repository',
                                         'internal_db_id', 'v1_metadata'])):
  """
  ImageWithBlob represents a user-facing alias for referencing an image, along with its blob.
  """

class Blob(namedtuple('Blob', ['uuid', 'size', 'uncompressed_size', 'uploading', 'locations'])):
  """
  Blob represents an opaque binary blob saved to the storage system.
  """

class TorrentInfo(namedtuple('TorrentInfo', ['piece_length', 'pieces'])):
  """
  TorrentInfo represents the torrent piece information associated with a blob.
  """


@add_metaclass(ABCMeta)
class VerbsDataInterface(object):
  """
  Interface that represents all data store interactions required by the registry's custom HTTP
  verbs.
  """
  @abstractmethod
  def repository_is_public(self, namespace_name, repo_name):
    """
    Returns a boolean for whether the repository with the given name and namespace is public.
    """
    pass

  @abstractmethod
  def get_manifest_layers_with_blobs(self, repo_image):
    """
    Returns the full set of manifest layers and their associated blobs starting at the given
    repository image and working upwards to the root image.
    """
    pass

  @abstractmethod
  def get_blob_path(self, blob):
    """
    Returns the storage path for the given blob.
    """
    pass

  @abstractmethod
  def get_derived_image_signature(self, derived_image, signer_name):
    """
    Returns the signature associated with the derived image and a specific signer or None if none.
    """
    pass

  @abstractmethod
  def set_derived_image_signature(self, derived_image, signer_name, signature):
    """
    Sets the calculated signature for the given derived image and signer to that specified.
    """
    pass

  @abstractmethod
  def delete_derived_image(self, derived_image):
    """
    Deletes a derived image and all of its storage.
    """
    pass

  @abstractmethod
  def set_blob_size(self, blob, size):
    """
    Sets the size field on a blob to the value specified.
    """
    pass

  @abstractmethod
  def get_repo_blob_by_digest(self, namespace_name, repo_name, digest):
    """
    Returns the blob with the given digest under the matching repository or None if none.
    """
    pass

  @abstractmethod
  def get_torrent_info(self, blob):
    """
    Returns the torrent information associated with the given blob or None if none.
    """
    pass

  @abstractmethod
  def set_torrent_info(self, blob, piece_length, pieces):
    """
    Sets the torrent infomation associated with the given blob to that specified.
    """
    pass

  @abstractmethod
  def lookup_derived_image(self, repo_image, verb, varying_metadata=None):
    """
    Looks up the derived image for the given repository image, verb and optional varying metadata
    and returns it or None if none.
    """
    pass

  @abstractmethod
  def lookup_or_create_derived_image(self, repo_image, verb, location, varying_metadata=None):
    """
    Looks up the derived image for the given repository image, verb and optional varying metadata
    and returns it. If none exists, a new derived image is created.
    """
    pass

  @abstractmethod
  def get_tag_image(self, namespace_name, repo_name, tag_name):
    """
    Returns the image associated with the live tag with the given name under the matching repository
    or None if none.
    """
    pass


class PreOCIModel(VerbsDataInterface):
  """
  PreOCIModel implements the data model for the registry's custom HTTP verbs using a database schema
  before it was changed to support the OCI specification.
  """

  def repository_is_public(self, namespace_name, repo_name):
    return model.repository.repository_is_public(namespace_name, repo_name)

  def get_manifest_layers_with_blobs(self, repo_image):
    repo_image_record = model.image.get_image_by_id(repo_image.repository.namespace_name,
                                                    repo_image.repository.name,
                                                    repo_image.image_id)

    parents = model.image.get_parent_images_with_placements(repo_image.repository.namespace_name,
                                                            repo_image.repository.name,
                                                            repo_image_record)

    yield repo_image

    for parent in parents:
      metadata = {}
      try:
        metadata = json.loads(parent.v1_json_metadata)
      except ValueError:
        pass

      yield ImageWithBlob(
        image_id=parent.docker_image_id,
        blob=_blob(parent.storage),
        repository=repo_image.repository,
        compat_metadata=metadata,
        v1_metadata=_docker_v1_metadata(repo_image.repository.namespace_name,
                                        repo_image.repository.name, parent),
        internal_db_id=parent.id,
      )

  def get_derived_image_signature(self, derived_image, signer_name):
    storage = model.storage.get_storage_by_uuid(derived_image.blob.uuid)
    signature_entry = model.storage.lookup_storage_signature(storage, signer_name)
    if signature_entry is None:
      return None

    return signature_entry.signature

  def set_derived_image_signature(self, derived_image, signer_name, signature):
    storage = model.storage.get_storage_by_uuid(derived_image.blob.uuid)
    signature_entry = model.storage.find_or_create_storage_signature(storage, signer_name)
    signature_entry.signature = signature
    signature_entry.uploading = False
    signature_entry.save()

  def delete_derived_image(self, derived_image):
    model.image.delete_derived_storage_by_uuid(derived_image.blob.uuid)

  def set_blob_size(self, blob, size):
    storage_entry = model.storage.get_storage_by_uuid(blob.uuid)
    storage_entry.image_size = size
    storage_entry.uploading = False
    storage_entry.save()

  def get_blob_path(self, blob):
    blob_record = model.storage.get_storage_by_uuid(blob.uuid)
    return model.storage.get_layer_path(blob_record)

  def get_repo_blob_by_digest(self, namespace_name, repo_name, digest):
    try:
      blob_record = model.blob.get_repo_blob_by_digest(namespace_name, repo_name, digest)
    except model.BlobDoesNotExist:
      return None

    return _blob(blob_record)

  def get_torrent_info(self, blob):
    blob_record = model.storage.get_storage_by_uuid(blob.uuid)

    try:
      torrent_info = model.storage.get_torrent_info(blob_record)
    except model.TorrentInfoDoesNotExist:
      return None

    return TorrentInfo(
      pieces=torrent_info.pieces,
      piece_length=torrent_info.piece_length,
    )

  def set_torrent_info(self, blob, piece_length, pieces):
    blob_record = model.storage.get_storage_by_uuid(blob.uuid)
    model.storage.save_torrent_info(blob_record, piece_length, pieces)

  def lookup_derived_image(self, repo_image, verb, varying_metadata=None):
    blob_record = model.image.find_derived_storage_for_image(repo_image.internal_db_id, verb,
                                                             varying_metadata)
    if blob_record is None:
      return None

    return _derived_image(blob_record, repo_image)

  def lookup_or_create_derived_image(self, repo_image, verb, location, varying_metadata=None):
    blob_record = model.image.find_or_create_derived_storage(repo_image.internal_db_id, verb,
                                                             location, varying_metadata)
    return _derived_image(blob_record, repo_image)

  def get_tag_image(self, namespace_name, repo_name, tag_name):
    try:
      found = model.tag.get_tag_image(namespace_name, repo_name, tag_name, include_storage=True)
    except model.DataModelException:
      return None

    metadata = {}
    try:
      metadata = json.loads(found.v1_json_metadata)
    except ValueError:
      pass

    return ImageWithBlob(
      image_id=found.docker_image_id,
      blob=_blob(found.storage),
      repository=RepositoryReference(
        namespace_name=namespace_name,
        name=repo_name,
        id=found.repository_id,
      ),
      compat_metadata=metadata,
      v1_metadata=_docker_v1_metadata(namespace_name, repo_name, found),
      internal_db_id=found.id,
    )


pre_oci_model = PreOCIModel()


def _docker_v1_metadata(namespace_name, repo_name, repo_image):
  """
  Returns a DockerV1Metadata object for the given Pre-OCI repo_image under the
  repository with the given namespace and name. Note that the namespace and
  name are passed here as an optimization, and are *not checked* against the
  image.
  """
  return DockerV1Metadata(
    namespace_name=namespace_name,
    repo_name=repo_name,
    image_id=repo_image.docker_image_id,
    checksum=repo_image.v1_checksum,
    compat_json=repo_image.v1_json_metadata,
    created=repo_image.created,
    comment=repo_image.comment,
    command=repo_image.command,

    # Note: These are not needed in verbs and are expensive to load, so we just skip them.
    content_checksum=None,
    parent_image_id=None,
  )


def _derived_image(blob_record, repo_image):
  """
  Returns a DerivedImage object for the given Pre-OCI data model blob and repo_image instance.
  """
  return DerivedImage(
    ref=repo_image.internal_db_id,
    blob=_blob(blob_record),
    internal_source_image_db_id=repo_image.internal_db_id,
  )


def _blob(blob_record):
  """
  Returns a Blob object for the given Pre-OCI data model blob instance.
  """
  if hasattr(blob_record, 'locations'):
    locations = blob_record.locations
  else:
    locations = model.storage.get_storage_locations(blob_record.uuid)

  return Blob(
    uuid=blob_record.uuid,
    size=blob_record.image_size,
    uncompressed_size=blob_record.uncompressed_size,
    uploading=blob_record.uploading,
    locations=locations,
  )