import dataclasses
from typing import Optional, List, Literal, Dict, Self
from collections.abc import Sequence, Iterable

from debputy.manifest_parser.tagging_types import DebputyDispatchableType
from debputy.manifest_parser.util import AttributePath

STD_CONTROL_SCRIPTS = frozenset(
    {
        "preinst",
        "prerm",
        "postinst",
        "postrm",
    }
)
UDEB_CONTROL_SCRIPTS = frozenset(
    {
        "postinst",
        "menutest",
        "isinstallable",
    }
)
ALL_CONTROL_SCRIPTS = STD_CONTROL_SCRIPTS | UDEB_CONTROL_SCRIPTS | {"config"}


@dataclasses.dataclass(slots=True, frozen=True)
class MaintscriptSnippet:
    definition_source: str
    snippet: str
    snippet_order: Literal["service"] | None = None

    def script_content(self) -> str:
        lines = [
            f"# Snippet source: {self.definition_source}\n",
            self.snippet,
        ]
        if not self.snippet.endswith("\n"):
            lines.append("\n")
        return "".join(lines)


class MaintscriptSnippetContainer:
    def __init__(self) -> None:
        self._generic_snippets: list[MaintscriptSnippet] = []
        self._snippets_by_order: dict[Literal["service"], list[MaintscriptSnippet]] = {}

    def copy(self) -> "MaintscriptSnippetContainer":
        instance = self.__class__()
        instance._generic_snippets = self._generic_snippets.copy()
        instance._snippets_by_order = self._snippets_by_order.copy()
        return instance

    def append(self, maintscript_snippet: MaintscriptSnippet) -> None:
        if maintscript_snippet.snippet_order is None:
            self._generic_snippets.append(maintscript_snippet)
        else:
            if maintscript_snippet.snippet_order not in self._snippets_by_order:
                self._snippets_by_order[maintscript_snippet.snippet_order] = []
            self._snippets_by_order[maintscript_snippet.snippet_order].append(
                maintscript_snippet
            )

    def has_content(self, snippet_order: Literal["service"] | None = None) -> bool:
        if snippet_order is None:
            return bool(self._generic_snippets)
        if snippet_order not in self._snippets_by_order:
            return False
        return bool(self._snippets_by_order[snippet_order])

    def all_snippets(self) -> Iterable[MaintscriptSnippet]:
        yield from self._generic_snippets
        for snippets in self._snippets_by_order.values():
            yield from snippets

    def generate_snippet(
        self,
        tool_with_version: str | None = None,
        snippet_order: Literal["service"] | None = None,
        reverse: bool = False,
    ) -> str | None:
        inner_content = ""
        if snippet_order is None:
            snippets = (
                reversed(self._generic_snippets) if reverse else self._generic_snippets
            )
            inner_content = "".join(s.script_content() for s in snippets)
        elif snippet_order in self._snippets_by_order:
            snippets = self._snippets_by_order[snippet_order]
            if reverse:
                snippets = reversed(snippets)
            inner_content = "".join(s.script_content() for s in snippets)

        if not inner_content:
            return None

        if tool_with_version:
            return (
                f"# Automatically added by {tool_with_version}\n"
                + inner_content
                + "# End automatically added section"
            )
        return inner_content


class DpkgMaintscriptHelperCommand(DebputyDispatchableType):
    __slots__ = ("cmdline", "definition_source")

    def __init__(self, cmdline: Sequence[str], definition_source: str) -> None:
        super().__init__()
        self.cmdline = cmdline
        self.definition_source = definition_source

    @classmethod
    def _finish_cmd(
        cls,
        definition_source: str,
        cmdline: list[str],
        prior_version: str | None,
        owning_package: str | None,
    ) -> Self:
        if prior_version is not None:
            cmdline.append(prior_version)
        if owning_package is not None:
            if prior_version is None:
                # Empty is allowed according to `man dpkg-maintscript-helper`
                cmdline.append("")
            cmdline.append(owning_package)
        return cls(
            tuple(cmdline),
            definition_source,
        )

    @classmethod
    def rm_conffile(
        cls,
        definition_source: AttributePath,
        conffile: str,
        prior_version: str | None = None,
        owning_package: str | None = None,
    ) -> Self:
        cmdline = ["rm_conffile", conffile]
        return cls._finish_cmd(
            definition_source.path, cmdline, prior_version, owning_package
        )

    @classmethod
    def mv_conffile(
        cls,
        definition_source: AttributePath,
        old_conffile: str,
        new_confile: str,
        prior_version: str | None = None,
        owning_package: str | None = None,
    ) -> Self:
        cmdline = ["mv_conffile", old_conffile, new_confile]
        return cls._finish_cmd(
            definition_source.path, cmdline, prior_version, owning_package
        )

    @classmethod
    def symlink_to_dir(
        cls,
        definition_source: AttributePath,
        pathname: str,
        old_target: str,
        prior_version: str | None = None,
        owning_package: str | None = None,
    ) -> Self:
        cmdline = ["symlink_to_dir", pathname, old_target]
        return cls._finish_cmd(
            definition_source.path, cmdline, prior_version, owning_package
        )

    @classmethod
    def dir_to_symlink(
        cls,
        definition_source: AttributePath,
        pathname: str,
        new_target: str,
        prior_version: str | None = None,
        owning_package: str | None = None,
    ) -> Self:
        cmdline = ["dir_to_symlink", pathname, new_target]
        return cls._finish_cmd(
            definition_source.path, cmdline, prior_version, owning_package
        )
