"""
Identity Domain - Combiner for domain enrollment
================================================

The combiner detects enrollment into identity domains such as IPA,
Active Directory, generic Kerberos realm, and generic LDAP. It parses
domains and realms from SSSD, KRB5, IPA, and Samba configuration.

Supported domain types
----------------------

* IPA (RHEL IdM, FreeIPA)
* Active Directory (SSSD)
* Active Directory (Samba winbind)
* generic LDAP domain (SSSD)
* generic LDAP domain with Kerberos authentication (SSSD)
* generic Kerberos realm (from ``krb5.conf``)

The combiner cannot detect generic Kerberos realms that solely rely upon
DNS realm lookup (``dns_lookup_realm``).

Examples::

    DomainInfo(
        name="ipa.test",
        domain_type="IPA",
        server_software="IPA",
        client_software="SSSD",
        domain="ipa.test",
        realm="IPA.TEST",
        workgroup=None,
        ipa_mode="client",
    )

    DomainInfo(
        name="ad-winbind.test",
        domain_type="Active Directory (winbind)",
        server_software="Active Directory",
        client_software="winbind",
        domain="ad-winbind.test",
        realm="AD-WINBIND.TEST",
        workgroup="AD-WINBIND",
        ipa_mode=None,
    )
"""
import collections

from insights.core.exceptions import SkipComponent
from insights.core.plugins import combiner
from insights.combiners.ipa import IPA
from insights.parsers.samba import SambaConfigs
from insights.combiners.sssd_conf import SSSDConfAll
from insights.combiners.krb5 import AllKrb5Conf


class DomainTypes(object):
    """Human readable domain type"""

    # SSSD domain types
    IPA = "IPA"
    AD_SSSD = "Active Directory (SSSD)"
    LDAP = "LDAP"
    LDAP_KRB5 = "LDAP/Kerberos"
    # krb5.conf but not in sssd.conf
    KRB5 = "Kerberos"
    # Samba winbind
    AD_WINBIND = "Active Directory (winbind)"


class ServerSoftware(object):
    """Server software"""

    IPA = "IPA"
    AD = "Active Directory"
    LDAP = "generic LDAP"
    LDAP_KRB5 = "generic LDAP/Kerberos"
    KRB5 = "generic Kerberos"


class ClientSoftware(object):
    """Client software"""

    SSSD = "SSSD"
    WINBIND = "winbind"
    KRB5 = "Kerberos"


class IPAMode(object):
    """IPA mode (server or client-only)"""

    IPA_CLIENT = "client"
    IPA_SERVER = "server"


DomainInfo = collections.namedtuple(
    "DomainInfo",
    [
        "name",
        "domain_type",
        "server_software",
        "client_software",
        "domain",
        "realm",
        "workgroup",
        "ipa_mode",
    ],
)
"""Identity domain information

Attributes:
    name (str): user-friendly name
        either SSSD's domain name, domain name, or lower-case realm name
    domain_type (str): domain type, e.g. ``IPA`` or ``Active Directory (SSSD)``
    server_software (str): name of the server software, e.g. ``Active Directory``
    client_software (str): name of the client software, e.g. ``SSSD`` or ``winbind``
    domain (str, None): name of the identity domain,
        not set for generic Kerberos or LDAP
    realm (str, None): Kerberos realm name,
        not set for generic LDAP
    workgroup (str, None): workgroup name,
        only set for AD with winbind
    ipa_mode (str, None): IPA mode (server or client),
        only set for IPA
"""


@combiner(optional=[SSSDConfAll, AllKrb5Conf, IPA, SambaConfigs])
class IdentityDomain(object):
    """
    A combiner for identity domains.

    Raises:
        SkipComponent: When no identity domains are detected.

    Attributes:
        domains (list): List of the namedtuple `DomainInfo`
        default_realm (str, None): default realm name (if configured)
        dns_lookup_realm (bool): is Kerberos realm DNS lookup enabled?
        dns_lookup_kdc (bool): is Kerberos KDC DNS lookup enabled?
    """

    def __init__(self, sssd=None, krb5=None, ipa=None, smb=None):
        if sssd is None and krb5 is None and smb is None:
            # ipa depends on sssd
            raise SkipComponent("KRB5, SSSD, and Samba are not configured")

        self.domains = []
        self._realms = set()

        if krb5 is not None:
            self.default_realm = krb5.default_realm
            self.dns_lookup_realm = krb5.dns_lookup_realm
            self.dns_lookup_kdc = krb5.dns_lookup_kdc
        else:
            self.default_realm = None
            # krb5.conf default is True
            self.dns_lookup_realm = True
            self.dns_lookup_kdc = True

        if sssd is not None:
            self._parse_sssd(sssd, ipa)
        if smb is not None:
            self._parse_smb(smb)
        if krb5 is not None:
            # parse /etc/krb5.conf last to skip SSSD and Samba realms
            self._parse_krb5(krb5)

        if not self.domains:
            raise SkipComponent("No identity domains detected")

    def _add_domaininfo(
        self, name, dtype, srv, clnt, domain, realm, workgroup=None, ipa_mode=None
    ):
        if realm is not None:
            if realm in self._realms:
                # already configured
                return
            self._realms.add(realm)

        di = DomainInfo(name, dtype, srv, clnt, domain, realm, workgroup, ipa_mode)
        self.domains.append(di)

    def _parse_sssd(self, sssd, ipa):
        """Extract domains from sssd.conf

        Supports id_providers "ad", "ipa", and "ldap".
        """
        id_auth_providers = set(["ldap", "krb5", "ipa", "ad", "proxy"])
        for name in sssd.enabled_domains:
            if "/" in name:
                # Ignore trusted domain (subdomain) configuration. Subdomain
                # settings are configured as
                # `[domain/parent.example/subdomain.example]`.
                continue
            conf = sssd.domain_config(name)
            id_provider = conf.get("id_provider")
            ipa_mode = None

            auth_provider = conf.get("auth_provider")
            if auth_provider is None and id_provider in id_auth_providers:
                # most id providers are also an auth providers
                auth_provider = id_provider
            elif auth_provider == "none":
                auth_provider = None

            if id_provider == "ad":
                dtype = DomainTypes.AD_SSSD
                srv = ServerSoftware.AD
                domain = conf.get("ad_domain", name)
                realm = conf.get("krb5_realm", domain.upper())
            elif id_provider == "ipa":
                if ipa is None or not ipa.is_client:
                    # unsupported configuration
                    continue
                dtype = DomainTypes.IPA
                srv = ServerSoftware.IPA
                domain = conf.get("ipa_domain", name)
                realm = conf.get("krb5_realm", domain.upper())
                ipa_mode = IPAMode.IPA_SERVER if ipa.is_server else IPAMode.IPA_CLIENT
            elif id_provider == "ldap":
                if auth_provider == "ldap":
                    dtype = DomainTypes.LDAP
                    srv = ServerSoftware.LDAP
                    domain = None
                    realm = None
                elif auth_provider == "krb5":
                    dtype = DomainTypes.LDAP_KRB5
                    srv = ServerSoftware.LDAP_KRB5
                    domain = None
                    # krb5_domain is required
                    realm = conf.get("krb5_realm", name.upper())
                else:
                    # unsupported configuration
                    continue
            elif id_provider in ("proxy", "files"):
                # not an identity domain
                continue
            else:
                # unsupported configuration
                continue

            self._add_domaininfo(
                name, dtype, srv, ClientSoftware.SSSD, domain, realm, ipa_mode=ipa_mode
            )

    def _parse_smb(self, smb):
        """Parse smb.conf to detect AD with winbind

        We ignore IPA DC here as the information is already provided by
        `sssd.conf`. IPA DC has either server role `ROLE_IPA_DC`
        (Samba >= 4.16.0) or `ROLE_DOMAIN_PDC`, and always
        `security=user`.
        """
        if (
            smb.server_role != "ROLE_DOMAIN_MEMBER" or
            not smb.has_option("global", "security") or
            smb.get("global", "security").upper() != "ADS" or
            not smb.has_option("global", "realm")
        ):
            return
        realm = smb.get("global", "realm")
        domain = realm.lower()

        if smb.has_option("global", "workgroup"):
            workgroup = smb.get("global", "workgroup")
        else:
            workgroup = realm.split(".", 1)[0]

        self._add_domaininfo(
            domain,
            DomainTypes.AD_WINBIND,
            ServerSoftware.AD,
            ClientSoftware.WINBIND,
            domain,
            realm,
            workgroup,
        )

    def _parse_krb5(self, krb5):
        """Parse krb5.conf to detect additional generic Kerberos realms"""
        for realm in sorted(krb5.realms):
            if realm in self._realms:
                continue

            self._add_domaininfo(
                realm.lower(),
                DomainTypes.KRB5,
                ServerSoftware.KRB5,
                ClientSoftware.KRB5,
                None,
                realm,
            )
