import json
import logging
import typing
from pathlib import Path

from pydantic import SecretField

from autogpt.core.configuration import (
    Configurable,
    SystemConfiguration,
    SystemSettings,
    UserConfigurable,
)
from autogpt.core.workspace.base import Workspace

if typing.TYPE_CHECKING:
    # Cyclic import
    from autogpt.core.agent.simple import AgentSettings


class WorkspaceConfiguration(SystemConfiguration):
    root: str
    parent: str = UserConfigurable()
    restrict_to_workspace: bool = UserConfigurable()


class WorkspaceSettings(SystemSettings):
    configuration: WorkspaceConfiguration


class SimpleWorkspace(Configurable, Workspace):
    default_settings = WorkspaceSettings(
        name="workspace",
        description="The workspace is the root directory for all agent activity.",
        configuration=WorkspaceConfiguration(
            root="",
            parent="~/auto-gpt/agents",
            restrict_to_workspace=True,
        ),
    )

    NULL_BYTES = ["\0", "\000", "\x00", "\u0000", "%00"]

    def __init__(
        self,
        settings: WorkspaceSettings,
        logger: logging.Logger,
    ):
        self._configuration = settings.configuration
        self._logger = logger.getChild("workspace")

    @property
    def root(self) -> Path:
        return Path(self._configuration.root)

    @property
    def debug_log_path(self) -> Path:
        return self.root / "logs" / "debug.log"

    @property
    def cycle_log_path(self) -> Path:
        return self.root / "logs" / "cycle.log"

    @property
    def configuration_path(self) -> Path:
        return self.root / "configuration.yml"

    @property
    def restrict_to_workspace(self) -> bool:
        return self._configuration.restrict_to_workspace

    def get_path(self, relative_path: str | Path) -> Path:
        """Get the full path for an item in the workspace.

        Parameters
        ----------
        relative_path
            The relative path to resolve in the workspace.

        Returns
        -------
        Path
            The resolved path relative to the workspace.

        """
        return self._sanitize_path(
            relative_path,
            root=self.root,
            restrict_to_root=self.restrict_to_workspace,
        )

    def _sanitize_path(
        self,
        relative_path: str | Path,
        root: str | Path = None,
        restrict_to_root: bool = True,
    ) -> Path:
        """Resolve the relative path within the given root if possible.

        Parameters
        ----------
        relative_path
            The relative path to resolve.
        root
            The root path to resolve the relative path within.
        restrict_to_root
            Whether to restrict the path to the root.

        Returns
        -------
        Path
            The resolved path.

        Raises
        ------
        ValueError
            If the path is absolute and a root is provided.
        ValueError
            If the path is outside the root and the root is restricted.

        """

        # Posix systems disallow null bytes in paths. Windows is agnostic about it.
        # Do an explicit check here for all sorts of null byte representations.

        for null_byte in self.NULL_BYTES:
            if null_byte in str(relative_path) or null_byte in str(root):
                raise ValueError("embedded null byte")

        if root is None:
            return Path(relative_path).resolve()

        self._logger.debug(f"Resolving path '{relative_path}' in workspace '{root}'")
        root, relative_path = Path(root).resolve(), Path(relative_path)
        self._logger.debug(f"Resolved root as '{root}'")

        if relative_path.is_absolute():
            raise ValueError(
                f"Attempted to access absolute path '{relative_path}' in workspace '{root}'."
            )
        full_path = root.joinpath(relative_path).resolve()

        self._logger.debug(f"Joined paths as '{full_path}'")

        if restrict_to_root and not full_path.is_relative_to(root):
            raise ValueError(
                f"Attempted to access path '{full_path}' outside of workspace '{root}'."
            )

        return full_path

    ###################################
    # Factory methods for agent setup #
    ###################################

    @staticmethod
    def setup_workspace(settings: "AgentSettings", logger: logging.Logger) -> Path:
        workspace_parent = settings.workspace.configuration.parent
        workspace_parent = Path(workspace_parent).expanduser().resolve()
        workspace_parent.mkdir(parents=True, exist_ok=True)

        agent_name = settings.agent.name

        workspace_root = workspace_parent / agent_name
        workspace_root.mkdir(parents=True, exist_ok=True)

        settings.workspace.configuration.root = str(workspace_root)

        with (workspace_root / "agent_settings.json").open("w") as f:
            settings_json = settings.json(
                encoder=lambda x: x.get_secret_value()
                if isinstance(x, SecretField)
                else x,
            )
            f.write(settings_json)

        # TODO: What are all the kinds of logs we want here?
        log_path = workspace_root / "logs"
        log_path.mkdir(parents=True, exist_ok=True)
        (log_path / "debug.log").touch()
        (log_path / "cycle.log").touch()

        return workspace_root

    @staticmethod
    def load_agent_settings(workspace_root: Path) -> "AgentSettings":
        # Cyclic import
        from autogpt.core.agent.simple import AgentSettings

        with (workspace_root / "agent_settings.json").open("r") as f:
            agent_settings = json.load(f)

        return AgentSettings.parse_obj(agent_settings)
