#
# DBus structures for subscription related data.
#
# Copyright (C) 2020  Red Hat, Inc.  All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
from dasbus.structure import DBusData
from dasbus.typing import *  # pylint: disable=wildcard-import

from pyanaconda.core.constants import DEFAULT_SUBSCRIPTION_REQUEST_TYPE

from pyanaconda.modules.common.structures.secret import SecretData, SecretDataList

__all__ = ["SystemPurposeData", "SubscriptionRequest", "AttachedSubscription"]


class SystemPurposeData(DBusData):
    """System purpose data."""

    def __init__(self):
        self._role = ""
        self._sla = ""
        self._usage = ""
        self._addons = []

    @property
    def role(self) -> Str:
        """Return the System Purpose role (if any).

        :return: system purpose role
        """
        return self._role

    @role.setter
    def role(self, role: Str):
        self._role = role

    @property
    def sla(self) -> Str:
        """Return the System Purpose SLA (if any).

        :return: system purpose SLA
        """
        return self._sla

    @sla.setter
    def sla(self, sla: Str):
        self._sla = sla

    @property
    def usage(self) -> Str:
        """Return the System Purpose usage (if any).

        :return: system purpose usage
        """
        return self._usage

    @usage.setter
    def usage(self, usage: Str):
        self._usage = usage

    @property
    def addons(self) -> List[Str]:
        """Return list of additional layered products or features (if any).

        :return: system purpose addons
        """
        return self._addons

    @addons.setter
    def addons(self, addons: List[Str]):
        self._addons = addons

    def check_data_available(self):
        """A helper function used to determining if some system purpose data is available.

        Otherwise we would have to query all the fields each time we want to check if we
        have any system purpose data available.

        :return: is any system purpose data is available
        :rtype: bool
        """
        return any((self.role, self.sla, self.usage, self.addons))

    def __eq__(self, other_instance):
        """Used to determining if other SystemPurposeData instance has the same data.

        Otherwise we would have to compare all the fields each time we want to check if the
        two SystemPurposeData instances have the same data.

        :param other_instance: another SystemPurposeData to compare with this one
        :type other_instance: SystemPurposeData instance
        :return: True if the other structure has the same system purpose data as this one,
                 False otherwise
        :rtype: bool
        """
        # if the other instance is not instance of SubscriptionRequest,
        # then it is always considered to be different
        if not isinstance(other_instance, SystemPurposeData):
            return False
        # addon ordering is not important
        if set(self.addons) != set(other_instance.addons):
            return False
        elif self.role != other_instance.role:
            return False
        elif self.sla != other_instance.sla:
            return False
        elif self.usage != other_instance.usage:
            return False
        else:
            return True


class SubscriptionRequest(DBusData):
    """Data for a subscription request.

    NOTE: Names of some of the fields are based on
          how the given keys are called in rhsm.conf.
    """

    def __init__(self):
        # subscription request type
        # (based on authentication method used)
        self._type = DEFAULT_SUBSCRIPTION_REQUEST_TYPE
        # user identification
        # - in case an account is member
        #   of multiple organizations, both
        #   organization and account username
        #   need to be set
        self._organization = ""
        self._redhat_account_username = ""
        # Candlepin instance
        self._server_hostname = ""
        # CDN base url
        self._rhsm_baseurl = ""
        # RHSM HTTP proxy
        self._server_proxy_hostname = ""
        self._server_proxy_port = -1
        self._server_proxy_user = ""
        # private data
        # - we are using SecretData & SecretDataList
        #   nested DBus structures to protect this
        #   sensitive data
        # - this way they can be set-only & easily
        #   removed from SubscriptionRequest on
        #   output from the Subscription module
        # - they also support a robust way of clearing
        #   previously set sensitive data if required
        self._redhat_account_password = SecretData()
        self._activation_keys = SecretDataList()
        self._server_proxy_password = SecretData()

    @property
    def type(self) -> Str:
        """Subscription request type.

        Subscription request type is based on the authentication method used.

        At the moment the following two are supported:
        - username + password
        - organization id + one or more activation keys

        By default username + password is used.

        Valid values are:
        "username_password"
        "org_activation_key"

        :return: subscription request type
        :rtype: str
        """
        return self._type

    @type.setter
    def type(self, request_type: Str):
        self._type = request_type

    @property
    def organization(self) -> Str:
        """Organization id for subscription purposes.

        In most cases one of the following will be used:
        - org id + one or more activation keys
        - username + password

        There is also a less often expected use case,
        which applies if the same user account exists
        in multiple organizations on the same Candlepin
        instance. In such a case both username and
        organization id needs to be set.

        :return: organization id
        :rtype: str
        """
        return self._organization

    @organization.setter
    def organization(self, organization: Str):
        self._organization = organization

    @property
    def account_username(self) -> Str:
        """Red Hat account username for subscription purposes.

        In case the account for the given username is member
        of multiple organizations, organization id needs to
        be specified as well or else the registration attempt
        will not be successful.

        :return: Red Hat account username
        :rtype: str
        """
        return self._redhat_account_username

    @account_username.setter
    def account_username(self, account_username: Str):
        self._redhat_account_username = account_username

    @property
    def server_hostname(self) -> Str:
        """Subscription server hostname.

        This is basically a URL pointing to a Candlepin
        instance to be used. It could be the one handling
        general subscriptions hosted by Red Hat or one
        embedded in a Satellite deployment.

        If no custom server hostname is set, the default
        value used by subscription manager will be used,
        which is usually the URL pointing to the general
        purpose Red Hat hosted Candlepin instance.

        :return: Candlepin instance URL
        :rtype: str
        """
        return self._server_hostname

    @server_hostname.setter
    def server_hostname(self, server_hostname: Str):
        self._server_hostname = server_hostname

    @property
    def rhsm_baseurl(self) -> Str:
        """CDN repository base URL.

        Sets the base URL for the RHSM generated
        repo file.

        Setting this to a non default value only
        makes sense if registering against Satellite
        (as you would want to use the repos hosted
        on the given Satellite instance) or possibly
        during testing.

        If no custom rhsm baseurl is set, the default
        value used by subscription managed will be used,
        which is generally baseurl for the Red Hat CDN.

        :return: RHSM base url
        :rtype: str
        """
        return self._rhsm_baseurl

    @rhsm_baseurl.setter
    def rhsm_baseurl(self, rhsm_baseurl: Str):
        self._rhsm_baseurl = rhsm_baseurl

    @property
    def server_proxy_hostname(self) -> Str:
        """RHSM HTTP proxy - hostname.

        This is the hostname of the RHSM HTTP
        proxy, which will be used for subscription
        purposes only, eq. this will not configure
        a system wide HTTP proxy.

        :return: RHSM HTTP proxy hostname
        :rtype: str
        """
        return self._server_proxy_hostname

    @server_proxy_hostname.setter
    def server_proxy_hostname(self, hostname: Str):
        self._server_proxy_hostname = hostname

    @property
    def server_proxy_port(self) -> Int:
        """RHSM HTTP proxy - port number.

        -1 means port has not been set.

        :returns: RHSM HTTP proxy port number
        :rtype: int
        """
        return self._server_proxy_port

    @server_proxy_port.setter
    def server_proxy_port(self, port_number: Int):
        self._server_proxy_port = port_number

    @property
    def server_proxy_user(self) -> Str:
        """RHSM HTTP proxy - access username.

        :return: RHSM HTTP proxy access username
        :rtype: str
        """
        return self._server_proxy_user

    @server_proxy_user.setter
    def server_proxy_user(self, username: Str):
        self._server_proxy_user = username

    # private data
    # - generally sensitive data such as passwords
    #   or activation keys
    # - these values should be "write only",
    #   meaning data goes to the Subscription
    #   module but can't be read out later one
    #   via the public API
    # - only the Subscription module should have
    #   have access to these data internally &
    #   use them appropriately (eq. register
    #   the system, authenticate to HTTP proxy, etc.)
    # - it should be also possible to explicitly
    #   clear a previously set secret
    # - to protect these values we are using
    #   SecretData & SecretDataList, see their
    #   implementation for more information

    @property
    def account_password(self) -> SecretData:
        """Red Hat account password.

        NOTE: This property is stored in SecretData
              nested DBus structure to protect its contents.

        :return: Red hat account password stored in a SecretData instance
        :rtype: SecretData instance
        """
        return self._redhat_account_password

    @account_password.setter
    def account_password(self, password: SecretData):
        self._redhat_account_password = password

    @property
    def activation_keys(self) -> SecretDataList:
        """List of activation keys.

        For a successful activation key based registration
        at least one activation key needs to be set.

        NOTE: This property is stored in SecretDataList
              nested DBus structure to protect its contents.

        :return: list of activation keys stored in SecretDataList instance
        :rtype: SecretDataList instance
        """
        return self._activation_keys

    @activation_keys.setter
    def activation_keys(self, activation_keys: SecretDataList):
        self._activation_keys = activation_keys

    @property
    def server_proxy_password(self) -> SecretData:
        """RHSM HTTP proxy - access password.

        NOTE: This property is stored in SecretData
              nested DBus structure to protect its contents.

        :return: RHSM HTTP proxy password stored in SecretData instance
        :rtype: SecretData instance
        """
        return self._server_proxy_password

    @server_proxy_password.setter
    def server_proxy_password(self, password: SecretData):
        self._server_proxy_password = password


class AttachedSubscription(DBusData):
    """Data for a single attached subscription."""

    def __init__(self):
        self._name = ""
        self._service_level = ""
        self._sku = ""
        self._contract = ""
        self._start_date = ""
        self._end_date = ""
        # we can expect at least one entitlement
        # to be consumed per attached subscription
        self._consumed_entitlement_count = 1

    @property
    def name(self) -> Str:
        """Name of the attached subscription.

        Example: "Red Hat Beta Access"

        :return: subscription name
        :rtype: str
        """
        return self._name

    @name.setter
    def name(self, name: Str):
        self._name = name

    @property
    def service_level(self) -> Str:
        """Service level of the attached subscription.

        Example: "Premium"

        :return: service level
        :rtype: str
        """
        return self._service_level

    @service_level.setter
    def service_level(self, service_level: Str):
        self._service_level = service_level

    @property
    def sku(self) -> Str:
        """SKU id of the attached subscription.

        Example: "MBT8547"

        :return: SKU id
        :rtype: str
        """
        return self._sku

    @sku.setter
    def sku(self, sku: Str):
        self._sku = sku

    @property
    def contract(self) -> Str:
        """Contract identifier.

        Example: "32754658"

        :return: contract identifier
        :rtype: str
        """
        return self._contract

    @contract.setter
    def contract(self, contract: Str):
        self._contract = contract

    @property
    def start_date(self) -> Str:
        """Subscription start date.

        We do not guarantee fixed date format,
        but we aim for the date to look good
        when displayed in a GUI and be human
        readable.

        For context see the following bug, that
        illustrates the issues we are having with
        the source date for this property, that
        prevent us from providing a consistent
        date format:
        https://bugzilla.redhat.com/show_bug.cgi?id=1793501

        Example: "Nov 04, 2019"

        :return: start date of the subscription
        :rtype: str
        """
        return self._start_date

    @start_date.setter
    def start_date(self, start_date: Str):
        self._start_date = start_date

    @property
    def end_date(self) -> Str:
        """Subscription end date.

        We do not guarantee fixed date format,
        but we aim for the date to look good
        when displayed in a GUI and be human
        readable.

        For context see the following bug, that
        illustrates the issues we are having with
        the source date for this property, that
        prevent us from providing a consistent
        date format:
        https://bugzilla.redhat.com/show_bug.cgi?id=1793501

        Example: "Nov 04, 2020"

        :return: end date of the subscription
        :rtype: str
        """
        return self._end_date

    @end_date.setter
    def end_date(self, end_date: Str):
        self._end_date = end_date

    @property
    def consumed_entitlement_count(self) -> Int:
        """Number of consumed entitlements for this subscription.

        Example: "1"

        :return: consumed entitlement number
        :rtype: int
        """
        return self._consumed_entitlement_count

    @consumed_entitlement_count.setter
    def consumed_entitlement_count(self, consumed_entitlement_count: Int):
        self._consumed_entitlement_count = consumed_entitlement_count
