"""
Module for stuff related to clones.

Previously, promotable clones were implemented in pacemaker as 'master'
elements whereas regular clones were 'clone' elements. Since pacemaker-2.0,
promotable clones are clones with meta attribute promotable=true. Master
elements are deprecated yet still supported in pacemaker. We provide read-only
support for them to be able to read, process and display CIBs containing them.
"""

import dataclasses
from collections.abc import Mapping
from typing import cast

from lxml import etree
from lxml.etree import _Element

from pcs.common import (
    const,
)
from pcs.common.pacemaker import nvset
from pcs.common.pacemaker.resource.clone import CibResourceCloneDto
from pcs.common.reports import ReportItemList
from pcs.common.tools import Version
from pcs.lib.cib import (
    nvpair,
    nvpair_multi,
    rule,
)
from pcs.lib.cib.const import TAG_RESOURCE_CLONE as TAG_CLONE
from pcs.lib.cib.const import TAG_RESOURCE_MASTER as TAG_MASTER
from pcs.lib.cib.nvpair_multi import (
    NVSET_META,
    find_nvsets,
    nvset_append_new,
    nvset_update,
)
from pcs.lib.cib.resource.operations import get_resource_operations
from pcs.lib.cib.tools import IdProvider
from pcs.lib.pacemaker.values import (
    is_true,
    validate_id,
)

ALL_TAGS = [TAG_CLONE, TAG_MASTER]

META_GLOBALLY_UNIQUE = "globally-unique"
META_PROMOTABLE = "promotable"


def is_clone(resource_el: _Element) -> bool:
    return resource_el.tag == TAG_CLONE


def is_master(resource_el: _Element) -> bool:
    return resource_el.tag == TAG_MASTER


def is_any_clone(resource_el: _Element) -> bool:
    return resource_el.tag in ALL_TAGS


def is_promotable_clone(resource_el: _Element) -> bool:
    """
    Return True if resource_el is a promotable clone, False on clone and master
    """
    return is_clone(resource_el) and is_true(
        nvpair.get_value(
            nvpair.META_ATTRIBUTES_TAG,
            resource_el,
            "promotable",
            default="false",
        )
    )


def clone_element_to_dto(
    clone_element: _Element,
    rule_eval: rule.RuleInEffectEval | None = None,
) -> CibResourceCloneDto:
    if rule_eval is None:
        rule_eval = rule.RuleInEffectEvalDummy()
    return CibResourceCloneDto(
        id=str(clone_element.attrib["id"]),
        description=clone_element.get("description"),
        member_id=str(get_inner_resource(clone_element).attrib["id"]),
        meta_attributes=[
            nvpair_multi.nvset_element_to_dto(nvset, rule_eval)
            for nvset in nvpair_multi.find_nvsets(
                clone_element, nvpair_multi.NVSET_META
            )
        ],
        instance_attributes=[
            nvpair_multi.nvset_element_to_dto(nvset, rule_eval)
            for nvset in nvpair_multi.find_nvsets(
                clone_element, nvpair_multi.NVSET_INSTANCE
            )
        ],
    )


def master_element_to_dto(
    master_element: _Element,
    rule_eval: rule.RuleInEffectEval | None = None,
) -> CibResourceCloneDto:
    clone_dto = clone_element_to_dto(master_element, rule_eval)
    promotable_nvpair = nvset.CibNvpairDto(
        id="", name="promotable", value="true"
    )
    if clone_dto.meta_attributes:
        first_meta_attributes = clone_dto.meta_attributes[0]
        clone_dto = dataclasses.replace(
            clone_dto,
            meta_attributes=[
                dataclasses.replace(
                    first_meta_attributes,
                    nvpairs=(
                        list(first_meta_attributes.nvpairs)
                        + [promotable_nvpair]
                    ),
                )
            ]
            + list(clone_dto.meta_attributes[1:]),
        )
    else:
        clone_dto = dataclasses.replace(
            clone_dto,
            meta_attributes=[
                nvset.CibNvsetDto(
                    id="", options={}, rule=None, nvpairs=[promotable_nvpair]
                )
            ],
        )

    return clone_dto


def get_parent_any_clone(resource_el: _Element) -> _Element | None:
    """
    Get any parent clone of a primitive (may be in a group) or group

    resource_el -- the primitive or group to get its parent clone
    """
    element = resource_el
    for _ in range(2):
        parent_el = element.getparent()
        if parent_el is None:
            return None
        if is_any_clone(parent_el):
            return parent_el
        element = parent_el
    return None


def append_new(
    resources_section: _Element,
    id_provider: IdProvider,
    primitive_element: _Element,
    options: Mapping[str, str],
    clone_id: str | None = None,
) -> _Element:
    """
    Append a new clone element (containing the primitive_element) to the
    resources_section.

    resources_section -- place where the new clone will be appended
    id_provider -- elements' ids generator
    primitive_element -- resource which will be cloned
    options -- source for clone meta options
    clone_id -- optional custom clone id
    """
    clone_element = etree.SubElement(
        resources_section,
        TAG_CLONE,
        id=(
            id_provider.allocate_id(
                "{0}-{1}".format(str(primitive_element.get("id")), TAG_CLONE)
            )
            if clone_id is None
            else clone_id
        ),
    )
    clone_element.append(primitive_element)

    if options:
        nvpair.append_new_meta_attributes(clone_element, options, id_provider)

    return clone_element


def get_inner_resource(clone_el: _Element) -> _Element:
    return cast(list[_Element], clone_el.xpath("./primitive | ./group"))[0]


def get_inner_primitives(clone_el: _Element) -> list[_Element]:
    """
    Also returns primitives inside cloned groups.
    """
    return cast(list[_Element], clone_el.xpath(".//primitive"))


def validate_clone_id(clone_id: str, id_provider: IdProvider) -> ReportItemList:
    """
    Validate that clone_id is a valid xml id and it is unique in the cib.

    clone_id -- identifier of clone element
    id_provider -- elements' ids generator
    """
    report_list: ReportItemList = []
    validate_id(clone_id, reporter=report_list)
    report_list.extend(id_provider.book_ids(clone_id))
    return report_list


def convert_master_to_promotable(
    id_provider: IdProvider, cib_validate_with: Version, master_el: _Element
) -> None:
    master_el.tag = TAG_CLONE
    meta_attrs = {META_PROMOTABLE: "true"}
    meta_attrs_nvset_list = find_nvsets(master_el, NVSET_META)
    if meta_attrs_nvset_list:
        nvset_update(meta_attrs_nvset_list[0], id_provider, meta_attrs)
    else:
        nvset_append_new(
            master_el,
            id_provider,
            cib_validate_with,
            NVSET_META,
            nvpair_dict=meta_attrs,
            nvset_options={},
        )

    clone_primitives = get_inner_primitives(master_el)

    for primitive_el in clone_primitives:
        ops_monitor = get_resource_operations(primitive_el, names=["monitor"])
        for op_monitor in ops_monitor:
            role = op_monitor.get("role", "")
            if role == const.PCMK_ROLE_PROMOTED_LEGACY:
                op_monitor.set("role", const.PCMK_ROLE_PROMOTED)
            if role == const.PCMK_ROLE_UNPROMOTED_LEGACY:
                op_monitor.set("role", const.PCMK_ROLE_UNPROMOTED)
