from __future__ import print_function
#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements.  See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF 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 logging
import os
import traceback
import yaml
import dependency_check.version_comparer as version_comparer

from datetime import datetime
from .jira_client import JiraClient


_JIRA_PROJECT_NAME = 'BEAM'
_JIRA_COMPONENT = 'dependencies'
_ISSUE_SUMMARY_PREFIX = 'Beam Dependency Update Request: '
_ISSUE_REOPEN_DAYS = 180

class JiraManager:

  def __init__(self, jira_url, jira_username, jira_password, owners_file):
    options = {
      'server': jira_url
    }
    basic_auth = (jira_username, jira_password)
    self.jira = JiraClient(options, basic_auth, _JIRA_PROJECT_NAME)
    with open(owners_file) as f:
      owners = yaml.load(f, Loader=yaml.BaseLoader)
    self.owners_map = owners['deps']
    logging.getLogger().setLevel(logging.INFO)


  def run(self, dep_name,
      dep_current_version,
      dep_latest_version,
      sdk_type,
      group_id=None):
    """
    Manage the jira issue for a dependency
    Args:
      dep_name,
      dep_current_version,
      dep_latest_version,
      sdk_type: Java, Python
      group_id (optional): only required for Java dependencies
    Return: Jira Issue
    """
    logging.info("Start handling the JIRA issues for {0} dependency: {1} {2}".format(
        sdk_type, dep_name, dep_latest_version))
    try:
      # find the parent issue for Java deps base on the groupID
      parent_issue = None
      if sdk_type == 'Java':
        summary = _ISSUE_SUMMARY_PREFIX + group_id
        parent_issues = self._search_issues(summary)
        for i in parent_issues:
          if i.fields.summary == summary:
            parent_issue = i
            break
        # Create a new parent issue if no existing found
        if not parent_issue:
          logging.info("""Did not find existing issue with name {0}. \n 
            Created a parent issue for {1}""".format(summary, group_id))
          try:
            parent_issue = self._create_issue(group_id, None, None)
          except:
            logging.error("""Failed creating a parent issue for {0}.
              Stop handling the JIRA issue for {1}, {2}""".format(group_id, dep_name, dep_latest_version))
            return
        # Reopen the existing parent issue if it was closed
        elif (parent_issue.fields.status.name != 'Open' and
          parent_issue.fields.status.name != 'Reopened'):
          logging.info("""The parent issue {0} is not opening. Attempt reopening the issue""".format(parent_issue.key))
          try:
            self.jira.reopen_issue(parent_issue)
          except:
            traceback.print_exc()
            logging.error("""Failed reopening the parent issue {0}.
              Stop handling the JIRA issue for {1}, {2}""".format(parent_issue.key, dep_name, dep_latest_version))
            return
        logging.info("Found the parent issue {0}. Continuous to create or update the sub-task for {1}".format(parent_issue.key, dep_name))

      # creating a new issue/sub-task or updating on the existing issue of the dep
      summary =  _ISSUE_SUMMARY_PREFIX + dep_name
      issues = self._search_issues(summary)
      issue = None
      for i in issues:
        if i.fields.summary == summary:
          issue = i
          break
      # Create a new JIRA if no existing one.
      if not issue:
        if sdk_type == 'Java':
          issue = self._create_issue(dep_name, dep_current_version, dep_latest_version, is_subtask=True, parent_key=parent_issue.key)
        else:
          issue = self._create_issue(dep_name, dep_current_version, dep_latest_version)
        logging.info('Created a new issue {0} of {1} {2}'.format(issue.key, dep_name, dep_latest_version))
      # Add descriptions in to the opening issue.
      elif issue.fields.status.name == 'Open' or issue.fields.status.name == 'Reopened':
        self._append_descriptions(issue, dep_name, dep_current_version, dep_latest_version)
        logging.info('Updated the existing issue {0} of {1} {2}'.format(issue.key, dep_name, dep_latest_version))
      # Check if we need reopen the issue if it was closed. If so, reopen it then add descriptions.
      elif self._need_reopen(issue, dep_latest_version):
        self.jira.reopen_issue(issue)
        self._append_descriptions(issue, dep_name, dep_current_version, dep_latest_version)
        logging.info("Reopened the issue {0} for {1} {2}".format(issue.key, dep_name, dep_latest_version))
      return issue
    except:
      raise


  def _create_issue(self, dep_name, dep_current_version, dep_latest_version, is_subtask=False, parent_key=None):
    """
    Create a new issue or subtask
    Args:
      dep_name,
      dep_latest_version,
      is_subtask,
      parent_key: only required if the 'is_subtask'is true.
    """
    logging.info("Creating a new JIRA issue to track {0} upgrade process".format(dep_name))
    summary =  _ISSUE_SUMMARY_PREFIX + dep_name
    description = self._create_descriptions(dep_name, dep_current_version, dep_latest_version)
    try:
      if not is_subtask:
        issue = self.jira.create_issue(summary, [_JIRA_COMPONENT], description)
      else:
        issue = self.jira.create_issue(summary, [_JIRA_COMPONENT], description, parent_key=parent_key)
    except Exception as e:
      logging.error("Failed creating issue: "+ str(e))
      raise e
    return issue


  def _search_issues(self, summary):
    """
    Search issues by using issues' summary.
    Args:
      summary: a string
    Return:
      A list of issues
    """
    try:
      issues = self.jira.get_issues_by_summary(summary)
    except Exception as e:
      logging.error("Failed searching issues: "+ str(e))
      return []
    return issues


  def _append_descriptions(self, issue, dep_name, dep_current_version, dep_latest_version):
    """
    Add descriptions on an existing issue.
    Args:
      issue: Jira issue
      dep_name
      dep_latest_version
    """
    logging.info("Updating JIRA issue {0} to track {1} upgrade process".format(
        issue.key,
        dep_name))
    description = self._create_descriptions(dep_name, dep_current_version, dep_latest_version, issue=issue)
    try:
      self.jira.update_issue(issue, description=description)
    except Exception as e:
      traceback.print_exc()
      logging.error("Failed updating issue: "+ str(e))


  def _create_descriptions(self, dep_name, dep_current_version, dep_latest_version, issue = None):
    """
    Create descriptions for JIRA issues.
    Args:
      dep_name
      dep_latest_version
      issue
    """
    description = ""
    if issue:
      description = issue.fields.description
    description +=  """\n\n ------------------------- {0} -------------------------\n
        Please consider upgrading the dependency {1}. \n
        The current version is {2}. The latest version is {3} \n
        cc: """.format(
        datetime.today(),
        dep_name,
        dep_current_version,
        dep_latest_version
    )
    owners = self._find_owners(dep_name)
    for owner in owners:
      description += "[~{0}], ".format(owner)
    description += ("\n Please refer to "
    "[Beam Dependency Guide |https://beam.apache.org/contribute/dependencies/]" 
    "for more information. \n"
    "Do Not Modify The Description Above. \n")
    return description


  def _find_owners(self, dep_name):
    """
    Find owners for a dependency/
    Args:
      dep_name
    Return:
      primary: The primary owner of the dep. The Jira issue will be assigned to the primary owner.
      others: A list of other owners of the dep. Owners will be cc'ed in the description.
    """
    try:
      dep_info = self.owners_map[dep_name]
      owners = dep_info['owners']
      if not owners:
        logging.warning("Could not find owners for " + dep_name)
        return []
    except KeyError:
      traceback.print_exc()
      logging.warning("Could not find the dependency info of {0} in the OWNERS configurations.".format(dep_name))
      return []
    except Exception as e:
      traceback.print_exc()
      logging.error("Failed finding dependency owners: "+ str(e))
      return None

    logging.info("Found owners of {0}: {1}".format(dep_name, owners))
    owners = owners.split(',')
    owners = map(str, owners)
    owners = map(str.strip, owners)
    owners = list(filter(None, owners))
    return owners


  def _need_reopen(self, issue, dep_latest_version):
    """
    Return a boolean that indicates whether reopen the closed issue.
    """
    # Check if the issue was closed with a "fix version/s"
    # Reopen the issue if it hits the next release version.
    next_release_version = self._get_next_release_version()
    for fix_version in issue.fields.fixVersions:
      if fix_version.name in next_release_version:
        return True

    # Check if there is other new versions released.
    # Reopen the issue if 3 new versions have been released in 6 month since closure.
    try:
      if issue.fields.resolutiondate:
        closing_date = datetime.strptime(issue.fields.resolutiondate[:19], "%Y-%m-%dT%H:%M:%S")
        if (datetime.today() - closing_date).days >= _ISSUE_REOPEN_DAYS:
          # Extract the previous version when JIRA closed.
          descriptions = issue.fields.description.splitlines()
          descriptions = descriptions[len(descriptions)-5]
          # The version info has been stored in the JIRA description in a specific format.
          # Such as "Please review and upgrade the <dep name> to the latest version <version>"
          previous_version = descriptions.split("The latest version is", 1)[1].strip()
          if version_comparer.compare_dependency_versions(previous_version, dep_latest_version):
            return True
    except Exception as e:
      traceback.print_exc()
      logging.error("Failed deciding to reopen the issue." + str(e))
      return False

    return False


  def _get_next_release_version(self):
    """
    Return the incoming release version from sdks/python/apache_beam/version.py
    """
    global_names = {}
    exec(
      open(os.path.join(
          os.path.dirname(os.path.abspath(__file__)),
          '../../../sdks/python/',
          'apache_beam/version.py')
      ).read(),
      global_names
    )
    return global_names['__version__']
