# domain management - domain level
#
# Copyright Matthias Dieter Wallnoefer 2009
# Copyright Andrew Kroeger 2009
# Copyright Jelmer Vernooij 2007-2012
# Copyright Giampaolo Lauria 2011
# Copyright Matthieu Patou <mat@matws.net> 2011
# Copyright Andrew Bartlett 2008-2015
# Copyright Stefan Metzmacher 2012
#
# 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 3 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/>.
#

import ldb
import samba.getopt as options
from samba.auth import system_session
from samba.dsdb import check_and_update_fl, DS_DOMAIN_FUNCTION_2000
from samba.netcmd import Command, CommandError, Option
from samba.samdb import SamDB

from samba import functional_level


class cmd_domain_level(Command):
    """Raise domain and forest function levels."""

    synopsis = "%prog (show|raise <options>) [options]"

    takes_optiongroups = {
        "sambaopts": options.SambaOptions,
        "credopts": options.CredentialsOptions,
        "versionopts": options.VersionOptions,
    }

    takes_options = [
        Option("-H", "--URL", help="LDB URL for database or target server", type=str,
               metavar="URL", dest="H"),
        Option("-q", "--quiet", help="Be quiet", action="store_true"),  # unused
        Option("--forest-level", type="choice", choices=["2003", "2008", "2008_R2", "2012", "2012_R2", "2016"],
               help="The forest function level (2003 | 2008 | 2008_R2 | 2012 | 2012_R2 | 2016)"),
        Option("--domain-level", type="choice", choices=["2003", "2008", "2008_R2", "2012", "2012_R2", "2016"],
               help="The domain function level (2003 | 2008 | 2008_R2 | 2012 | 2012_R2 | 2016)")
    ]

    takes_args = ["subcommand"]

    def run(self, subcommand, H=None, forest_level=None, domain_level=None,
            quiet=False, credopts=None, sambaopts=None, versionopts=None):
        if subcommand not in ["show", "raise"]:
            raise CommandError("invalid argument: '%s' (choose from 'show', 'raise')" % subcommand)

        lp = sambaopts.get_loadparm()
        creds = credopts.get_credentials(lp, fallback_machine=True)

        samdb = SamDB(url=H, session_info=system_session(),
                      credentials=creds, lp=lp)

        domain_dn = samdb.domain_dn()

        in_transaction = False
        if subcommand == "raise" and (H is None or not H.startswith("ldap")):
            samdb.transaction_start()
            in_transaction = True
            try:
                check_and_update_fl(samdb, lp)
            except Exception as e:
                samdb.transaction_cancel()
                raise e

        try:
            res_forest = samdb.search("CN=Partitions,%s" % samdb.get_config_basedn(),
                                      scope=ldb.SCOPE_BASE, attrs=["msDS-Behavior-Version"])
            if len(res_forest) != 1:
                raise CommandError("Forest not found")

            res_domain = samdb.search(domain_dn, scope=ldb.SCOPE_BASE,
                                      attrs=["msDS-Behavior-Version", "nTMixedDomain"])
            if len(res_domain) != 1:
                raise CommandError("domain not found")

            res_domain_cross = samdb.search("CN=Partitions,%s" % samdb.get_config_basedn(),
                                            scope=ldb.SCOPE_SUBTREE,
                                            expression="(&(objectClass=crossRef)(nCName=%s))" % domain_dn,
                                            attrs=["msDS-Behavior-Version"])
            if len(res_domain_cross) != 1:
                raise CommandError("no crossRef objects found")

            res_dc_s = samdb.search("CN=Sites,%s" % samdb.get_config_basedn(),
                                    scope=ldb.SCOPE_SUBTREE, expression="(objectClass=nTDSDSA)",
                                    attrs=["msDS-Behavior-Version"])
            if len(res_dc_s) == 0:
                raise CommandError("no nTDSDSA objects found")

            # default values, since "msDS-Behavior-Version" does not exist on Windows 2000 AD
            level_forest = DS_DOMAIN_FUNCTION_2000
            level_domain = DS_DOMAIN_FUNCTION_2000

            if "msDS-Behavior-Version" in res_forest[0]:
                level_forest = int(res_forest[0]["msDS-Behavior-Version"][0])
            if "msDS-Behavior-Version" in res_domain[0]:
                level_domain = int(res_domain[0]["msDS-Behavior-Version"][0])
            level_domain_mixed = int(res_domain[0]["nTMixedDomain"][0])

            min_level_dc = None
            for msg in res_dc_s:
                if "msDS-Behavior-Version" in msg:
                    if min_level_dc is None or int(msg["msDS-Behavior-Version"][0]) < min_level_dc:
                        min_level_dc = int(msg["msDS-Behavior-Version"][0])
                else:
                    min_level_dc = DS_DOMAIN_FUNCTION_2000
                    # well, this is the least
                    break

            if level_forest < DS_DOMAIN_FUNCTION_2000 or level_domain < DS_DOMAIN_FUNCTION_2000:
                raise CommandError("Domain and/or forest function level(s) is/are invalid. Correct them or reprovision!")
            if min_level_dc < DS_DOMAIN_FUNCTION_2000:
                raise CommandError("Lowest function level of a DC is invalid. Correct this or reprovision!")
            if level_forest > level_domain:
                raise CommandError("Forest function level is higher than the domain level(s). Correct this or reprovision!")
            if level_domain > min_level_dc:
                raise CommandError("Domain function level is higher than the lowest function level of a DC. Correct this or reprovision!")
        except Exception as e:
            if in_transaction:
                samdb.transaction_cancel()
            raise e

        def do_show():
            self.message("Domain and forest function level for domain '%s'" % domain_dn)
            if level_forest == DS_DOMAIN_FUNCTION_2000 and level_domain_mixed != 0:
                self.message("\nATTENTION: You run SAMBA 4 on a forest function level lower than Windows 2000 (Native). This isn't supported! Please raise!")
            if level_domain == DS_DOMAIN_FUNCTION_2000 and level_domain_mixed != 0:
                self.message("\nATTENTION: You run SAMBA 4 on a domain function level lower than Windows 2000 (Native). This isn't supported! Please raise!")
            if min_level_dc == DS_DOMAIN_FUNCTION_2000 and level_domain_mixed != 0:
                self.message("\nATTENTION: You run SAMBA 4 on a lowest function level of a DC lower than Windows 2003. This isn't supported! Please step-up or upgrade the concerning DC(s)!")

            self.message("")

            outstr = functional_level.level_to_string(level_forest)
            self.message("Forest function level: (Windows) " + outstr)

            if level_domain == DS_DOMAIN_FUNCTION_2000 and level_domain_mixed:
                outstr = "2000 mixed (NT4 DC support)"
            else:
                outstr = functional_level.level_to_string(level_domain)
            self.message("Domain function level: (Windows) " + outstr)

            outstr = functional_level.level_to_string(min_level_dc)
            self.message("Lowest function level of a DC: (Windows) " + outstr)

        def do_raise():
            msgs = []

            current_level_domain = level_domain

            if domain_level is not None:
                try:
                    new_level_domain = functional_level.string_to_level(domain_level)
                except KeyError:
                    raise CommandError(f"New functional level '{domain_level}' is not known to Samba as an AD functional level")

                if new_level_domain <= level_domain and level_domain_mixed == 0:
                    raise CommandError("Domain function level can't be smaller than or equal to the actual one!")
                if new_level_domain > min_level_dc:
                    raise CommandError("Domain function level can't be higher than the lowest function level of a DC!")

                # Deactivate mixed/interim domain support
                if level_domain_mixed != 0:
                    # Directly on the base DN
                    m = ldb.Message()
                    m.dn = ldb.Dn(samdb, domain_dn)
                    m["nTMixedDomain"] = ldb.MessageElement("0",
                                                            ldb.FLAG_MOD_REPLACE, "nTMixedDomain")
                    samdb.modify(m)
                    # Under partitions
                    m = ldb.Message()
                    m.dn = res_domain_cross[0].dn
                    m["nTMixedDomain"] = ldb.MessageElement("0",
                                                            ldb.FLAG_MOD_REPLACE, "nTMixedDomain")
                    try:
                        samdb.modify(m)
                    except ldb.LdbError as e:
                        (enum, emsg) = e.args
                        if enum != ldb.ERR_UNWILLING_TO_PERFORM:
                            raise

                # Directly on the base DN
                m = ldb.Message()
                m.dn = ldb.Dn(samdb, domain_dn)
                m["msDS-Behavior-Version"] = ldb.MessageElement(
                    str(new_level_domain), ldb.FLAG_MOD_REPLACE,
                    "msDS-Behavior-Version")
                samdb.modify(m)
                # Under partitions
                m = ldb.Message()
                m.dn = res_domain_cross[0].dn
                m["msDS-Behavior-Version"] = ldb.MessageElement(
                    str(new_level_domain), ldb.FLAG_MOD_REPLACE,
                    "msDS-Behavior-Version")
                try:
                    samdb.modify(m)
                except ldb.LdbError as e2:
                    (enum, emsg) = e2.args
                    if enum != ldb.ERR_UNWILLING_TO_PERFORM:
                        raise

                current_level_domain = new_level_domain
                msgs.append("Domain function level changed!")

            if forest_level is not None:
                new_level_forest = functional_level.string_to_level(forest_level)

                if new_level_forest <= level_forest:
                    raise CommandError("Forest function level can't be smaller than or equal to the actual one!")
                if new_level_forest > current_level_domain:
                    raise CommandError("Forest function level can't be higher than the domain function level(s). Please raise it/them first!")

                m = ldb.Message()
                m.dn = ldb.Dn(samdb, "CN=Partitions,%s" % samdb.get_config_basedn())
                m["msDS-Behavior-Version"] = ldb.MessageElement(
                    str(new_level_forest), ldb.FLAG_MOD_REPLACE,
                    "msDS-Behavior-Version")
                samdb.modify(m)
                msgs.append("Forest function level changed!")
            msgs.append("All changes applied successfully!")
            self.message("\n".join(msgs))
            return

        if subcommand == "show":
            assert not in_transaction
            do_show()
            return
        elif subcommand == "raise":
            try:
                do_raise()
            except Exception as e:
                if in_transaction:
                    samdb.transaction_cancel()
                raise e
            if in_transaction:
                samdb.transaction_commit()
            return

        raise AssertionError("Internal Error subcommand[%s] not handled" % subcommand)
