#!/usr/bin/env python
# Copyright 2021-2022 VMware, Inc.  All rights reserved. -- VMware Confidential

import os

NO_KEY_PREFIX = 'NO_KEY'
ALL_KEYS = '*'
PROP_NAME_PART = 0
PROP_VAL_PART = 1

PROXY_CONF_PATH = '/etc/vmware-rhttpproxy/endpoints.conf.d/'

PROXY_EP_SPEC_PREFIX = 'rhttpproxy.endpoint'

K_INDEX = 'index'
K_NAMESPACE = 'namespace'
K_CONNECTIONTYPE = 'connectionType'
K_ADDRESS = 'address'
K_HTTPACCESSMODE = 'httpAccessMode'
K_HTTPSACCESSMODE = 'httpsAccessMode'

DEFAULT_VAL = {
    K_NAMESPACE : '',
    K_CONNECTIONTYPE : 'local',
    K_ADDRESS : '${vsm.int.http}',
    K_HTTPACCESSMODE : 'allow',
    K_HTTPSACCESSMODE : 'allow',
}

ORIG_VSM_PROP_PATH = '/etc/vmware-vsm/vsm.properties'
ORIG_VSMSPEC_PROP_PATH = '/etc/vmware-vsm/vsmspec.properties'
ORIG_VSM_PROXY_CONF_FILE = 'vsm-proxy.conf'
ORIG_VSM_PROXY_CONF_PATH = PROXY_CONF_PATH + 'vsm-proxy.conf'

# [ namespace (ep path), httpAccessMode, httpsAccessMode ]
# no mode specified is "allow"
# if httpsAccessMode to be changed must also specify httpAccess mode.
VSM_PROXY_NS_MODES = [
    ['/vsm/ovfConsumer', 'redirect', 'reject', ],
    ['/vsm/sdk', ],
    ['/vsm/healthstatus', ],
    ['/vsm/extensionService', ],
    ['/vsm/resourcebundle', ],
]


def get_entry_index(ent_str, prefix_str):
    """
    get the index in string a.b<index>.c
    """
    ent_idx = ent_str[len(prefix_str):ent_str.find(".", len(prefix_str))]
    ent_name = ent_str[ent_str.rfind(".") + 1:]
    return ent_idx, ent_name

def read_properties_file(prop_path, keys, key_prefix, all_lines=False):
    """
    Read properties file and retrieve k-v for keys specified
    or a key prefix for multiple keys.
    return dict of kv for keys dict or list of dict of kv for prefix match..
    """
    if not os.path.exists(prop_path):
        raise Exception("%s file not found" % prop_path)

    result_kv_dict = {}
    result_kv_list = []
    no_key_count = 0
    with open(prop_path, 'r', encoding='utf-8') as pf:
        for prop_line in pf:
            if not '=' in prop_line:
                if all_lines:
                    no_key = '{0}_{1}'.format(NO_KEY_PREFIX, no_key_count)
                    result_kv_dict[no_key] = prop_line
                    no_key_count += 1
                continue
            prop_kv = prop_line.split('=', 1)
            prop_k = prop_kv[PROP_NAME_PART].strip()
            prop_v = prop_kv[PROP_VAL_PART].strip()
            if key_prefix and prop_k.startswith(key_prefix):
                ent_idx_str, ent_name = get_entry_index(prop_k, key_prefix)
                ent_idx = int(ent_idx_str)
                if ent_idx + 1 > len(result_kv_list):
                    ent_pos = len(result_kv_list)
                    while ent_pos < ent_idx + 1:
                        result_kv_list.append({ K_INDEX: ent_idx_str })
                        ent_pos += 1
                prop_kv_dict = result_kv_list[ent_idx]
                prop_kv_dict[ent_name] = prop_v
            elif ALL_KEYS in keys or prop_k in keys:
                result_kv_dict[prop_k] = prop_v
    return result_kv_dict, result_kv_list

def format_kv_line(k, v, subst_kv, skip_keys):
    """
    simple k=v line formatter.
    """
    out_str = ''
    if skip_keys and k in skip_keys:
        return ''
    if k:
        out_str = "{0} = {1}\n".format(k, v)
    elif v:
        if "\n" != v:
            out_str = "{0}\n".format(v)
        else:
            out_str = v
    return out_str

def write_file(file_path, mode, kv_dict, kv_list, subst_kv, skip_keys, kv_list_formatter_fn):
    """
    append or overwrite the file specified using the formatter function.
    """
    if not kv_list:
        return

    with open(file_path, mode, encoding='utf-8') as wf:
        if kv_dict:
            for k, v in kv_dict.items():
                if NO_KEY_PREFIX in k:
                    k = ''
                fmtd_line = format_kv_line(k, v, subst_kv, skip_keys)
                if fmtd_line:
                    wf.write(fmtd_line)
            wf.write("\n")
        for ent_kv_dict in kv_list:
            fmtd_lines = kv_list_formatter_fn(ent_kv_dict, subst_kv, skip_keys)
            if fmtd_lines:
                wf.write(fmtd_lines)

def subst_kv_in_val(key_str, val_str, subst_kv):
    """
    substitute parameters in value string.
    """
    if not val_str:
        val_str = DEFAULT_VAL[key_str]
    if '${' in val_str:
        for k, v in subst_kv.items():
            replace_str = '${' + k + '}'
            val_str = val_str.replace(replace_str, v)
    return val_str

def format_proxy_line(kv_dict, subst_kv, skip_keys):
    """
    format a string as a rhttpproxy line with lf.
    return string and count ov kv_list consumed.
    assumes properties are in index order as example:
        rhttpproxy.endpoint0.namespace = /vsm/sdk
        rhttpproxy.endpoint0.connectionType = remote
        rhttpproxy.endpoint0.address = ${vsm.int.http}
        rhttpproxy.endpoint0.httpAccessMode = allow
        rhttpproxy.endpoint0.httpsAccessMode = allow
    """
    namespace_str = subst_kv_in_val(K_NAMESPACE, kv_dict[K_NAMESPACE], subst_kv)
    if not namespace_str:
        Exception('Namespace not found for proxy rule')
    addr_str = subst_kv_in_val(K_ADDRESS, kv_dict[K_ADDRESS], subst_kv)
    if not addr_str:
        Exception('Address not found for proxy rule %s' % namespace_str)
    out_str = "{0} {1} {2} {3} {4}\n".format(
        namespace_str,
        subst_kv_in_val(K_CONNECTIONTYPE, kv_dict[K_CONNECTIONTYPE], subst_kv),
        addr_str,
        subst_kv_in_val(K_HTTPACCESSMODE, kv_dict[K_HTTPACCESSMODE], subst_kv),
        subst_kv_in_val(K_HTTPSACCESSMODE, kv_dict[K_HTTPSACCESSMODE], subst_kv))
    return out_str

def create_proxy_file(prop_keys, prop_file_path, subst_keys, svc_spec_file_path, proxy_file_path):
    """
    create a new proxy file from a properties file containing a port or a port string
    and a service spec property file containing proxy entries in properties format.
    subst_keys must match prop_keys to allow retrieval of properties from property file
    to be substituted in proxy confg file.
    proxy_file_path not specified implies use proxy file in the svc_spec_file.
    assumption:
    vsm properties is completely filled in and has values for property keys.
    """
    if not prop_keys:
        raise Exception('No property keys to retrieve values')

    if len(prop_keys) != len(subst_keys):
        raise Exception('property keys count must match substitution keys count')

    prop_key_prefix = ''
    kv_dict, kv_list =\
        read_properties_file(prop_file_path, prop_keys, prop_key_prefix)
    if len(kv_dict) <= 0 or not kv_dict[prop_keys[0]]:
        raise Exception("Property key value not found")
    proxy_port = kv_dict[prop_keys[0]]
    if not proxy_port:
        raise Exception("Property key different")

    subst_kv = {}
    for (pk), (sk) in zip(prop_keys, subst_keys):
        subst_kv[sk] = kv_dict[pk]

    proxy_keys = { 'rhttpproxy.file' }
    proxy_key_prefix = PROXY_EP_SPEC_PREFIX
    kv_dict, kv_list =\
        read_properties_file(svc_spec_file_path, proxy_keys, proxy_key_prefix)

    if not kv_dict or len(kv_list) <= 0:
        Exception("Proxy key values not found")

    # complete file path provided as input takes precedence.
    if not '/' in proxy_file_path:
        # proxy conf file name specified in spec file takes precedence.
        if proxy_keys and len(kv_dict) and kv_dict[next(iter(proxy_keys))]:
            proxy_file_path = kv_dict[next(iter(proxy_keys))]

        if not '/' in proxy_file_path:
            proxy_file_path = PROXY_CONF_PATH + proxy_file_path

    kv_dict = {}
    skip_keys = []
    write_file(proxy_file_path, 'w', kv_dict, kv_list, subst_kv, skip_keys, format_proxy_line)

def create_vsm_proxy_file(vsm_svc_spec_file = ORIG_VSMSPEC_PROP_PATH,
                          vsm_proxy_file = ORIG_VSM_PROXY_CONF_FILE,
                          vsm_prop_file = ORIG_VSM_PROP_PATH):
    """
    create or clobber proxy conf file for vsm.
    """
    vsm_prop_keys = [ 'vsm.http.port' ]
    vsm_subst_keys = [ 'vsm.int.http' ]
    # vsm properties file will already have port value during upgrade/b2b.
    create_proxy_file(vsm_prop_keys, vsm_prop_file, vsm_subst_keys, vsm_svc_spec_file, vsm_proxy_file)

def format_vsmspec_proxy_lines(kv_dict, subst_kv, skip_keys):
    """
    format proxy entries for vsp spec properties file.
    multiple lines will be returned each with '\n' in the string.
    subst_kv is unused for now.
    """
    out_str = ""
    ep_pos_str = kv_dict[K_INDEX]
    for k, v in kv_dict.items():
        if skip_keys and k in skip_keys:
            continue
        out_str = out_str + "{0}{1}.{2} = {3}\n".format(PROXY_EP_SPEC_PREFIX, ep_pos_str, k, v)
    out_str = out_str + "\n"
    return out_str

def append_proxy_entry(kv_list, ns_mode, ep_pos):
    """
    append set of entries {namespace, connectionType, address and access modes}
    for each rhttpproxy.endpoint with a incrementing endpoint number,
    as rhttpproxy.endpointN.{entry_name}.
    only "local" connectionType entries are used.
    """
    httpAccessMode = DEFAULT_VAL[K_HTTPACCESSMODE]
    if len(ns_mode) > 1:
        httpAccessMode = ns_mode[1]
    httpsAccessMode = DEFAULT_VAL[K_HTTPSACCESSMODE]
    if len(ns_mode) > 2:
        httpsAccessMode = ns_mode[2]
    kv_list.append({ K_INDEX: '{0}'.format(ep_pos) })
    ent_kv_dict = kv_list[ep_pos]
    ent_kv_dict[K_NAMESPACE] = ns_mode[0]
    ent_kv_dict[K_CONNECTIONTYPE] = DEFAULT_VAL[K_CONNECTIONTYPE]
    ent_kv_dict[K_ADDRESS] = DEFAULT_VAL[K_ADDRESS]
    ent_kv_dict[K_HTTPACCESSMODE] = httpAccessMode
    ent_kv_dict[K_HTTPSACCESSMODE] = httpsAccessMode
    return kv_list

def update_proxy_spec_file(svc_spec_file_path = ORIG_VSMSPEC_PROP_PATH):
    """
    previously vsmspec.properties had only /vsm path for proxy.
    this allowed access to even internal VSM APIs like ovfconsumer.
    replace existing endpoint entries in vsmspec.properties and
    update spec file to add all vsm paths to vsmspec.properties file:
    Doc: (keep updated if rules are changed)
    /vsm/ovfConsumer local ${vsm.int.http} redirect reject
    /vsm/sdk local ${vsm.int.http} allow allow
    /vsm/healthstatus local ${vsm.int.http} allow allow
    /vsm/extensionService local ${vsm.int.http} allow allow
    /vsm/resourcebundle local ${vsm.int.http} allow allow
    """
    proxy_keys = { '*' }
    proxy_key_prefix = PROXY_EP_SPEC_PREFIX
    kv_dict, kv_list =\
        read_properties_file(svc_spec_file_path, proxy_keys, proxy_key_prefix, True)

    if not kv_dict or len(kv_list) <= 0:
        Exception("Proxy key values not found")

    # No modification as required entry count already exists.
    if len(kv_list) == len(VSM_PROXY_NS_MODES):
        return

    if len(kv_list) > 1:
        Exception('Unexpected number of proxy entries. Must be 1')

    # Clear proxy entry list and add updated entries
    ep_max = len(VSM_PROXY_NS_MODES)
    ep_pos = 0
    kv_list = []
    for ns_mode in VSM_PROXY_NS_MODES:
        kv_list = append_proxy_entry(kv_list, ns_mode, ep_pos)
        ep_pos += 1

    subst_kv = {}
    skip_keys = [K_INDEX]
    write_file(svc_spec_file_path, 'w', kv_dict, kv_list, subst_kv, skip_keys, format_vsmspec_proxy_lines)

def is_proxy_conf_uptodate(vsm_svc_spec_file = ORIG_VSMSPEC_PROP_PATH,
                           vsm_proxy_file = ORIG_VSM_PROXY_CONF_PATH):
    """
    Check if vsm proxy con and vsmspec properties files have required
    proxy rules so we only update files if required rules are not present.
    Also requires the entries are ordered as in VSM_PROXY_NS_MODES.

    :param vsm_svc_spec_file: path to vsmspec properties file.
    :param vsm_proxy_file: path to vsm rhttpproxy config file.
    :return: true if all required vsm proxy rules exist.
    """
    entry_count = 0
    with open(vsm_proxy_file, 'r') as pf:
        for entryline in pf:
            if VSM_PROXY_NS_MODES[entry_count][0] in entryline:
                entry_count += 1
    if not entry_count == len(VSM_PROXY_NS_MODES):
        return False
    entry_count = 0
    ns_prop_entry = '{0}{1}.{2}'.format(PROXY_EP_SPEC_PREFIX,
                                        entry_count,
                                        K_NAMESPACE)
    with open(vsm_svc_spec_file, 'r') as psf:
        for specline in psf:
            if ns_prop_entry in specline and \
                    VSM_PROXY_NS_MODES[entry_count][0] in specline:
                entry_count += 1
                ns_prop_entry = '{0}{1}.{2}'.format(PROXY_EP_SPEC_PREFIX,
                                                    entry_count,
                                                    K_NAMESPACE)
    if not entry_count == len(VSM_PROXY_NS_MODES):
        return False
    return True
