########################################################################
# File :   CSCLI.py
# Author : Adria Casajus
########################################################################

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division
import sys
import atexit
import os
import readline
import six

from DIRAC import gLogger

from DIRAC.Core.Utilities.File import mkDir
from DIRAC.Core.Base.CLI import CLI, colorize
from DIRAC.ConfigurationSystem.private.Modificator import Modificator
from DIRAC.ConfigurationSystem.Client.ConfigurationData import gConfigurationData
from DIRAC.ConfigurationSystem.Client.ConfigurationClient import ConfigurationClient

__RCSID__ = "$Id$"


def _showTraceback():
  import traceback
  excepType, execpValue = sys.exc_info()[:2]
  print("________________________\n")
  print("Exception", excepType, ":", execpValue)
  traceback.print_tb(sys.exc_info()[2])
  print("________________________\n")


def _printComment(comment):
  commentList = comment.split("\n")
  for commentLine in commentList[:-1]:
    print("# %s" % commentLine.strip())


def _appendExtensionIfMissing(filename):
  dotPosition = filename.rfind(".")
  if dotPosition > -1:
    filename = filename[:dotPosition]
  return "%s.cfg" % filename


class CSCLI(CLI):

  def __init__(self):
    CLI.__init__(self)
    self.connected = False
    self.masterURL = "unset"
    self.writeEnabled = False
    self.modifiedData = False
    self.rpcClient = None
    self.do_connect()
    if self.connected:
      self.modificator = Modificator(self.rpcClient)
    else:
      self.modificator = Modificator()
    self.indentSpace = 20
    self.backupFilename = "dataChanges"
    # store history
    histfilename = os.path.basename(sys.argv[0])
    historyFile = os.path.expanduser("~/.dirac/%s.history" % histfilename[0:-3])
    mkDir(os.path.dirname(historyFile))
    if os.path.isfile(historyFile):
      readline.read_history_file(historyFile)
    readline.set_history_length(1000)
    atexit.register(readline.write_history_file, historyFile)

  def start(self):
    if self.connected:
      self.modificator.loadFromRemote()
      retVal = self.modificator.loadCredentials()
      if not retVal['OK']:
        print("There was an error gathering your credentials")
        print(retVal['Message'])
        self._setStatus(False)
    try:
      self.cmdloop()
    except KeyboardInterrupt:
      gLogger.warn("Received a keyboard interrupt.")
      self.do_quit("")

  def _setConnected(self, connected, writeEnabled):
    self.connected = connected
    self.modifiedData = False
    self.writeEnabled = writeEnabled
    if connected:
      if writeEnabled:
        self.prompt = "(%s)-%s> " % (self.masterURL, colorize("Connected", "green"))
      else:
        self.prompt = "(%s)-%s> " % (self.masterURL, colorize("Connected (RO)", "yellow"))
    else:
      self.prompt = "(%s)-%s> " % (self.masterURL, colorize("Disconnected", "red"))

  def do_quit(self, dummy):
    """
    Exits the application without sending changes to server

    Usage: quit
    """
    print()
    if self.modifiedData:
      print("Changes are about to be written to file for later use.")
      self.do_writeToFile(self.backupFilename)
      print("Changes written to %s.cfg" % self.backupFilename)
    sys.exit(0)

  def _setStatus(self, connected=True):
    if not connected:
      self.masterURL = "unset"
      self._setConnected(False, False)
    else:
      retVal = self.rpcClient.writeEnabled()
      if retVal['OK']:
        if retVal['Value']:
          self._setConnected(True, True)
        else:
          self._setConnected(True, False)
      else:
        print("Server returned an error: %s" % retVal['Message'])
        self._setConnected(True, False)

  def _tryConnection(self):
    print("Trying connection to %s" % self.masterURL)
    try:
      self.rpcClient = ConfigurationClient(url=self.masterURL)
      self._setStatus()
    except Exception as x:
      gLogger.error("Couldn't connect to master CS server", "%s (%s)" % (self.masterURL, str(x)))
      self._setStatus(False)

  def do_connect(self, args=''):
    """
    Connects to configuration master server (in specified url if provided).

    Usage: connect <url>
    """
    if not args or not isinstance(args, six.string_types):
      self.masterURL = gConfigurationData.getMasterServer()
      if self.masterURL != "unknown" and self.masterURL:
        self._tryConnection()
      else:
        self._setStatus(False)
    else:
      splitted = args.split()
      if len(splitted) == 0:
        print("Must specify witch url to connect")
        self._setStatus(False)
      else:
        self.masterURL = splitted[0].strip()
        self._tryConnection()

  def do_sections(self, args):
    """
    Shows all sections with their comments.
    If no section is specified, root is taken.

    Usage: sections <section>
    """
    try:
      argList = args.split()
      if argList:
        baseSection = argList[0].strip()
      else:
        baseSection = "/"
      if not self.modificator.existsSection(baseSection):
        print("Section %s does not exist" % baseSection)
        return
      sectionList = self.modificator.getSections(baseSection)
      if not sectionList:
        print("Section %s is empty" % baseSection)
        return
      for section in sectionList:
        section = "%s/%s" % (baseSection, section)
        self.printPair(section, self.modificator.getComment(section), " #")
    except Exception:
      _showTraceback()

  def do_options(self, args):
    """
    Shows all options and values of a specified section

    Usage: options <section>
    """
    try:
      argList = args.split()
      if argList:
        section = argList[0].strip()
      else:
        print("Which section?")
        return
      if not self.modificator.existsSection(section):
        print("Section %s does not exist" % section)
        return
      optionsList = self.modificator.getOptions(section)
      if not optionsList:
        print("Section %s has no options" % section)
        return
      for option in optionsList:
        _printComment(self.modificator.getComment("%s/%s" % (section, option)))
        self.printPair(option, self.modificator.getValue("%s/%s" % (section, option)), "=")
    except Exception:
      _showTraceback()

  def do_get(self, args):
    """
    Shows value and comment for specified option in section

    Usage: get <path to option>
    """
    try:
      argList = args.split()
      if argList:
        optionPath = argList[0].strip()
      else:
        print("Which option?")
        return
      if self.modificator.existsOption(optionPath):
        option = optionPath.split("/")[-1]
        _printComment(self.modificator.getComment(optionPath))
        self.printPair(option, self.modificator.getValue(optionPath), "=")
      else:
        print("Option %s does not exist" % optionPath)
    except Exception:
      _showTraceback()

  def do_writeToServer(self, dummy):
    """
    Sends changes to server.

    Usage: writeToServer
    """
    if not self.connected:
      print("You are not connected!")
      return
    try:
      if not self.writeEnabled:
        print("This server can't receive data modifications")
        return
      if not self.modifiedData:
        while True:
          choice = six.moves.input("Data has not been modified, do you still want to upload changes? yes/no [no]: ")
          choice = choice.lower()
          if choice in ("yes", "y"):
            break
          else:
            print("Commit aborted")
            return

      choice = six.moves.input("Do you really want to send changes to server? yes/no [no]: ")
      choice = choice.lower()
      if choice in ("yes", "y"):
        print("Uploading changes to %s (It may take some seconds)..." % self.masterURL)
        response = self.modificator.commit()
        if response['OK']:
          self.modifiedData = False
          print("Data sent to server.")
          self.modificator.loadFromRemote()
        else:
          print("Error sending data, server said: %s" % response['Message'])
        return
      else:
        print("Commit aborted")
    except Exception as x:
      _showTraceback()
      print("Could not upload changes. ", str(x))

  def do_set(self, args):
    """
    Sets option's value

    Usage: set <optionPath> <value>...

    From second argument until the last one is considered option's value

    NOTE: If specified section does not exist it is created.
    """
    try:
      argsList = args.split()
      if len(argsList) < 2:
        print("Must specify option and value to use")
        return
      optionPath = argsList[0].strip()
      value = " ".join(argsList[1:]).strip()
      self.modificator.setOptionValue(optionPath, value)
      self.modifiedData = True
    except Exception as x:
      print("Cannot insert value: ", str(x))

  def do_removeOption(self, args):
    """
    Removes an option.

    Usage: removeOption <option>

    There can be empty sections.
    """
    try:
      argsList = args.split()
      if len(argsList) < 1:
        print("Must specify option to delete")
        return
      optionPath = argsList[0].strip()
      choice = six.moves.input("Are you sure you want to delete %s? yes/no [no]: " % optionPath)
      choice = choice.lower()
      if choice in ("yes", "y", "true"):
        if self.modificator.removeOption(optionPath):
          self.modifiedData = True
        else:
          print("Can't be deleted")
      else:
        print("Aborting removal.")
    except Exception as x:
      print("Error removing option, %s" % str(x))

  def do_removeSection(self, args):
    """
    Removes a section.

    Usage: removeSection <section>
    """
    try:
      argsList = args.split()
      if len(argsList) < 1:
        print("Must specify section to delete")
        return
      section = argsList[0].strip()
      choice = six.moves.input("Are you sure you want to delete %s? yes/no [no]: " % section)
      choice = choice.lower()
      if choice in ("yes", "y", "true"):
        if self.modificator.removeSection(section):
          self.modifiedData = True
        else:
          print("Can't be deleted")
      else:
        print("Aborting removal.")
    except Exception as x:
      print("Error removing section, %s" % str(x))

  def do_setComment(self, args):
    """
    Sets option or section's comment. Requested entry MUST exist.

    Usage: set <option/section> <comment>...

    From third argument until the last one is considered option's comment.
    """
    try:
      argsList = args.split()
      if len(argsList) < 2:
        print("Must specify option and value to use")
        return
      entryPath = argsList[0].strip()
      value = " ".join(argsList[1:]).strip()
      self.modificator.setComment(entryPath, value)
      self.modifiedData = True
    except Exception as x:
      print("Cannot insert comment: ", str(x))

  def do_writeToFile(self, args):
    """
    Writes modification to file for later use.

    Usage: writeToFile <filename>.cfg

    Note that if a file extension is specified, it is replaced by .cfg suffix.
    If not it is added automatically
    """
    try:
      if len(args) == 0:
        print("Filename to write must be specified!")
        return
      filename = args.split()[0].strip()
      filename = _appendExtensionIfMissing(filename)
      self.modificator.dumpToFile(filename)
    except Exception as x:
      print("Couldn't write to file %s: %s" % (filename, str(x)))

  def do_readFromFile(self, args):
    """
    Reads data from filename to be used. Actual data will be replaced!

    Usage: readFromFile <filename>.cfg

    Note that if a file extension is specified, it is replaced by .cfg suffix.
    If not it is added automatically
    """
    try:
      if len(args) == 0:
        print("Filename to read must be specified!")
        return
      filename = args.split()[0].strip()
      filename = _appendExtensionIfMissing(filename)
      self.modificator.loadFromFile(filename)
      self.modifiedData = True
    except Exception as x:
      print("Couldn't read from file %s: %s" % (filename, str(x)))

  def do_mergeFromFile(self, args):
    """
    Reads data from filename and merges it with current data.
    Data read from file has more precedence that current one.

    Usage: mergeFromFile <filename>.cfg

    Note that if a file extension is specified, it is replaced by .cfg suffix.
    If not it is added automatically
    """
    try:
      if len(args) == 0:
        print("Filename to read must be specified!")
        return
      filename = args.split()[0].strip()
      filename = _appendExtensionIfMissing(filename)
      self.modificator.mergeFromFile(filename)
      self.modifiedData = True
    except Exception as x:
      _showTraceback()
      print("Couldn't read from file %s: %s" % (filename, str(x)))

  def do_showData(self, dummy):
    """
    Shows the current modified configuration
    Usage: showData
    """
    print(self.modificator)

  def do_showHistory(self, args):
    """
    Shows the last commit history
    Usage: showHistory <update limit>
    """
    try:
      argsList = args.split()
      limit = 100
      if len(argsList) > 0:
        limit = int(argsList[0])
      history = self.modificator.getHistory(limit)
      print("%s recent commits:" % limit)
      for entry in history:
        self.printPair(entry[0], entry[1], "@")
    except Exception:
      _showTraceback()

  def do_showDiffWithServer(self, dummy):
    """
    Shows diff with lastest version in server
    Usage: showDiffWithServer
    """
    try:
      diffData = self.modificator.showCurrentDiff()
      print("Diff with latest from server ( + local - remote )")
      for line in diffData:
        if line[0] in ('-'):
          print(colorize(line, "red"))
        elif line[0] in ('+'):
          print(colorize(line, "green"))
        elif line[0] in ('?'):
          print(colorize(line, "yellow"), end=' ')
    except Exception:
      _showTraceback()

  def do_showDiffBetweenVersions(self, args):
    """
    Shows diff between two versions
    Usage: showDiffBetweenVersions <version 1 with spaces> <version 2 with spaces>
    """
    try:
      argsList = args.split()
      if len(argsList) < 4:
        print("What are the two versions to compare?")
        return
      v1 = " ".join(argsList[0:2])
      v2 = " ".join(argsList[2:4])
      print("Comparing '%s' with '%s' " % (v1, v2))
      diffData = self.modificator.getVersionDiff(v1, v2)
      print("Diff with latest from server ( + %s - %s )" % (v2, v1))
      for line in diffData:
        if line[0] in ('-'):
          print(colorize(line, "red"))
        elif line[0] in ('+'):
          print(colorize(line, "green"))
        elif line[0] in ('?'):
          print(colorize(line, "yellow"), end=' ')
        else:
          print(line)
    except Exception:
      _showTraceback()

  def do_rollbackToVersion(self, args):
    """
    rolls back to user selected version of the configuration
    Usage: rollbackToVersion <version with spaces>>
    """
    try:
      argsList = args.split()
      if len(argsList) < 2:
        print("What version to rollback?")
        return
      version = " ".join(argsList[0:2])
      choice = six.moves.input("Do you really want to rollback to version %s? yes/no [no]: " % version)
      choice = choice.lower()
      if choice in ("yes", "y"):
        response = self.modificator.rollbackToVersion(version)
        if response['OK']:
          self.modifiedData = False
          print("Rolled back.")
          self.modificator.loadFromRemote()
        else:
          print("Error sending data, server said: %s" % response['Message'])
    except Exception:
      _showTraceback()

  def do_mergeWithServer(self, dummy):
    """
    Shows diff with lastest version in server
    Usage: diffWithServer
    """
    try:
      choice = six.moves.input("Do you want to merge with server configuration? yes/no [no]: ")
      choice = choice.lower()
      if choice in ("yes", "y"):
        retVal = self.modificator.mergeWithServer()
        if retVal['OK']:
          print("Merged")
        else:
          print("There was an error: ", retVal['Message'])
      else:
        print("Merge aborted")
    except Exception:
      _showTraceback()
