import dataclasses
from enum import Enum
from typing import Optional, Iterable, Set

from debputy.filesystem_scan import FSPath
from debputy.intermediate_manifest import TarMember, PathType
from debputy.manifest_conditions import ManifestCondition
from debputy.path_matcher import MatchRule, ExactFileSystemPath, MatchRuleType
from debputy.util import (
    debian_policy_normalize_symlink_target,
    apply_symbolic_mode,
    _error,
)


class ManifestPathInfoRuleType(Enum):
    ENSURE_METADATA = ("ensure-metadata", False, True)
    EXCLUDE_PATH = ("remove-path", False, False)
    CREATE_SYMLINK = ("create-symlink", True, False)
    CREATE_DIRECTORY = ("create-directory", True, True)
    PACKAGER_PROVIDED_FILE = ("packager-provided-file", True, False)

    @property
    def creates_path(self) -> bool:
        return self.value[1]

    @property
    def can_have_children(self):
        return self.value[2]


@dataclasses.dataclass(slots=True, frozen=False)
class ManifestPathRule:
    rule_type: ManifestPathInfoRuleType
    definition_source: str
    match_rule: MatchRule
    ensure_path_type: Optional["PathType"] = None
    symbolic_mode: Optional[str] = None
    mode: Optional[int] = None
    dirmode: Optional[int] = None
    special_mode: Optional[int] = None
    uid: int = 0
    uname: str = "root"
    gid: int = 0
    gname: str = "root"
    link_target: Optional[str] = None
    is_implicit_definition: bool = False
    is_builtin_definition: bool = False
    triggered_by: Optional["ManifestPathRule"] = None
    condition: Optional[ManifestCondition] = None
    source_path: Optional[str] = None
    _used: bool = False
    _children: Optional[Set[ExactFileSystemPath]] = None

    @classmethod
    def ensure_metadata(cls, definition_source: str, match_rule: MatchRule, **kwargs):
        if "match_rule" in kwargs:
            raise ValueError(
                f"The match_rule is a positional parameter for ensure_metadata, saw it in the kwargs part"
            )
        return EnsurePathMetadataPathRule(definition_source, match_rule, **kwargs)

    @classmethod
    def symlink(
        cls,
        definition_source: str,
        link_path: ExactFileSystemPath,
        link_target: str,
        condition: Optional["ManifestPathRule"] = None,
    ):
        return EnsureSymlinkPathRule(
            definition_source,
            link_path,
            link_target,
            condition=condition,
        )

    @classmethod
    def directory(
        cls,
        definition_source: str,
        directory_path: ExactFileSystemPath,
        mode: Optional[int] = None,
        uid=0,
        uname="root",
        gid=0,
        gname="root",
        triggered_by: Optional["ManifestPathRule"] = None,
        dirmode=None,
        special_mode=None,
        condition: Optional["ManifestPathRule"] = None,
    ) -> "EnsureDirectoryPathRule":
        is_implicit_definition = True if triggered_by is not None else False
        if (
            dirmode is not None
            and dirmode != mode
            or special_mode is not None
            and special_mode != mode
        ):
            raise ValueError('Please pass only "mode" to the directory method')
        if mode is None and not is_implicit_definition:
            mode = 0o755
        return EnsureDirectoryPathRule(
            definition_source,
            directory_path,
            mode=mode,
            uid=uid,
            uname=uname,
            gid=gid,
            gname=gname,
            is_implicit_definition=is_implicit_definition,
            triggered_by=triggered_by,
            condition=condition,
        )

    @classmethod
    def packager_provided_file(
        cls,
        definition_source: str,
        match_rule: ExactFileSystemPath,
        source_path: str,
        **kwargs,
    ) -> "PackagerProvidedFilePathRule":
        return PackagerProvidedFilePathRule(
            definition_source,
            match_rule,
            source_path,
            **kwargs,
        )

    @property
    def member_path(self) -> Optional[str]:
        match_rule = self.match_rule
        if match_rule.rule_type != MatchRuleType.EXACT_MATCH:
            return None
        assert match_rule.lookup_key() is not None
        return match_rule.lookup_key()

    def add_virtual_child(self, child_path_info: "ManifestPathRule") -> None:
        if self.ensure_path_type is None:
            self.ensure_path_type = PathType.DIRECTORY
        if (
            self.ensure_path_type != PathType.DIRECTORY
            or not self.rule_type.can_have_children
        ):
            assert self.member_path is not None
            _error(
                f'Conflicting definition for path "{self.member_path}":\n'
                f' * The path /must/ be a directory due to "{child_path_info.definition_source}"\n'
                f' * However, at the same time, the path /cannot/ be a directory due to "{self.definition_source}"\n'
                f"Please resolve the conflict by removing or changing the path in one of the definitions"
            )
        child_match_rule = child_path_info.match_rule
        assert isinstance(child_match_rule, ExactFileSystemPath)
        if self._children is None:
            self._children = set()
        self._children.add(child_match_rule)

    def children(self) -> Iterable[ExactFileSystemPath]:
        if self._children:
            yield from self._children

    def mark_used(self):
        self._used = True

    @property
    def has_been_used(self):
        return self._used

    # EnsureMetadata and EnsureSymlink can both cause a symlink to be created, so we have this in both places.
    def _create_symlink(
        self,
        tar_path: "FSPath",
        link_target: str,
        mtime: int,
    ) -> TarMember:
        return TarMember.virtual_path(
            tar_path.tar_path,
            PathType.SYMLINK,
            mtime,
            link_target=link_target,
            # Force mode to be 0777 as that is the mode we see in the data.tar.  In theory, tar lets you set
            # it to whatever. However, for reproducibility, we have to be well-behaved - and that is 0777.
            mode=0o0777,
            owner=self.uname,
            uid=self.uid,
            group=self.gname,
            gid=self.gid,
        )

    # EnsureMetadata is likely to hit virtual directories.
    def _create_virtual_directory(
        self, path: "FSPath", clamp_mtime_to: int
    ) -> TarMember:
        assert path.is_dir
        mode = self.mode
        mtime = clamp_mtime_to
        if path.has_fs_path and mode is None:
            # Implicit directory entries (or modeless ones) can end up here and will use the file system path version
            # if present.
            mode = path.mode()
            mtime = min(path.mtime(), clamp_mtime_to)
        return TarMember.virtual_path(
            path.tar_path,
            PathType.DIRECTORY,
            mtime,
            mode=mode if mode is not None else 0o755,
            owner=self.uname,
            uid=self.uid,
            group=self.gname,
            gid=self.gid,
        )

    def generate_tar_member(self, tar_path: "FSPath", clamp_mtime_to: int) -> TarMember:
        raise NotImplementedError


class EnsurePathMetadataPathRule(ManifestPathRule):
    def __init__(
        self,
        definition_source: str,
        match_rule: MatchRule,
        symbolic_mode: Optional[str] = None,
        mode: Optional[int] = None,
        dirmode: Optional[int] = None,
        special_mode: Optional[int] = None,
        uid=0,
        uname="root",
        gid=0,
        gname="root",
        is_builtin_definition=False,
        condition: Optional["ManifestPathRule"] = None,
    ) -> None:
        if symbolic_mode is not None and (
            mode is not None or dirmode is not None or special_mode is not None
        ):
            raise ValueError(
                "symbolic_mode is mutually exclusive with any of the other modes"
            )
        super().__init__(
            rule_type=ManifestPathInfoRuleType.ENSURE_METADATA,
            definition_source=definition_source,
            match_rule=match_rule,
            symbolic_mode=symbolic_mode,
            mode=mode,
            dirmode=dirmode,
            special_mode=special_mode,
            uid=uid,
            uname=uname,
            gid=gid,
            gname=gname,
            is_builtin_definition=is_builtin_definition,
            condition=condition,
        )

    def generate_tar_member(self, tar_path: "FSPath", clamp_mtime_to: int) -> TarMember:
        self._used = True
        path_type = None
        if tar_path.is_dir:
            # Special-case (implicit directory)
            if not tar_path.has_fs_path:
                return self._create_virtual_directory(tar_path, clamp_mtime_to)
            mode = self.dirmode
            path_type = PathType.DIRECTORY
        elif tar_path.is_file:
            mode = self.mode
            # TODO: someday we will need to deal with hardlinks and it might appear here.
            path_type = PathType.FILE
        elif tar_path.is_symlink:
            # Special-case,
            link_target = tar_path.readlink()
            return self._create_symlink(
                tar_path,
                debian_policy_normalize_symlink_target(tar_path.path, link_target),
                min(int(tar_path.mtime()), clamp_mtime_to),
            )
        else:
            mode = self.special_mode
            # No path type here; pass through to from_file and let it figure it out from an lstat call.

        if self.symbolic_mode is not None:
            assert mode is None
            mode = apply_symbolic_mode(tar_path, tar_path.mode(), self.symbolic_mode)

        tar_member = TarMember.from_file(
            tar_path.tar_path,
            tar_path.fs_path,
            mode=mode,
            uid=self.uid,
            owner=self.uname,
            gid=self.gid,
            group=self.gname,
            path_type=path_type,
            path_mtime=tar_path.mtime(),
            clamp_mtime_to=clamp_mtime_to,
        )

        if (
            self.ensure_path_type is not None
            and self.ensure_path_type != tar_member.path_type
        ):
            fixit_hint = ""
            if self.ensure_path_type == PathType.DIRECTORY:
                fixit_hint = (
                    "\n"
                    "You can often fix this by either removing the physical path in the file system"
                    " OR by explicitly defining this path as a directory in the manifest.  In that case,"
                    " the result will be a directory.  Alternatively, check if the manifest reference"
                    " point to something that should create a directory here.  Maybe the path in the"
                    " manifest has a typo."
                )
            _error(
                f"Had expected {tar_member.member_path} to be a {self.ensure_path_type.name} due"
                f" to {self.definition_source}, but in the file system the path pointed to a"
                f" {tar_member.path_type.name} (fs path is: {tar_member.fs_path}).{fixit_hint}"
            )

        return tar_member


class PackagerProvidedFilePathRule(ManifestPathRule):
    def __init__(
        self,
        definition_source: str,
        match_rule: ExactFileSystemPath,
        source_path: str,
        mode: int = 0o644,
        uid=0,
        uname="root",
        gid=0,
        gname="root",
        is_builtin_definition=True,
        condition: Optional["ManifestPathRule"] = None,
    ) -> None:
        super().__init__(
            rule_type=ManifestPathInfoRuleType.PACKAGER_PROVIDED_FILE,
            definition_source=definition_source,
            match_rule=match_rule,
            source_path=source_path,
            ensure_path_type=PathType.FILE,
            mode=mode,
            uid=uid,
            uname=uname,
            gid=gid,
            gname=gname,
            is_builtin_definition=is_builtin_definition,
            condition=condition,
        )

    def generate_tar_member(self, tar_path: "FSPath", clamp_mtime_to: int) -> TarMember:
        self._used = True
        assert tar_path.tar_path == self.member_path
        assert self.source_path is not None
        return TarMember.from_file(
            tar_path.tar_path,
            self.source_path,
            mode=self.mode,
            uid=self.uid,
            owner=self.uname,
            gid=self.gid,
            group=self.gname,
            clamp_mtime_to=clamp_mtime_to,
            path_type=PathType.FILE,
        )


class EnsureSymlinkPathRule(ManifestPathRule):
    def __init__(
        self,
        definition_source: str,
        link_path: ExactFileSystemPath,
        link_target: str,
        condition: Optional["ManifestPathRule"],
    ) -> None:
        super().__init__(
            rule_type=ManifestPathInfoRuleType.CREATE_SYMLINK,
            definition_source=definition_source,
            match_rule=link_path,
            link_target=debian_policy_normalize_symlink_target(
                link_path.path, link_target
            ),
            ensure_path_type=PathType.SYMLINK,
            condition=condition,
        )

    def generate_tar_member(self, tar_path: "FSPath", clamp_mtime_to: int) -> TarMember:
        self._used = True
        return self._create_symlink(
            tar_path,
            self.link_target,
            clamp_mtime_to,
        )


class EnsureDirectoryPathRule(ManifestPathRule):
    def __init__(
        self,
        definition_source: str,
        directory_path: ExactFileSystemPath,
        mode: Optional[int] = None,
        uid=0,
        uname="root",
        gid=0,
        gname="root",
        triggered_by: Optional["ManifestPathRule"] = None,
        is_implicit_definition: bool = False,
        condition: Optional["ManifestPathRule"] = None,
    ) -> None:
        super().__init__(
            rule_type=ManifestPathInfoRuleType.CREATE_DIRECTORY,
            definition_source=definition_source,
            match_rule=directory_path,
            ensure_path_type=PathType.DIRECTORY,
            mode=mode,
            uid=uid,
            uname=uname,
            gid=gid,
            gname=gname,
            is_implicit_definition=is_implicit_definition,
            triggered_by=triggered_by,
            condition=condition,
        )

    def generate_tar_member(self, tar_path: "FSPath", clamp_mtime_to: int) -> TarMember:
        self._used = True
        return self._create_virtual_directory(tar_path, clamp_mtime_to)
