#!/usr/bin/env python
# Copyright 2020-2021 VMware, Inc.  All rights reserved. -- VMware Confidential
# coding: utf-8

# This script is used to run once at the first boot

import logging
import os
import shutil
import re

from cis.tools import gen_random_pwd, get_install_parameter
from cis.exceptions import ServiceStartException, ServiceWaitException
from cis.dbutils import prepare_db, get_embedded_db_port, db_is_ready
from cis.utils import invoke_command, FileBuffer, wait_for_svc, quote
from cis.defaults import LOG_QUIET
from cis.baseCISException import BaseInstallException
from cryptography.fernet import Fernet
from .utils import get_psql_connection, setup_logging
from .const import DB_DNS, DB_PROVIDER, DB_SCHEMA, DB_SERVER_NAME, \
    DB_USER, LOG_FILE
from .upgrade import execute_expand, execute_contract

import featureState
featureState.init(enableLogging=False)

logger = logging.getLogger(__name__)
setup_logging(logger)

VLCM_INFO = {
   'vlcmdriver': "org.postgresql.Driver",
   'vlcmurl': "vlcm:postgresql://%SERVER_NAME%:%SERVER_PORT%/%DB_INSTANCE%",
   'port': "5432"
   }


class DbSetup(object):
   """
   Implements a class that handles the database initialization and setup
   """
   def __init__(self, tpl_path, sql_file_path):
      self._config_path = os.path.abspath(os.path.join(tpl_path, "vlcm_db"))
      self._sql_file_path = "/usr/lib/vmware-vlcm/db_config/sql"
      self._pg_svc_name = "vmware-vpostgres"


   def get_embedded_db_params(self):
      self._db_instance = get_install_parameter('db.instance', 'VCDB')
      self._db_dsn = DB_DNS
      self._db_user = DB_USER
      self._db_schema = DB_SCHEMA
      self._db_pass = gen_random_pwd()
      # Defined in cis/dbutils.
      # Gets port from the install parameter
      # if no port is specified returns the default port
      self._db_server_port = get_embedded_db_port()
      self._db_provider = DB_PROVIDER
      self._db_server_name = DB_SERVER_NAME

   def wait_for_db(self):
      logger.debug("Waiting for DB to start...")
      if wait_for_svc(self._pg_svc_name, wait_time=600):
         logger.debug("ERROR failed waiting for DB to start")
         raise ServiceStartException(self._pg_svc_name)

      if not db_is_ready(port=self._db_server_port):
         logger.debug("ERROR waiting for DB to accept connections")
         raise ServiceWaitException(self._pg_svc_name)

      logger.debug("Successfully waited for DB to start and accept connections")


   def prepare_embedded_db(self):
      # Create DB user and schema
      sql_script = os.path.join(self._sql_file_path,
                                'vLCM_Initialsetup_PostgreSQL.sql')
      script_vars = {
         'quoted_dbname': quote(self._db_instance),
         'dbschema': self._db_schema,
         'quoted_dbschema':  quote(self._db_schema),
         'dbuser': self._db_user,
         'quoted_dbpass': '\'%s\'' % self._db_pass
      }
      prepare_db(self._db_instance, sql_script, script_vars, LOG_QUIET)

      key = Fernet.generate_key()
      keyfile = os.path.abspath(os.path.join(self._config_path, "encrypt.txt"))

      with os.fdopen(os.open(keyfile, os.O_CREAT | os.O_WRONLY, 0o600), "w") as f:
         f.write(key.decode())



   def do_vlcm_write(self):
      vlcm_properties_in = os.path.join(self._config_path, "vlcm.properties.tpl")
      vlcm_properties_out = os.path.join(self._config_path, "vlcm.properties")

      vlcm_url = VLCM_INFO['vlcmurl']
      vlcm_url = vlcm_url.replace("%SERVER_NAME%", self._db_server_name)
      vlcm_url = vlcm_url.replace("%DB_INSTANCE%", self._db_instance)
      vlcm_url = vlcm_url.replace("%SERVER_PORT%", self._db_server_port)

      replacements = {
         "DRIVER_NAME": VLCM_INFO["vlcmdriver"],
         "DB_TYPE": self._db_provider,
         "URL": vlcm_url,
         "USER_ID": self._db_user,
         "USER_PASS": self._db_pass
      }
      with open(vlcm_properties_in, "r") as f_in:
         with os.fdopen(
            os.open(vlcm_properties_out, os.O_CREAT | os.O_WRONLY, 0o600), "w"
            ) as f_out:
            for line in f_in.readlines():
               line = line.split("%")
               if len(line) > 1:
                  line[1] = replacements[line[1]]

               f_out.write("".join(line))


   def do_db_write_all(self):
      # create DB schema
      self.prepare_embedded_db()

      # create vlcm url
      self.do_vlcm_write()

      # Create tables
      fssValues = {
      }
      execute_expand(fssValues)
      execute_contract(fssValues)


   def do_db_setup(self):
      if not self.setup_precheck():
         return

      #  /etc/vmware-vlcm/vlcm_db
      source = os.path.abspath(
         os.path.join(os.path.dirname(__file__), "vlcm.properties.tpl")
         )
      destination = self._config_path
      if not os.path.exists(destination):
         os.makedirs(destination)
      shutil.move(source, destination)

      self.get_embedded_db_params()

      # Wait if the postgres service has not yet started if using embedded db
      self.wait_for_db()

      # do_db_write_all
      self.do_db_write_all()


   def setup_precheck(self):
      """
      Check for a number of conditions that determine if setup should run or not
      :return: True if setup should continue, false if setup should be skipped
      :rtype: Boolean
      :raise: BaseInstallException if invalid database state
      """
      def validate_auth_file():
         """
         Validates /etc/vmware-vlcm/vlcm_db/vlcm.properties
         :return: True if setup should continue, false if setup should be skipped
         :rtype: Boolean
         :raise: BaseInstallException if invalid database state
         """
         auth_file = os.path.abspath(os.path.join(self._config_path, "vlcm.properties"))
         if not os.path.exists(auth_file):
            logger.debug("No previous authentication file found")
            return True

         try:
            (conn, cursor) = get_psql_connection()
         except:
            raise BaseInstallException("Invalid credentials for the vLCM database. Setup aborted")

         cursor.execute(
            "SELECT EXISTS(SELECT * FROM information_schema.tables WHERE table_name = %s);", ('vlcm_task',)
         )
         if cursor.fetchone()[0]:
            logger.debug("Authentication file and vLCM_task table exist")
            return False
         else:
            raise BaseInstallException(
               "Authentication file exists, but vLCM database does not. Setup aborted."
            )


      def validate_encryption_file():
         """
         Validates /etc/vmware-vlcm/vlcm_db/encrypt.txt
         :return: True if setup should continue, false if setup should be skipped
         :rtype: Boolean
         :raise: BaseInstallException if invalid database state
         """
         encryption_file = os.path.abspath(os.path.join(self._config_path, "encrypt.txt"))

         if not os.path.exists(encryption_file):
            logger.debug("No previous encryption file found")
            return True

         try:
            with open(encryption_file, "r") as f:
               FERNET = Fernet(f.readline().encode())
               logger.debug("Encryption key found")
               return False
         except (FileNotFoundError, IOError):
            raise BaseInstallException("Unable to parse encryption key from file. Setup aborted.")


      def validate_source_files():
         """
         Check if template and sql files exist in firstboot
         :return: None
         :raise: BaseInstallException if files do not exist
         """
         if os.path.exists(os.path.abspath(os.path.join(os.path.dirname(__file__), "vlcm.properties.tpl"))):
            logger.debug("Template and sql files found in firstboot directory")
         else:
            raise BaseInstallException("Invalid state of file locations. Setup aborted.")


      auth_check = validate_auth_file()
      encryption_check = validate_encryption_file()

      if not auth_check and not encryption_check:
         logger.debug("Previous authentication and encryption information found. Skipping setup.")
         return False
      elif auth_check != encryption_check:
         raise BaseInstallException("Invalid database state. Setup aborted.")

      validate_source_files()
      logger.debug("Database state consistent with fresh installation. Continuing setup.")
      return True

