#!/usr/bin/env python3
#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License.  You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

import argparse
import sys
import json
import traceback
import yaml
import tempfile
import fileinput
import time
import os.path
import io

from io import StringIO
from string import Template
from pydoc import locate
from jsonschema import validate
from jsonschema.exceptions import ValidationError
from colorama import init, Fore, Back, Style

init(autoreset=True)

Remapped_Vars = {}

Renamed_Records = {
    # as it can have a numeric value [0-2]
    'proxy.config.output.logfile': 'proxy.config.output.logfile.name',
    'proxy.config.exec_thread.autoconfig': 'proxy.config.exec_thread.autoconfig.enabled',
    'proxy.config.hostdb': 'proxy.config.hostdb.enabled',
    'proxy.config.tunnel.prewarm': 'proxy.config.tunnel.prewarm.enabled',
    'proxy.config.ssl.origin_session_cache': 'proxy.config.ssl.origin_session_cache.enabled',
    'proxy.config.ssl.session_cache': 'proxy.config.ssl.session_cache.mode',
    'proxy.config.ssl.TLSv1_3': 'proxy.config.ssl.TLSv1_3.enabled',
    'proxy.config.ssl.client.TLSv1_3': 'proxy.config.ssl.client.TLSv1_3.enabled'
}

###############################################################################################
###############################################################################################


def red(text, bright=False):
    return f"{Style.BRIGHT if bright else ''}{Fore.RED}{text}{Style.RESET_ALL}"


def bright(text, color=Fore.GREEN):
    return f'{Style.BRIGHT}{color}{text}{Style.RESET_ALL}'


def error():
    return f"{red('[E] ', True)}"


def fancy_chain_print(lst, isError=False):
    output = StringIO()
    if len(lst) == 1:
        output.write(f'» {lst[0]}\n')
        s = output.getvalue()
        output.close()
        return s
    space = ''
    output.write(f'┌■ {lst[0]}\n')
    if len(lst) > 2:
        space = ' '
        # from 2nd till end-1
        output.write(f'└┬──» {lst[1]}\n')
        for x in lst[2:-1]:
            output.write(f' ├──» {x}\n')

    z = lst[-1]
    output.write(f'{space}└──» {z}\n')

    s = output.getvalue()
    output.close()
    return s


def progressbar(it, size=60, out=sys.stdout, mute=False):
    count = len(it)

    def show(j):
        x = int(size * j / count)
        if not mute:
            print("[{}{}] {}/{}".format("█" * x, "░" * (size - x), j, count), end='\r', file=out, flush=True)

    show(0)
    for i, item in enumerate(it):
        yield item

        show(i + 1)
    if not mute:
        print("\n", flush=True, file=out)


def validate_schema(data, schema_filename):
    if schema_filename:
        with open(schema_filename, 'r') as f:
            s = f.read()
            sdata = json.loads(s)
    try:
        # validate will throw if invalid schema.
        if schema_filename:
            validate(instance=data, schema=sdata)
    except ValidationError as ve:
        return str(ve)

    return ""


def save_to_file(filename, is_json, typerepr, no_backup, data):

    def float_representer(dumper, value):
        return dumper.represent_scalar(u'tag:yaml.org,2002:float', str(value), style="'")

    def int_representer(dumper, value):
        return dumper.represent_scalar(u'tag:yaml.org,2002:int', str(value), style="'")

    def bool_representer(dumper, value):
        return dumper.represent_scalar(u'tag:yaml.org,2002:bool', str(value), style="'")

    def null_representer(dumper, value):
        return dumper.represent_scalar(u'tag:yaml.org,2002:null', str(value), style="'")

    # Make sure we do not wipe out an existing file.
    if no_backup == False and os.path.exists(filename):
        os.system(f'cp {filename} {filename}.{int(time.time())}')

    with open(filename, 'w') as f:
        if is_json:
            json.dump(data, f, indent=4, sort_keys=True)
        else:
            if 'float' in typerepr:
                yaml.add_representer(float, float_representer)
            if 'int' in typerepr:
                yaml.add_representer(int, int_representer)
            if 'bool' in typerepr:
                yaml.add_representer(bool, bool_representer)
            if 'null' in typerepr:
                yaml.add_representer(None, null_representer)
            yaml.dump(data, f)


def print_summary():
    if len(Remapped_Vars):
        out = []
        out.append(f'{Style.BRIGHT}{Fore.GREEN}{len(Remapped_Vars)} Renamed records:{Style.RESET_ALL}')
        idx = 1
        for k, v in Remapped_Vars.items():
            out.append(f'{Style.BRIGHT}{Fore.GREEN}#{idx}{Style.RESET_ALL} : {k} -> {Style.BRIGHT}{Fore.GREEN}{v}{Style.RESET_ALL}')
            idx = idx + 1

        print(fancy_chain_print(out))


def get_value(type, value):

    def have_multipliers(value, mps):
        for m in mps:
            if value.endswith(m):
                return True

        return False

    if value == 'nullptr' or value == 'NULL':
        return None

    # We want to make the type right when inserting the element into the dict.
    if type == 'FLOAT':
        return float(value)
    elif type == 'INT':
        # We need to make it YAML compliant as this will be an int, so if contains
        # any special character like hex or a multiplier, then we make a string. ATS will
        # parse it as string anyway.
        if value.startswith('0x') or have_multipliers(value, ['K', 'M', 'G', 'T']):
            return str(value)
        else:
            return int(value)
    elif type == 'STRING':
        return str(value)

    return None


def add_object(config, var, value, type, track_info):
    key = ''
    index = var.find('.')
    if index < 0:  # last part
        if isinstance(config, dict) == False:
            line, rec = track_info
            raise Exception(
                f"We cannot continue with '{rec}' at line '{line}' as a value node will be overridden.\nPlease check your config.")
        elif var in config and isinstance(config[var], dict):
            line, rec = track_info
            raise Exception(
                f"We cannot continue with '{rec}' at line '{line}' as an existing YAML map will be overridden.\nPlease check your config."
            )
        config[var] = get_value(type, value)
    else:
        key = var[:index]
        if key not in config:
            config[key] = {}

        add_object(config[key], var[index + 1:], value, type, track_info)


def make_tmp_file_with_renamed_fields(file):
    temp = tempfile.NamedTemporaryFile(prefix='records.config_', delete=False)

    for l in fileinput.input(files=file):
        elements = l.split()
        if len(elements) == 4:
            rec_name = elements[1]
            if rec_name in Renamed_Records:
                elements[1] = Renamed_Records[rec_name]

                rec_line = ' '.join(elements)
                temp.write(rec_line.encode('utf-8'))
                temp.write(b'\n')
                # Store the record to keep track of the renamed fields.
                Remapped_Vars[rec_name] = Renamed_Records[rec_name]

                continue
        # if elements len diff from 4 .. well.. it shouldn't.
        temp.write(l.encode('utf-8'))

    temp.close()
    return temp.name


def handle_file_input(args):
    config = {}

    # Fix the names first.
    filename = make_tmp_file_with_renamed_fields(args.file)
    with open(filename, "r+") as f:
        lines = f.readlines()
        num_lines = len(lines)
        for idx in progressbar(range(num_lines), 40, mute=args.mute):
            line = lines[idx]
            if '#' == line[0] or '\n' == line[0] or ' ' == line[0]:
                idx = idx + 1
                continue
            s = line.split(' ', maxsplit=3)  # keep values all together
            name = s[1]
            type = s[2]
            value = s[3]

            track_info = (idx + 1, name)  # in case we want to show any error. rec name is always handy.
            # We ignore the prefix and work away from  there.
            if name.startswith("proxy.config."):
                name = name[len("proxy.config."):]
            elif name.startswith("local.config."):
                name = name[len("local.config."):]
            elif args.node and name.startswith("proxy.node."):
                name = name[len("proxy."):]

            # Build the object (use rstrip to handle files without trailing newline)
            add_object(config, name, value.rstrip('\n'), type, track_info)
            idx = idx + 1

    ts = {}
    ts['records'] = config

    if args.schema:
        err = validate_schema(ts, args.schema)
        if err:
            raise Exception("Schema failed.")

    save_to_file(args.output, args.json, args.typerepr, args.no_backup, ts)

    f.close()

    # Clean up the temp file we've created with the renamed fields.
    try:
        os.remove(filename)
    except OSError as e:
        raise Exception(f"Error removing temporary file  {e.filename} - {e.strerror}. YAML file was created")

    return


if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='records.config to YAML/JSON convert tool')
    parser.add_argument('-f', '--file', help='records.config input file.', required=False, default="records.config")
    parser.add_argument('-n', '--node', help="Include 'proxy.node' variables in the parser.", action='store_true')
    parser.add_argument('-t', '--typerepr', help="Use type representer (list)", required=False, default=[''])
    parser.add_argument('-s', '--schema', help="Validate the output using a json schema file.")
    parser.add_argument('-o', '--output', help="Save to output file.", required=False, default="records.yaml")
    parser.add_argument(
        '-m', '--mute', help="Be quiet, do not output anything, except for errors", required=False, action='store_true')
    parser.add_argument('-e', '--error', help="Show traceback", required=False, action='store_true', default=False)
    parser.add_argument(
        '-x',
        '--no-backup',
        help="If it exists, do not backup the output file.",
        required=False,
        action='store_true',
        default=False,
        dest='no_backup')
    kk = parser.add_mutually_exclusive_group(required=False)
    kk.add_argument('-j', '--json', help="Output as json", action='store_true', default=False)
    kk.add_argument('-y', '--yaml', help="Output as yaml", action='store_true', default=True)
    parser.set_defaults(func=handle_file_input)
    args = parser.parse_args()

    try:
        func = args.func
    except AttributeError:
        parser.error("Too few arguments")
    try:
        func(args)
        if args.mute is False:
            print_summary()

    except Exception as e:
        print("Something went wrong:\n{}".format(e))

        if args.error:
            print(f"\n{traceback.format_exc()}")
        sys.exit(1)

    sys.exit(0)
