import json
from json import JSONDecodeError

from pygeai.core.base.clients import BaseClient
from pygeai.lab.tools.endpoints import CREATE_TOOL_V2, LIST_TOOLS_V2, GET_TOOL_V2, UPDATE_TOOL_V2, UPSERT_TOOL_V2, \
    PUBLISH_TOOL_REVISION_V2, GET_PARAMETER_V2, SET_PARAMETER_V2, DELETE_TOOL_V2


class ToolClient(BaseClient):

    def create_tool(
            self,
            project_id: str,
            name: str,
            description: str,
            scope: str,
            parameters: list,
            open_api_json: dict = None,
            automatic_publish: bool = False,
    ) -> dict:
        """
        Creates a new tool with the specified parameters.

        :param project_id: str - Unique identifier of the project.
        :param name: str - Name of the tool.
        :param description: str - Description of the tool.
        :param scope: str - Scope of the tool (e.g., "builtin").
        :param parameters: list - List of parameter dictionaries defining inputs and configurations for the tool.
        :param open_api_json: dict - OpenAPI JSON specification for the tool, required for tools with scope 'api'.
            This dictionary is serialized to a JSON string before being sent in the request payload.
            If None, the existing OpenAPI specification (if any) is not updated.
        :param automatic_publish: bool - Whether to automatically publish the tool after updating. Defaults to False.
        :return: dict - JSON response containing the created tool details if successful, otherwise the raw response text.
        """
        data = {
            "tool": {
                "name": name,
                "description": description,
                "scope": scope,
                "parameters": parameters
            }
        }

        if open_api_json:
            open_api_str = json.dumps(open_api_json, indent=2)
            data["openApiJson"] = open_api_str.replace('"', r'\"')

        endpoint = CREATE_TOOL_V2
        if automatic_publish:
            endpoint = f"{endpoint}?automaticPublish=true"

        headers = {
            "Accept": "application/json",
            "Content-Type": "application/json",
            "Authorization": self.api_service.token,
            "ProjectId": project_id
        }

        response = self.api_service.post(
            endpoint=endpoint,
            headers=headers,
            data=data
        )

        try:
            result = response.json()
        except JSONDecodeError as e:
            result = response.text

        return result

    def list_tools(
            self,
            project_id: str,
            id: str = "",
            count: str = "100",
            access_scope: str = "public",
            allow_drafts: bool = True,
            scope: str = "api",
            allow_external: bool = True
    ) -> dict | str:
        """
        Retrieves a list of tools associated with the specified project.

        :param project_id: str - Unique identifier of the project.
        :param id: str - ID of the tool to filter by. Defaults to "" (no filtering).
        :param count: str - Number of tools to retrieve. Defaults to "100".
        :param access_scope: str - Access scope of the tools, either "public" or "private". Defaults to "public".
        :param allow_drafts: bool - Whether to include draft tools. Defaults to True.
        :param scope: str - Scope of the tools, must be "builtin", "external", or "api". Defaults to "api".
        :param allow_external: bool - Whether to include external tools. Defaults to True.
        :return: dict or str - JSON response containing the list of tools if successful, otherwise the raw response text.
        """
        endpoint = LIST_TOOLS_V2
        headers = {
            "Authorization": self.api_service.token,
            "ProjectId": project_id
        }

        valid_scopes = ["builtin", "external", "api"]
        if scope not in valid_scopes:
            raise ValueError("Scope must be one of 'builtin', 'external', or 'api'.")

        response = self.api_service.get(
            endpoint=endpoint,
            headers=headers,
            params={
                "id": id,
                "count": count,
                "accessScope": access_scope,
                "allowDrafts": allow_drafts,
                "scope": scope,
                "allowExternal": allow_external
            }
        )
        try:
            result = response.json()
        except JSONDecodeError as e:
            result = response.text

        return result

    def get_tool(
            self,
            project_id: str,
            tool_id: str,
            revision: str = 0,
            version: int = 0,
            allow_drafts: bool = True
    ):
        """
        Retrieves details of a specific tool from the specified project.

        :param project_id: str - Unique identifier of the project.
        :param tool_id: str - Unique identifier of the tool to retrieve.
        :param revision: str - Revision of the tool to retrieve. Defaults to 0 (latest revision).
        :param version: int - Version of the tool to retrieve. Defaults to 0 (latest version).
        :param allow_drafts: bool - Whether to include draft tool in the retrieval. Defaults to True.
        :return: dict or str - JSON response containing the tool details if successful, otherwise the raw response text.
        """
        endpoint = GET_TOOL_V2.format(toolId=tool_id)
        headers = {
            "Authorization": self.api_service.token,
            "ProjectId": project_id
        }

        response = self.api_service.get(
            endpoint=endpoint,
            headers=headers,
            params={
                "revision": revision,
                "version": version,
                "allowDrafts": allow_drafts,
            }
        )
        try:
            result = response.json()
        except JSONDecodeError as e:
            result = response.text

        return result

    def delete_tool(
            self,
            project_id: str,
            tool_id: str = None,
            tool_name: str = None
    ) -> dict | str:
        """
        Deletes a specific tool from the specified project.

        :param project_id: str - Unique identifier of the project.
        :param tool_id: str, optional - Unique identifier of the tool to delete. Defaults to None.
        :param tool_name: str, optional - Name of the tool to delete. Defaults to None.
        :return: dict or str - JSON response containing the result of the delete operation if successful,
            otherwise the raw response text.
        :raises ValueError: If neither tool_id nor tool_name is provided.
        """
        if not (tool_id or tool_name):
            raise ValueError("Either tool_id or tool_name must be provided.")

        endpoint = DELETE_TOOL_V2.format(toolId=tool_id if tool_id else tool_name)
        headers = {
            "Authorization": self.api_service.token,
            "ProjectId": project_id
        }

        response = self.api_service.delete(
            endpoint=endpoint,
            headers=headers
        )

        try:
            result = response.json()
        except JSONDecodeError as e:
            result = response.text

        return result

    def update_tool(
            self,
            project_id: str,
            tool_id: str,
            open_api_json: dict,
            name: str,
            description: str,
            scope: str,
            parameters: list,
            automatic_publish: bool = False,
            upsert: bool = False
    ) -> dict | str:
        """
        Updates an existing tool in the specified project or upserts it if specified.

        :param project_id: str - Unique identifier of the project.
        :param tool_id: str - Unique identifier of the tool to update.
        :param open_api_json: dict - OpenAPI JSON specification for the tool, required for tools with scope 'api'.
            This dictionary is serialized to a JSON string before being sent in the request payload.
            If None, the existing OpenAPI specification (if any) is not updated.
        :param name: str - Updated name of the tool.
        :param description: str - Updated description of the tool.
        :param scope: str - Updated scope of the tool, must be "builtin", "external", or "api".
        :param parameters: list - Updated list of parameter dictionaries defining inputs and configurations for the tool.
        :param automatic_publish: bool - Whether to automatically publish the tool after updating. Defaults to False.
        :param upsert: bool - Whether to insert the tool if it does not exist (upsert) instead of just updating. Defaults to False.
        :return: dict or str - JSON response containing the updated tool details if successful, otherwise the raw response text.

        :raises ValueError:
            If `scope` is not one of "builtin", "external", or "api".
        :raises JSONDecodeError:
            Caught internally if the response cannot be parsed as JSON; in this case, the raw response text is returned.
        :raises Exception:
            May be raised by `self.api_service.put` if the request fails due to network issues, authentication errors,
            or server-side problems (not explicitly caught here; depends on `api_service` implementation).
        """
        valid_scopes = ["builtin", "external", "api"]
        if scope not in valid_scopes:
            raise ValueError("Scope must be one of 'builtin', 'external', or 'api'.")

        data = {
            "tool": {
                "name": name,
                "description": description,
                "scope": scope,
                "parameters": parameters
            }
        }
        if open_api_json:
            open_api_str = json.dumps(open_api_json, indent=2)
            data["openApiJson"] = open_api_str.replace('"', r'\"')

        endpoint = UPSERT_TOOL_V2 if upsert else UPDATE_TOOL_V2
        endpoint = endpoint.format(toolId=tool_id) if tool_id else endpoint.format(toolId=name)

        if automatic_publish:
            endpoint = f"{endpoint}?automaticPublish=true"

        headers = {
            "Authorization": self.api_service.token,
            "ProjectId": project_id
        }
        response = self.api_service.put(
            endpoint=endpoint,
            headers=headers,
            data=data
        )

        try:
            result = response.json()
        except JSONDecodeError as e:
            result = response.text

        return result

    def publish_tool_revision(
            self,
            project_id: str,
            tool_id: str,
            revision: str
    ):
        """
        Publishes a specific revision of a tool in the specified project.

        :param project_id: str - Unique identifier of the project.
        :param tool_id: str - Unique identifier of the tool to publish.
        :param revision: str - Revision of the tool to publish.
        :return: dict or str - JSON response containing the result of the publish operation if successful, otherwise the raw response text.
        """
        endpoint = PUBLISH_TOOL_REVISION_V2.format(toolId=tool_id)
        headers = {
            "Authorization": self.api_service.token,
            "ProjectId": project_id
        }

        response = self.api_service.post(
            endpoint=endpoint,
            headers=headers,
            data={
                "revision": revision,
            }
        )
        try:
            result = response.json()
        except JSONDecodeError as e:
            result = response.text

        return result

    def get_parameter(
            self,
            project_id: str,
            tool_id: str = None,
            tool_public_name: str = None,
            revision: str = 0,
            version: int = 0,
            allow_drafts: bool = True
    ):
        """
        Retrieves details of parameters for a specific tool identified by either its ID or public name in the specified project.

        :param project_id: str - Unique identifier of the project.
        :param tool_id: str, optional - Unique identifier of the tool whose parameters are to be retrieved. Defaults to None.
        :param tool_public_name: str, optional - Public name of the tool whose parameters are to be retrieved. Defaults to None.
        :param revision: str - Revision of the parameters to retrieve. Defaults to "0" (latest revision).
        :param version: int - Version of the parameters to retrieve. Defaults to 0 (latest version).
        :param allow_drafts: bool - Whether to include draft parameters in the retrieval. Defaults to True.
        :return: dict or str - JSON response containing the parameter details if successful, otherwise the raw response text.
        :raises ValueError: If neither tool_id nor tool_public_name is provided.
        """
        if not (tool_id or tool_public_name):
            raise ValueError("Either tool_id or tool_public_name must be provided.")

        endpoint = GET_PARAMETER_V2.format(toolPublicName=tool_public_name) if tool_public_name else GET_PARAMETER_V2.format(toolPublicName=tool_id)
        headers = {
            "Authorization": self.api_service.token,
            "ProjectId": project_id
        }

        response = self.api_service.get(
            endpoint=endpoint,
            headers=headers,
            params={
                "revision": revision,
                "version": version,
                "allowDrafts": allow_drafts,
            }
        )
        try:
            result = response.json()
        except JSONDecodeError as e:
            result = response.text

        return result

    def set_parameter(
            self,
            project_id: str,
            tool_id: str = None,
            tool_public_name: str = None,
            parameters: list = None
    ):
        """
        Sets or updates parameters for a specific tool identified by either its ID or public name in the specified project.

        :param project_id: str - Unique identifier of the project.
        :param tool_id: str, optional - Unique identifier of the tool whose parameters are to be set. Defaults to None.
        :param tool_public_name: str, optional - Public name of the tool whose parameters are to be set. Defaults to None.
        :param parameters: list - List of parameter dictionaries defining the tool's parameters.
                                 Each dictionary must contain 'key', 'dataType', 'description', and 'isRequired'.
                                 For config parameters, include 'type', 'fromSecret', and 'value'. Defaults to None.
        :return: dict or str - JSON response containing the result of the set operation if successful, otherwise the raw response text.
        :raises ValueError: If neither tool_id nor tool_public_name is provided, or if parameters is None or empty.
        """
        if not (tool_id or tool_public_name):
            raise ValueError("Either tool_id or tool_public_name must be provided.")
        if not parameters:
            raise ValueError("Parameters list must be provided and non-empty.")

        endpoint = SET_PARAMETER_V2.format(toolPublicName=tool_public_name) if tool_public_name else SET_PARAMETER_V2.format(toolPublicName=tool_id)
        headers = {
            "Authorization": self.api_service.token,
            "ProjectId": project_id,
            "Content-Type": "application/json",
            "Accept": "application/json"
        }

        data = {
            "parameterDefinition": {
                "parameters": parameters
            }
        }

        response = self.api_service.post(
            endpoint=endpoint,
            headers=headers,
            data=data
        )
        try:
            result = response.json()
        except JSONDecodeError as e:
            result = response.text

        return result
