"""Class definition of the ``skore`` project."""

from __future__ import annotations

import re
from typing import TYPE_CHECKING, Any

from skore._project import plugin
from skore._project._summary import Summary
from skore._project.types import ProjectMode

if TYPE_CHECKING:
    from skore import CrossValidationReport, EstimatorReport


class Project:
    r"""
    API to manage a collection of key-report pairs.

    Its constructor initializes a project by creating a new project or by loading an
    existing one.

    The class main methods are :func:`~skore.Project.put`,
    :func:`~skore.Project.summarize` and :func:`~skore.Project.get`, respectively to
    insert a key-report pair into the project, to obtain the metadata/metrics of the
    inserted reports and to get a specific report by its id.

    Three mutually exclusive modes are available and can be configured using the
    ``mode`` parameter of the constructor:

    .. rubric:: Hub mode

    The project is configured to communicate with ``skore hub``.

    In this mode, ``name`` is expected to be of the form ``<workspace>/<name>``, where
    the workspace is a ``skore hub`` concept that must be configured on the
    ``skore hub`` interface. It represents an isolated entity managing users, projects,
    and resources. It can be a company, organization, or team that operates
    independently within the system.

    Note: Using Project in ``hub`` mode requires an account on ``skore hub``, with
    access rights to the specified workspace. Authentication to ``skore hub`` is done by
    running ``skore.login()`` before instantiating the Project.

    .. rubric:: Local mode

    Otherwise, the project is configured to the ``local`` mode to be persisted on
    the user machine in a directory called ``workspace``.

    | The workspace can be shared between all the projects.
    | The workspace can be set using kwargs or the environment variable
      ``SKORE_WORKSPACE``.
    | If not, it will be by default set to a ``skore/`` directory in the user
      cache directory:

    - on Windows, usually ``C:\Users\%USER%\AppData\Local\skore``,
    - on Linux, usually ``${HOME}/.local/share/skore``,
    - on macOS, usually ``${HOME}/Library/Application Support/skore``.

    .. rubric:: MLflow mode

    In this mode, ``name`` is used as the MLflow experiment name. Reports are persisted
    as MLflow model artifacts in runs created under this experiment.

    Refer to the :ref:`project` section of the user guide for more details.

    Parameters
    ----------
    name : str
        The name of the project.
    mode : {"hub", "local", "mlflow"}
        The mode of the project.
    **kwargs : dict
        Extra keyword arguments passed to the project, depending on its mode.

        workspace : Path, mode:local only.
            The directory where the local project is persisted.

    Attributes
    ----------
    name : str
        The name of the project.
    mode : {"hub", "local", "mlflow"}
        The mode of the project.
    ml_task : MLTask
        The ML task of the project; unset until a first report is put.

    Examples
    --------
    Construct reports.

    >>> from sklearn.datasets import make_regression
    >>> from sklearn.linear_model import LinearRegression
    >>> from skore import evaluate
    >>>
    >>> X, y = make_regression(random_state=42)
    >>> regressor = LinearRegression()
    >>> regressor_report = evaluate(regressor, X, y, splitter=0.2)

    Construct the project in local mode, persisted in a temporary directory.

    >>> from pathlib import Path
    >>> from tempfile import TemporaryDirectory
    >>> from skore import Project
    >>>
    >>> tmpdir = TemporaryDirectory().name
    >>> local_project = Project(mode="local", name="my-xp", workspace=Path(tmpdir))

    Put reports in the project.

    >>> local_project.put("my-simple-regression", regressor_report)

    Investigate metadata/metrics to filter the best reports.

    >>> summary = local_project.summarize()
    >>> summary = summary.query("rmse < 67")
    >>> reports = summary.reports()

    See Also
    --------
    :meth:`Project.summarize` :
        Create a summary view to investigate persisted reports' metadata/metrics.
    """

    __HUB_NAME_PATTERN = re.compile(r"(?P<workspace>[^/]+)/(?P<name>.+)")

    @staticmethod
    def __setup_plugin(mode: ProjectMode, name: str) -> tuple[Any, dict]:
        if mode == "hub":
            if not (match := re.match(Project.__HUB_NAME_PATTERN, name)):
                raise ValueError(
                    f'In hub mode, the name must be formatted as "<workspace>/<name>" '
                    f"(found {name})"
                )

            parameters = {"workspace": match["workspace"], "name": match["name"]}
        elif mode == "local" or mode == "mlflow":
            parameters = {"name": name}
        else:
            raise ValueError(
                f'`mode` must be "hub", "local" or "mlflow" (found {mode})'
            )

        return plugin.get(group="skore.plugins.project", mode=mode), parameters

    def __init__(self, name: str, *, mode: ProjectMode = "local", **kwargs):
        r"""
        Initialize a project.

        Parameters
        ----------
        name : str
            The name of the project.
            For mode:mlflow, this name will be used as the experiment name.
        mode : {"hub", "local", "mlflow"}, default "local"
            The mode of the project.
        **kwargs : dict
            Extra keyword arguments passed to the project, depending on its mode.

            workspace : Path, mode:local only.
                The directory where the local project is persisted.

                | The workspace can be shared between all the projects.
                | The workspace can be set using kwargs or the environment variable
                  ``SKORE_WORKSPACE``.
                | If not, it will be by default set to a ``skore/`` directory in the
                  USER cache directory:

                - on Windows, usually ``C:\Users\%USER%\AppData\Local\skore``,
                - on Linux, usually ``${HOME}/.cache/skore``,
                - on macOS, usually ``${HOME}/Library/Caches/skore``.

            tracking_uri : str, mode:mlflow only.
                The URI of the MLflow tracking server.
        """
        plugin, parameters = Project.__setup_plugin(mode, name)

        self.__mode = mode
        self.__name = name
        self.__project = plugin(**(kwargs | parameters))

        ml_tasks = {report["ml_task"] for report in self.__project.summarize()}

        if len(ml_tasks) > 1:
            raise RuntimeError(
                "Expected every report in the Project to have the same ML task. "
                f"Got ML tasks {ml_tasks}."
            )

        self.ml_task = ml_tasks.pop() if ml_tasks else None

    @property
    def mode(self):
        """The mode of the project."""
        return self.__mode

    @property
    def name(self):
        """The name of the project."""
        return self.__name

    def put(self, key: str, report: EstimatorReport | CrossValidationReport):
        """
        Put a key-report pair to the project.

        If the key already exists, its last report is modified to point to this new
        report, while keeping track of the report history.

        Parameters
        ----------
        key : str
            The key to associate with ``report`` in the project.
            Name of the run for mode:mlflow
        report : EstimatorReport | CrossValidationReport
            The report to associate with ``key`` in the project.

        Raises
        ------
        TypeError
            If the combination of parameters are not valid.
        """
        from skore import CrossValidationReport, EstimatorReport

        if not isinstance(key, str):
            raise TypeError(f"Key must be a string (found '{type(key)}')")

        if not isinstance(report, EstimatorReport | CrossValidationReport):
            raise TypeError(
                f"Report must be `EstimatorReport` or `CrossValidationReport` "
                f"(found '{type(report)}')"
            )

        if self.ml_task is not None:
            if report.ml_task != self.ml_task:
                raise ValueError(
                    "At this time, a Project can only contain reports associated with "
                    "a single ML task. "
                    f"Project {self.name!r} expected ML task {self.ml_task!r}; "
                    f"got a report associated with ML task {report.ml_task!r}."
                )
        else:
            self.ml_task = report.ml_task

        return self.__project.put(key=key, report=report)

    def get(self, id: str) -> EstimatorReport | CrossValidationReport:
        """
        Get a persisted report by its id.

        Report IDs can be found via :meth:`skore.Project.summarize`, which is also the
        preferred method of interacting with a ``skore.Project``.

        Parameters
        ----------
        id : str
            The id of a report already put in the ``project``.

        Raises
        ------
        KeyError
            If a non-existent ID is passed.
        """
        return self.__project.get(id)

    def summarize(self) -> Summary:
        """Obtain metadata/metrics for all persisted reports."""
        return Summary.factory(self.__project)

    def __repr__(self) -> str:  # noqa: D105
        return self.__project.__repr__()

    @staticmethod
    def delete(name: str, *, mode: ProjectMode = "local", **kwargs):
        r"""
        Delete a project. Not implemented for MLFlow projects.

        Parameters
        ----------
        name : str
            The name of the project.
        mode : {"hub", "local", "mlflow"}, default "local"
            The mode of the project.
        **kwargs : dict
            Extra keyword arguments passed to the project, depending on its mode.

            workspace : Path, mode:local only.
                The directory where the local project is persisted.

                | The workspace can be shared between all the projects.
                | The workspace can be set using kwargs or the environment variable
                  ``SKORE_WORKSPACE``.
                | If not, it will be by default set to a ``skore/`` directory in the
                  USER cache directory:

                - on Windows, usually ``C:\Users\%USER%\AppData\Local\skore``,
                - on Linux, usually ``${HOME}/.cache/skore``,
                - on macOS, usually ``${HOME}/Library/Caches/skore``.

            tracking_uri : str, mode:mlflow only.
                The URI of the MLflow tracking server.
        """
        plugin, parameters = Project.__setup_plugin(mode, name)

        return plugin.delete(**(kwargs | parameters))
