import argparse
from ast import parse
from genericpath import isdir
import sys
import json
import csv
import os
import os.path
import sys
import re
import requests
from datetime import datetime

from dump_warnings import CommandLineException, add_analysis_args, add_filtering_args, add_network_args, parse_options
from dump_warnings import process_analysis_args, invoke_codesonar_get, warnings_search_url, GtrCsvWriter

SUPPORTED_PLATFORMS = ('qlik',)
SUPPORTED_DATA_COLLECTIONS = ('warning',)


class Log(object):
    @staticmethod
    def error(message):
        print("Error: {}" .format(message))

    @staticmethod
    def warning(message):
        print("Warning: {}" .format(message))

    @staticmethod
    def success(message):
        print("Success: {}" .format(message))

    @staticmethod
    def info(message):
        print("Info: {}" .format(message))

    @staticmethod
    def message(message):
        print(message)

    @staticmethod
    def exit_with_failure():
        sys.exit(1)

class Validator(object):

    def __init__(self, pattern):
        self._pattern = re.compile(pattern)

    def __call__(self, value):
        if not self._pattern.match(value):
            raise argparse.ArgumentTypeError(
                "Argument has to match '{}'".format(self._pattern.pattern))
        return value

class Qlik(object):
    date_start = datetime.now()
    host = ''
    token = ''
    max_file_size = 0

    def __init__(self, host: str, token: str, max_file_size: int):
        self.host = host
        self.token = token
        self.max_file_size = max_file_size

    @staticmethod
    def get_config_model(host='', auth_key='', space='codesonar'):
        default_max_size_of_uploaded_file = 500
        return {
            "tool": "qlik",
            "host": host,
            "authKey": auth_key,
            "space": space,
            "maxFileSize": 1024 * 1024 * (default_max_size_of_uploaded_file - 5)
        }

    def header(self, contentType='application/json'):
        header = {
            'Authorization': 'Bearer ' + self.token
        }
        if contentType is not None:
            header['Content-type'] = contentType
        return header

    def __error_handler(self, response, fnName=''):
        try:
            response.raise_for_status()
        except requests.exceptions.HTTPError as e:
            if fnName:
                Log.error("Http Error in {}".format(fnName))
            Log.error(e.args)
            Log.error(e.response.text)
            Log.exit_with_failure()
        except requests.exceptions.RequestException as e:
            if fnName:
                Log.error("Http Error in {}".format(fnName))
            Log.error(e.args)
            Log.exit_with_failure()

    def get_status_connection(self):
        headers = self.header(None)
        is_connected = False
        try:
            response = requests.get(
                self.host + '/api/v1/data-files/quotas', headers=headers)
            self.__error_handler(response, 'get_status_connection')
            response_body = response.json()
            if response_body['maxFileSize'] > 0:
                is_connected = True
        except requests.exceptions.RequestException as e:
            Log.error("RequestException get_status_connection {}".format(e.args))
        except json.JSONDecodeError as e:
            Log.error("JSONDecodeError get_status_connection {}".format(e))
        
        return is_connected

    def create_space(self, spaceName):
        headers = self.header(None)
        space = None

        try:
            # TODO make this work with on-prem qlik servers using self-signed TLS certificates
            response = requests.post(
                self.host + '/api/v1/spaces', json={"name": spaceName, "type": "shared"}, headers=headers)
            self.__error_handler(response, 'create_space')
            response_body = response.json()

            if 'id' in response_body:
                space = response_body['id']

        except requests.exceptions.RequestException as e:
            Log.error("RequestException create_space {}".format(e.args))
            Log.exit_with_failure()
        except json.JSONDecodeError as e:
            Log.error("JSONDecodeError create_space {}".format(e))
            Log.exit_with_failure()

        return space

    def get_space_id(self, spaceName: str):
        headers = self.header()
        space = None

        try:
            response = requests.get(
                self.host + '/api/v1/spaces', params={'name': spaceName}, headers=headers)
            self.__error_handler(response, 'get_space_id')
            response_body = response.json()

            if len(response_body['data']) > 0 and 'id' in response_body['data'][0]:
                space = response_body['data'][0]['id']

        except requests.exceptions.RequestException as e:
            Log.error("RequestException get_space_id {}".format(e.args))
            Log.exit_with_failure()
        except json.JSONDecodeError as e:
            Log.error("JSONDecodeError get_space_id {}".format(e))
            Log.exit_with_failure()

        return space

    def get_connection_id_by_space_id(self, spaceId: str) -> str | None:
        headers = self.header()
        connection_id = None

        try:
            response = requests.get(
                self.host + '/api/v1/data-files/connections', params={'spaceId': spaceId}, headers=headers)
            self.__error_handler(response, 'get_connection_id_by_space_id')
            response_body = response.json()
            connection_id = response_body['data'][0]['id']
        except requests.exceptions.RequestException as e:
            Log.error(e.args)
            Log.exit_with_failure()
        except json.JSONDecodeError as e:
            Log.error("JSONDecodeError get_space_id {}".format(e))
            Log.exit_with_failure()

        return connection_id

    def prepare_collection_for_files(self, space_name: str) -> str:
        space = self.get_space_id(space_name)

        if space is None:
            space = self.create_space(space_name)

        space_id = space

        connection_id = self.get_connection_id_by_space_id(space_id)
        Log.success('Preparation Qlik for uploading files')

        return connection_id

    def upload_data_collection_from_file_inline(self, connection_id: str, file_name: str, file_path: str):
        Log.info("Start uploading {}".format(file_name))
        headers = self.header(None)
        res = None

        data = {
            "Json": json.dumps({"name": file_name, "connectionId": connection_id})
        }
        with open(file_path, 'rb') as file_stream:
            files = {'File': file_stream}

            try:
                response = requests.post(
                    self.host + '/api/v1/data-files', files=files, data=data, headers=headers)
                self.__error_handler(response, 'upload_data_collection')
                res = response.json()
                Log.success("Uploaded file {}".format(file_name))
            except requests.exceptions.RequestException as e:
                Log.error(
                    "RequestException upload_data_collection {}".format(e.args))
                Log.exit_with_failure()
            except json.JSONDecodeError as e:
                Log.error("JSONDecodeError get_space_id {}".format(e))
                Log.exit_with_failure()

        return res


class ConfigManager(object):
    __dataConfig = None

    def __new__(cls):
        if not hasattr(cls, 'instance'):
            cls.instance = super(ConfigManager, cls).__new__(cls)
        return cls.instance

    def __load_config_file(self, path_config_file: str):
        if not os.path.exists(path_config_file):
            Log.error(
                'Config file was not found. Please create config file before')
            Log.exit_with_failure()

        try:
            with open(path_config_file, 'r') as f:
                self.__dataConfig = json.load(f)
        except IOError as e:
            Log.error(e)
        except json.JSONDecodeError as e:
            Log.error('Invalid config file format')
            Log.error(e)

    def create_config_file(self, path_config_file: str, config):
        try:
            with open(path_config_file, 'w') as file:
                json.dump(config, file, indent=4)
        except IOError as e:
            Log.error(e)
            Log.exit_with_failure()

    def get_bi_config(self, path_config_file: str):
        if self.__dataConfig is None:
            self.__load_config_file(path_config_file)

        if not self.__dataConfig:
            Log.error("Invalid configuration file. There are no data")
            Log.exit_with_failure()

        if not 'tool' in self.__dataConfig:
            Log.error("Invalid configuration file. There are no BI tool name")
            Log.exit_with_failure()
        
        if not self.__dataConfig['tool'] in SUPPORTED_PLATFORMS:
            Log.error("The script does not support {}".format(self.__dataConfig['tool']))
            Log.exit_with_failure()

        return self.__dataConfig


class DataManager(object):

    @staticmethod
    def convert_bytes(size):
        """ Convert bytes to KB, or MB or GB"""
        for x in ['bytes', 'KB', 'MB', 'GB', 'TB']:
            if size < 1024.0:
                break
            size /= 1024.0
        return "%3.1f %s" % (size, x)

    def download_warnings(self, options: dict, args: argparse.Namespace) -> str:
        try:
            hub, aid = process_analysis_args(
                args, extra_spec_args=(args.search,))
        except CommandLineException as e:
            Log.error(e.args)
            Log.exit_with_failure()

        url = warnings_search_url('csv', args, options, hub, aid)

        cmd_kwargs = {}
        cmd_kwargs['encoding'] = 'utf-8'

        file_name = "warnings.csv"
        if args.download is not None:
            file_name = args.download

        with invoke_codesonar_get(args, file_name, url, cmd_kwargs) as p:
            Log.message("Downloading...")
            if p.wait():
                try:
                    os.remove(file_name)
                except OSError:
                    pass
                Log.exit_with_failure()
            Log.success(f"The file {file_name!r} was downloaded")
        return file_name

    def download_data(self, data_name: str, options: dict, args: argparse.Namespace) -> str:
        file_path = None
        if data_name == 'warning':
            file_path = self.download_warnings(options, args)

        if file_path is not None:
            file_size = os.path.getsize(file_path)
            size_value = DataManager.convert_bytes(file_size)
            Log.info("Size of file: {}".format(size_value))

        return file_path

    def separate_file_at_chunks(self, file_path: str, file_size_limit: int, file_size: int, out_dir: str):
        def generate_file_name(base_name: str, index: int, ext: str, out_dir: str):
            return "{}{}_{}{}".format(out_dir, base_name, index, ext)

        list_of_files = []
        file_name = os.path.basename(file_path)
        base_file = os.path.splitext(file_name)
        index = 0

        out_file_name = generate_file_name(
            base_file[0], index, base_file[1], out_dir)
        list_of_files.append(out_file_name)
        out_file = open(out_file_name, 'w', newline='')
        progress = -1
        progressEndLine = "\r" if sys.stdout.isatty() else None

        try:
            with open(file_path, "r") as in_file:
                in_csv = csv.reader(in_file)
                for row in in_csv:
                    head_line = row
                    break
                else:
                    raise Exception(f'{file_path} is empty')

                out_csv = csv.writer(out_file)
                out_csv.writerow(head_line)

                content_transfer = 0

                for data in in_csv:
                    out_csv.writerow(data)
                    file_content_size = out_file.tell()

                    progressStep = round((content_transfer + file_content_size) * 100 / file_size, 0)
                    if progressStep - progress >= 1:
                        progress = progressStep
                        print("Process: {}%".format(min([progress, 100])), end=progressEndLine, flush=True)

                    if file_content_size >= file_size_limit:
                        index += 1
                        content_transfer += file_content_size
                        out_file.close()
                        out_file_name = generate_file_name(
                            base_file[0], index, base_file[1], out_dir)
                        list_of_files.append(out_file_name)

                        #new line symbol has present in data so we remove this one
                        out_file = open(out_file_name, 'w', newline='')
                        out_csv = csv.writer(out_file)
                        out_csv.writerow(head_line)

                print("Process: 100%        ")
        except (OSError, UnicodeDecodeError) as e:
            out_file.close()
            try:
                for file_path in list_of_files:
                    os.remove(file_path)
                Log.error(e)
                Log.exit_with_failure()
            except OSError as e2:
                Log.error(e2)
                Log.exit_with_failure()
        finally:
            out_file.close()

        return list_of_files


class BIManager(object):

    def __init__(self, bi_tool_path: str):
        self.bi_tool_path = bi_tool_path
        self.configManager = ConfigManager()

    def create_config_file(self, bi_tool: str):
        if os.path.exists(self.bi_tool_path):
            if input("This config file in this directory already exists. Do you want to rewrite the file [Y/N]?") == 'N':
                Log.info("Generation of config file was canceled")
                sys.exit(0)

        if bi_tool == 'qlik':
            Log.message("Input config data!")
            host = input("Input host:")
            auth_key = input("Auth Key:")
            space = input("Space [codesonar]:") or 'codesonar'

            config_obj = Qlik.get_config_model(host, auth_key, space)
            self.configManager.create_config_file(self.bi_tool_path, config_obj)
        return

    def check_auth_connection(self) -> bool:
        qlikConfig = self.configManager.get_bi_config(self.bi_tool_path)
        qlik = Qlik(qlikConfig['host'],
                    qlikConfig['authKey'], qlikConfig['maxFileSize'])

        return qlik.get_status_connection()

    def upload_data_qlik(self, files: list[str], file_name_prefix: str):
        list_of_files = []
        qlikConfig = self.configManager.get_bi_config(self.bi_tool_path)
        qlik = Qlik(qlikConfig['host'],
                    qlikConfig['authKey'], qlikConfig['maxFileSize'])
        connectionId = qlik.prepare_collection_for_files(qlikConfig['space'])

        date_start = datetime.now()
        for index, file_path in enumerate(files):
            base_file = os.path.splitext(file_path)
            out_file_name = "{}_{}_{}{}".format(
                file_name_prefix, index, date_start.strftime("%Y%m%d_%H%M%S"), base_file[1])
            list_of_files.append(out_file_name)
            qlik.upload_data_collection_from_file_inline(
                connectionId, out_file_name, file_path)

        return list_of_files

    def upload_data(self, file: str, args: argparse.Namespace):
        qlikConfig = self.configManager.get_bi_config(self.bi_tool_path)
        max_file_size = qlikConfig['maxFileSize']
        file_size = os.path.getsize(file)
        list_of_files = [file]

        if file_size == 0:
            Log.error("The file {} is empty".format(file, self.bi_tool_path))
            Log.exit_with_failure()


        if file_size > max_file_size:
            Log.info("The file {} will be separated into chunks".format(file))
            Log.message("Total size of chunks will be {}".format(DataManager.convert_bytes(file_size)))
            data_manager = DataManager()
            path_to_save = args.validation

            if not os.path.isdir(path_to_save):
                Log.error('The directory [{}] for saving temp files does not exist'.format(path_to_save))
                Log.exit_with_failure()
            
            list_of_files = data_manager.separate_file_at_chunks(
                file,
                max_file_size,
                file_size,
                path_to_save
            )
        else:
            list_of_files = [file]

        if len(list_of_files) > 0:
            Log.info(list_of_files)
            self.upload_data_qlik(list_of_files, args.remote_file_name_prefix)

            if len(list_of_files) > 1:
                for file_path in list_of_files:
                    os.remove(file_path)
                Log.info("Temporary files were deleted")
        else:
            Log.error("No files to upload")
            Log.exit_with_failure()


# ===================================================================================
# ===================================================================================

def error_support_platform(platform):
    Log.error('The platform {} does not support!'.format(platform))
    Log.message('Supported platforms: ' + ', '.join(SUPPORTED_PLATFORMS))


def error_required_fields():
    Log.error(
        'should be used one of required flag --test-connection or --bi-tool with name of platform')
    Log.message('Supported platforms: ' + ', '.join(SUPPORTED_PLATFORMS))


def error_file_input(file_path):
    if file_path is None:
        Log.error("The parameter --upload is mandatory")
        Log.exit_with_failure()
    if not os.path.exists(file_path):
        Log.error("File \"{}\" does not exist".format(file_path))
        Log.exit_with_failure()


def main(argv):
    parser = argparse.ArgumentParser(description='''Upload data collections on BI platform.  Examples:
    codesonar bi_transfer.py --create-bi-conf qlik_config.conf --bi-tool qlik
    codesonar bi_transfer.py --test-connection qlik_config.conf
    codesonar bi_transfer.py --type warning --analysis-url http://[::1]:7340/analysis/1.html --download warnings.csv
    codesonar bi_transfer.py --type warning --hub http://[::1]:7340 --search "aid:1" --download my_file.csv
    codesonar bi_transfer.py --type warning --analysis-url http://[::1]:7340/analysis/1.html --gained-since-previous-analysis --download my_file.csv

    codesonar bi_transfer.py --bi-config qlik_config.conf --upload <file_path+file_name>.csv --validation ./
    codesonar bi_transfer.py --bi-config ./qlik_config.conf --upload my_file.csv --validation ./ --remote-file-name-prefix my_warnings
    codesonar bi_transfer.py --bi-config ./qlik_config.conf --upload my_file.csv  --validation ./ --remote-file-name-prefix my_warnings
''',
                                     formatter_class=argparse.RawTextHelpFormatter,
                                     prog='codesonar bi_transfer.py')
    csv_file = Validator(r".*\.csv$")
    conf_file = Validator(r".*\.conf$")
    parser.add_argument(
        '--create-bi-config', type=conf_file, help='run to create config file')
    parser.add_argument(
        '--test-connection', type=conf_file, help='run to test if configuration of authentication data is correct')
    parser.add_argument(
        '--bi-tool', help='option to define BI tool', choices=SUPPORTED_PLATFORMS)
    parser.add_argument('--type', help='run to download data in a file',
                        choices=SUPPORTED_DATA_COLLECTIONS)
    parser.add_argument(
        '--bi-config', type=conf_file, help='run to upload data on a BI platform')
    parser.add_argument('--validation', help='run to validate data file')

    add_analysis_args(parser)
    add_filtering_args(parser)

    parser.add_argument('--gained-since-previous-analysis', action='store_true',
                        help='dump only warnings that did not exist in the previous analysis of the same project')
    parser.add_argument('--lost-since-previous-analysis', action='store_true',
                        help='dump only warnings that exist in the previous analysis of the same project but not the current one')
    parser.add_argument(
        '--visible-warnings', help='saved search id/name to use (defaults to "all")', default='all', metavar='ID/NAME')
    add_network_args(parser)
    parser.add_argument(
        '--download', help='output download filename', type=csv_file, metavar='OUTPUT_FILENAME')
    parser.add_argument('--upload', type=csv_file, help='input upload filename',
                        metavar='OUTPUT_FILENAME')
    parser.add_argument('--remote-file-name-prefix', help='output upload filename',
                        default='file', metavar='OUTPUT_FILENAME')

    args = parser.parse_args(argv[1:])
    args.csv = True
    args.json = False

    if args.upload is not None and args.download is not None:
        Log.error("You can't use --download and --upload in the same command line")
        Log.exit_with_failure()

    if args.create_bi_config is not None:
        if args.bi_tool is None:
            Log.error("You should define name of platform. PLease add --bi-tool <name of BI platform>")
            Log.exit_with_failure()
        bi_config_path = args.create_bi_config
        bi_tool = args.bi_tool

        manager = BIManager(bi_config_path)
        manager.create_config_file(bi_tool)
        Log.success("Config for {} was created".format(bi_tool))

    elif args.test_connection is not None:
        bi_tool = args.test_connection
        manager = BIManager(bi_tool)
        res = manager.check_auth_connection()

        if res:
            Log.success('Connected to {}'.format(bi_tool))
        else:
            Log.error("No connection to selected BI tool")
            Log.exit_with_failure()

    elif args.type is not None:
        type_data = args.type

        if args.download is None:
            Log.error("The parameter --download is mandatory")
            Log.exit_with_failure()

        args.sort = []
        options: dict = parse_options(parser, args)

        data_manager = DataManager()
        data_manager.download_data(type_data, options, args)

    elif args.bi_config is not None:
        bi_config_path = args.bi_config
        file = args.upload
        error_file_input(file)

        if args.validation is None:
            Log.error("The parameter --validation is mandatory")
            Log.exit_with_failure()

        Log.info("The file to read {}".format(file))

        manager = BIManager(bi_config_path)
        manager.upload_data(file, args)

    return 0


if __name__ == '__main__':
    sys.exit(main(sys.argv))
