#!/usr/bin/env python
#
# Copyright 2016-2018, 2021 VMware, Inc.  All rights reserved. -- VMware Confidential
#
"""
This module contains functions for reading and manipulating properties files
"""

import logging
import os
import pathlib
import re
import shutil

REGEX_WHITESPACE = "[ \\t]*"
REGEX_KEY_FINDER_TEMPLATE = "^%s" + REGEX_WHITESPACE + "="
REGEX_VALUE_FINDER_GROUP_NAME = "value"
REGEX_VALUE_FINDER_TEMPLATE = REGEX_KEY_FINDER_TEMPLATE +\
                              REGEX_WHITESPACE +\
                              "(?P<" + REGEX_VALUE_FINDER_GROUP_NAME + ">.*)"
PROPERTY_TEMPLATE = "%s = %s\n"

BACKUP_DIR = "upgrade_auto_backup"

README_NAME = "readme.txt"
README_TEXT = "This directory consists of backup files created automatically during upgrade. \n"

logger = logging.getLogger(__name__)

def backup(file):
   """
   Copies the supplied file to a specific directory.
   Does nothing if there is a previous backup file.

   :param file: Path to the file
   :type file: String

   :return: The path to the resulting file
   :rtype: String
   """

   dir_name = os.path.dirname(file)
   backup_dir_path = os.path.join(dir_name, BACKUP_DIR)

   if not os.path.exists(backup_dir_path):
      os.mkdir(backup_dir_path)

   backup_file_path = _copy_file_and_metadata(file, backup_dir_path)

   _add_readme_to_dir(backup_dir_path)

   return backup_file_path

def revert_backup_files(dir_name):
   """
   Restores backup files.

   :param dir_name: Path to the file
   :type dir_name: String
   """

   backup_dir_path = os.path.join(dir_name, BACKUP_DIR)

   # The upgrade might be cancelled during the expand of the vapi endpoint
   # and at the point before the backup is created. So if there is no backup
   # directory - do nothing.
   if not os.path.exists(backup_dir_path):
      logger.info("Backup dir does not exist %s", backup_dir_path)
      logger.info("No backups to restore.")
      return

   for file in os.listdir(backup_dir_path):
      new_file = os.path.join(dir_name, file)
      if os.path.exists(new_file):
         os.remove(new_file)

      backup_file = os.path.join(backup_dir_path, file)
      _copy_file_and_metadata(backup_file, new_file)

      logger.info("Backup of %s is restored", new_file)

   logger.info("Restoration of backup files complete")

def delete_backup_files(dir_name):
   """
   Removes all backup files for the given directory.

   :param dir_name: Path to the dir
   :type dir_name: String
   """

   backup_dir_path = os.path.join(dir_name, BACKUP_DIR)
   if os.path.exists(backup_dir_path):
      shutil.rmtree(os.path.join(dir_name, BACKUP_DIR))

def _copy_file_and_metadata(src, dst):
   """
   Copies a file and its metadata. Does nothing if the dst already exists.
   If a directory is given for the destionation the name of the source file
   will be used.

   :param src: Path to source file
   :type src: String

   :param dst: Path to destination file or just destionation dir.
   :type dst: String

   :return: Destination file name
   :rtype: String
   """

   backup_file_path = os.path.join(dst, os.path.basename(src)) if os.path.isdir(dst) else dst

   if os.path.exists(backup_file_path):
      logger.info("There is already a backup of %s. Do nothing.", src)
   else:
      # copy the file and its metadata; However copy2 is unable to copy all file
      # metadata on posix platforms, so we have to change the owner and group
      # manually.
      backup_file_path = shutil.copy2(src, dst)

      path_object = pathlib.Path(src)
      owner = path_object.owner()
      group = path_object.group()

      shutil.chown(backup_file_path, owner, group)

      logger.info("%s backed up to %s", src, backup_file_path)

   return backup_file_path

def _add_readme_to_dir(dir_name):
   """
   Adds a readme with information about the backup directory.
   If a readme already exists it is replaced by the new one.
   """

   readme_path = os.path.join(dir_name, README_NAME)
   with open(readme_path, "w") as fp_readme:
      fp_readme.write(README_TEXT)

   logger.info("Readme for backup dir %s created.", dir_name)

def add(file, key, value):
   """
   Adds a property to the specified file if the property does not exist;
   Otherwise, does nothing.

   :param file: Path to the file
   :type file: String
   :param key: Key of the property to add
   :type key: String
   :param value: Value of the property to add
   :type value: String

   :return: True if the property was added; otherwise, False
   :rtype: Boolean
   """

   with open(file, "r+") as fp:
      content = fp.read()
      if not re.search(_build_regex(REGEX_KEY_FINDER_TEMPLATE, key), content, re.MULTILINE):
         # Go to end of file first; otherwise, write may fail on some systems
         fp.seek(0, 2)

         if not content.endswith("\n"):
            fp.write("\n")
         fp.write(PROPERTY_TEMPLATE % (key, value))

         logger.info("Property %s has been added to %s", key, file)
         return True

   return False

def delete(file, key):
   """
   Deletes a property from the specified file if the property exists;
   otherwise, does nothing.

   :param file: Path to the file
   :type file: String
   :param key: Key of the property to delete
   :type key: String

   :return: True if the property was deleted; otherwise False
   :rtype: Boolean
   """

   return replace(file, key, None)

def replace(file, key, value):
   """
   Replaces a property in the specified file if the property exists;
   otherwise, does nothing.

   :param file: Path to the file
   :type file: String
   :param key: Key of the property to add
   :type key: String
   :param value: Value of the property to add
   :type key: String

   :return: True if the property was replaced; otherwise False
   :rtype: Boolean
   """

   regex = re.compile(_build_regex(REGEX_KEY_FINDER_TEMPLATE, key))
   result = False

   with open(file, "r+") as fp:
      lines = fp.readlines()

      # Goes to the beginning of the file
      fp.seek(0)

      # Writes all the valid lines
      for line in lines:
         if regex.match(line):
            result = True
            if value is not None:
               fp.write(PROPERTY_TEMPLATE % (key, value))
               logger.info("Property %s has been replaced in %s", key, file)
            else:
               logger.info("Property %s has been deleted from %s", key, file)
         else:
            fp.write(line)

      # Scrubs the rest
      fp.truncate()

   return result

def value(file, key):
   """
   Gets the value of a property from the specified file.

   :param file: Path to the file
   :type file: String
   :param key: Key of the property
   :type key: String

   :return: Value of the property if found; otherwise, None
   :rtype: String
   """

   with open(file) as fp:
      content = fp.read()
      match = re.search(_build_regex(REGEX_VALUE_FINDER_TEMPLATE, key),
                        content,
                        re.MULTILINE)
      if match:
         return match.group(REGEX_VALUE_FINDER_GROUP_NAME)

   return None

def _build_regex(template, key):
   return template % re.escape(key)
