"""deport - the opposite of import."""

import codecs
import encodings
import importlib
import importlib.abc
import importlib.machinery
import importlib.util
import os as _os
import re
import sys

__all__ = ["deport"]

# ---------------------------------------------------------------------------
# Source-code transformation
# ---------------------------------------------------------------------------

_DEPORT_RE = re.compile(
    r"^(\s*)deport\s+([\w.\s,]+?)(?:\s*#.*)?$",
    re.MULTILINE,
)


def _transform_source(source: str) -> str:
    """Rewrite ``deport X`` statements into real Python."""

    def _replace(match: re.Match) -> str:
        indent = match.group(1)
        names = [n.strip() for n in match.group(2).split(",")]
        parts = []
        for name in names:
            top = name.split(".")[0]
            parts.append(
                f'{indent}__import__("deport")._deport("{name}"); del {top}'
            )
        return "\n".join(parts)

    return _DEPORT_RE.sub(_replace, source)


# ---------------------------------------------------------------------------
# Custom codec (activated with ``# coding: deport``)
# ---------------------------------------------------------------------------

_utf8 = encodings.search_function("utf8")


class _IncrementalDecoder(codecs.BufferedIncrementalDecoder):
    def _buffer_decode(self, input: bytes, errors: str, final: bool):
        if final:
            return _transform_source(input.decode("utf-8", errors)), len(input)
        return "", 0


class _StreamReader(codecs.StreamReader):
    def decode(self, input: bytes, errors: str = "strict"):
        return _transform_source(input.decode("utf-8", errors)), len(input)


def _deport_decode(input: bytes, errors: str = "strict"):
    source, length = _utf8.decode(input, errors)
    return _transform_source(source), length


def _codec_search(name: str):
    if name != "deport":
        return None
    return codecs.CodecInfo(
        name="deport",
        encode=_utf8.encode,
        decode=_deport_decode,
        incrementaldecoder=_IncrementalDecoder,
        incrementalencoder=_utf8.incrementalencoder,
        streamreader=_StreamReader,
        streamwriter=_utf8.streamwriter,
    )


codecs.register(_codec_search)

# ---------------------------------------------------------------------------
# Import hook — transparently preprocesses all .py files on import
# ---------------------------------------------------------------------------


class _DeportLoader(importlib.abc.Loader):
    def __init__(self, origin, source):
        self._origin = origin
        self._source = source

    def create_module(self, spec):
        return None  # use default semantics

    def exec_module(self, module):
        source = _transform_source(self._source)
        code = compile(source, self._origin, "exec")
        exec(code, module.__dict__)


class _DeportFinder(importlib.abc.MetaPathFinder):
    """Finder that intercepts .py imports to preprocess ``deport`` statements."""

    _active = False  # prevent re-entrancy

    def find_spec(self, fullname, path, target=None):
        if self._active:
            return None
        self._active = True
        try:
            spec = importlib.util.find_spec(fullname)
        except (ModuleNotFoundError, ValueError):
            return None
        finally:
            self._active = False
        if spec is None or spec.origin is None:
            return None
        if not spec.origin.endswith(".py"):
            return None
        try:
            with open(spec.origin, "r", encoding="utf-8") as f:
                source = f.read()
        except (OSError, UnicodeDecodeError):
            return None
        if "deport " not in source:
            return None
        return importlib.util.spec_from_file_location(
            fullname,
            spec.origin,
            loader=_DeportLoader(spec.origin, source),
            submodule_search_locations=spec.submodule_search_locations,
        )


if not any(isinstance(f, _DeportFinder) for f in sys.meta_path):
    sys.meta_path.insert(0, _DeportFinder())

# ---------------------------------------------------------------------------
# Excepthook — catches SyntaxError in the main script and re-runs it
# ---------------------------------------------------------------------------

_original_excepthook = sys.excepthook


def _deport_excepthook(exc_type, exc_value, exc_tb):
    # Only intercept SyntaxErrors that could be caused by ``deport``
    if exc_type is not SyntaxError:
        return _original_excepthook(exc_type, exc_value, exc_tb)

    filename = exc_value.filename
    if filename is None or not filename.endswith(".py"):
        return _original_excepthook(exc_type, exc_value, exc_tb)

    try:
        with open(filename, "r", encoding="utf-8") as f:
            source = f.read()
    except OSError:
        return _original_excepthook(exc_type, exc_value, exc_tb)

    if not _DEPORT_RE.search(source):
        return _original_excepthook(exc_type, exc_value, exc_tb)

    # Transform and re-execute the main script
    transformed = _transform_source(source)
    try:
        code = compile(transformed, filename, "exec")
    except SyntaxError:
        # Transformation didn't fix it — show the *original* error
        return _original_excepthook(exc_type, exc_value, exc_tb)

    try:
        globs = {"__name__": "__main__", "__file__": filename}
        exec(code, globs)
    except SystemExit as e:
        sys.stdout.flush()
        sys.stderr.flush()
        _os._exit(e.code if isinstance(e.code, int) else (1 if e.code else 0))
    except BaseException:
        import traceback

        traceback.print_exc()
        sys.stdout.flush()
        sys.stderr.flush()
        _os._exit(1)

    sys.stdout.flush()
    sys.stderr.flush()
    _os._exit(0)


sys.excepthook = _deport_excepthook

# ---------------------------------------------------------------------------
# Runtime helpers
# ---------------------------------------------------------------------------


def _deport(name: str) -> None:
    """Remove *name* and all its sub-modules from ``sys.modules``."""
    to_remove = [k for k in sys.modules if k == name or k.startswith(name + ".")]
    for k in to_remove:
        del sys.modules[k]
