import inspect
import os
import sys
from pathlib import Path
from typing import Optional, Tuple


class AnchorNotFoundError(RuntimeError):
    """Raised when the anchor directory cannot be found in parent chain."""


def _in_notebook() -> bool:
    # Avoid hard dependency on IPython
    try:
        import IPython  # type: ignore
        return IPython.get_ipython() is not None
    except Exception:
        return False


def _caller_file() -> Optional[Path]:
    """
    Return the calling file path if available.
    - In normal python execution: returns the caller module's file.
    - In notebook/interactive: returns None.
    """
    try:
        frame = inspect.currentframe()
        if frame is None or frame.f_back is None:
            return None
        caller_globals = frame.f_back.f_globals
        filename = caller_globals.get("__file__")
        if not filename:
            return None
        return Path(str(filename)).resolve()
    except Exception:
        return None


def reel(
    anchor_dirname: str = "scripts",
    *,
    add_to_sys_path: bool = True,
    start: Optional[os.PathLike | str] = None,
) -> Tuple[Path, bool]:
    """
    Locate project root by finding `anchor_dirname` (e.g., "scripts") in the parent chain.

    Returns:
        (root_dir, is_notebook)

    Rules:
        - Search upward from `start` if provided.
        - Else if running in a notebook: try to use notebook path via ipynbname (optional);
          if unavailable, fall back to current working directory.
        - Else: use the caller's __file__.
        - If a directory named `anchor_dirname` is found, root_dir is its parent directory.
        - Optionally append root_dir to sys.path.

    Raises:
        AnchorNotFoundError: if the anchor directory cannot be found.
    """
    is_nb = _in_notebook()

    if start is not None:
        this_path = Path(start).resolve()
    else:
        if is_nb:
            # Optional dependency: ipynbname
            nb_path = None
            try:
                import ipynbname  # type: ignore
                nb_path = Path(ipynbname.path()).resolve()
            except Exception:
                nb_path = None

            this_path = nb_path if nb_path is not None else Path.cwd().resolve()
        else:
            caller = _caller_file()
            if caller is None:
                # As a last resort, use cwd (rare, but safer than crashing on __file__)
                this_path = Path.cwd().resolve()
            else:
                this_path = caller

    # If it's a file, search from its parent directory.
    search_from = this_path if this_path.is_dir() else this_path.parent

    for p in (search_from, *search_from.parents):
        if p.name == anchor_dirname and p.is_dir():
            root_dir = p.parent
            if add_to_sys_path:
                root_str = str(root_dir)
                if root_str not in sys.path:
                    sys.path.append(root_str)
            return root_dir, is_nb

    raise AnchorNotFoundError(
        f"Anchor directory '{anchor_dirname}' was not found in the parent chain. "
        f"Start path: '{search_from}'. "
        "Tip: ensure your code is located under '<project>/{anchor_dirname}/...'."
    )
