# Copyright 2016 Google Inc. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Define API Loggers."""

import json

from google.protobuf.json_format import MessageToJson


class Logger(object):
    """Loggers represent named targets for log entries.

    See:
    https://cloud.google.com/logging/docs/api/ref_v2beta1/rest/v2beta1/projects.logs

    :type name: string
    :param name: the name of the logger

    :type client: :class:`gcloud.logging.client.Client`
    :param client: A client which holds credentials and project configuration
                   for the logger (which requires a project).

    :type labels: dict or :class:`NoneType`
    :param labels: (optional) mapping of default labels for entries written
                   via this logger.
    """
    def __init__(self, name, client, labels=None):
        self.name = name
        self._client = client
        self.labels = labels

    @property
    def client(self):
        """Clent bound to the logger."""
        return self._client

    @property
    def project(self):
        """Project bound to the logger."""
        return self._client.project

    @property
    def full_name(self):
        """Fully-qualified name used in logging APIs"""
        return 'projects/%s/logs/%s' % (self.project, self.name)

    @property
    def path(self):
        """URI path for use in logging APIs"""
        return '/%s' % (self.full_name,)

    def _require_client(self, client):
        """Check client or verify over-ride.

        :type client: :class:`gcloud.logging.client.Client` or ``NoneType``
        :param client: the client to use.  If not passed, falls back to the
                       ``client`` stored on the current logger.

        :rtype: :class:`gcloud.logging.client.Client`
        :returns: The client passed in or the currently bound client.
        """
        if client is None:
            client = self._client
        return client

    def batch(self, client=None):
        """Return a batch to use as a context manager.

        :type client: :class:`gcloud.logging.client.Client` or ``NoneType``
        :param client: the client to use.  If not passed, falls back to the
                       ``client`` stored on the current topic.

        :rtype: :class:`Batch`
        :returns: A batch to use as a context manager.
        """
        client = self._require_client(client)
        return Batch(self, client)

    def _make_entry_resource(self, text=None, info=None, message=None,
                             labels=None, insert_id=None, severity=None,
                             http_request=None):
        """Return a log entry resource of the appropriate type.

        Helper for :meth:`log_text`, :meth:`log_struct`, and :meth:`log_proto`.

        Only one of ``text``, ``info``, or ``message`` should be passed.

        :type text: string or :class:`NoneType`
        :param text: text payload

        :type info: dict or :class:`NoneType`
        :param info: struct payload

        :type message: Protobuf message or :class:`NoneType`
        :param message: protobuf payload

        :type labels: dict or :class:`NoneType`
        :param labels: labels passed in to calling method.

        :type insert_id: string or :class:`NoneType`
        :param insert_id: (optional) unique ID for log entry.

        :type severity: string or :class:`NoneType`
        :param severity: (optional) severity of event being logged.

        :type http_request: dict or :class:`NoneType`
        :param http_request: (optional) info about HTTP request associated with
                             the entry
        """
        resource = {
            'logName': self.full_name,
            'resource': {'type': 'global'},
        }

        if text is not None:
            resource['textPayload'] = text

        if info is not None:
            resource['jsonPayload'] = info

        if message is not None:
            as_json_str = MessageToJson(message)
            as_json = json.loads(as_json_str)
            resource['protoPayload'] = as_json

        if labels is None:
            labels = self.labels

        if labels is not None:
            resource['labels'] = labels

        if insert_id is not None:
            resource['insertId'] = insert_id

        if severity is not None:
            resource['severity'] = severity

        if http_request is not None:
            resource['httpRequest'] = http_request

        return resource

    def log_text(self, text, client=None, labels=None, insert_id=None,
                 severity=None, http_request=None):
        """API call:  log a text message via a POST request

        See:
        https://cloud.google.com/logging/docs/api/ref_v2beta1/rest/v2beta1/entries/write

        :type text: text
        :param text: the log message.

        :type client: :class:`gcloud.logging.client.Client` or ``NoneType``
        :param client: the client to use.  If not passed, falls back to the
                       ``client`` stored on the current logger.

        :type labels: dict or :class:`NoneType`
        :param labels: (optional) mapping of labels for the entry.

        :type insert_id: string or :class:`NoneType`
        :param insert_id: (optional) unique ID for log entry.

        :type severity: string or :class:`NoneType`
        :param severity: (optional) severity of event being logged.

        :type http_request: dict or :class:`NoneType`
        :param http_request: (optional) info about HTTP request associated with
                             the entry
        """
        client = self._require_client(client)
        entry_resource = self._make_entry_resource(
            text=text, labels=labels, insert_id=insert_id, severity=severity,
            http_request=http_request)
        data = {'entries': [entry_resource]}

        client.connection.api_request(
            method='POST', path='/entries:write', data=data)

    def log_struct(self, info, client=None, labels=None, insert_id=None,
                   severity=None, http_request=None):
        """API call:  log a structured message via a POST request

        See:
        https://cloud.google.com/logging/docs/api/ref_v2beta1/rest/v2beta1/entries/write

        :type info: dict
        :param info: the log entry information

        :type client: :class:`gcloud.logging.client.Client` or ``NoneType``
        :param client: the client to use.  If not passed, falls back to the
                       ``client`` stored on the current logger.

        :type labels: dict or :class:`NoneType`
        :param labels: (optional) mapping of labels for the entry.

        :type insert_id: string or :class:`NoneType`
        :param insert_id: (optional) unique ID for log entry.

        :type severity: string or :class:`NoneType`
        :param severity: (optional) severity of event being logged.

        :type http_request: dict or :class:`NoneType`
        :param http_request: (optional) info about HTTP request associated with
                             the entry
        """
        client = self._require_client(client)
        entry_resource = self._make_entry_resource(
            info=info, labels=labels, insert_id=insert_id, severity=severity,
            http_request=http_request)
        data = {'entries': [entry_resource]}

        client.connection.api_request(
            method='POST', path='/entries:write', data=data)

    def log_proto(self, message, client=None, labels=None, insert_id=None,
                  severity=None, http_request=None):
        """API call:  log a protobuf message via a POST request

        See:
        https://cloud.google.com/logging/docs/api/ref_v2beta1/rest/v2beta1/entries/write

        :type message: Protobuf message
        :param message: the message to be logged

        :type client: :class:`gcloud.logging.client.Client` or ``NoneType``
        :param client: the client to use.  If not passed, falls back to the
                       ``client`` stored on the current logger.

        :type labels: dict or :class:`NoneType`
        :param labels: (optional) mapping of labels for the entry.

        :type insert_id: string or :class:`NoneType`
        :param insert_id: (optional) unique ID for log entry.

        :type severity: string or :class:`NoneType`
        :param severity: (optional) severity of event being logged.

        :type http_request: dict or :class:`NoneType`
        :param http_request: (optional) info about HTTP request associated with
                             the entry
        """
        client = self._require_client(client)
        entry_resource = self._make_entry_resource(
            message=message, labels=labels, insert_id=insert_id,
            severity=severity, http_request=http_request)
        data = {'entries': [entry_resource]}

        client.connection.api_request(
            method='POST', path='/entries:write', data=data)

    def delete(self, client=None):
        """API call:  delete all entries in a logger via a DELETE request

        See:
        https://cloud.google.com/logging/docs/api/ref_v2beta1/rest/v2beta1/projects.logs/delete

        :type client: :class:`gcloud.logging.client.Client` or ``NoneType``
        :param client: the client to use.  If not passed, falls back to the
                       ``client`` stored on the current logger.
        """
        client = self._require_client(client)
        client.connection.api_request(method='DELETE', path=self.path)

    def list_entries(self, projects=None, filter_=None, order_by=None,
                     page_size=None, page_token=None):
        """Return a page of log entries.

        See:
        https://cloud.google.com/logging/docs/api/ref_v2beta1/rest/v2beta1/entries/list

        :type projects: list of strings
        :param projects: project IDs to include. If not passed,
                            defaults to the project bound to the client.

        :type filter_: string
        :param filter_: a filter expression. See:
                        https://cloud.google.com/logging/docs/view/advanced_filters

        :type order_by: string
        :param order_by: One of :data:`gcloud.logging.ASCENDING` or
                         :data:`gcloud.logging.DESCENDING`.

        :type page_size: int
        :param page_size: maximum number of entries to return, If not passed,
                          defaults to a value set by the API.

        :type page_token: string
        :param page_token: opaque marker for the next "page" of entries. If not
                           passed, the API will return the first page of
                           entries.

        :rtype: tuple, (list, str)
        :returns: list of :class:`gcloud.logging.entry.TextEntry`, plus a
                  "next page token" string:  if not None, indicates that
                  more entries can be retrieved with another call (pass that
                  value as ``page_token``).
        """
        log_filter = 'logName:%s' % (self.name,)
        if filter_ is not None:
            filter_ = '%s AND %s' % (filter_, log_filter)
        else:
            filter_ = log_filter
        return self.client.list_entries(
            projects=projects, filter_=filter_, order_by=order_by,
            page_size=page_size, page_token=page_token)


class Batch(object):
    """Context manager:  collect entries to log via a single API call.

    Helper returned by :meth:`Logger.batch`

    :type logger: :class:`gcloud.logging.logger.Logger`
    :param logger: the logger to which entries will be logged.

    :type client: :class:`gcloud.logging.client.Client`
    :param client: The client to use.
    """
    def __init__(self, logger, client):
        self.logger = logger
        self.entries = []
        self.client = client

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        if exc_type is None:
            self.commit()

    def log_text(self, text, labels=None, insert_id=None, severity=None,
                 http_request=None):
        """Add a text entry to be logged during :meth:`commit`.

        :type text: string
        :param text: the text entry

        :type labels: dict or :class:`NoneType`
        :param labels: (optional) mapping of labels for the entry.

        :type insert_id: string or :class:`NoneType`
        :param insert_id: (optional) unique ID for log entry.

        :type severity: string or :class:`NoneType`
        :param severity: (optional) severity of event being logged.

        :type http_request: dict or :class:`NoneType`
        :param http_request: (optional) info about HTTP request associated with
                             the entry.
        """
        self.entries.append(
            ('text', text, labels, insert_id, severity, http_request))

    def log_struct(self, info, labels=None, insert_id=None, severity=None,
                   http_request=None):
        """Add a struct entry to be logged during :meth:`commit`.

        :type info: dict
        :param info: the struct entry

        :type labels: dict or :class:`NoneType`
        :param labels: (optional) mapping of labels for the entry.

        :type insert_id: string or :class:`NoneType`
        :param insert_id: (optional) unique ID for log entry.

        :type severity: string or :class:`NoneType`
        :param severity: (optional) severity of event being logged.

        :type http_request: dict or :class:`NoneType`
        :param http_request: (optional) info about HTTP request associated with
                             the entry.
        """
        self.entries.append(
            ('struct', info, labels, insert_id, severity, http_request))

    def log_proto(self, message, labels=None, insert_id=None, severity=None,
                  http_request=None):
        """Add a protobuf entry to be logged during :meth:`commit`.

        :type message: protobuf message
        :param message: the protobuf entry

        :type labels: dict or :class:`NoneType`
        :param labels: (optional) mapping of labels for the entry.

        :type insert_id: string or :class:`NoneType`
        :param insert_id: (optional) unique ID for log entry.

        :type severity: string or :class:`NoneType`
        :param severity: (optional) severity of event being logged.

        :type http_request: dict or :class:`NoneType`
        :param http_request: (optional) info about HTTP request associated with
                             the entry.
        """
        self.entries.append(
            ('proto', message, labels, insert_id, severity, http_request))

    def commit(self, client=None):
        """Send saved log entries as a single API call.

        :type client: :class:`gcloud.logging.client.Client` or ``NoneType``
        :param client: the client to use.  If not passed, falls back to the
                       ``client`` stored on the current batch.
        """
        if client is None:
            client = self.client

        data = {
            'logName': self.logger.path,
            'resource': {'type': 'global'},
        }
        if self.logger.labels is not None:
            data['labels'] = self.logger.labels

        entries = data['entries'] = []
        for entry_type, entry, labels, iid, severity, http_req in self.entries:
            if entry_type == 'text':
                info = {'textPayload': entry}
            elif entry_type == 'struct':
                info = {'jsonPayload': entry}
            elif entry_type == 'proto':
                as_json_str = MessageToJson(entry)
                as_json = json.loads(as_json_str)
                info = {'protoPayload': as_json}
            else:
                raise ValueError('Unknown entry type: %s' % (entry_type,))
            if labels is not None:
                info['labels'] = labels
            if iid is not None:
                info['insertId'] = iid
            if severity is not None:
                info['severity'] = severity
            if http_req is not None:
                info['httpRequest'] = http_req
            entries.append(info)

        client.connection.api_request(
            method='POST', path='/entries:write', data=data)
        del self.entries[:]
