# SPDX-FileCopyrightText: 2025-2026 Univention GmbH
# SPDX-License-Identifier: AGPL-3.0-only

"""
**Class Distribution**\n

.. module:: distribution
    :platform: Unix

.. moduleauthor:: Ammar Najjar <najjar@univention.de>
"""

from __future__ import print_function

import os
import time

import six

import univention.testing.strings as uts
import univention.testing.ucr as ucr_test
from univention.testing import utils
from univention.testing.umc import Client


class Distribution(object):
    """
    Contains the needed functionality for Materials distribution.
    By default the distribution is manual.\n
    :param school: name of the ou
    :type school: str
    :param connection:
    :type connection: UMC connection object
    :param ucr:
    :type ucr: UCR object
    :param name: name of distribution project to be added later
    :type name: str
    :param description: description of distribution project to be added  later
    :type description: str
    :param sender: name of the creater user (teacher or admin)
    :type sender: str
    :param flavor: flavor of the acting user
    :type flavor: str ('teacher' or 'admin')
    :param distributeTime: time for automatic distribution
    :type distributeTime: str ('%I:%M')
    :param distributionDate: date for automatic distribution
    :type distributionDate: str ('%Y-%m-%d)
    :param collectionTime: time for automatic collection
    :type collectionTime: str ('%I:%M')
    :param collectionDate: date for automatic collection
    :type collectionDate: str ('%Y-%m-%d)
    :param distributeType: type of the distribution
    :type distributionType: str ('automatic' or 'manual')
    :param collectionTye: type of the collection
    :type collectionType: str ('automatic' or 'manual')
    :param files: names of material files for the distribution project
    :type files: list of str
    :param recipients: groups which are included in the distribution project
    :type recipients: list of group objects
    """

    def __init__(
        self,
        school,
        connection=None,
        sender=None,
        flavor=None,
        ucr=None,
        description=None,
        name=None,
        distributeType="manual",
        distributeTime=None,
        distributeDate=None,
        collectType="manual",
        collectTime=None,
        collectDate=None,
        files=[],
        recipients=[],
    ):
        account = utils.UCSTestDomainAdminCredentials()
        admin = account.username
        passwd = account.bindpw
        self.school = school
        self.name = name if name else uts.random_string()
        self.description = description if description else uts.random_string()
        if distributeTime:
            self.distributeTime = distributeTime
        else:
            self.distributeTime = time.strftime("%I:%M")
        if distributeDate:
            self.distributeDate = distributeDate
        else:
            self.distributeDate = time.strftime("%Y-%m-%d")
        self.collectTime = collectTime if collectTime else time.strftime("%I:%M")
        self.collectDate = collectDate if collectDate else time.strftime("%Y-%m-%d")
        self.distributeType = distributeType
        self.collectType = collectType
        self.filename_encodings = files
        self.recipients = recipients
        self.ucr = ucr if ucr else ucr_test.UCSTestConfigRegistry()
        self.sender = sender if sender else admin
        self.flavor = flavor if flavor else "admin"
        if connection:
            self.client = connection
        else:
            self.client = Client(None, admin, passwd)
        self.distributed_version = 1

    @property
    def files(self):
        return [file_name for file_name, encoding in self.filename_encodings]

    @property
    def files_encoded(self):
        if six.PY2:
            return [
                file_name.decode(encoding).encode("UTF-8")
                for file_name, encoding in self.filename_encodings
            ]
        return [
            file_name.decode(encoding) if isinstance(file_name, bytes) else file_name
            for file_name, encoding in self.filename_encodings
        ]

    def query(self, filt="private", pattern=""):
        """
        Calles 'distribution/query'
        :param pattern: the pattern to use in the search
        :type pattern: str
        """
        flavor = self.flavor
        param = {"filter": filt, "pattern": pattern}
        reqResult = self.client.umc_command("distribution/query", param, flavor).result
        result = [x["name"] for x in reqResult if reqResult is not None]
        return result

    def get(self):
        """Calls 'distribute/get'"""
        name = [self.name]
        reqResult = self.client.umc_command("distribution/get", name, self.flavor).result
        return reqResult[0]

    def idir(self, path):
        """
        Dir a specific path.\n
        :param path: wanted path
        :type path: str
        :return: list of file names
        """
        files = []
        for root, _, filenames in os.walk(path):
            for f in filenames:
                files.append(os.path.relpath(os.path.join(root, f), path))
        return files

    def genData(self, file_name, content_type, boundary, flavor, override_file_name=None):
        """
        Generates data in the form to be sent via http POST request.\n
        :param file_name: file name to be uploaded
        :type file_name: str
        :param content_type: type of the content of the file
        :type content_type: str ('text/plain',..)
        :param boundary: the boundary
        :type boundary: str (-------123091)
        :param flavor: flavor of the acting user
        :type flavor: str
        """
        mime_file_name = override_file_name or os.path.basename(file_name)
        with open(file_name) as f:
            data = r"""--{0}
Content-Disposition: form-data; name="uploadedfile"; filename="{1}"
Content-Type: {2}

{3}
--{0}
Content-Disposition: form-data; name="flavor"

{4}
--{0}
Content-Disposition: form-data; name="iframe"

false
--{0}
Content-Disposition: form-data; name="uploadType"

html5
--{0}--
""".format(boundary, mime_file_name, content_type, f.read(), flavor)
        return data.replace("\n", "\r\n")

    def uploadFile(self, file_name, content_type=None, override_file_name=None):
        """
        Uploads a file via http POST request.\n
        :param file_name: file name to be uploaded
        :type file_name: str
        :param content_type: type of the content of the file
        :type content_type: str ('text/plain',..)
        """
        print("Uploading a file")
        content_type = content_type or "application/octet-stream"
        boundary = "---------------------------103454444410473823401882756"
        data = self.genData(
            file_name, content_type, boundary, self.flavor, override_file_name=override_file_name
        )
        header_content = {"Content-Type": "multipart/form-data; boundary=%s" % (boundary,)}
        self.client.request(  # noqa: B018
            "POST", "upload/distribution/upload", data, headers=header_content
        ).result

    def add(self):
        """
        Create files and upload them then add the project,
        calls: 'distribution/add'
        """
        # creatng and uploading the files
        content_type = "text/plain"
        for filename in self.files:
            with open(filename, "w") as g:
                g.write("test_content")
            self.uploadFile(filename, content_type)
        print("Adding Project %s" % (self.name))
        flavor = self.flavor
        recipients = []
        for item in self.recipients:
            recipients.append(item.dn())
        print("recipients=", recipients)
        param = [
            {
                "object": {
                    "collectDate": self.collectDate,
                    "collectTime": self.collectTime,
                    "collectType": self.collectType,
                    "description": self.description,
                    "distributeDate": self.distributeDate,
                    "distributeTime": self.distributeTime,
                    "distributeType": self.distributeType,
                    "files": self.files_encoded,
                    "name": self.name,
                    "recipients": recipients,
                },
                "options": None,
            }
        ]
        print("param=", param)
        reqResult = self.client.umc_command("distribution/add", param, flavor).result
        print("reqResult =", reqResult)
        if not reqResult[0]["success"]:
            utils.fail("Unable to add project (%r)" % (param,))

    def check_add(self):
        """
        Calls 'distribution/query'
        and check the existance of the added project
        """
        print("Checking %s addition" % (self.name,))
        current = self.query(pattern=self.name)
        if self.name not in current:
            utils.fail("Project %s was not added successfully" % (self.name,))

    def put(
        self,
        description=None,
        distributeType=None,
        distributeTime=None,
        distributeDate=None,
        collectType=None,
        collectTime=None,
        collectDate=None,
        files=[],
        recipients=[],
    ):
        """
        Modifies the already existing project.\n
        :param description: description of the project to be added later
        :type description: str
        :param distributeTime: time for automatic distribution
        :type distributeTime: str ('%I:%M')
        :param distributionDate: date for automatic distribution
        :type distributionDate: str ('%Y-%m-%d)
        :param collectionTime: time for automatic collection
        :type collectionTime: str ('%I:%M')
        :param collectionDate: date for automatic collection
        :type collectionDate: str ('%Y-%m-%d)
        :param distributeType: type of the distribution
        :type distributionType: str ('automatic' or 'manual')
        :param collectionTye: type of the collection
        :type collectionType: str ('automatic' or 'manual')
        :param files: names of material files for the distribution project
        :type files: list of str
        :param recipients: groups which are included in the project
        :type recipients: list of group objects
        """
        print("Editing Project %s" % (self.name))
        description = description if description else self.description
        if not distributeType:
            distributeType = self.distributeType
        if not distributeTime:
            distributeTime = self.distributeTime
        if not distributeDate:
            distributeDate = self.distributeDate
        collectType = collectType if collectType else self.collectType
        collectTime = collectTime if collectTime else self.collectTime
        collectDate = collectDate if collectDate else self.collectDate
        files = files if files else self.files
        recipients = recipients if recipients else self.recipients
        new_recipients = []
        for item in recipients:
            new_recipients.append(item.dn())
        flavor = self.flavor
        param = [
            {
                "object": {
                    "collectDate": collectDate,
                    "collectTime": collectTime,
                    "collectType": collectType,
                    "description": description,
                    "distributeDate": distributeDate,
                    "distributeTime": distributeTime,
                    "distributeType": distributeType,
                    "files": files,
                    "name": self.name,
                    "recipients": new_recipients,
                },
                "options": None,
            }
        ]
        reqResult = self.client.umc_command("distribution/put", param, flavor).result
        print("reqResult =", reqResult)
        if not reqResult[0]["success"]:
            utils.fail("Unable to edit project with params =(%r)" % (param,))
        else:
            self.description = description
            self.distributeType = distributeType
            self.distributeTime = distributeTime
            self.distributeDate = distributeDate
            self.collectType = collectType
            self.collectTime = collectTime
            self.collectDate = collectDate
            self.filename_encodings = [(x, "utf8") for x in files]
            self.recipients = recipients

    def check_put(self, previousGetResult):
        """
        Calls 'distribution/get' and check the modified project
        :param previousGetResult: info from previous get
        :type previousGetResult: dict
        check changing sates for distribution and collection
        """
        print("Checking %s modification" % (self.name,))
        found = self.get()
        supposed = {
            "files": found["files"],
            "sender": found["sender"],
            "description": found["description"],
            "recipients": found["recipients"],
            "distributeType": found["distributeType"],
            "__type__": found["__type__"],
            "collectType": found["collectType"],
            "name": found["name"],
            "starttime": found["starttime"],
            "deadline": found["deadline"],
        }
        recips = [{"id": y.dn(), "label": y.name} for y in self.recipients]

        if self.distributeType != "automatic":
            sTime = None
        else:
            sTime = "%s %s" % (self.distributeDate, self.distributeTime)
        if self.collectType != "automatic":
            dTime = None
        else:
            dTime = "%s %s" % (self.collectDate, self.collectTime)
        current = {
            "files": self.files,
            "sender": self.sender,
            "description": self.description,
            "recipients": recips,
            "distributeType": self.distributeType,
            "__type__": "PROJECT",
            "collectType": self.collectType,
            "name": self.name,
            "starttime": sTime,
            "deadline": dTime,
        }
        print("supposed = ", supposed)
        print("current = ", current)

        assert supposed == current, "Project %s was not modified successfully,supposed!=current" % (
            self.name,
        )

        # check distribute
        check = "distribution"
        before_type = previousGetResult["distributeType"]
        after_type = found["distributeType"]
        before_time = previousGetResult["starttime"]
        after_time = found["starttime"]
        before_atJob = previousGetResult["atJobNumDistribute"]
        after_atJob = found["atJobNumDistribute"]
        fail_state = self.put_fail(
            before_type, after_type, before_time, after_time, before_atJob, after_atJob
        )
        assert not fail_state, "Project %s was not modified successfully, %s: %s -> %s" % (
            self.name,
            check,
            before_type,
            after_type,
        )

        # check collect
        check = "collection"
        before_type = previousGetResult["collectType"]
        after_type = found["collectType"]
        before_time = previousGetResult["deadline"]
        after_time = found["deadline"]
        before_atJob = previousGetResult["atJobNumCollect"]
        after_atJob = found["atJobNumCollect"]
        fail_state = self.put_fail(
            before_type, after_type, before_time, after_time, before_atJob, after_atJob
        )
        assert not fail_state, "Project %s was not modified successfully, %s: %s -> %s" % (
            self.name,
            check,
            before_type,
            after_type,
        )

    def put_fail(self, before_type, after_type, before_time, after_time, before_atJob, after_atJob):
        """
        Checks if the atjobs are in the expected formats
        :param before_type: type before using put command
        :type before_type: str
        :param after_type: type after using put command
        :type after_type: str
        :param before_atJob: atJobNum before using put command
        :type before_atJob: str or None
        :param after_atJob: atJobNum after using put command
        :type after_atJob: str or None
        :param before_time: time before using put command
        :type before_time: str
        :param after_time: time after using put command
        :type after_time: str
        """
        fail_state = False
        # manual -> manual
        # atJobs == don't care
        if before_type == "manual" and after_type == "manual":
            pass

        # manual -> automatic
        # atJobs don't care -> int
        if before_type == "manual" and after_type == "automatic":
            fail_state = not (isinstance(after_atJob, int))

        # automatic -> manual
        # atJobs int -> don't care
        if before_type == "automatic" and after_type == "manual":
            fail_state = not (isinstance(before_atJob, int))

        # automatic -> automatic
        # atJobs int1 -> int2 & int1 < int2
        if before_type == "automatic" and after_type == "automatic":
            fail1 = not (isinstance(before_atJob, int) and isinstance(after_atJob, int))
            fail2 = not (before_time != after_time and (before_atJob < after_atJob))
            fail_state = fail1 or fail2
        return fail_state

    def distribute(self):
        """Calls 'distribution/distribute'"""
        print("Distributing Project %s" % (self.name))
        flavor = self.flavor
        reqResult = self.client.umc_command("distribution/distribute", [self.name], flavor).result
        assert reqResult[0]["success"], "Unable to distribute project (%r)" % (self.name,)

    def check_distribute(self, users):
        """
        Checks if the distribution was successful
        by checking the file system.\n
        :param users: names of users to have the material distributed for
        :type users: list of str
        """
        print("Checking %s distribution" % (self.name,))
        for user in users:
            path = self.getUserFilesPath(user, "distribute")
            print("file_path=", path)
            existingFiles = self.idir(path)
            print("existingFiles=", existingFiles)
            files = self.files
            assert files == existingFiles, "Project files were not distributed for user %s:\n%r!=%r" % (
                user,
                files,
                existingFiles,
            )

    def collect(self):
        """Calls 'distribution/collect'"""
        print("Collecting Project %s" % (self.name))
        flavor = self.flavor
        reqResult = self.client.umc_command("distribution/collect", [self.name], flavor).result
        assert reqResult[0]["success"], "Unable to collect project (%r)" % (self.name,)

    def check_collect(self, users):
        """
        Checks if the collection was successful
        by checking the file system.\n
        :param users: names of users to have the material collected from
        :type users: list of str
        """
        print("Checking %s collection" % (self.name,))
        for user in users:
            path = self.getUserFilesPath(user, "collect", self.distributed_version)
            print("file_path=", path)
            existingFiles = self.idir(path)
            print("existingFiles=", existingFiles)
            files = self.files
            assert files == existingFiles, "Project files were not collected for user %s:\n%r!=%r" % (
                user,
                files,
                existingFiles,
            )

    def remove(self):
        """Calls 'distribution/remove'"""
        print("Removing Project %s" % (self.name))
        flavor = self.flavor
        param = [{"object": self.name, "options": None}]
        reqResult = self.client.umc_command("distribution/remove", param, flavor).result
        assert not reqResult, "Unable to remove project (%r)" % (param,)

    def check_remove(self):
        """
        Calls 'distribution/query'
        and check the existance of the removed project
        """
        print("Checking %s removal" % (self.name,))
        current = self.query(pattern=self.name)
        assert self.name not in current, "Project %s was not removed successfully" % (self.name,)

    def checkFiles(self, files):
        """Calls 'distribution/checkfiles'"""
        print("Checking files Project %s" % (self.name))
        flavor = self.flavor
        param = {"project": self.name, "filenames": files}
        reqResult = self.client.umc_command("distribution/checkfiles", param, flavor).result
        assert not reqResult, "Unable to chack files for project (%r)" % (param,)

    def adopt(self, project_name):
        """Calls 'distribute/adopt'"""
        print("Adopting project", self.name)
        flavor = self.flavor
        reqResult = self.client.umc_command("distribution/adopt", [project_name], flavor).result
        assert not reqResult, "Failed to adopt project (%r)" % (project_name,)

    def check_adopt(self, project_name):
        print("Checking adopting")
        q = self.query(pattern=project_name)
        assert project_name in q, "Project %s was not adopted successfully" % (project_name,)

    def getUserFilesPath(self, user, purpose="distribute", version=1):
        """
        Gets the correct files path for a specific user depending on
        the value of the ucr variable ucsschool/import/roleshare.\n
        :param user: user name
        :type user: str
        :param purpose: either for distribution or collection
        :type purpose: str ('distribute' or 'collect')
        """
        path = ""
        self.ucr.load()
        sender_dir_name = self.ucr.get(
            "ucsschool/datadistribution/datadir/sender", "Unterrichtsmaterial"
        )
        project_dir_suffix = self.ucr.get(
            "ucsschool/datadistribution/datadir/sender/project/suffix", "-Ergebnisse"
        )
        recipient_dir_name = self.ucr.get(
            "ucsschool/datadistribution/datadir/recipient", "Unterrichtsmaterial"
        )
        if purpose == "distribute":
            path = "/home/{}/schueler/{}/{}/{}".format(self.school, user, recipient_dir_name, self.name)
        elif purpose == "collect":
            path = "/home/{}/lehrer/{}/{}/{}{}/{}-{:03d}".format(
                self.school,
                self.sender,
                sender_dir_name,
                self.name,
                project_dir_suffix,
                user,
                version,
            )
        return path
