import os
import uuid
from lxml import etree
from datetime import datetime

import metsrw
from metsrw import utils, exceptions, MDWrap, AMDSec

# UTILS

utils.NAMESPACES['csip'] = "https://DILCIS.eu/XML/METS/CSIPExtensionMETS"
utils.NAMESPACES['sip'] = "https://DILCIS.eu/XML/METS/SIPExtensionMETS"

utils.SCHEMA_LOCATIONS = (utils.SCHEMA_LOCATIONS
                          + " http://www.w3.org/1999/xlink http://www.loc.gov/standards/mets/xlink.xsd "  # CL
                          + "https://dilcis.eu/XML/METS/CSIPExtensionMETS "  # CL
                          + "https://earkcsip.dilcis.eu/schema/DILCISExtensionMETS.xsd "  # CL
                          + "https://dilcis.eu/XML/METS/SIPExtensionMETS "  # CL
                          + "https://earksip.dilcis.eu/schema/DILCISExtensionSIPMETS.xsd "  # CL
                          )


# METADATA

class Agent(metsrw.Agent):
    def __init__(self, role, **kwargs):
        super().__init__(role, **kwargs)
        self.csip_notetype = kwargs.get(u"csip_notetype", None)  # CL

    def serialize(self):
        attrs = {}

        if self.id:
            attrs[u"ID"] = self.id

        if self.role in self.ROLES:
            attrs[u"ROLE"] = self.role
        else:
            attrs[u"ROLE"] = u"OTHER"
            attrs[u"OTHERROLE"] = self.role

        if self.type and self.type in self.TYPES:
            attrs[u"TYPE"] = self.type
        elif self.type:
            attrs[u"TYPE"] = u"OTHER"
            attrs[u"OTHERTYPE"] = self.type

        element = etree.Element(self.AGENT_TAG, **attrs)
        if self.name:
            name_element = etree.Element(self.NAME_TAG)
            name_element.text = self.name
            element.append(name_element)

        for note in self.notes:
            note_element = etree.Element(self.NOTE_TAG)
            note_element.text = note
            if self.csip_notetype:
                note_element.attrib["{}NOTETYPE".format(utils.lxmlns("csip"))] = self.csip_notetype
            element.append(note_element)

        return element


class SubSection(metsrw.SubSection):
    def __init__(self, subsection, contents):
        super().__init__(subsection, contents)
        self.id_string = "dmdSec-" + str(uuid.uuid4())  # CL

    def get_status(self):
        """
        Returns the STATUS when serializing.

        Calculates based on the subsection type and if it's replacing anything.

        :returns: None or the STATUS string.
        """
        if self.status is not None:
            return self.status
        if self.subsection == "dmdSec":
            if self.older is None:
                # return "original"
                return 'CURRENT'  # CL
            else:
                # return "updated"
                return 'UPDATED'  # CL
        if self.subsection in ("techMD", "rightsMD"):
            # TODO how to handle ones where newer has been deleted?
            if self.newer is None:
                return "current"
            else:
                return "superseded"
        return None


class MDRef(metsrw.MDRef):
    def __init__(self, target, mdtype, loctype, label=None, otherloctype=None, mimetype=None, size=None, created=None,
                 lastmod=None, checksum=None, checksumtype=None, othermdtype=None):
        super().__init__(target, mdtype, loctype, label, otherloctype)
        self.mimetype = mimetype  # CL
        self.size = size  # CL
        self.created = created  # CL
        self.lastmod = lastmod  # CL
        self.checksum = checksum  # CL
        self.checksumtype = checksumtype  # CL
        self.othermdtype = othermdtype  # CL

    def serialize(self):
        # If the source document is a METS document, the XPTR attribute of
        # this mdRef element should point to the IDs of each dmdSec element
        # in that document.

        # XPTR removed in override

        el = etree.Element(utils.lxmlns("mets") + "mdRef")
        if self.label:
            el.attrib["LABEL"] = self.label
        if self.target:
            try:
                el.attrib[utils.lxmlns("xlink") + "href"] = self.target
                el.attrib[utils.lxmlns("xlink") + "type"] = "simple"  # CL
                # CL el.attrib[utils.lxmlns("xlink") + "href"] = utils.urlencode(self.target)
            except ValueError:
                raise exceptions.SerializeError(
                    'Value "{}" (for attribute xlink:href) is not a valid'
                    " URL.".format(self.target)
                )
        el.attrib["MDTYPE"] = self.mdtype
        el.attrib["LOCTYPE"] = self.loctype
        if self.otherloctype:
            el.attrib["OTHERLOCTYPE"] = self.otherloctype
        if self.mimetype:
            el.attrib["MIMETYPE"] = self.mimetype  # CL
        if self.size:
            el.attrib["SIZE"] = self.size  # CL
        if self.created:
            el.attrib["CREATED"] = self.created  # CL
        if self.lastmod:
            el.attrib["LASTMOD"] = self.lastmod  # CL
        if self.checksum:
            el.attrib["CHECKSUM"] = self.checksum  # CL
        if self.checksumtype:
            el.attrib["CHECKSUMTYPE"] = self.checksumtype  # CL
        if self.othermdtype:
            el.attrib["OTHERMDTYPE"] = self.othermdtype  # CL

        return el


# METS

AIP_ENTRY_TYPE = metsrw.mets.AIP_ENTRY_TYPE


class METSDocument(metsrw.METSDocument):
    def __init__(self):
        super().__init__()
        self.type = None  # CL
        self.objid = "123"

    def _document_root(self, fully_qualified=True):
        """
            Return the mets Element for the document root.
        """
        nsmap = {"xsi": utils.NAMESPACES["xsi"], "xlink": utils.NAMESPACES["xlink"], "csip": utils.NAMESPACES["csip"],
                 "sip": utils.NAMESPACES["sip"], }  # CL
        if fully_qualified:
            nsmap["mets"] = utils.NAMESPACES["mets"]
        else:
            nsmap[None] = utils.NAMESPACES["mets"]
        attrib = {
            "{}schemaLocation".format(utils.lxmlns("xsi")): utils.SCHEMA_LOCATIONS,
            "{}OTHERTYPE".format(utils.lxmlns("csip")): "Patient Medical Records",  # CL
            "{}CONTENTINFORMATIONTYPE".format(utils.lxmlns("csip")): "citsehpj_v1_0"  # CL
        }

        if self.objid:
            attrib["OBJID"] = self.objid

        if self.type:  # CL
            attrib["TYPE"] = self.type  # CL
        else:  # CL
            attrib["TYPE"] = "OTHER"  # CL

        if "patientrecord_" in self.objid.lower():  # CL
            attrib["PROFILE"] = "https://earkehealth1.dilcis.eu/profile/E-ARK-eHealth1-REPRESENTATION.xml"  # CL
        else:
            attrib["PROFILE"] = "https://earkehealth1.dilcis.eu/profile/E-ARK-eHealth1-ROOT.xml"  # CL

        return etree.Element(utils.lxmlns("mets") + "mets", nsmap=nsmap, attrib=attrib)

    def _mets_header(self, now):
        """
        Return the metsHdr Element.
        """
        header_tag = etree.QName(utils.NAMESPACES[u"mets"], u"metsHdr")
        header_attrs = {}

        if self.createdate is None:
            header_attrs[u"CREATEDATE"] = now
            header_attrs[u"LASTMODDATE"] = now  # CL
        else:
            header_attrs[u"CREATEDATE"] = self.createdate
            header_attrs[u"LASTMODDATE"] = now

        header_attrs[u"RECORDSTATUS"] = "NEW"  # CL
        header_attrs["{}OAISPACKAGETYPE".format(utils.lxmlns("csip"))] = "SIP"  # CL

        header_element = etree.Element(header_tag, **header_attrs)
        for agent in self.agents:
            header_element.append(agent.serialize())
        for alternate_id in self.alternate_ids:
            header_element.append(alternate_id.serialize())

        return header_element

    def _filesec(self, files=None):
        """
        Returns fileSec Element containing all files grouped by use.
        """
        if files is None:
            files = self.all_files()

        filesec = etree.Element(utils.lxmlns("mets") + "fileSec", ID="fileSec-" + str(uuid.uuid4()))  # Cl - added ID
        filegrps = {}
        for file_ in files:
            if file_.type.lower() not in ("item", AIP_ENTRY_TYPE):
                continue
            # Get fileGrp, or create if not exist
            filegrp = filegrps.get(file_.use)
            if filegrp is None or file_.use.lower() == "representations":  # CL separate USE:Representations filegroups
                filegrp = etree.SubElement(
                    filesec, utils.lxmlns("mets") + "fileGrp", USE=file_.use,
                    ID="group-" + file_.derived_from.file_uuid,  # CL set filegrp_id
                )
                filegrps[file_.use] = filegrp
                if file_.use.startswith("Representations"):
                    filegrp.set(utils.lxmlns("csip") + "CONTENTINFORMATIONTYPE", "citsehpj_v1_0")

            file_el = file_.serialize_filesec()
            if file_el is not None:
                filegrp.append(file_el)

        return filesec

    def _csip_structmap(self):  # CL
        """
        Returns structMap element for all files.
        """
        structmap = etree.Element(
            utils.lxmlns("mets") + "structMap",
            ID="structmap-" + str(uuid.uuid4()),
            TYPE="PHYSICAL",
            LABEL="CSIP",
        )
        for item in self._root_elements:
            child = item.serialize_csip_structmap(recurse=True)
            if child is not None:
                structmap.append(child)

        return structmap

    def serialize(self, fully_qualified=True):
        """
        Returns this document serialized to an xml Element.

        :return: Element for this document
        """
        now = datetime.utcnow().replace(microsecond=0).isoformat("T")
        files = self.all_files()
        mdsecs = self._collect_mdsec_elements(files)
        root = self._document_root(fully_qualified=fully_qualified)
        root.append(self._mets_header(now=now))
        for section in mdsecs:
            root.append(section.serialize(now=now))
        root.append(self._filesec(files))
        root.append(self._csip_structmap())  # CL
        return root


# FSENTRY

class FSEntry(metsrw.FSEntry):
    ALLOWED_CHECKSUMS = ("SHA-256")

    def __init__(self,
                 path=None,
                 fileid=None,
                 label=None,
                 use="original",
                 type=u"Item",
                 children=None,
                 file_uuid=None,
                 derived_from=None,
                 checksum=None,
                 checksumtype=None,
                 transform_files=None,
                 mets_div_type=None,
                 # CL
                 mimetype=None,
                 filesize=0,
                 created=None,
                 lastmodified=None, ):
        super().__init__(path, fileid, label, use, type, children, file_uuid, derived_from, checksum, checksumtype,
                         transform_files, mets_div_type)
        self.mimetype = mimetype
        self.filesize = filesize
        self.created = created
        self.lastmodified = lastmodified

    @classmethod
    def from_fptr(cls, label, type_, fptr):
        """Return ``FSEntry`` object."""
        return FSEntry(
            fileid=fptr.fileid,
            label=label,
            type=type_,
            path=fptr.path,
            use=fptr.use,
            file_uuid=fptr.file_uuid,
            derived_from=fptr.derived_from,
            checksum=fptr.checksum,
            checksumtype=fptr.checksumtype,
            transform_files=fptr.transform_files,
            # CL
            mimetype=fptr.mimetype,
            filesize=fptr.filesize,
            created=fptr.created,
            # modifieddate=modifieddate,
        )

    def _add_metadata_element(self, md, subsection, mdtype, mode="mdwrap", **kwargs):
        """
        :param md: Value to pass to the MDWrap/MDRef
        :param str subsection: Metadata tag to create.  See :const:`SubSection.ALLOWED_SUBSECTIONS`
        :param str mdtype: Value for mdWrap/mdRef @MDTYPE
        :param str mode: 'mdwrap' or 'mdref'
        :param str loctype: Required if mode is 'mdref'. LOCTYPE of a mdRef
        :param str label: Optional. Label of a mdRef
        :param str otherloctype: Optional. OTHERLOCTYPE of a mdRef.
        :param str othermdtype: Optional. OTHERMDTYPE of a mdWrap.
        self.mimetype = mimetype #CL
        self.size = size #CL
        self.created = created #CL
        self.lastmod = lastmod #CL
        self.checksum = checksum #CL
        self.checksumtype = checksum #CL
            othermdtype = kwargs.get("othermdtype") #CL
        """
        # HELP how handle multiple amdSecs?
        # When adding *MD which amdSec to add to?
        if mode.lower() == "mdwrap":
            print("Shouldn't use MDWrap")
            othermdtype = kwargs.get("othermdtype")
            mdsec = MDWrap(md, mdtype, othermdtype)
        elif mode.lower() == "mdref":
            loctype = kwargs.get("loctype")
            label = kwargs.get("label")
            otherloctype = kwargs.get("otherloctype")

            mimetype = kwargs.get("mimetype")  # CL
            size = kwargs.get("size")  # CL
            created = kwargs.get("created")  # CL
            lastmod = kwargs.get("lastmod")  # CL
            checksum = kwargs.get("checksum")  # CL
            checksumtype = kwargs.get("checksumtype")  # CL
            othermdtype = kwargs.get("othermdtype")  # CL

            mdsec = MDRef(md, mdtype, loctype, label, otherloctype,
                          mimetype, size, created, lastmod, checksum, checksumtype, othermdtype  # CL
                          )

        subsection = SubSection(subsection, mdsec)
        if subsection.subsection == "dmdSec":
            self.dmdsecs.append(subsection)
        else:
            try:
                amdsec = self.amdsecs[0]
            except IndexError:
                amdsec = AMDSec()
                self.amdsecs.append(amdsec)
            amdsec.subsections.append(subsection)
        return subsection

    def serialize_filesec(self):
        """
        Return the file Element for this file, appropriate for use in a fileSec.

        If this is not an Item or has no use, return None.

        :return: fileSec element for this FSEntry
        """
        if (
                self.type.lower() not in ("item", "archival information package")
                or self.use is None
        ):
            return None
        el = etree.Element(utils.lxmlns("mets") + "file", ID=self.file_id())
        # if self.group_id():
        #     el.attrib["GROUPID"] = self.group_id()
        # if self.admids:
        #    el.set("ADMID", " ".join(self.admids))
        if self.checksum and self.checksumtype:
            el.attrib["CHECKSUM"] = self.checksum
            el.attrib["CHECKSUMTYPE"] = self.checksumtype

        # CL
        if self.mimetype:
            el.attrib["MIMETYPE"] = self.mimetype
        if self.filesize:
            el.attrib["SIZE"] = self.filesize
        if self.created:
            el.attrib["CREATED"] = self.created
        if self.lastmodified:
            el.attrib["LASTMODIFIED"] = self.lastmodified

        if self.path:
            flocat = etree.SubElement(el, utils.lxmlns("mets") + "FLocat")
            # Setting manually so order is correct
            try:
                flocat.set(utils.lxmlns("xlink") + "href", self.path)  # CL
                # flocat.set(utils.lxmlns("xlink") + "href", utils.urlencode(self.path))
            except ValueError:
                raise exceptions.SerializeError(
                    'Value "{}" (for attribute xlink:href) is not a valid'
                    " URL.".format(self.path)
                )
            flocat.set("LOCTYPE", "URL")
            # flocat.set("OTHERLOCTYPE", "URL")
            flocat.set(utils.lxmlns("xlink") + "type", "simple")  # CL
        for transform_file in self.transform_files:
            transform_file_el = etree.SubElement(
                el, utils.lxmlns("mets") + "transformFile"
            )
            for key, val in transform_file.items():
                attribute = "transform{}".format(key).upper()
                transform_file_el.attrib[attribute] = str(val)
        return el

    def serialize_csip_structmap(self, recurse=True):  # CL
        """Return the div Element for this file, appropriate for use in a
        structMap.

        For use with EARK compliant SIP package structures

        :param bool recurse: If true, serialize and apppend all children.
            Otherwise, only serialize this element but not any children.
            Currently only used to gather children the first parent - for EARK compliant METs structmap
        :return: structMap element for this FSEntry
        """
        if not self.label:
            return None
        # Empty directories are not included in the physical structmap.
        if self.is_empty_dir:
            return None
        el = etree.Element(utils.lxmlns("mets") + "div", ID="structmap-" + str(uuid.uuid4()))

        if recurse:
            el.attrib["LABEL"] = os.path.basename(os.path.normpath(self.label))
        else:
            el.attrib["LABEL"] = self.path.capitalize()

        if el.attrib["LABEL"] in ["Documentation", "Schemas"]:  # CL
            etree.SubElement(el, utils.lxmlns("mets") + "fptr", FILEID=self.group_id().lower())
        elif el.attrib["LABEL"] == "Representations":  # CL
            for child in self._children:
                el.append(child.get_representations_structmap())
        elif el.attrib["LABEL"] == "Data":
            for child in self.children:
                el.append(child.get_data_structmap())

        if self.file_id():
            etree.SubElement(el, utils.lxmlns("mets") + "fptr", FILEID=self.file_id())

        for dmdid in self.dmdids:  # CL
            etree.SubElement(el, utils.lxmlns("mets") + "div", ID="structmap-" + str(uuid.uuid4()), LABEL="Metadata",
                             DMDID=dmdid)  # CL

        if recurse and self._children:
            for child in self._children:
                child_el = child.serialize_csip_structmap(recurse=False)
                if child_el is not None:
                    el.append(child_el)
        return el

    def get_representations_structmap(self):  # CL
        el = etree.Element(utils.lxmlns("mets") + "div", ID="structmap-" + str(uuid.uuid4()), LABEL=self.path)
        flocat = etree.SubElement(el, utils.lxmlns("mets") + "mptr", LOCTYPE="URL")
        flocat.set(utils.lxmlns("xlink") + "type", "simple")
        flocat.set(utils.lxmlns("xlink") + "href", self.path)
        flocat.set(utils.lxmlns("xlink") + "title", self.group_id().lower())
        return el

    def get_data_structmap(self):
        el = etree.Element(utils.lxmlns("mets") + "div", ID="structmap-" + str(uuid.uuid4()),
                           LABEL=self.path.split("/").pop())
        if self.children:
            for child in self.children:
                if child.type == "Directory":
                    el.append(child.get_data_structmap())
                else:
                    etree.SubElement(el, utils.lxmlns("mets") + "fptr", FILEID=self.group_id().lower())
        return el
