from functools import partial

from lxml import etree

from pcs.common import reports
from pcs.common.reports.item import ReportItem
from pcs.lib.cib.constraint import constraint
from pcs.lib.cib import tools
from pcs.lib.errors import LibraryError
from pcs.lib.pacemaker.values import RESOURCE_ROLES
from pcs.lib.xml_tools import remove_when_pointless

TAG_NAME = "rsc_ticket"
DESCRIPTION = "constraint id"
ATTRIB = {
    "loss-policy": ("fence", "stop", "freeze", "demote"),
    "ticket": None,
}
ATTRIB_PLAIN = {
    "rsc": None,
    "rsc-role": RESOURCE_ROLES,
}


def _validate_options_common(options):
    report_list = []
    if "loss-policy" in options:
        loss_policy = options["loss-policy"].lower()
        if options["loss-policy"] not in ATTRIB["loss-policy"]:
            report_list.append(
                ReportItem.error(
                    reports.messages.InvalidOptionValue(
                        "loss-policy",
                        options["loss-policy"],
                        ATTRIB["loss-policy"],
                    )
                )
            )
        options["loss-policy"] = loss_policy
    return report_list


def _create_id(cib, ticket, resource_id, resource_role):
    return tools.find_unique_id(
        cib,
        "-".join(("ticket", ticket, resource_id))
        + ("-{0}".format(resource_role) if resource_role else ""),
    )


def prepare_options_with_set(cib, options, resource_set_list):
    options = constraint.prepare_options(
        tuple(ATTRIB.keys()),
        options,
        create_id_fn=partial(
            constraint.create_id, cib, "ticket", resource_set_list
        ),
        validate_id=partial(tools.check_new_id_applicable, cib, DESCRIPTION),
    )
    report_list = _validate_options_common(options)
    if "ticket" not in options or not options["ticket"].strip():
        report_list.append(
            ReportItem.error(
                reports.messages.RequiredOptionsAreMissing(["ticket"])
            )
        )
    if report_list:
        raise LibraryError(*report_list)
    return options


def prepare_options_plain(cib, options, ticket, resource_id):
    options = options.copy()

    report_list = _validate_options_common(options)

    if not ticket:
        report_list.append(
            ReportItem.error(
                reports.messages.RequiredOptionsAreMissing(["ticket"])
            )
        )
    options["ticket"] = ticket

    if not resource_id:
        report_list.append(
            ReportItem.error(
                reports.messages.RequiredOptionsAreMissing(["rsc"])
            )
        )
    options["rsc"] = resource_id

    if "rsc-role" in options:
        if options["rsc-role"]:
            resource_role = options["rsc-role"].lower().capitalize()
            if resource_role not in ATTRIB_PLAIN["rsc-role"]:
                report_list.append(
                    ReportItem.error(
                        reports.messages.InvalidOptionValue(
                            "rsc-role",
                            options["rsc-role"],
                            ATTRIB_PLAIN["rsc-role"],
                        )
                    )
                )
            options["rsc-role"] = resource_role
        else:
            del options["rsc-role"]

    if report_list:
        raise LibraryError(*report_list)

    return constraint.prepare_options(
        tuple(list(ATTRIB) + list(ATTRIB_PLAIN)),
        options,
        partial(
            _create_id,
            cib,
            options["ticket"],
            resource_id,
            options.get("rsc-role", ""),
        ),
        partial(tools.check_new_id_applicable, cib, DESCRIPTION),
    )


def create_plain(constraint_section, options):
    element = etree.SubElement(constraint_section, TAG_NAME)
    element.attrib.update(options)
    return element


def remove_plain(constraint_section, ticket_key, resource_id):
    ticket_element_list = constraint_section.xpath(
        './/rsc_ticket[@ticket="{0}" and @rsc="{1}"]'.format(
            ticket_key, resource_id
        )
    )

    for ticket_element in ticket_element_list:
        ticket_element.getparent().remove(ticket_element)

    return len(ticket_element_list) > 0


def remove_with_resource_set(constraint_section, ticket_key, resource_id):
    ref_element_list = constraint_section.xpath(
        (
            './/rsc_ticket[@ticket="{0}"]/resource_set/resource_ref[@id="{1}"]'
        ).format(ticket_key, resource_id)
    )

    for ref_element in ref_element_list:
        set_element = ref_element.getparent()
        set_element.remove(ref_element)
        # set_element is lxml element, therefore we have to use len() here
        # pylint: disable=len-as-condition
        if not len(set_element):
            ticket_element = set_element.getparent()
            ticket_element.remove(set_element)
            # We do not care about attributes since without an attribute "rsc"
            # they are pointless. Attribute "rsc" is mutually exclusive with
            # resource_set (see rng) so it cannot be in this ticket_element.
            remove_when_pointless(ticket_element, attribs_important=False)

    return len(ref_element_list) > 0


def are_duplicate_plain(element, other_element):
    return all(
        element.attrib.get(name, "") == other_element.attrib.get(name, "")
        for name in ("ticket", "rsc", "rsc-role")
    )


def are_duplicate_with_resource_set(element, other_element):
    return element.attrib["ticket"] == other_element.attrib[
        "ticket"
    ] and constraint.have_duplicate_resource_sets(element, other_element)
