#!/usr/bin/env python
# Licensed to Cloudera, Inc. under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  Cloudera, Inc. licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License.  You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Import an external workflow by providing an XML definition.
The workflow definition is imported via the method 'import_workflow'.
The XML is first transformed into a django serialized string that can be deserialized and interpreted.
The interpreted objects are then assigned the worklow, stripped of any useless IDs and saved.
Then the links are interpreted from the original XML definition.
First the basic links are interpreted for basic hierarchy traversal.
Then the related links are infered, including Decision node ends.
See oozie.models.Decision for more information on decision ends.

The XSLTs are partitioned by version.
For every new workflow DTD version a new directory should be created.
IE: uri:oozie:workflow:0.4 => 0.4 directory in xslt dir.
Action extensions are also versioned.
Every action extension will have its own version via /xslt/<workflow version>/extensions/<name of extensions>.<version>.xslt
"""

import json
import logging
from lxml import etree
import os
import re

from django.core import serializers
from django.utils.encoding import smart_str
from django.utils.translation import ugettext as _

from desktop.models import Document

from oozie.conf import DEFINITION_XSLT_DIR, DEFINITION_XSLT2_DIR
from oozie.models import Workflow, Node, Link, Start, End,\
                         Decision, DecisionEnd, Fork, Join,\
                         Kill

LOG = logging.getLogger(__name__)

OOZIE_NAMESPACES = ['uri:oozie:workflow:0.1', 'uri:oozie:workflow:0.2', 'uri:oozie:workflow:0.3', 'uri:oozie:workflow:0.4', 'uri:oozie:workflow:0.5']

LINKS = ('ok', 'error', 'path')


def _set_properties(workflow, root, namespace):
  # root should be config element.
  properties = []
  seen = {}
  namespaces = {
    'n': namespace
  }

  for prop in root.xpath('n:property', namespaces=namespaces):
    name = prop.xpath('n:name', namespaces=namespaces)[0].text
    value = prop.xpath('n:value', namespaces=namespaces)[0].text
    if name not in seen:
      properties.append({'name': name, 'value': value})
      seen[name] = True

  workflow.job_properties = json.dumps(properties)


def _global_configuration(workflow, root, namespace):
  # root should be global config element.
  namespaces = {
    'n': namespace
  }

  job_xml = root.xpath('n:job-xml', namespaces=namespaces)
  configuration = root.xpath('n:configuration', namespaces=namespaces)
  if job_xml:
    workflow.job_xml = job_xml[0].text
  if configuration:
    _set_properties(workflow, configuration[0], namespace)


def _assign_workflow_properties(workflow, root, namespace):
  namespaces = {
    'n': namespace
  }

  global_config = root.xpath('n:global', namespaces=namespaces)
  if global_config:
    _global_configuration(workflow, global_config[0], namespace)

  LOG.debug("Finished assigning properties to workflow %s" % smart_str(workflow.name))


def _save_links(workflow, root):
  """
  Iterates over all links in the passed XML doc and creates links.
  First non-META links are resolved and created, then META links.
  Link name is chosen with the following logic:
    If node is start, then use 'to'.
    Else If node is Join, then use 'to'.
    Else If node is Decision, then
      If tag is 'default', then use 'default'
      Else use 'start'
    Else
      If tag is 'path', use 'start'
      Else use tag as name ('ok' or 'error')

  This strategy has the following resolution:
    - Fork and Decision nodes have Links named 'start'.
    - Decision nodes have a 'default' link.
    - Decision nodes may have a 'related' link that is there end.
    - Fork nodes always have a 'related' node that is there end join node.
    - Start and Join nodes have links named 'to'.
    - All action nodes have 'ok' and 'error' links.

  Note: The nodes that these links point to should exist already.
  Note: Nodes are looked up by workflow and name.
  Note: Unknown elements should throw an error.
  """
  # Iterate over nodes
  for child_el in root:
    # Skip special nodes (like comments).
    if not isinstance(child_el.tag, basestring):
      continue

    # Skip kill nodes.
    if child_el.tag.endswith('kill'):
      continue

    # Skip global configuration.
    if child_el.tag.endswith('global'):
      continue

    # Skip credentials configuration.
    if child_el.tag.endswith('credentials'):
      continue

    tag = etree.QName(child_el).localname
    name = child_el.attrib.get('name', tag)
    LOG.debug("Getting node with data - XML TAG: %(tag)s\tLINK NAME: %(node_name)s\tWORKFLOW NAME: %(workflow_name)s" % {
      'tag': smart_str(tag),
      'node_name': smart_str(name),
      'workflow_name': smart_str(workflow.name)
    })

    # Iterate over node members
    # Join nodes have attributes which point to the next node
    # Start node has attribute which points to first node
    try:
      parent = Node.objects.get(name=name, workflow=workflow).get_full_node()
    except Node.DoesNotExist:
      raise RuntimeError(_('Node with name %s for workflow %s does not exist.') % (name, workflow.name))

    if isinstance(parent, Start):
      _start_relationships(workflow, parent, child_el)

    elif isinstance(parent, Join):
      _join_relationships(workflow, parent, child_el)

    elif isinstance(parent, Decision):
      _decision_relationships(workflow, parent, child_el)

    else:
      _node_relationships(workflow, parent, child_el)

  workflow.end = End.objects.get(workflow=workflow).get_full_node()
  workflow.save()

  _resolve_start_relationships(workflow)
  _resolve_fork_relationships(workflow)
  _resolve_decision_relationships(workflow)

  LOG.debug("Finished resolving links for workflow %s" % smart_str(workflow.name))


def _start_relationships(workflow, parent, child_el):
  """
  Resolve start node links.
  Will always use 'to' link type.
  """
  if 'to' not in child_el.attrib:
    raise RuntimeError(_("Node %s has a link that is missing 'to' attribute.") % parent.name)

  workflow.start = parent
  to = child_el.attrib['to']

  try:
    child = Node.objects.get(workflow=workflow, name=to)
  except Node.DoesNotExist:
    raise RuntimeError(_("Node %s has not been defined.") % to)

  try:
    obj = Link.objects.filter(parent=parent).get(name='to')
    obj.child = child
  except Link.DoesNotExist:
    obj = Link.objects.create(name='to', parent=parent, child=child)
  obj.save()


def _join_relationships(workflow, parent, child_el):
  """
  Resolves join node links.
  Will always use 'to' link type.
  """
  if 'to' not in child_el.attrib:
    raise RuntimeError(_("Node %s has a link that is missing 'to' attribute.") % parent.name)

  to = child_el.attrib['to']

  try:
    child = Node.objects.get(workflow=workflow, name=to)
  except Node.DoesNotExist, e:
    raise RuntimeError(_("Node %s has not been defined.") % to)

  obj = Link.objects.create(name='to', parent=parent, child=child)
  obj.save()


def _decision_relationships(workflow, parent, child_el):
  """
  Resolves the switch statement like nature of decision nodes.
  Will use 'to' link type, except for default case.
  """
  for switch in child_el:
    # Skip special nodes (like comments).
    if not isinstance(switch.tag, basestring):
      continue

    for case in switch:
      # Skip special nodes (like comments).
      if not isinstance(case.tag, basestring):
        continue

      if 'to' not in case.attrib:
        raise RuntimeError(_("Node %s has a link that is missing 'to' attribute.") % parent.name)

      to = case.attrib['to']
      try:
        child = Node.objects.get(workflow=workflow, name=to)
      except Node.DoesNotExist, e:
        raise RuntimeError(_("Node %s has not been defined.") % to)

      if etree.QName(case).localname == 'default':
        name = 'default'
        obj = Link.objects.create(name=name, parent=parent, child=child)

      else:
        name = 'start'
        comment = case.text.strip()
        obj = Link.objects.create(name=name, parent=parent, child=child, comment=comment)

      obj.save()


def _node_relationships(workflow, parent, child_el):
  """
  Resolves node links.
  Will use 'start' link type for fork nodes and 'to' link type for all other nodes.
  Error links will automatically resolve to a single kill node.
  """
  for el in child_el:
    # Skip special nodes (like comments).
    if not isinstance(el.tag, basestring):
      continue

    # Links
    name = etree.QName(el).localname
    if name in LINKS:
      if name == 'path':
        if 'start' not in el.attrib:
          raise RuntimeError(_("Node %s has a link that is missing 'start' attribute.") % parent.name)
        to = el.attrib['start']
        name = 'start'

      else:
        if 'to' not in el.attrib:
          raise RuntimeError(_("Node %s has a link that is missing 'to' attribute.") % parent.name)
        to = el.attrib['to']

      try:
        child = Node.objects.get(workflow=workflow, name=to)
      except Node.DoesNotExist, e:
        if name == 'error':
          child, create = Kill.objects.get_or_create(name='kill', workflow=workflow, node_type=Kill.node_type)
        else:
          raise RuntimeError(_("Node %s has not been defined") % to)

      obj = Link.objects.create(name=name, parent=parent, child=child)
      obj.save()


def _resolve_start_relationships(workflow):
  if not workflow.start:
    raise RuntimeError(_("Workflow start has not been created."))

  if not workflow.end:
    raise RuntimeError(_("Workflow end has not been created."))

  obj = Link.objects.get_or_create(name='related', parent=workflow.start, child=workflow.end)


def _resolve_fork_relationships(workflow):
  """
  Requires proper workflow structure.
  Fork must come before a join.
  """
  def helper(workflow, node, last_fork):
    if isinstance(node, Fork):
      join = None
      children = node.get_children()
      for child in children:
        join = helper(workflow, child.get_full_node(), node) or join
      link = Link(name='related', parent=node, child=join)
      link.save()

      node = join

    elif isinstance(node, Join):