# Copyright (C) 2021-2022 HERE Global B.V. and its affiliate(s).
# All rights reserved.
#
# This software and other materials contain proprietary information
# controlled by HERE and are protected by applicable copyright legislation.
# Any use and utilization of this software and other materials and
# disclosure to any third parties is conditional upon having a separate
# agreement with HERE for the access, use, utilization or disclosure of this
# software. In the absence of such agreement, the use of the software is not
# allowed.

"""HERE platform service abstraction."""

import webbrowser
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union

from here.platform.api.base_api import BaseApi
from here.platform.api.factory.apifactory import API
from here.platform.auth import Auth
from here.platform.config.application_config import ApplicationConfig
from here.platform.config.platform_config import PlatformConfig
from here.platform.credentials import PlatformCredentials
from here.platform.environment import Environment
from here.platform.exceptions import PlatformException
from here.platform.utils import JsonDictDocument

if TYPE_CHECKING:
    from here.platform.platform import Platform


class ServiceConfiguration(JsonDictDocument):
    """
    A JSON document for service configuration used in APIs loaded into a ``Dict``.

    Basic fields like name, summary, base_url, logging_url,
    and tags are provided for reading.
    """

    @property
    def version(self) -> Optional[str]:
        """Version in the service config"""
        return str(self.json["version"]) if "version" in self.json else None

    @property
    def name(self) -> Optional[str]:
        """Name in the service config"""
        return str(self.json["name"]) if "name" in self.json else None

    @property
    def summary(self) -> Optional[str]:
        """Summary in the service config"""
        return str(self.json["summary"]) if "summary" in self.json else None

    @property
    def base_url(self) -> Optional[str]:
        """Base URL in the service config"""
        return str(self.json["baseUrl"]) if "baseUrl" in self.json else None

    @property
    def logging_url(self):
        """Logging URL of the service"""
        return str(self.json["loggingUrl"]) if "loggingUrl" in self.json else None

    @property
    def tags(self) -> List[str]:
        """List of tags present in the service"""
        return [str(t) for t in self.json.get("tags", [])]


class ServiceVersion(JsonDictDocument):
    """
    A JSON document for service version used in APIs loaded into a ``Dict``.
    """

    @property
    def api_version(self) -> str:
        """API Version of the service"""
        return str(self.json["apiVersion"])

    @property
    def service_version(self) -> str:
        """Service Version of the service"""
        return str(self.json["serviceVersion"])

    @property
    def data_versions(self) -> List[Dict]:
        """List of data versions in the service"""
        return list(self.json["dataVersions"])


class ServiceHealth(JsonDictDocument):
    """
    A JSON document for service health used in APIs loaded into a ``Dict``.
    """

    @property
    def status(self) -> str:
        """Health status of the service"""
        return str(self.json["status"])


class Service:
    """
    HERE platform service abstraction.

    Access basic fields like name, summary, base_url,
    logging_url, and tags of a service. Example: ``service.name``

    More fields can be read by using service configuration
    Example: ``service.configuration``
    """

    def __init__(
        self,
        hrn: str,
        configuration: ServiceConfiguration,
        platform: "Platform",
    ):
        """
        Instantiate the :class:`Service` for the given :param:`hrn`

        :param hrn: the HERE Resource Name of the service
        :param configuration: an instance of ServiceConfiguration
        :param platform: instance of Platform
        :raises ValueError: No service config for HRN given

        For more information about proxy configuration, please see:
        https://requests.readthedocs.io/en/master/user/advanced/#proxies

        """
        self.hrn: str = hrn
        self.platform = platform
        self.version = configuration.version
        self.name = configuration.name
        self.summary = configuration.summary
        self.base_url = configuration.base_url
        self.logging_url: str = configuration.logging_url
        self.tags: list = configuration.tags
        if not configuration:
            raise ValueError(f"No service config for HRN {hrn} given")
        self.configuration: ServiceConfiguration = configuration
        self._credentials: Optional[PlatformCredentials] = platform.credentials
        self._environment: Environment = platform.environment
        self._platform_config: PlatformConfig = platform.platform_config
        self._application_config: ApplicationConfig = platform.application_config  # type: ignore
        self._proxies: dict = platform.proxies  # type: ignore
        self._auth: Auth = platform.auth  # type: ignore
        self._base_api: BaseApi = platform.base_api
        self._api_factory = self.platform.api_factory

    @property
    def health(self):
        """Current health of the service"""
        service_api = self._api_factory.get_api(API.SERVICE, service_base_url=self.base_url)
        service_health = service_api.health()
        return ServiceHealth.from_dict(service_health)

    def get_version(self):
        """Detailed version information of the service"""
        service_version_dict = self._api_factory.get_api(
            API.SERVICE, service_base_url=self.base_url
        ).version()
        return ServiceVersion.from_dict(service_version_dict)

    def open_in_portal(self):
        """Opens the service page on HERE platform portal."""
        portal_url = self._platform_config.portal_url
        if portal_url is None:
            raise RuntimeError("here_platform_portal_url is not present in configuration.")
        webbrowser.open_new("{}/services/details/{}/overview".format(portal_url, self.hrn))

    def _format_url(self, path: str) -> str:
        """Builds absolute URL of the service using its base URL and given path"""
        if not path.startswith("/"):
            raise ValueError("Malformed path for service '{}'. Must begin with '/'".format(path))
        return "{}{}".format(self.base_url, path)

    def get(
        self,
        path: str,
        params: Optional[dict] = None,
        headers: Optional[dict] = None,
        as_json: bool = True,
        success_codes: List[int] = [200],
        **kwargs,
    ) -> Any:
        """
        Perform a GET request on the service for the given :param:`path` of the service
        Example, `service.get("/path")`

        :param path: path to be called on the service.
        :param params: Parameters to pass to the API.
        :param headers: Request headers. Defaults to the Api headers property.
        :param as_json: A boolean to indicate type of response. If `True` then response
            will be returned in json format. If `False`, `requests.Response` object
            will be returned
        :param success_codes: List of http response codes which are treated as success
        :param kwargs: Optional arguments that can be passed to requests' GET call
        :return: response from the API.
        :raises PlatformException: in case of unsuccessful response
        """
        url = self._format_url(path)
        resp = self._api_factory.get_api(API.SERVICE, service_base_url=self.base_url).get(
            url, params, headers, **kwargs
        )
        if resp.status_code in success_codes:
            return resp.json() if as_json else resp
        else:
            raise PlatformException(resp)

    def head(
        self,
        path: str,
        params: Optional[dict] = None,
        headers: Optional[dict] = None,
        as_json: bool = False,
        success_codes: List[int] = [200],
        **kwargs,
    ) -> Any:
        """
        Perform a HEAD request on the service for the given :param:`path` of the service
        Example, `service.head("/path")`

        :param path: path to be called on the service.
        :param params: Parameters to pass to the API.
        :param headers: Request headers. Defaults to the api headers property.
        :param as_json: A boolean to indicate type of response. If `True` then response
            will be returned in json format.
        :param success_codes: List of http response codes which are treated as success
        :param kwargs: Optional arguments that can be passed to requests' HEAD call
        :return: response from the API.
        :raises PlatformException: in case of unsuccessful response
        """
        url = self._format_url(path)
        resp = self._api_factory.get_api(API.SERVICE, service_base_url=self.base_url).head(
            url, params, headers, **kwargs
        )
        if resp.status_code in success_codes:
            return resp.json() if as_json else resp
        else:
            raise PlatformException(resp)

    def post(
        self,
        path: str,
        data: Optional[Union[dict, List, bytes, str]] = None,
        params: Optional[dict] = None,
        headers: Optional[dict] = None,
        as_json: bool = True,
        success_codes: List[int] = [200, 201],
        **kwargs,
    ) -> Any:
        """
        Perform a POST request on the service for the given :param:`path` of the service
        Example, `service.post("/path", params={...})`

        :param path: path to be called on the service.
        :param data: Dictionary, list of tuples, bytes, or file-like
            object to send in the body of the http request.
        :param params: Parameters to pass to the API.
        :param headers: Request headers. Defaults to the api headers property.
            Overrides the default headers one by one leaving the ones that are not overridden
        :param as_json: A boolean to indicate type of response. If `True` then response
            will be returned in json format.
        :param success_codes: List of http response codes which are treated as success
        :param kwargs: Optional arguments that can be passed to requests' POST call
        :return: response from the API.
        :raises PlatformException: in case of unsuccessful response
        """
        url = self._format_url(path)
        resp = self._api_factory.get_api(API.SERVICE, service_base_url=self.base_url).post(
            url, data, params, headers, **kwargs
        )
        if resp.status_code in success_codes:
            return resp.json() if as_json else resp
        else:
            raise PlatformException(resp)

    def put(
        self,
        path: str,
        data: Optional[dict] = None,
        params: Optional[dict] = None,
        headers: Optional[dict] = None,
        as_json: bool = True,
        success_codes: List[int] = [200],
        **kwargs,
    ) -> Any:
        """
        Perform a PUT request on the service for the given :param:`path` of the service
        Example, `service.put("/path", params={...})`

        :param path: path to be called on the service.
        :param data: Put data for http request.
        :param params: Parameters to pass to the API.
        :param headers: Request headers. Defaults to the api headers property.
        :param as_json: A boolean to indicate type of response. If `True` then response
            will be returned in json format.
        :param success_codes: List of http response codes which are treated as success
        :param kwargs: Optional arguments that can be passed to requests' PUT call
        :return: response from the API.
        :raises PlatformException: in case of unsuccessful response
        """
        url = self._format_url(path)
        resp = self._api_factory.get_api(API.SERVICE, service_base_url=self.base_url).put(
            url, data, params, headers, **kwargs
        )
        if resp.status_code in success_codes:
            return resp.json() if as_json else resp
        else:
            raise PlatformException(resp)

    def patch(
        self,
        path: str,
        data: Optional[dict] = None,
        params: Optional[dict] = None,
        headers: Optional[dict] = None,
        as_json: bool = True,
        success_codes: List[int] = [200],
        **kwargs,
    ) -> Any:
        """
        Perform a PATCH request on the service for the given :param:`path` of the service
        Example, `service.patch("/path", params={...})`

        :param path: path to be called on the service.
        :param data: Patch data for http request.
        :param params: Parameters to pass to the API.
        :param headers: Request headers. Defaults to the api headers property.
        :param as_json: A boolean to indicate type of response. If `True` then response
            will be returned in json format.
        :param success_codes: List of http response codes which are treated as success
        :param kwargs: Optional arguments that can be passed to requests' PATCH call
        :return: response from the API.
        :raises PlatformException: in case of unsuccessful response
        """
        url = self._format_url(path)
        resp = self._api_factory.get_api(API.SERVICE, service_base_url=self.base_url).patch(
            url, data, params, headers, **kwargs
        )
        if resp.status_code in success_codes:
            return resp.json() if as_json else resp
        else:
            raise PlatformException(resp)

    def delete(
        self,
        path: str,
        params: Optional[Dict] = None,
        headers: Optional[dict] = None,
        as_json: bool = True,
        success_codes: List[int] = [200],
        **kwargs,
    ) -> Any:
        """
        Perform a DELETE request on the service for the given :param:`path` of the service
        Example, `service.delete("/path", params={...})`

        :param path: path to be called on the service.
        :param params: parameters to pass to the API.
        :param headers: Request headers. Defaults to the api headers property.
        :param as_json: A boolean to indicate type of response. If `True` then response
            will be returned in json format.
        :param success_codes: List of http response codes which are treated as success
        :param kwargs: Optional arguments that can be passed to requests' DELETE call
        :raises PlatformException: in case of unsuccessful response
        :return: response from the API.
        """
        url = self._format_url(path)
        resp = self._api_factory.get_api(API.SERVICE, service_base_url=self.base_url).delete(
            url, params, headers, **kwargs
        )
        if resp.status_code in success_codes:
            return resp.json() if as_json else resp
        else:
            raise PlatformException(resp)

    def __str__(self) -> str:
        """
        String representation of service
        """
        return 'Service "HRN" {} "Name" {} "Version" {} "Base URL" {}'.format(
            self.hrn, self.name, self.version, self.base_url
        )
