import dataclasses
import os.path
from typing import Optional, Callable, FrozenSet, Dict, List, Tuple

from debputy.exceptions import DebputyRuntimeError
from debputy.filesystem_scan import DebputyFSIsROError
from debputy.packages import BinaryPackage
from debputy.plugin.api import (
    VirtualPath,
    BinaryCtrlAccessor,
    MetadataDetectorPackageContext,
)
from debputy.plugin.api.spec import (
    DebputyPluginInitializer,
    MetadataAutoDetector,
    DpkgTriggerType,
)
from debputy.util import _normalize_path

_DEB_ONLY = frozenset(["deb"])


@dataclasses.dataclass(slots=True)
class DebputyPluginMetadata:
    plugin_name: str
    plugin_version: str
    api_compat_version: int
    plugin_loader: Optional[Callable[[], Callable[["DebputyPluginInitializer"], None]]]
    plugin_initializer: Optional[Callable[["DebputyPluginInitializer"], None]]
    plugin_path: str
    _is_initialized: bool = False

    @property
    def is_loaded(self) -> bool:
        return self.plugin_initializer is not None

    @property
    def is_initialized(self) -> bool:
        return self._is_initialized

    def initialize_plugin(self, api: "DebputyPluginInitializer") -> None:
        if self.is_initialized:
            raise RuntimeError("Cannot load plugins twice")
        if not self.is_loaded:
            self._load()
        self.plugin_initializer(api)
        self._is_initialized = True

    def _load(self) -> None:
        self.plugin_initializer = self.plugin_loader()
        assert self.plugin_initializer is not None


@dataclasses.dataclass(slots=True, frozen=True)
class PackagerProvidedFileClassSpec:
    debputy_plugin_metadata: DebputyPluginMetadata
    stem: str
    installed_as_format: str
    default_mode: int
    default_priority: Optional[int]
    allow_name_segment: bool
    allow_architecture_segment: bool
    post_formatting_rewrite: Optional[Callable[[str], str]]
    packageless_is_fallback_for_all_packages: bool
    reservation_only: bool

    @property
    def supports_priority(self) -> bool:
        return self.default_priority is not None

    def compute_dest(
        self,
        assigned_name: str,
        # Note this method is currently used 1:1 inside plugin tests.
        *,
        owning_package: Optional[str] = None,
        assigned_priority: Optional[int] = None,
    ) -> Tuple[str, str]:
        if assigned_priority is not None and not self.supports_priority:
            raise ValueError(
                f"Cannot assign priority to packager provided files with stem"
                f' "{self.stem}" (e.g., "debian/foo.{self.stem}"). They'
                " do not use priority at all."
            )

        path_format = self.installed_as_format
        if self.supports_priority and assigned_priority is None:
            assigned_priority = self.default_priority

        path = path_format.format(
            priority=assigned_priority,
            name=assigned_name,
            owning_package=owning_package,
        )

        dirname, basename = os.path.split(path)
        dirname = _normalize_path(dirname)

        if self.post_formatting_rewrite:
            basename = self.post_formatting_rewrite(basename)
        return dirname, basename


@dataclasses.dataclass(slots=True)
class MetadataOrMaintscriptDetector:
    plugin_metadata: DebputyPluginMetadata
    detector_id: str
    detector: MetadataAutoDetector
    applies_to_package_types: FrozenSet[str]
    enabled: bool = True

    def applies_to(self, binary_package: BinaryPackage) -> bool:
        return binary_package.package_type in self.applies_to_package_types

    def run_detector(
        self,
        fs_root: "VirtualPath",
        ctrl: "BinaryCtrlAccessor",
        context: "MetadataDetectorPackageContext",
    ) -> None:
        try:
            self.detector(fs_root, ctrl, context)
        except DebputyFSIsROError as e:
            nv = f"{self.plugin_metadata.plugin_name}/{self.plugin_metadata.plugin_version}"
            raise PluginAPIViolationError(
                f'The plugin {nv} violated the API contract for "metadata detectors"'
                " by attempting to mutate the provided file system in its metadata detector"
                f" with id {self.detector_id}.  File system mutation is *not* supported at"
                " this stage (file system layout is committed and the attempted changes"
                " would be lost)."
            ) from e
        except (ChildProcessError, RuntimeError, AttributeError) as e:
            nv = f"{self.plugin_metadata.plugin_name}/{self.plugin_metadata.plugin_version}"
            raise UnhandledOrUnexpectedErrorFromPluginError(
                f"The plugin {nv} threw an unhandled or unexpected exception from its metadata"
                f" detector with id {self.detector_id}."
            ) from e


@dataclasses.dataclass
class PluginProvidedFeatureSet:
    plugin_data: Dict[str, DebputyPluginMetadata]
    packager_provided_files: Dict[str, PackagerProvidedFileClassSpec]
    metadata_maintscript_detectors: Dict[str, List[MetadataOrMaintscriptDetector]]


@dataclasses.dataclass(slots=True, frozen=True)
class MetadataDetectorPackageContextProvider(MetadataDetectorPackageContext):
    binary_package: BinaryPackage
    related_udeb_package: Optional[BinaryPackage]


class PluginError(DebputyRuntimeError):
    pass


class PluginNotFoundError(PluginError):
    pass


class PluginInitializationError(PluginError):
    pass


class PluginMetadataError(PluginError):
    pass


class PluginConflictError(PluginError):
    @property
    def plugin_a(self) -> DebputyPluginMetadata:
        return self.args[1]

    @property
    def plugin_b(self) -> DebputyPluginMetadata:
        return self.args[2]


class PluginAPIViolationError(PluginError):
    pass


class UnhandledOrUnexpectedErrorFromPluginError(PluginError):
    pass


@dataclasses.dataclass(slots=True, frozen=True)
class PluginProvidedTrigger:
    dpkg_trigger_type: DpkgTriggerType
    dpkg_trigger_target: str
    provider: DebputyPluginMetadata
    provider_source_id: str

    def serialized_format(self) -> str:
        return f"{self.dpkg_trigger_type} {self.dpkg_trigger_target}"
