from pathlib import Path

# An example lives in a file called "model.py" under a directory named after the example, e.g. "cantilever/model.py". 

# The file model.py should define the following:
# - a function called "create_model" that returns a xara.Model object. 
# - a function called "solve" whose first argument is a xara.Model, and the remaining arguments are the options declared in solve_options. 
#   
# The file test.py must define at least the following:
# - a variable called model_options that is an instance of the Options class defined and 
#   declares the arguments to the create_model function.
# - a variable called solve_options that is an instance of the Options class defined and 
#   declares the additional arguments to the solve function.

# Options is a container of Option objects. an Option must be convertible to 
# args,kwds that can be passed to the create_model and solve functions.

# model.py should be simple and readable by beginners. 
# test.py can be more complex and is for internal validation purposes

# An example lives in a file called "model.py" under a directory named after
# the example, e.g. "cantilever/model.py".
#
# The file model.py should define the following:
#   - a function called "create_model" that returns a xara.Model object.
#   - a function called "solve" whose first argument is a xara.Model, and
#     the remaining arguments are the options declared in solve_options.
#
# model.py should be simple and readable by beginners.
#
# The file test.py must define at least the following:
#   - a variable called model_options (Options instance) declaring the
#     arguments to create_model.
#   - a variable called solve_options (Options instance) declaring the
#     additional arguments to solve.
#
# test.py can be more complex and is for internal validation purposes.

"""
xara.examples
=============

An example lives in a directory named after the example (e.g.
``frame-0001/``).  The directory must contain at least:

  model.py
      Defines ``create_model(**kwds)`` and ``solve(model, **kwds)``.
      May import from other files in the same directory.
      Should be simple and readable by beginners.

  test.py
      Defines ``Cases``: a list of dicts whose keys are arguments to
      ``create_model`` and/or ``solve``.  The Example object inspects
      function signatures to route each key to the right function.

      May also contain ``test_*()`` functions for pytest.
"""

from pathlib import Path
import importlib.util
import inspect
import sys

EXAMPLE_ROOT = Path("~/online/benchmarks/benchmarks").expanduser()


# ------------------------------------------------------------------
# Import helper
# ------------------------------------------------------------------

def _import_module_from_file(filepath: Path, module_name: str | None = None):
    """Import a .py file as a module.

    Adds the file's parent directory to ``sys.path`` so that sibling
    imports (e.g. ``from helpers import ...`` inside model.py) work,
    even when the directory name is not a valid Python identifier.
    """
    filepath = Path(filepath).resolve()

    # Ensure sibling imports work
    parent = str(filepath.parent)
    if parent not in sys.path:
        sys.path.insert(0, parent)

    if module_name is None:
        safe_parent = filepath.parent.name.replace("-", "_").replace(" ", "_")
        module_name = f"_xara_example_.{safe_parent}.{filepath.stem}"

    spec = importlib.util.spec_from_file_location(module_name, filepath)
    if spec is None or spec.loader is None:
        raise ImportError(f"Cannot import {filepath}")
    module = importlib.util.module_from_spec(spec)
    sys.modules[module_name] = module
    spec.loader.exec_module(module)
    return module


# ------------------------------------------------------------------
# Example
# ------------------------------------------------------------------

class Example:
    """A loaded example.

    Attributes
    ----------
    name : str
        Directory name (e.g. ``"frame-0001"``).
    path : Path
        Absolute path to the example directory.
    model : module
        The imported ``model.py``.
    cases : list[dict]
        The ``Cases`` list from ``test.py``.
    """

    def __init__(self, name, path, model_module, test_module):
        self.name = name
        self.path = Path(path)
        self.model = model_module
        self.test = test_module
        self.cases: list[dict] = getattr(test_module, "Cases", [{}])

        # Cache the parameter names so we can split case dicts
        self._model_params = set(inspect.signature(self.model.create_model).parameters)
        self._solve_params = set(inspect.signature(self.model.solve).parameters) - {"model"}

    def __repr__(self):
        return f"Example({self.name!r}, {len(self.cases)} cases)"

    def _split(self, case: dict):
        """Split a flat case dict into (model_kwds, solve_kwds)."""
        model_kwds = {}
        solve_kwds = {}
        for k, v in case.items():
            if k in self._model_params:
                model_kwds[k] = v
            elif k in self._solve_params:
                solve_kwds[k] = v
            else:
                raise KeyError(
                    f"Case key {k!r} not found in create_model{sorted(self._model_params)} "
                    f"or solve{sorted(self._solve_params)}"
                )
        return model_kwds, solve_kwds

    def create_model(self, case: dict | None = None):
        model_kwds, _ = self._split(case or {})
        return self.model.create_model(**model_kwds)

    def solve(self, model, case: dict | None = None):
        _, solve_kwds = self._split(case or {})
        return self.model.solve(model, **solve_kwds)

    def run(self, case: dict | None = None):
        """``create_model`` then ``solve``.  Returns ``(model, result)``."""
        case = case or {}
        model_kwds, solve_kwds = self._split(case)
        model = self.model.create_model(**model_kwds)
        result = self.model.solve(model, **solve_kwds)
        return model, result


def load(name: str, root: Path | str | None = None) -> Example:
    """Load an example by directory name.

    Parameters
    ----------
    name : str
        Example directory name, e.g. ``"frame-0001"``.
    root : Path, optional
        Override ``EXAMPLE_ROOT``.
    """
    root = Path(root) if root is not None else EXAMPLE_ROOT
    example_dir = root / name

    model_file = example_dir / "model.py"
    test_file  = example_dir / "test.py"

    if not model_file.exists():
        raise FileNotFoundError(f"No model.py at {model_file}")
    if not test_file.exists():
        raise FileNotFoundError(f"No test.py at {test_file}")

    model_module = _import_module_from_file(model_file)
    test_module  = _import_module_from_file(test_file)

    return Example(name, example_dir, model_module, test_module)