import json
import re
import calendar

from uuid import uuid4

from app import app
from util.registry.streamlayerformat import StreamLayerMerger
from util.dict_wrappers import JSONPathDict
from image.common import TarImageFormatter


ACNAME_REGEX = re.compile(r'[^a-z-]+')


class AppCImageFormatter(TarImageFormatter):
  """
  Image formatter which produces an tarball according to the AppC specification.
  """

  def stream_generator(self, repo_image, tag, synthetic_image_id, get_image_iterator,
                       tar_stream_getter_iterator, reporter=None):
    image_mtime = 0
    created = next(get_image_iterator()).v1_metadata.created
    if created is not None:
      image_mtime = calendar.timegm(created.utctimetuple())

    # ACI Format (.tar):
    #   manifest - The JSON manifest
    #   rootfs - The root file system

    # Yield the manifest.
    manifest = json.dumps(DockerV1ToACIManifestTranslator.build_manifest(
      repo_image,
      tag,
      synthetic_image_id
    ))
    yield self.tar_file('manifest', manifest, mtime=image_mtime)

    # Yield the merged layer dtaa.
    yield self.tar_folder('rootfs', mtime=image_mtime)

    layer_merger = StreamLayerMerger(tar_stream_getter_iterator, path_prefix='rootfs/',
                                     reporter=reporter)
    for entry in layer_merger.get_generator():
      yield entry


class DockerV1ToACIManifestTranslator(object):
  @staticmethod
  def _build_isolators(docker_config):
    """
    Builds ACI isolator config from the docker config.
    """

    def _isolate_memory(memory):
      return {
        "name": "memory/limit",
        "value": {
          "request": str(memory) + 'B',
        }
      }

    def _isolate_swap(memory):
      return {
        "name": "memory/swap",
        "value": {
          "request": str(memory) + 'B',
        }
      }

    def _isolate_cpu(cpu):
      return {
        "name": "cpu/shares",
        "value": {
          "request": str(cpu),
        }
      }

    def _isolate_capabilities(capabilities_set_value):
      capabilities_set = re.split(r'[\s,]', capabilities_set_value)
      return {
        "name": "os/linux/capabilities-retain-set",
        "value": {
          "set": capabilities_set,
        }
      }

    mappers = {
      'Memory': _isolate_memory,
      'MemorySwap': _isolate_swap,
      'CpuShares': _isolate_cpu,
      'Cpuset': _isolate_capabilities
    }

    isolators = []

    for config_key in mappers:
      value = docker_config.get(config_key)
      if value:
        isolators.append(mappers[config_key](value))

    return isolators

  @staticmethod
  def _build_ports(docker_config):
    """
    Builds the ports definitions for the ACI.

    Formats:
      port/tcp
      port/udp
      port
    """
    ports = []

    exposed_ports = docker_config['ExposedPorts']
    if exposed_ports is not None:
      port_list = exposed_ports.keys()
    else:
      port_list = docker_config['Ports'] or docker_config['ports'] or []

    for docker_port in port_list:
      protocol = 'tcp'
      port_number = -1

      if '/' in docker_port:
        (port_number, protocol) = docker_port.split('/')
      else:
        port_number = docker_port

      try:
        port_number = int(port_number)
        ports.append({
          "name": "port-%s" % port_number,
          "port": port_number,
          "protocol": protocol,
        })
      except ValueError:
        pass

    return ports

  @staticmethod
  def _ac_name(value):
    sanitized = ACNAME_REGEX.sub('-', value.lower()).strip('-')
    if sanitized == '':
      return str(uuid4())
    return sanitized

  @staticmethod
  def _build_volumes(docker_config):
    """ Builds the volumes definitions for the ACI. """
    volumes = []

    def get_name(docker_volume_path):
      volume_name = DockerV1ToACIManifestTranslator._ac_name(docker_volume_path)
      return "volume-%s" % volume_name

    volume_list = docker_config['Volumes'] or docker_config['volumes'] or {}
    for docker_volume_path in volume_list.iterkeys():
      if not docker_volume_path:
        continue

      volumes.append({
        "name": get_name(docker_volume_path),
        "path": docker_volume_path,
        "readOnly": False,
      })
    return volumes

  @staticmethod
  def build_manifest(repo_image, tag, synthetic_image_id):
    """ Builds an ACI manifest of an existing repository image. """
    docker_layer_data = JSONPathDict(repo_image.compat_metadata)
    config = docker_layer_data['config'] or JSONPathDict({})

    namespace = repo_image.repository.namespace_name
    repo_name = repo_image.repository.name
    source_url = "%s://%s/%s/%s:%s" % (app.config['PREFERRED_URL_SCHEME'],
                                       app.config['SERVER_HOSTNAME'],
                                       namespace, repo_name, tag)

    # ACI requires that the execution command be absolutely referenced. Therefore, if we find
    # a relative command, we give it as an argument to /bin/sh to resolve and execute for us.
    entrypoint = config['Entrypoint'] or []
    exec_path = entrypoint + (config['Cmd'] or [])
    if exec_path and not exec_path[0].startswith('/'):
      exec_path = ['/bin/sh', '-c', '""%s""' % ' '.join(exec_path)]

    # TODO(jschorr): ACI doesn't support : in the name, so remove any ports.
    hostname = app.config['SERVER_HOSTNAME']
    hostname = hostname.split(':', 1)[0]

    # Calculate the environment variables.
    docker_env_vars = config.get('Env') or []
    env_vars = []
    for var in docker_env_vars:
      pieces = var.split('=')
      if len(pieces) != 2:
        continue

      env_vars.append(pieces)

    manifest = {
      "acKind": "ImageManifest",
      "acVersion": "0.6.1",
      "name": '%s/%s/%s' % (hostname.lower(), namespace.lower(), repo_name.lower()),
      "labels": [
        {
          "name": "version",
          "value": tag,
        },
        {
          "name": "arch",
          "value": docker_layer_data.get('architecture') or 'amd64'
        },
        {
          "name": "os",
          "value": docker_layer_data.get('os') or 'linux'
        }
      ],
      "app": {
        "exec": exec_path,
        # Below, `or 'root'` is required to replace empty string from Dockerfiles.
        "user": config.get('User') or 'root',
        "group": config.get('Group') or 'root',
        "eventHandlers": [],
        "workingDirectory": config.get('WorkingDir') or '/',
        "environment": [{"name": key, "value": value} for (key, value) in env_vars],
        "isolators": DockerV1ToACIManifestTranslator._build_isolators(config),
        "mountPoints": DockerV1ToACIManifestTranslator._build_volumes(config),
        "ports": DockerV1ToACIManifestTranslator._build_ports(config),
        "annotations": [
          {"name": "created", "value": docker_layer_data.get('created') or ''},
          {"name": "homepage", "value": source_url},
          {"name": "quay.io/derived-image", "value": synthetic_image_id},
        ]
      },
    }

    return manifest