# -*- coding: utf-8 -*-

#  Copyright (c) 2017 Citrix Systems
#
# This code is part of Ansible, but is an independent component.
# This particular file snippet, and this file snippet only, is BSD licensed.
# Modules you write using this snippet, which is embedded dynamically by Ansible
# still belong to the author of the module, and may assign their own license
# to the complete work.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
#
#    * Redistributions of source code must retain the above copyright
#      notice, this list of conditions and the following disclaimer.
#    * Redistributions in binary form must reproduce the above copyright notice,
#      this list of conditions and the following disclaimer in the documentation
#      and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#

from __future__ import absolute_import, division, print_function
__metaclass__ = type

import json
import re
import sys

from ansible.module_utils.basic import env_fallback
from ansible.module_utils.six import binary_type, text_type
from ansible.module_utils._text import to_native


class ConfigProxy(object):

    def __init__(self, actual, client, attribute_values_dict, readwrite_attrs, transforms=None, readonly_attrs=None, immutable_attrs=None, json_encodes=None):
        transforms = {} if transforms is None else transforms
        readonly_attrs = [] if readonly_attrs is None else readonly_attrs
        immutable_attrs = [] if immutable_attrs is None else immutable_attrs
        json_encodes = [] if json_encodes is None else json_encodes

        # Actual config object from nitro sdk
        self.actual = actual

        # nitro client
        self.client = client

        # ansible attribute_values_dict
        self.attribute_values_dict = attribute_values_dict

        self.readwrite_attrs = readwrite_attrs
        self.readonly_attrs = readonly_attrs
        self.immutable_attrs = immutable_attrs
        self.json_encodes = json_encodes
        self.transforms = transforms

        self.attribute_values_processed = {}
        for attribute, value in self.attribute_values_dict.items():
            if value is None:
                continue
            if attribute in transforms:
                for transform in self.transforms[attribute]:
                    if transform == 'bool_yes_no':
                        if value is True:
                            value = 'YES'
                        elif value is False:
                            value = 'NO'
                    elif transform == 'bool_on_off':
                        if value is True:
                            value = 'ON'
                        elif value is False:
                            value = 'OFF'
                    elif callable(transform):
                        value = transform(value)
                    else:
                        raise Exception('Invalid transform %s' % transform)
            self.attribute_values_processed[attribute] = value

        self._copy_attributes_to_actual()

    def _copy_attributes_to_actual(self):
        for attribute in self.readwrite_attrs:
            if attribute in self.attribute_values_processed:
                attribute_value = self.attribute_values_processed[attribute]

                if attribute_value is None:
                    continue

                # Fallthrough
                if attribute in self.json_encodes:
                    attribute_value = json.JSONEncoder().encode(attribute_value).strip('"')
                setattr(self.actual, attribute, attribute_value)

    def __getattr__(self, name):
        if name in self.attribute_values_dict:
            return self.attribute_values_dict[name]
        else:
            raise AttributeError('No attribute %s found' % name)

    def add(self):
        self.actual.__class__.add(self.client, self.actual)

    def update(self):
        return self.actual.__class__.update(self.client, self.actual)

    def delete(self):
        self.actual.__class__.delete(self.client, self.actual)

    def get(self, *args, **kwargs):
        result = self.actual.__class__.get(self.client, *args, **kwargs)

        return result

    def has_equal_attributes(self, other):
        if self.diff_object(other) == {}:
            return True
        else:
            return False

    def diff_object(self, other):
        diff_dict = {}
        for attribute in self.attribute_values_processed:
            # Skip readonly attributes
            if attribute not in self.readwrite_attrs:
                continue

            # Skip attributes not present in module arguments
            if self.attribute_values_processed[attribute] is None:
                continue

            # Check existence
            if hasattr(other, attribute):
                attribute_value = getattr(other, attribute)
            else:
                diff_dict[attribute] = 'missing from other'
                continue

            # Compare values
            param_type = self.attribute_values_processed[attribute].__class__
            if attribute_value is None or param_type(attribute_value) != self.attribute_values_processed[attribute]:
                str_tuple = (
                    type(self.attribute_values_processed[attribute]),
                    self.attribute_values_processed[attribute],
                    type(attribute_value),
                    attribute_value,
                )
                diff_dict[attribute] = 'difference. ours: (%s) %s other: (%s) %s' % str_tuple
        return diff_dict

    def get_actual_rw_attributes(self, filter='name'):
        if self.actual.__class__.count_filtered(self.client, '%s:%s' % (filter, self.attribute_values_dict[filter])) == 0:
            return {}
        server_list = self.actual.__class__.get_filtered(self.client, '%s:%s' % (filter, self.attribute_values_dict[filter]))
        actual_instance = server_list[0]
        ret_val = {}
        for attribute in self.readwrite_attrs:
            if not hasattr(actual_instance, attribute):
                continue
            ret_val[attribute] = getattr(actual_instance, attribute)
        return ret_val

    def get_actual_ro_attributes(self, filter='name'):
        if self.actual.__class__.count_filtered(self.client, '%s:%s' % (filter, self.attribute_values_dict[filter])) == 0:
            return {}
        server_list = self.actual.__class__.get_filtered(self.client, '%s:%s' % (filter, self.attribute_values_dict[filter]))
        actual_instance = server_list[0]
        ret_val = {}
        for attribute in self.readonly_attrs:
            if not hasattr(actual_instance, attribute):
                continue
            ret_val[attribute] = getattr(actual_instance, attribute)
        return ret_val

    def get_missing_rw_attributes(self):
        return list(set(self.readwrite_attrs) - set(self.get_actual_rw_attributes().keys()))

    def get_missing_ro_attributes(self):
        return list(set(self.readonly_attrs) - set(self.get_actual_ro_attributes().keys()))


def get_immutables_intersection(config_proxy, keys):
    immutables_set = set(config_proxy.immutable_attrs)
    keys_set = set(keys)
    # Return list of sets' intersection
    return list(immutables_set & keys_set)


def ensure_feature_is_enabled(client, feature_str):
    enabled_features = client.get_enabled_features()

    if enabled_features is None:
        enabled_features = []

    if feature_str not in enabled_features:
        client.enable_features(feature_str)
        client.save_config()


def get_nitro_client(module):
    from nssrc.com.citrix.netscaler.nitro.service.nitro_service import nitro_service

    client = nitro_service(module.params['nsip'], module.params['nitro_protocol'])
    client.set_credential(module.params['nitro_user'], module.params['nitro_pass'])
    client.timeout = float(module.params['nitro_timeout'])
    client.certvalidation = module.params['validate_certs']
    return client


netscaler_common_arguments = dict(
    nsip=dict(
        required=True,
        fallback=(env_fallback, ['NETSCALER_NSIP']),
    ),
    nitro_user=dict(
        required=True,
        fallback=(env_fallback, ['NETSCALER_NITRO_USER']),
        no_log=True
    ),
    nitro_pass=dict(
        required=True,
        fallback=(env_fallback, ['NETSCALER_NITRO_PASS']),
        no_log=True
    ),
    nitro_protocol=dict(
        choices=['http', 'https'],
        fallback=(env_fallback, ['NETSCALER_NITRO_PROTOCOL']),
        default='http'
    ),
    validate_certs=dict(
        default=True,
        type='bool'
    ),
    nitro_timeout=dict(default=310, type='float'),
    state=dict(
        choices=[
            'present',
            'absent',
        ],
        default='present',
    ),
    save_config=dict(
        type='bool',
        default=True,
    ),
)


loglines = []


def complete_missing_attributes(actual, attrs_list, fill_value=None):
    for attribute in attrs_list:
        if not hasattr(actual, attribute):
            setattr(actual, attribute, fill_value)


def log(msg):
    loglines.append(msg)


def get_ns_version(client):
    from nssrc.com.citrix.netscaler.nitro.resource.config.ns.nsversion import nsversion
    result = nsversion.get(client)
    m = re.match(r'^.*NS(\d+)\.(\d+).*$', result[0].version)
    if m is None:
        return None
    else:
        return int(m.group(1)), int(m.group(2))


def get_ns_hardware(client):
    from nssrc.com.citrix.netscaler.nitro.resource.config.ns.nshardware import nshardware
    result = nshardware.get(client)
    return result


def monkey_patch_nitro_api():

    from nssrc.com.citrix.netscaler.nitro.resource.base.Json import Json

    def new_resource_to_string_convert(self, resrc):
        # Line below is the actual patch
        dict_valid_values = dict((k.replace('_', '', 1), v) for k, v in resrc.__dict__.items() if v)
        return json.dumps(dict_valid_values)
    Json.resource_to_string_convert = new_resource_to_string_convert

    from nssrc.com.citrix.netscaler.nitro.util.nitro_util import nitro_util

    @classmethod
    def object_to_string_new(cls, obj):
        output = []
        flds = obj.__dict__
        for k, v in ((k.replace('_', '', 1), v) for k, v in flds.items() if v):
            if isinstance(v, bool):
                output.append('"%s":%s' % (k, v))
            elif isinstance(v, (binary_type, text_type)):
                v = to_native(v, errors='surrogate_or_strict')
                output.append('"%s":"%s"' % (k, v))
            elif isinstance(v, int):
                output.append('"%s":"%s"' % (k, v))
        return ','.join(output)

    @classmethod
    def object_to_string_withoutquotes_new(cls, obj):
        output = []
        flds = obj.__dict__
        for k, v in ((k.replace('_', '', 1), v) for k, v in flds.items() if v):
            if isinstance(v, (int, bool)):
                output.append('%s:%s' % (k, v))
            elif isinstance(v, (binary_type, text_type)):
                v = to_native(v, errors='surrogate_or_strict')
                output.append('%s:%s' % (k, cls.encode(v)))
        return ','.join(output)

    nitro_util.object_to_string = object_to_string_new
    nitro_util.object_to_string_withoutquotes = object_to_string_withoutquotes_new
