"""All tasks should be have name come first."""

from __future__ import annotations

import functools
import sys
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any

from ansiblelint.constants import ANNOTATION_KEYS
from ansiblelint.errors import MatchError, RuleMatchTransformMeta
from ansiblelint.rules import AnsibleLintRule, TransformMixin

if TYPE_CHECKING:
    from ruamel.yaml.comments import CommentedMap, CommentedSeq

    from ansiblelint.file_utils import Lintable
    from ansiblelint.utils import Task


SORTER_TASKS = (
    "name",
    # "__module__",
    # "action",
    # "args",
    None,  # <-- None include all modules that not using action and *
    # "when",
    # "notify",
    # "tags",
    "block",
    "rescue",
    "always",
)


def get_property_sort_index(name: str) -> int:
    """Return the index of the property in the sorter."""
    a_index = -1
    for i, v in enumerate(SORTER_TASKS):
        if v == name:
            return i
        if v is None:
            a_index = i
    return a_index


def task_property_sorter(property1: str, property2: str) -> int:
    """Sort task properties based on SORTER."""
    v_1 = get_property_sort_index(property1)
    v_2 = get_property_sort_index(property2)
    return (v_1 > v_2) - (v_1 < v_2)


@dataclass(frozen=True)
class KeyOrderTMeta(RuleMatchTransformMeta):
    """Key Order transform metadata.

    :param fixed: tuple with updated key order
    """

    fixed: tuple[str | int, ...]

    def __str__(self) -> str:  # pragma: no cover
        """Return string representation."""
        return f"Fixed to {self.fixed}"


class KeyOrderRule(AnsibleLintRule, TransformMixin):
    """Ensure specific order of keys in mappings."""

    id = "key-order"
    severity = "LOW"
    tags = ["formatting"]
    version_changed = "6.6.2"
    needs_raw_task = True
    _ids = {
        "key-order[task]": "You can improve the task key order",
    }

    def matchplay(self, file: Lintable, data: dict[str, Any]) -> list[MatchError]:
        """Return matches found for a specific play (entry in playbook)."""
        result: list[MatchError] = []
        if file.kind != "playbook":
            return result
        keys = [str(key) for key, val in data.items() if key not in ANNOTATION_KEYS]
        sorted_keys = sorted(keys, key=functools.cmp_to_key(task_property_sorter))
        if keys != sorted_keys:
            result.append(
                self.create_matcherror(
                    f"You can improve the play key order to: {', '.join(sorted_keys)}",
                    filename=file,
                    tag=f"{self.id}[play]",
                    transform_meta=KeyOrderTMeta(fixed=tuple(sorted_keys)),
                    data=data,
                ),
            )
        return result

    def matchtask(
        self,
        task: Task,
        file: Lintable | None = None,
    ) -> list[MatchError]:
        result = []
        raw_task = task["__raw_task__"]
        keys = [str(key) for key in raw_task if not key.startswith("_")]
        sorted_keys = sorted(keys, key=functools.cmp_to_key(task_property_sorter))
        if keys != sorted_keys:
            result.append(
                self.create_matcherror(
                    f"You can improve the task key order to: {', '.join(sorted_keys)}",
                    filename=file,
                    tag="key-order[task]",
                    transform_meta=KeyOrderTMeta(fixed=tuple(sorted_keys)),
                ),
            )
        return result

    def transform(
        self,
        match: MatchError,
        lintable: Lintable,
        data: CommentedMap | CommentedSeq | str,
    ) -> None:
        if not isinstance(match.transform_meta, KeyOrderTMeta):  # pragma: no cover
            return

        if match.tag == f"{self.id}[play]":
            play = self.seek(match.yaml_path, data)
            for key in match.transform_meta.fixed:
                # other transformation might change the key
                if key in play:
                    play[key] = play.pop(key)
            match.fixed = True
        if match.tag == f"{self.id}[task]":
            task = self.seek(match.yaml_path, data)
            for key in match.transform_meta.fixed:
                # other transformation might change the key
                if key in task:
                    task[key] = task.pop(key)
            match.fixed = True


# testing code to be loaded only with pytest or when executed the rule file
if "pytest" in sys.modules:
    import pytest

    # pylint: disable=ungrouped-imports
    from ansiblelint.rules import RulesCollection
    from ansiblelint.runner import Runner

    @pytest.mark.parametrize(
        ("test_file", "failures"),
        (
            pytest.param("examples/playbooks/rule-key-order-pass.yml", 0, id="pass"),
            pytest.param("examples/playbooks/rule-key-order-fail.yml", 6, id="fail"),
        ),
    )
    def test_key_order_rule(
        default_rules_collection: RulesCollection,
        test_file: str,
        failures: int,
    ) -> None:
        """Test rule matches."""
        results = Runner(test_file, rules=default_rules_collection).run()
        assert len(results) == failures
        for result in results:
            assert result.rule.id == "key-order"

    @pytest.mark.parametrize(
        ("properties", "expected"),
        (
            pytest.param([], []),
            pytest.param(["block", "name"], ["name", "block"]),
            pytest.param(
                ["block", "name", "action", "..."],
                ["name", "action", "...", "block"],
            ),
        ),
    )
    def test_key_order_property_sorter(
        properties: list[str],
        expected: list[str],
    ) -> None:
        """Test the task property sorter."""
        result = sorted(properties, key=functools.cmp_to_key(task_property_sorter))
        assert expected == result

    @pytest.mark.parametrize(
        ("key", "order"),
        (
            pytest.param("name", 0),
            pytest.param("action", 1),
            pytest.param("foobar", SORTER_TASKS.index(None)),
            pytest.param("block", len(SORTER_TASKS) - 3),
            pytest.param("rescue", len(SORTER_TASKS) - 2),
            pytest.param("always", len(SORTER_TASKS) - 1),
        ),
    )
    def test_key_order_property_sort_index(key: str, order: int) -> None:
        """Test sorting index."""
        assert get_property_sort_index(key) == order

    @pytest.mark.parametrize(
        ("prop1", "prop2", "result"),
        (
            pytest.param("name", "block", -1),
            pytest.param("block", "name", 1),
            pytest.param("block", "block", 0),
        ),
    )
    def test_key_order_property_sortfunc(prop1: str, prop2: str, result: int) -> None:
        """Test sorting function."""
        assert task_property_sorter(prop1, prop2) == result
