import json
import re
import calendar

from uuid import uuid4

from app import app
from util.registry.streamlayerformat import StreamLayerMerger
from formats.tarimageformatter import TarImageFormatter


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


class ACIImage(TarImageFormatter):
  """ Image formatter which produces an ACI-compatible TAR.
  """

  def stream_generator(self, namespace, repository, tag, synthetic_image_id,
                       layer_json, get_image_iterator, get_layer_iterator, get_image_json):
    image_mtime = 0
    created = next(get_image_iterator()).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 = self._build_manifest(namespace, repository, tag, layer_json, 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(get_layer_iterator, path_prefix='rootfs/')
    for entry in layer_merger.get_generator():
      yield entry

  @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 _get_docker_config_value(docker_config, key, default_value):
    # Try the key itself.
    result = docker_config.get(key)
    if result is not None:
      return result or default_value

    # The the lowercase version of the key.
    result = docker_config.get(key.lower())
    if result is not None:
      return result or default_value

    return default_value

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

    for docker_port_definition in ACIImage._get_docker_config_value(docker_config, 'Ports', []):
      # Formats:
      # port/tcp
      # port/udp
      # port

      protocol = 'tcp'
      port_number = -1

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

      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):
      return "volume-%s" % ACIImage._ac_name(docker_volume_path)

    for docker_volume_path in ACIImage._get_docker_config_value(docker_config, 'Volumes', []):
      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(namespace, repository, tag, docker_layer_data, synthetic_image_id):
    """ Builds an ACI manifest from the docker layer data. """

    config = docker_layer_data.get('config', {})

    source_url = "%s://%s/%s/%s:%s" % (app.config['PREFERRED_URL_SCHEME'],
                                       app.config['SERVER_HOSTNAME'],
                                       namespace, repository, 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.get('Entrypoint', []) or []
    exec_path = entrypoint + (config.get('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(), repository.lower()),
      "labels": [
        {
          "name": "version",
          "value": tag,
        },
        {
          "name": "arch",
          "value": docker_layer_data.get('architecture', 'amd64')
        },
        {
          "name": "os",
          "value": docker_layer_data.get('os', '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": ACIImage._build_isolators(config),
        "mountPoints": ACIImage._build_volumes(config),
        "ports": ACIImage._build_ports(config),
        "annotations": [
          {"name": "created", "value": docker_layer_data.get('created', '')},
          {"name": "homepage", "value": source_url},
          {"name": "quay.io/derived-image", "value": synthetic_image_id},
        ]
      },
    }

    return json.dumps(manifest)