#!/usr/bin/python3
"""
Description: Initialization of data import
            Import the data in the sqlite database into the mysql database
Class: InitDataBase,MysqlDatabaseOperations,SqliteDatabaseOperations
"""
import os
import pathlib
import yaml
from sqlalchemy.exc import SQLAlchemyError, InternalError
from packageship.libs.dbutils.sqlalchemy_helper import DBHelper
from packageship.libs.exception import ContentNoneException
from packageship.libs.exception import DatabaseRepeatException
from packageship.libs.exception import Error
from packageship.libs.exception import ConfigurationException
from packageship.libs.configutils.readconfig import ReadConfig
from packageship.libs.log import Log
from packageship.application.models.package import SrcPack
from packageship.application.models.package import BinPack
from packageship.application.models.package import BinRequires
from packageship.application.models.package import SrcRequires
from packageship.application.models.package import BinProvides
from packageship.application.models.package import Packages
from packageship import system_config

LOGGER = Log(__name__)


class InitDataBase():
    """
    Description: Database initialization, generate multiple databases and data
                 based on configuration files

    Attributes:
        config_file_path: configuration file path
        config_file_datas: initialize the configuration content of the database
        db_type: type of database
    """

    def __init__(self, config_file_path=None):
        """
        Description: Class instance initialization

        Args:
            config_file_path: Configuration file path
        """
        self.config_file_path = config_file_path

        if self.config_file_path:
            # yaml configuration file content
            self.config_file_datas = self.__read_config_file()
        self._read_config = ReadConfig(system_config.SYS_CONFIG_PATH)
        self.db_type = 'sqlite'
        self.sql = None
        self._database = None
        self._sqlite_db = None
        self._database_engine = {
            'sqlite': SqliteDatabaseOperations,
            'mysql': MysqlDatabaseOperations
        }
        self.database_name = None
        self._tables = ['src_pack', 'bin_pack',
                        'bin_requires', 'src_requires', 'bin_provides']
        # Create life cycle related databases and tables
        if not self.create_database(db_name='lifecycle',
                                    tables=['packages_issue',
                                            'packages_maintainer'],
                                    storage=True):
            raise SQLAlchemyError(
                'Failed to create the specified database and table：lifecycle')

    def __read_config_file(self):
        """
        Read the contents of the configuration file load each
        node data in the yaml configuration file as a list to return

        Returns:
            Initialize the contents of the database configuration file
        Raises:
            FileNotFoundError: The specified file does not exist
            TypeError: Wrong type of data
        """

        if not os.path.exists(self.config_file_path):
            raise FileNotFoundError(
                'system initialization configuration file \
                    does not exist: %s' % self.config_file_path)
        # load yaml configuration file
        with open(self.config_file_path, 'r', encoding='utf-8') as file_context:
            try:
                init_database_config = yaml.load(
                    file_context.read(), Loader=yaml.FullLoader)
            except yaml.YAMLError as yaml_error:

                raise ConfigurationException(' '.join("The format of the yaml configuration\
                     file is wrong please check and try again:{0}".format(yaml_error).split()))

            if init_database_config is None:
                raise ConfigurationException(
                    'The content of the database initialization configuration file cannot be empty')
            if not isinstance(init_database_config, list):
                raise ConfigurationException(
                    ' '.join('The format of the initial database configuration file\
                        is incorrect.When multiple databases need to be initialized, \
                        it needs to be configured in the form of multiple \
                        nodes:{}'.format(self.config_file_path).split()))
            for config_item in init_database_config:
                if not isinstance(config_item, dict):
                    raise ConfigurationException(' '.join('The format of the initial database\
                        configuration file is incorrect, and the value in a single node should\
                        be presented in the form of key - val pairs: \
                        {}'.format(self.config_file_path).split()))
            return init_database_config

    def init_data(self):
        """
        Initialization of the database

        Raises:
            IOError: An error occurred while deleting the database information file
        """
        if getattr(self, 'config_file_datas', None) is None or \
                self.config_file_datas is None:
            raise ContentNoneException('The content of the database initialization \
                configuration file is empty')

        if self.__exists_repeat_database():
            raise DatabaseRepeatException(
                'There is a duplicate database configuration')
        if not InitDataBase.delete_settings_file():
            raise IOError(
                'An error occurred while deleting the database configuration file')

        for database_config in self.config_file_datas:
            if not database_config.get('dbname'):
                LOGGER.logger.error(
                    'The database name in the database initialization configuration file is empty')
                continue
            priority = database_config.get('priority')
            if not isinstance(priority, int) or priority < 0 or priority > 100:
                LOGGER.logger.error('The priority value type in the database initialization \
                    configuration file is incorrect')
                continue
            lifecycle_status_val = database_config.get('lifecycle')
            if lifecycle_status_val not in ('enable', 'disable'):
                LOGGER.logger.error('The status value of the life cycle in the initialization\
                    configuration file can only be enable or disable')
                continue
            # Initialization data
            self._init_data(database_config)

    def _create_database(self, db_name, tables, storage=False):
        """
        create related databases

        Args:
            database_config: Initialize the configuration content of the database_config
        Returns:
            The generated mysql database or sqlite database
        Raises:
            SQLAlchemyError: Abnormal database operation
        """
        _database_engine = self._database_engine.get(self.db_type)
        if not _database_engine:
            raise Error('The database engine is set incorrectly, \
            currently only the following engines are supported: %s '
                        % '、'.join(self._database_engine.keys()))
        _create_table_result = _database_engine(
            db_name=db_name, tables=tables, storage=storage).create_database(self)
        return _create_table_result

    def _init_data(self, database_config):
        """
        data initialization operation

        Args:
            database: Initialize the configuration content of the database
        Returns:

        Raises:
            ContentNoneException: Exception with empty content
            TypeError: Data type error
            SQLAlchemyError: Abnormal database operation
            IOError: An error occurred while deleting the database information file
        """

        try:
            # 1. create a database and related tables in the database
            _db_name = database_config.get('dbname')
            _create_database_result = self._create_database(
                _db_name, self._tables)
            if not _create_database_result:
                raise SQLAlchemyError(
                    'Failed to create the specified database and table：%s'
                    % database_config['dbname'])
            # 2. get the data of binary packages and source packages
            src_db_file = database_config.get('src_db_file')
            bin_db_file = database_config.get('bin_db_file')

            if src_db_file is None or bin_db_file is None:
                raise ContentNoneException(
                    'The path to the sqlite file in the database initialization configuration \
                    is incorrect ')
            if not os.path.exists(src_db_file) or not os.path.exists(bin_db_file):
                raise FileNotFoundError("sqlite file {src} or {bin} does not exist, please \
                    check and try again".format(src=src_db_file, bin=bin_db_file))
            # 3. Obtain temporary source package files and binary package files
            if self.__save_data(database_config,
                                self.database_name):
                # Update the configuration file of the database
                database_content = {
                    'database_name': _db_name,
                    'priority': database_config.get('priority'),
                }
                InitDataBase.__updata_settings_file(
                    database_content=database_content)

        except (SQLAlchemyError, ContentNoneException, TypeError,
                Error, FileNotFoundError) as error_msg:
            LOGGER.logger.error(error_msg)
            # Delete the specified database
            self.__del_database(_db_name)

    def __del_database(self, db_name):
        try:
            _database_engine = self._database_engine.get(self.db_type)
            del_result = _database_engine(db_name=db_name).drop_database()
            return del_result
        except (IOError, Error) as exception_msg:
            LOGGER.logger.error(exception_msg)
            return False

    @staticmethod
    def __columns(cursor):
        """
        functional description:Returns all the column names
        queried by the current cursor

        Args:
            cursor: Cursor
        Returns:
            The first columns
        Raises:

        """
        return [col[0] for col in cursor.description]

    def __get_data(self):
        """
        According to different sql statements, query related table data

        Args:

        Returns:

        Raises:

        """
        if self.sql is None:
            return None
        try:
            src_packages_data = self._database.session.execute(self.sql)
            columns = InitDataBase.__columns(
                src_packages_data.cursor)
            return [dict(zip(columns, row)) for row in src_packages_data.fetchall()]
        except SQLAlchemyError as sql_error:
            LOGGER.logger.error(sql_error)
            return None

    def __save_data(self, database_config, db_name):
        """
        integration of multiple data files

        Args:
            src_package_paths: Source package database file
            bin_package_paths: Binary package database file
        Returns:
            Path of the generated temporary database file
        Raises:

        """
        src_db_file = database_config.get('src_db_file')
        bin_db_file = database_config.get('bin_db_file')
        table_name = database_config.get('dbname')
        lifecycle_status_val = database_config.get('lifecycle')
        try:
            with DBHelper(db_name=src_db_file, db_type='sqlite:///', complete_route_db=True) \
                    as database:
                self._database = database
                # Save data related to source package
                self._save_src_packages(
                    db_name, table_name, lifecycle_status_val)
                self._save_src_requires(db_name)

            with DBHelper(db_name=bin_db_file, db_type='sqlite:///', complete_route_db=True)\
                    as database:
                self._database = database
                # Save binary package related data
                self._save_bin_packages(db_name)
                self._save_bin_requires(db_name)
                self._save_bin_provides(db_name)
        except (SQLAlchemyError, ContentNoneException) as sql_error:
            LOGGER.logger.error(sql_error)
            self.__del_database(db_name)
            return False
        else:
            return True

    def _save_src_packages(self, db_name, table_name, lifecycle_status_val):
        """
        Save the source package data

        Args:
            db_name: Saved database name
        Returns:

        Raises:

        """
        # Query all source packages
        self.sql = " select * from packages "
        packages_datas = self.__get_data()
        if packages_datas is None:
            raise ContentNoneException(
                '{db_name}:There is no relevant data in the source \
                    package provided '.format(db_name=db_name))
        with DBHelper(db_name=db_name) as database:
            database.batch_add(packages_datas, SrcPack)
        if lifecycle_status_val == 'enable':
            InitDataBase._storage_packages(table_name, packages_datas)

    @staticmethod
    def _storage_packages(table_name, package_data):
        """
            Bulk storage of source code package data
        """
        add_packages = []
        cls_model = Packages.package_meta(table_name)
        pkg_keys = ('name', 'url', 'rpm_license', 'version',
                    'release', 'summary', 'description')
        with DBHelper(db_name="lifecycle") as database:
            if table_name not in database.engine.table_names():
                database.create_table([table_name])
            # Query data that already exists in the database
            exist_packages_dict = dict()
            for pkg in database.session.query(cls_model).all():
                exist_packages_dict[pkg.name] = pkg
            _packages = []
            for pkg in package_data:
                _package_dict = {key: pkg[key] for key in pkg_keys}
                _packages.append(_package_dict)

            # Combine all package data, save or update
            for package_item in _packages:
                package_model = exist_packages_dict.get(package_item['name'])
                if package_model:
                    for key, val in package_item.items():
                        setattr(package_model, key, val)
                else:
                    add_packages.append(package_item)

            if add_packages:
                database.batch_add(add_packages, cls_model)
                database.session.commit()

    def _save_src_requires(self, db_name):
        """
        Save the dependent package data of the source package

        Args:
            db_name：Name database
        Returns:

        Raises:

        """
        # Query all source packages
        self.sql = " select * from requires "
        requires_datas = self.__get_data()
        if requires_datas is None:
            raise ContentNoneException('{db_name}: The package data that the source package \
                depends on is empty'.format(db_name=db_name))
        with DBHelper(db_name=db_name) as database:
            database.batch_add(requires_datas, SrcRequires)

    def _save_bin_packages(self, db_name):
        """
        Save binary package data

        Args:
            db_name：Name database
        Returns:

        Raises:

        """
        self.sql = " select * from packages "
        bin_packaegs = self.__get_data()
        if bin_packaegs is None:
            raise ContentNoneException(
                '{db_name}:There is no relevant data in the provided \
                     binary package '.format(db_name=db_name))
        for index, bin_package_item in enumerate(bin_packaegs):
            try:
                src_package_name = bin_package_item.get('rpm_sourcerpm').split(
                    '-' + bin_package_item.get('version'))[0]
            except AttributeError as exception_msg:
                src_package_name = None
                LOGGER.logger.warning(exception_msg)
            finally:
                bin_packaegs[index]['src_name'] = src_package_name

        with DBHelper(db_name=db_name) as database:
            database.batch_add(bin_packaegs, BinPack)

    def _save_bin_requires(self, db_name):
        """
        Save the dependent package data of the binary package

        Args:
            db_name：Name database
        Returns:

        Raises:

        """
        self.sql = " select * from requires "
        requires_datas = self.__get_data()
        if requires_datas is None:
            raise ContentNoneException(
                '{db_name}:There is no relevant data in the provided binary \
                    dependency package'.format(db_name=db_name))

        with DBHelper(db_name=db_name) as database:
            database.batch_add(requires_datas, BinRequires)

    def _save_bin_provides(self, db_name):
        """
        Save the component data provided by the binary package

        Args:
            db_name：Name database
        Returns:

        Raises:

        """
        self.sql = " select * from provides "
        provides_datas = self.__get_data()
        if provides_datas is None:
            raise ContentNoneException(
                '{db_name}:There is no relevant data in the provided \
                    binary component '.format(db_name=db_name))

        with DBHelper(db_name=db_name) as database:
            database.batch_add(provides_datas, BinProvides)

    def __exists_repeat_database(self):
        """
        Determine if the same database name exists

        Returns:
            True if there are duplicate databases, false otherwise
        Raises:

        """
        db_names = [name.get('dbname')
                    for name in self.config_file_datas]

        if len(set(db_names)) != len(self.config_file_datas):
            return True

        return False

    @staticmethod
    def __updata_settings_file(**Kwargs):
        """
        update some configuration files related to the database in the system

        Args:
            **Kwargs: data related to configuration file nodes
            database_name: Name database
        Returns:

        Raises:
            FileNotFoundError: The specified file was not found
            IOError: File or network operation io abnormal
        """
        try:
            if not os.path.exists(system_config.DATABASE_FILE_INFO):
                pathlib.Path(system_config.DATABASE_FILE_INFO).touch()
            with open(system_config.DATABASE_FILE_INFO, 'a+', encoding='utf8') as file_context:
                setting_content = []
                if 'database_content' in Kwargs.keys():
                    content = Kwargs.get('database_content')
                    if content:
                        setting_content.append(content)
                yaml.dump(setting_content, file_context)

        except FileNotFoundError as not_found:
            LOGGER.logger.error(not_found)
        except IOError as exception_msg:
            LOGGER.logger.error(exception_msg)

    @staticmethod
    def delete_settings_file():
        """
        Delete the configuration file of the database

        Args:

        Returns:
            True if the deletion is successful, otherwise false
        Raises:
            IOError: File or network operation io abnormal
        """

        try:
            if os.path.exists(system_config.DATABASE_FILE_INFO):
                os.remove(system_config.DATABASE_FILE_INFO)
        except (IOError, Error) as exception_msg:
            LOGGER.logger.error(exception_msg)
            return False
        else:
            return True

    def delete_db(self, db_name):
        """
        delete the database

        Args:
            db_name: The name of the database
        Returns:

        Raises:
            IOError: File or network operation io abnormal
        """
        try:
            del_result = True
            file_read = open(
                system_config.DATABASE_FILE_INFO, 'r', encoding='utf-8')
            _databases = yaml.load(
                file_read.read(), Loader=yaml.FullLoader)
            for database in _databases:
                if database.get('database_name') == db_name:
                    _databases.remove(database)
            # Delete the successfully imported database configuration node
            with open(system_config.DATABASE_FILE_INFO, 'w+', encoding='utf-8') as file_context:
                yaml.safe_dump(_databases, file_context)
        except (IOError, Error) as del_config_error:
            LOGGER.logger.error(del_config_error)
            del_result = False
        finally:
            file_read.close()

        if del_result:
            del_result = self.__del_database(db_name)

        return del_result

    def create_database(self, db_name, tables=None, storage=True):
        """
            Create databases and tables related to the package life cycle

            Args:
                db_name: The name of the database
                tables: Table to be created
        """
        _create_database_result = self._create_database(
            db_name, tables, storage)
        return _create_database_result


class MysqlDatabaseOperations():
    """
    Related to database operations, creating databases, creating tables

    Attributes:
        db_name: The name of the database
        create_database_sql: SQL statement to create a database
        drop_database_sql: Delete the SQL statement of the database
    """

    def __init__(self, db_name, tables=None, storage=False):
        """
        Class instance initialization

        Args:
            db_name: Database name
        """
        self.db_name = db_name
        self.create_database_sql = ''' CREATE DATABASE if not exists `{db_name}` \
                                    DEFAULT CHARACTER SET utf8mb4; '''.format(db_name=self.db_name)
        self.drop_database_sql = '''drop DATABASE if exists `{db_name}` '''.format(
            db_name=self.db_name)
        self.tables = tables
        self.storage = storage

    def create_database(self, init_db):
        """
        create a mysql database

        Returns:
            True if successful, otherwise false
        Raises:
            SQLAlchemyError: An exception occurred while creating the database
        """
        _create_success = True
        if isinstance(init_db, InitDataBase):
            init_db.database_name = self.db_name
        with DBHelper(db_name='mysql') as data_base:

            try:
                # create database
                if not self.storage:
                    data_base.session.execute(self.drop_database_sql)
                data_base.session.execute(self.create_database_sql)
            except InternalError as internal_error:
                LOGGER.logger.info(internal_error)
            except SQLAlchemyError as exception_msg:
                LOGGER.logger.error(exception_msg)
                return False
        if self.tables:
            _create_success = self.__create_tables()
        return _create_success

    def drop_database(self):
        """
        Delete the database according to the specified name

        Args:
            db_name: The name of the database to be deleted
        Returns:
            True if successful, otherwise false
        Raises:
            SQLAlchemyError: An exception occurred while creating the database
        """
        if self.db_name is None:
            raise IOError(
                "The name of the database to be deleted cannot be empty")
        with DBHelper(db_name='mysql') as data_base:
            drop_database = '''  drop DATABASE if exists `{db_name}` '''.format(
                db_name=self.db_name)
            try:
                data_base.session.execute(drop_database)
            except SQLAlchemyError as exception_msg:
                LOGGER.logger.error(exception_msg)
                return False
            else:
                return True

    def __create_tables(self):
        """
        Create the specified data table

        Returns:
            True if successful, otherwise false
        Raises:
            SQLAlchemyError: An exception occurred while creating the database
        """
        try:
            with DBHelper(db_name=self.db_name) as database:
                if self.tables:
                    _tables = list(set(self.tables).difference(
                        set(database.engine.table_names())))
                    database.create_table(_tables)

        except SQLAlchemyError as exception_msg:
            LOGGER.logger.error(exception_msg)
            return False
        else:
            return True


class SqliteDatabaseOperations():
    """
    sqlite database related operations

    Attributes:
        db_name: Name database
        database_file_folder: Database folder path
    """

    def __init__(self, db_name, tables=None, storage=False, ** kwargs):
        """
        Class instance initialization

        Args:
            db_name: Database name
            kwargs: data related to configuration file nodes
        """
        self.db_name = db_name
        self._read_config = ReadConfig(system_config.SYS_CONFIG_PATH)
        if getattr(kwargs, 'database_path', None) is None:
            self._database_file_path()
        else:
            self.database_file_folder = kwargs.get('database_path')
        self.tables = tables
        self.storage = storage

    def _database_file_path(self):
        """
        Database file path

        Returns:

        Raises:
            IOError: File or network operation io abnormal
        """
        self.database_file_folder = self._read_config.get_system(
            'data_base_path')
        if not self.database_file_folder:
            self.database_file_folder = system_config.DATABASE_FOLDER_PATH

        if not os.path.exists(self.database_file_folder):
            try:
                os.makedirs(self.database_file_folder)
            except IOError as makedirs_error:
                LOGGER.logger.error(makedirs_error)
                self.database_file_folder = None

    def create_database(self, init_db):
        """
        create sqlite database and table

        Returns:
            After successful generation, return the database file address,
            otherwise return none
        Raises:
            FileNotFoundError: The specified folder path does not exist
            SQLAlchemyError: An error occurred while generating the database
        """
        _create_success = False
        if self.database_file_folder is None:
            raise FileNotFoundError('Database folder does not exist')

        _db_file = os.path.join(
            self.database_file_folder, self.db_name)

        if not self.storage and os.path.exists(_db_file + '.db'):
            os.remove(_db_file + '.db')

        # create a sqlite database
        with DBHelper(db_name=_db_file) as database:
            try:
                if self.tables:
                    _tables = list(set(self.tables).difference(
                        set(database.engine.table_names())))
                    database.create_table(_tables)
            except (SQLAlchemyError, InternalError) as create_table_err:
                LOGGER.logger.error(create_table_err)
                return _create_success
        if isinstance(init_db, InitDataBase):
            init_db.database_name = _db_file
            _create_success = True
        return _create_success

    def drop_database(self):
        """
        Delete the specified sqlite database

        Returns:
            Return true after successful deletion, otherwise return false
        Raises:
            IOError: An io exception occurred while deleting the specified database file
        """
        try:
            db_path = os.path.join(
                self.database_file_folder, self.db_name + '.db')
            if os.path.exists(db_path):
                os.remove(db_path)
        except IOError as exception_msg:
            LOGGER.logger.error(exception_msg)
            return False
        else:
            return True
