# waypin/core.py
import sys
from pathlib import Path
import inspect
import IPython
import ipynbname


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


# ---- exported state (set by init) ----
ROOT_DIR: Path | None = None
NOTEBOOK: bool | None = None


def _is_under(child: Path, parent: Path) -> bool:
    try:
        child.resolve().relative_to(parent.resolve())
        return True
    except Exception:
        return False


def _get_callsite_path() -> Path:
    """
    1) If running in a notebook, use `ipynbname.path()` (most reliable).
    2) If running as a script, traverse the call stack and select the first file
       that is located outside the `waypin` package.
    """
    ip = IPython.get_ipython()
    is_notebook = (ip is not None) and ("IPKernelApp" in ip.config)

    if is_notebook:
        return Path(ipynbname.path()).resolve()

    # waypin(current module) root detect: .../waypin/...
    lib_root = Path(__file__).resolve()

    while lib_root.name != "waypin" and lib_root.parent != lib_root:
        lib_root = lib_root.parent

    if lib_root.name != "waypin":
        lib_root = Path(__file__).resolve().parent

    for frame_info in inspect.stack()[1:]:
        fn = frame_info.filename
        if not fn or fn.startswith("<"):
            continue
        p = Path(fn)
        if not p.exists():
            continue
        p = p.resolve()

        if _is_under(p, lib_root):
            continue

        return p

    argv0 = Path(sys.argv[0]) if sys.argv and sys.argv[0] else None
    if argv0 and argv0.exists():
        return argv0.resolve()

    raise RuntimeError("Cannot determine callsite path (no file-backed frame found).")


def reel(anchor_dirname: str = "scripts"):
    """
    Starting from the caller's file location, walk up the parent directory chain
    to locate the directory named `anchor_dirname`.
    The parent of that directory is treated as the project root and is appended
    to `sys.path`.
    """
    this_path = _get_callsite_path()

    # NOTE: suffix includes the dot, e.g. ".ipynb"
    _is_notebook = (this_path.suffix.lower() == ".ipynb")

    anchor_dir = None
    for p in [this_path.parent, *this_path.parents]:
        if p.name == anchor_dirname:
            anchor_dir = p
            break

    if anchor_dir is None:
        raise AnchorNotFoundError(
            f"Anchor directory '{anchor_dirname}' was not found from: {this_path}\n"
            f"Tip: ensure your callsite is under '<project>/{anchor_dirname}/...'."
        )

    root_dir = anchor_dir.parent
    root_str = str(root_dir)
    if root_str not in sys.path:
        sys.path.append(root_str)

    return root_dir, _is_notebook


def init(anchor_dirname: str = "scripts"):
    """
    Initialize module-level state (ROOT_DIR, IS_NOTEBOOK) using reel().
    This function is idempotent.
    """
    global ROOT_DIR, NOTEBOOK
    if ROOT_DIR is not None and NOTEBOOK is not None:
        return ROOT_DIR, NOTEBOOK

    ROOT_DIR, NOTEBOOK = reel(anchor_dirname=anchor_dirname)
    return ROOT_DIR, NOTEBOOK
