import collections.abc
import contextlib
import dataclasses
import glob
import logging
import os.path
import typing

from typing import Mapping, Tuple, IO, Iterable

from deb_build_artifact_gather.rules import ArtifactRule, ENV_VARS, VARIABLES
from deb_build_artifact_gather.variables import Variable


@dataclasses.dataclass(slots=True, frozen=True)
class MatchedArtifactRule:
    matched_path: str
    artifact_rule: ArtifactRule
    matched_directory: str


@dataclasses.dataclass(slots=True, frozen=True)
class BuildSystemFile:
    path: str
    file_reference: str


def read_pattern_line_based_format(
    fd: IO[str],
) -> collections.abc.Iterable[Tuple[Variable, str]]:
    for line_no, line in enumerate(fd, start=1):
        line = line.strip()
        if not line or line.startswith("#"):
            continue
        yield line


def read_variables_line_based_format(
    file_reference: str,
    fd: IO[str],
) -> collections.abc.Iterable[Tuple[Variable, str]]:
    for line_no, line in enumerate(fd, start=1):
        line = line.strip()
        if not line or line.startswith("#"):
            continue
        parts = line.split("=", 1)
        if len(parts) != 2:
            logging.warn(
                f"{file_reference}:{line_no}: Expected VAR=VALUE, got: {line!r}"
            )
            continue
        name, value = (p.strip() for p in parts)
        variable = VARIABLES.get(name)
        if variable is None:
            logging.info(
                f"{file_reference}:{line_no}: Unknown variable {name}, ignoring."
            )
            continue
        yield variable, value


class BuildSystemDescriptor:

    def environ(self) -> Mapping[str, str]:
        raise NotImplementedError

    def collect_variable_files(
        self,
        tool_provided_dir: str,
    ) -> Iterable[BuildSystemFile]:
        raise NotImplementedError

    @contextlib.contextmanager
    def open_file(self, path: str) -> IO[str]:
        raise NotImplementedError

    def expand_glob(
        self,
        directory: str,
        glob_pattern: str,
    ) -> Iterable[tuple[str, str]]:
        raise NotImplementedError

    def collect_variable_definitions(
        self,
        tool_provided_dir: str,
    ) -> Iterable[tuple[Variable, str]]:
        raise NotImplementedError

    def collect_extra_patterns(
        self,
        tool_provided_dir: str,
    ) -> Iterable[str]:
        raise NotImplementedError


class DirectlyAccessibleBuildSystemDescriptor(BuildSystemDescriptor):

    @contextlib.contextmanager
    def open_file(self, path: str) -> IO[str]:
        with open(path) as fd:
            yield fd

    def environ(self) -> Mapping[str, str]:
        return {k: os.environ[k] for k in ENV_VARS if k in os.environ}

    def expand_glob(
        self,
        directory: str,
        glob_pattern: str,
    ) -> Iterable[tuple[str, str]]:
        fs_dir = (
            self._abspath2host_abspath(directory)
            if os.path.isabs(directory)
            else directory
        )
        yield from (
            (os.path.join(directory, f), os.path.join(fs_dir, f))
            for f in glob.iglob(
                glob_pattern,
                root_dir=fs_dir,
            )
        )

    def _abspath2host_abspath(self, directory: str) -> str:
        raise NotImplementedError

    def collect_variable_definitions(
        self,
        tool_provided_dir: str,
    ) -> Iterable[tuple[Variable, str]]:
        ipc_dir = (
            self._abspath2host_abspath(tool_provided_dir)
            if os.path.isabs(tool_provided_dir)
            else tool_provided_dir
        )
        tool_provided_variable_dir = os.path.join(ipc_dir, "variables")
        if not os.path.isdir(tool_provided_variable_dir):
            return

        for path in self._relevant_files(tool_provided_dir, "variables", ".varlist"):
            with open(path) as fd:
                yield from read_variables_line_based_format(
                    path.path,
                    fd,
                )

    def _relevant_files(
        self,
        tool_provided_dir: str,
        subdir: str,
        extension: str | tuple[str, ...],
    ) -> Iterable[os.DirEntry[str]]:
        ipc_dir = (
            self._abspath2host_abspath(tool_provided_dir)
            if os.path.isabs(tool_provided_dir)
            else tool_provided_dir
        )
        tool_provided_subdir_dir = os.path.join(ipc_dir, subdir)
        if not os.path.isdir(tool_provided_subdir_dir):
            return

        with os.scandir(tool_provided_subdir_dir) as var_iter:
            for path in var_iter:
                if not path.name.endswith(extension) or not path.is_file(
                    follow_symlinks=False
                ):
                    continue
                yield path

    def collect_extra_patterns(
        self,
        tool_provided_dir: str,
    ) -> Iterable[str]:
        for path in self._relevant_files(
            tool_provided_dir, "collection-patterns", ".pattern-list"
        ):
            with open(path) as fd:
                yield from read_pattern_line_based_format(fd)


class HostWasBuildSystem(DirectlyAccessibleBuildSystemDescriptor):

    @typing.override
    def _abspath2host_abspath(self, directory: str) -> str:
        return directory


class BindmountAccessToBuildSystemDescriptor(DirectlyAccessibleBuildSystemDescriptor):

    def __init__(
        self,
        bindmount_path: str,
        source_root_path: str,
    ) -> None:
        self.bindmount_path = bindmount_path
        self.source_root_path = source_root_path

    def _abspath2host_abspath(self, abs_path_directory: str) -> str:
        return os.path.join(self.bindmount_path, abs_path_directory.removeprefix("/"))
