#!/usr/bin/env python
# Copyright 2014 Scalyr Inc.
#
# 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.
# ------------------------------------------------------------------------
#
# Contains the data structures used to represent a snapshot of the
# agent's status, giving such details as the number of log bytes copied,
# whether or not the configuration file was successfully parsed, etc.
#
# These data structures are generated by the ScalyrAgent's __generate_status
# method and are used to create both the interactive status (which is emitted when
# the 'status -v' command is invoked by the user) and the status periodically recorded
# in the agent log.
#
# author: Steven Czerwinski <czerwin@scalyr.com>

from __future__ import unicode_literals
from __future__ import absolute_import
from __future__ import print_function

__author__ = "czerwin@scalyr.com"

import os
import copy
import io

if False:
    from typing import TextIO
    from typing import List
    from typing import Optional
    from typing import Generator

import scalyr_agent.util as scalyr_util
from scalyr_agent import compat

import six

from six.moves.urllib.parse import quote_plus


class BaseAgentStatus(object):
    """
    Base agent status class which implements "to_dict()" method.
    """

    def to_dict(self):
        # type: () -> dict
        """
        Return dictionary version of the status object. This dictionary contains only simple /
        native values and is JSON serializable.
        """
        result = copy.deepcopy(self.__dict__)

        # Recursively convert nested objects to dicts
        for key, value in result.items():
            if isinstance(value, (list, tuple)):
                items = []
                for item in value:
                    if hasattr(item, "to_dict"):
                        item = item.to_dict()
                    items.append(item)
                result[key] = items
            elif isinstance(value, dict):
                for item_key, item_value in value.items():
                    if hasattr(item_value, "to_dict"):
                        item_value = item_value.to_dict()
                    result[key][item_key] = item_value
            elif hasattr(value, "to_dict"):
                result[key] = value.to_dict()

            # Ensure each value is text / unicode type since orjson doesn't seem to like bytes
            if isinstance(value, six.binary_type):
                value = six.ensure_text(value)
                result[key] = value

        return result


class AgentStatus(BaseAgentStatus):
    """The main status container object, holding references to all other status elements."""

    def __init__(self):
        # The time (in seconds past epoch) when the agent process was launched.
        self.launch_time = None
        # The user name the agent process is running under.
        self.user = None
        # The version string for the agent.
        self.version = None
        # Git revision agent package is based on
        self.revision = None
        # The name of the host the agent is running on.
        self.server_host = None
        # The URL of the scalyr server that the agent is connected to (such as https://www.scalyr.com/).
        self.scalyr_server = None
        # Compression algorithm used by the agent
        self.compression_type = None
        # Compression level used by the agent
        self.compression_level = None
        # The path for the agent's log file.
        self.log_path = None
        # The ConfigStatus object recording the status for the configuration file.
        self.config_status = None
        # The CopyingManagerStatus object recording the status of the log copying manager (or none if CopyingManager
        # has not been started). This contains information about the different log paths being watched and the
        # progress of copying their bytes.
        self.copying_manager_status = None  # type: Optional[CopyingManagerStatus]
        # The MonitorManagerStatus object recording the status of the monitor manager (or none if the MonitorManager
        # has not been started).  This contains information about the different ScalyrMonitors being run.
        self.monitor_manager_status = None
        # version of the running python interpreter.
        self.python_version = None
        # average time in seconds spent on agent's status report.
        self.avg_status_report_duration = None


class GCStatus(BaseAgentStatus):
    """
    Class which holds garbage collection statistics.

    Those stats are disabled by default because they have an impact on memory usage so they must
    be explicitly enabled.
    """

    def __init__(self):
        # This metric indicates number of objects which are unreachable but can't be freed.
        self.garbage = 0

    def to_dict(self):
        return self.__dict__


class OverallStats(AgentStatus):
    """Used to track stats that are calculated over the lifetime of the agent."""

    def __init__(self):
        # The time in seconds past epoch when the agent was started.
        self.launch_time = None
        # The version string for the agent.
        self.version = None
        # The current number of paths the log copier is watching.
        self.num_watched_paths = 0
        # The current number of file paths the log copier is copying.
        self.num_copying_paths = 0
        # The current number of running monitors.
        self.num_running_monitors = 0
        # The current number of monitors that should be running but are not.
        self.num_dead_monitor = 0
        # the current number of worker sessions that run by the copying manager.
        self.num_worker_sessions = 0
        # The total amount of user time CPU used by the agent (cpu secs).
        self.user_cpu = 0
        # The total amount of system time CPU used by the agent (cpu secs)
        self.system_cpu = 0
        # The current resident size in bytes of the agent process.
        self.rss_size = 0

        # The total number of log bytes copied to the Scalyr servers.
        self.total_bytes_copied = 0
        # The number of bytes that still need to be sent to the Scalyr servers.
        self.total_bytes_pending = 0
        # The total number of log bytes that were skipped and were not considered to be sent to the Scalyr servers.
        self.total_bytes_skipped = 0
        # The total number of log bytes that were skipped from files just picked up by a processor.
        self.skipped_new_bytes = 0
        # The total number of log bytes that were skipped from already tracked files.
        self.skipped_preexisting_bytes = 0
        # The total time in seconds we were blocked by the rate limiter
        self.total_rate_limited_time = 0
        # The total number of log bytes that were not sent to the Scalyr servers due to subsampling rules.
        self.total_bytes_subsampled = 0
        # The total number of log bytes that were not sent to Scalyr due to errors on either the client or server side.
        self.total_bytes_failed = 0
        # The total number of redactions that were applied to the log lines before being sent to the Scalyr servers.
        self.total_redactions = 0
        # The total number of errors seen when issuing a copy request.
        self.total_copy_requests_errors = 0
        # The total number of lines reported by monitors.
        self.total_monitor_reported_lines = 0
        # The total number of errors seen by executing monitors.
        self.total_monitor_errors = 0

        # The total number of RPC requests sent.
        self.total_requests_sent = 0
        # The total number of RPC requests that failed.
        self.total_requests_failed = 0
        # The total number of bytes sent over the network.
        self.total_request_bytes_sent = 0
        # The total number of compressed bytes sent over the network.
        self.total_compressed_request_bytes_sent = 0
        # The total number of bytes received.
        self.total_response_bytes_received = 0
        # The total number of secs spent waiting for a responses (so average latency can be calculated by dividing
        # this number by self.total_requests_sent).  This includes connection establishment time.
        self.total_request_latency_secs = 0
        # The total number of HTTP connections successfully created.
        self.total_connections_created = 0

        # Fields needed for copying manager status
        self.total_scan_iterations = 0
        self.total_read_time = 0
        self.total_compression_time = 0
        self.total_waiting_time = 0
        self.total_blocking_response_time = 0
        self.total_request_time = 0
        self.total_pipelined_requests = 0
        self.avg_bytes_produced_rate = 0
        self.avg_bytes_copied_rate = 0
        self.rate_limited_time_since_last_status = 0

        # average time in seconds spent on agent's status report.
        self.avg_status_report_duration = None

    def __add__(self, other):
        """Adds all of the 'total_' fields of this instance and other together and returns a new OverallStats containing
        the result.
        """
        result = OverallStats()
        result.total_bytes_copied = self.total_bytes_copied + other.total_bytes_copied
        result.total_bytes_pending = (
            self.total_bytes_pending + other.total_bytes_pending
        )
        result.total_bytes_skipped = (
            self.total_bytes_skipped + other.total_bytes_skipped
        )
        result.skipped_new_bytes = self.skipped_new_bytes + other.skipped_new_bytes
        result.skipped_preexisting_bytes = (
            self.skipped_preexisting_bytes + other.skipped_preexisting_bytes
        )
        result.total_bytes_subsampled = (
            self.total_bytes_subsampled + other.total_bytes_subsampled
        )
        result.total_bytes_failed = self.total_bytes_failed + other.total_bytes_failed
        result.total_redactions = self.total_redactions + other.total_redactions
        result.total_copy_requests_errors = (
            self.total_copy_requests_errors + other.total_copy_requests_errors
        )
        result.total_monitor_reported_lines = (
            self.total_monitor_reported_lines + other.total_monitor_reported_lines
        )
        result.total_monitor_errors = (
            self.total_monitor_errors + other.total_monitor_errors
        )

        result.total_requests_sent = (
            self.total_requests_sent + other.total_requests_sent
        )
        result.total_requests_failed = (
            self.total_requests_failed + other.total_requests_failed
        )
        result.total_request_bytes_sent = (
            self.total_request_bytes_sent + other.total_request_bytes_sent
        )
        result.total_compressed_request_bytes_sent = (
            self.total_compressed_request_bytes_sent
            + other.total_compressed_request_bytes_sent
        )
        result.total_response_bytes_received = (
            self.total_response_bytes_received + other.total_response_bytes_received
        )
        result.total_request_latency_secs = (
            self.total_request_latency_secs + other.total_request_latency_secs
        )
        result.total_connections_created = (
            self.total_connections_created + other.total_connections_created
        )
        result.total_scan_iterations = (
            self.total_scan_iterations + other.total_scan_iterations
        )
        result.total_read_time = self.total_read_time + other.total_read_time
        result.total_compression_time = (
            self.total_compression_time + other.total_compression_time
        )
        result.total_waiting_time = self.total_waiting_time + other.total_waiting_time
        result.total_blocking_response_time = (
            self.total_blocking_response_time + other.total_blocking_response_time
        )
        result.total_request_time = self.total_request_time + other.total_request_time
        result.total_pipelined_requests = (
            self.total_pipelined_requests + other.total_pipelined_requests
        )
        result.avg_bytes_produced_rate = (
            self.avg_bytes_produced_rate + other.avg_bytes_produced_rate
        )
        result.avg_bytes_copied_rate = (
            self.avg_bytes_copied_rate + other.avg_bytes_copied_rate
        )
        result.rate_limited_time_since_last_status = (
            self.rate_limited_time_since_last_status
            + other.rate_limited_time_since_last_status
        )

        return result


class ConfigStatus(BaseAgentStatus):
    """The status pertaining to parsing of the configuration file."""

    def __init__(self):
        # The path of the configuration file.
        self.path = None
        # The paths for additional configuration files read from the config directory.
        self.additional_paths = []
        # The last time the configuration file changed and was read by this agent.
        self.last_read_time = None
        # A status line describing if the configuration file was successfully parsed.
        self.status = None
        # If the status file count not be parsed/used, a string describing the error.
        self.last_error = None
        # The last time the configuration file was successfully parsed.
        self.last_good_read = None
        # The last time the agent checked to see if the configuration file has changed.
        self.last_check_time = None

    def to_dict(self):
        return self.__dict__


class CopyingManagerWorkerSessionStatus(BaseAgentStatus):
    """The status object containing information about the agent's copying manager worker session components."""

    def __init__(self):
        self.session_id = None
        # If the 'use_multiprocess_workers'config option is set to True,
        # then it is a PID of the process in which the session is running.
        # Otherwise, it equals to the agent's process PID.
        self.pid = None  # type: Optional[int]
        # The total number of bytes successfully uploaded.
        self.total_bytes_uploaded = 0  # type: int
        # The last time the agent successfully copied bytes from log files to the Scalyr servers.
        self.last_success_time = None  # type: Optional[float]
        # The last time the agent attempted to copy bytes from log files to the Scalyr servers.
        self.last_attempt_time = None  # type: Optional[float]
        # The size of the request for the last attempt.
        self.last_attempt_size = None  # type: Optional[int]
        # The last response from the Scalyr servers.
        self.last_response = None  # type: Optional[six.text_type]
        # The last status from the last response (should be 'success').
        self.last_response_status = None  # type: Optional[six.text_type]
        # The total number of failed copy requests.
        self.total_errors = 0  # type: int
        # The total time in seconds we were blocked by the rate limiter
        self.total_rate_limited_time = 0
        # The time in seconds we were blocked by the rate limiter since the last status
        self.rate_limited_time_since_last_status = 0

        self.total_copy_iterations = 0
        self.total_read_time = 0
        self.total_waiting_time = 0
        self.total_blocking_response_time = 0
        self.total_request_time = 0
        self.total_pipelined_requests = 0

        self.health_check_result = None

        # LogProcessorStatus objects for each of the log files being processed by worker.
        self.log_processors = []  # type: List[LogProcessorStatus]


class CopyingManagerWorkerStatus(BaseAgentStatus):
    def __init__(self):
        self.worker_id = None
        # the status objects from all sessions in the worker.
        self.sessions = []  # type: List[CopyingManagerWorkerSessionStatus]

    def get_pids(self):
        # type: () -> List[int]
        worker_pids = []
        for session in self.sessions:
            if session.pid:
                worker_pids.append(session.pid)

        return worker_pids

    @property
    def has_files(self):
        # type: () -> bool
        """
        Shows if there is at least one file exists on some worker.
        :return:
        """
        for worker in self.sessions:
            if worker.log_processors:
                return True
        else:
            return False


class CopyingManagerStatus(BaseAgentStatus):
    """The status object containing information about the agent's copying components."""

    def __init__(self):
        # The total number of bytes successfully uploaded by all workers.
        self.total_bytes_uploaded = 0  # type: int
        # The total number of all failures from all workers.
        self.total_errors = 0  # type: int
        # The overall text message with an information about the health check.
        # For example it equals to "Good" if everything is ok.
        self.health_check_result = None  # type: Optional[six.text_type]

        # The overall text message with an information about the health check of the worker sessions..
        self.worker_sessions_health_check = None  # type: Optional[six.text_type]

        # How many times the copying manager scanned the file system for new files.
        self.total_scan_iterations = 0  # type: int

        # status objects for all log matchers.
        self.log_matchers = []  # type: List[LogMatcherStatus]

        # status object for each worker object.
        self.workers = []  # type: List[CopyingManagerWorkerStatus]

        # overall stats from workers.
        self.total_rate_limited_time = 0
        self.rate_limited_time_since_last_status = 0
        self.total_read_time = 0
        self.total_waiting_time = 0
        self.total_blocking_response_time = 0
        self.total_request_time = 0
        self.total_pipelined_requests = 0

        # the number of all running worker sessions.
        self.num_worker_sessions = 0

    def is_single_worker_session(self):
        # type: () -> bool
        """
        Checks if the copying manager is in default configuration (1 worker, 1 session)
        """
        if len(self.workers) == 1:
            worker = self.workers[-1]
            if len(worker.sessions) == 1:
                return True
        return False

    def get_all_worker_pids(self):
        all_pids = []

        for worker in self.workers:
            all_pids.extend(worker.get_pids())

        return all_pids

    def _all_worker_sessions(self):
        # type: () -> Generator
        """
        Generator that yields all sessions from all workers.
        """
        for worker_status in self.workers:
            for session_status in worker_status.sessions:
                yield session_status

    def _verify_worker_sessions_health_check(self):
        """
        Prepare the message string with an information about worker sessions health check.
        """
        if self.is_single_worker_session():
            worker_session = next(iter(self._all_worker_sessions()))
            # just copy the health check from the single worker.
            self.worker_sessions_health_check = worker_session.health_check_result
        else:
            # get the message for the worker session health check
            all_healthy = True
            for worker_session_status in self._all_worker_sessions():
                if worker_session_status.health_check_result is None:
                    # if some worker sessions do not have their health check results, then the result is None too.
                    all_healthy = False
                if worker_session_status.health_check_result != "Good":
                    # if there are worker sessions with errors, set the message about it.
                    self.worker_sessions_health_check = "Some workers have failed."
                    all_healthy = False
                    break

            if all_healthy:
                # every worker session is healthy, so the manager is healthy too.
                self.worker_sessions_health_check = "Good"

    def calculate_status(self):
        """
        Calculate overall stats based on all worker sessions.
        :return:
        """

        self._verify_worker_sessions_health_check()

        # sum up some worker session stats to overall stats.
        for worker_session_status in self._all_worker_sessions():
            self.num_worker_sessions += 1
            self.total_errors += worker_session_status.total_errors
            self.total_bytes_uploaded += worker_session_status.total_bytes_uploaded

            self.total_rate_limited_time += (
                worker_session_status.total_rate_limited_time
            )
            self.total_read_time += worker_session_status.total_read_time
            self.total_waiting_time += worker_session_status.total_waiting_time
            self.total_blocking_response_time += (
                worker_session_status.total_blocking_response_time
            )
            self.total_request_time += worker_session_status.total_request_time
            self.total_pipelined_requests += (
                worker_session_status.total_pipelined_requests
            )
            self.rate_limited_time_since_last_status += (
                worker_session_status.rate_limited_time_since_last_status
            )

    def to_dict(self):  # type: () -> dict
        result = super(CopyingManagerStatus, self).to_dict()
        if self.is_single_worker_session():
            # In case of default configuration,
            # On previous versions, the copying manager has those stats in its own dict,
            # but now they are moved to worker sessions.
            # We put stats from single worker session and copy them to copying manager's dict,
            # to not break things for customers with default configuration
            worker_session = self.workers[-1].sessions[-1]
            result.update(worker_session.to_dict())

        return result


class LogMatcherStatus(BaseAgentStatus):
    """The status object containing information about all of the copying being performed for a particular
    log path including globbing."""

    def __init__(self):
        # The path.
        self.log_path = None
        # True if the log path contains globbing characters.
        self.is_glob = None
        # The last time the agent checked the path for new matches.
        self.last_check_time = None
        # For any matching file paths, a LogProcessorStatus object describing the copying.
        self.log_processors_status = []


class LogProcessorStatus(BaseAgentStatus):
    """The status object containing information about the progress of the bytes being copied for a particular
    file."""

    def __init__(self):
        # The path of the file (will not contain glob characters).  This will be a path to an existing file.
        self.log_path = None
        # The last time the file was checked for new bytes.
        self.last_scan_time = None
        # The total bytes copied to the Scalyr servers.
        self.total_bytes_copied = 0
        # The number of bytes that still need to be sent to the Scalyr servers.
        self.total_bytes_pending = 0
        # The total bytes that were skipped (due to the log lines being too old, or the agent falling behind).
        self.total_bytes_skipped = 0
        # The total number of log bytes that were skipped from files just picked up by a processor.
        self.skipped_new_bytes = 0
        # The total number of log bytes that were skipped from already tracked files.
        self.skipped_preexisting_bytes = 0
        # The total bytes that failed due to errors at either the server or client.
        self.total_bytes_failed = 0
        # The total bytes that were not sent to the server due to subsampling rules.
        self.total_bytes_dropped_by_sampling = 0
        # The total number of log lines copied to the Scalyr servers.
        self.total_lines_copied = 0
        # The total number of log lines that were not sent to the server due to subsampling rules.
        self.total_lines_dropped_by_sampling = 0
        # The total number of redactions applied to the log lines copied to the server.
        self.total_redactions = 0


class MonitorManagerStatus(BaseAgentStatus):
    """The status object containing information about all of the running monitors."""

    def __init__(self):
        # The total number of monitors that are currently running.
        self.total_alive_monitors = 0
        # The MonitorStatus object for each monitor that is currently running or should be running.
        self.monitors_status = []


class MonitorStatus(BaseAgentStatus):
    """The status object for a specific instance of a ScalyrMonitor."""

    def __init__(self):
        # The name of the monitor.
        self.monitor_name = None
        # monitor's identifier
        self.monitor_id = None
        # monitors unique short_hash
        self.monitor_short_hash = None
        # bool flag that indicates that the 'stop_agent_on_failure' config option is enabled.
        self.stop_agent_on_failure = None
        # The total number of metric lines reported by the monitor.
        self.reported_lines = 0
        # The total number of errors produced by the monitor.
        self.errors = 0
        # Whether or not the monitor is running.
        self.is_alive = False


def report_status(output, status, current_time):
    """

    :param output: File object to output.
    :param status: Agent status instance.
    :param current_time: Time of the status report.
    """
    print(
        "Scalyr Agent status.  See https://www.scalyr.com/help/scalyr-agent-2 for help",
        file=output,
    )
    print("", file=output)
    print(
        "Current time:            %s" % scalyr_util.format_time(current_time),
        file=output,
    )
    print(
        "Agent started at:        %s" % scalyr_util.format_time(status.launch_time),
        file=output,
    )

    parent_process_pid = os.getpid()
    print("Main process pid:        %s" % (os.getpid()), file=output)

    # If parent and worker pid is the same, this means we are using a single worker or not using
    # multi process functionality so we don't report pids for child worker processes
    if status.copying_manager_status:
        worker_processes_pids = status.copying_manager_status.get_all_worker_pids()
    else:
        worker_processes_pids = []

    if (
        len(worker_processes_pids) >= 1
        and parent_process_pid not in worker_processes_pids
    ):
        worker_processes_pids = ", ".join([str(pid) for pid in worker_processes_pids])
        print("Worker processes pids:   %s" % worker_processes_pids, file=output)

    print("Version:                 %s" % status.version, file=output)
    print("VCS revision:            %s" % status.revision, file=output)
    print("Python version:          %s" % status.python_version, file=output)
    print("Agent running as:        %s" % status.user, file=output)
    print("Agent log:               %s" % status.log_path, file=output)
    print("ServerHost:              %s" % status.server_host, file=output)
    print("Compression algorithm:   %s" % status.compression_type, file=output)
    print("Compression level:       %s" % status.compression_level, file=output)
    if status.avg_status_report_duration:
        print(
            "Average status time:     %s sec." % status.avg_status_report_duration,
            file=output,
        )
    print("", file=output)
    server = scalyr_util.get_web_url_from_upload_url(status.scalyr_server)
    # We default to https://agent.scalyr.com for the Scalyr server, but to see the status on the web,
    # you should go to https://www.scalyr.com.  So, we do a little clean up before sticking it in
    # the url.  Same goes for https://log.scalyr.com  -- it is really is just https://www.scalyr.com
    print(
        "View data from this agent at: %s/events?filter=$serverHost%%3D%%27%s%%27"
        % (
            server,
            quote_plus(status.server_host),
        ),
        file=output,
    )
    print("", file=output)
    print("", file=output)

    # Configuration file status:
    print("Agent configuration:", file=output)
    print("====================", file=output)
    print("", file=output)
    if len(status.config_status.additional_paths) == 0:
        print("Configuration file:    %s" % status.config_status.path, file=output)
    else:
        print("Configuration files:   %s" % status.config_status.path, file=output)
        for x in status.config_status.additional_paths:
            print("                       %s" % x, file=output)

    if status.config_status.last_error is None:
        print("Status:                Good (files parsed successfully)", file=output)
    else:
        print(
            "Status:                Bad (could not parse, using last good version)",
            file=output,
        )
    print(
        "Last checked:          %s"
        % scalyr_util.format_time(status.config_status.last_check_time),
        file=output,
    )
    print(
        "Last changed observed: %s"
        % scalyr_util.format_time(status.config_status.last_read_time),
        file=output,
    )

    if status.config_status.last_error is not None:
        print(
            "Parsing error:         %s"
            % six.text_type(status.config_status.last_error),
            file=output,
        )

    def print_environment():

        # Print scalyr-related env variables in sorted order with critical variables up top. Redact API key.
        main_keys = ["SCALYR_API_KEY", "SCALYR_SERVER"]
        special_case_keys = set(["K8S_EVENT_DISABLE"])
        redacted_keys = set(["SCALYR_API_KEY"])

        # Make a map of uppercase keys -> relevant environment vars (beginning with SCALYR)
        upper2actualkey = {}
        for k in os.environ.keys():
            kup = k.upper()
            if kup.startswith("SCALYR") or kup in special_case_keys:
                upper2actualkey[kup] = k

        # Sorted list of all scalyr keys, including main_keys which may not be present
        # Sort order does not consider letter case.
        sorted_upperkeys = main_keys + sorted(
            set(upper2actualkey.keys()) - set(main_keys)
        )

        print("", file=output)
        row = 0
        for kup in sorted_upperkeys:
            key = upper2actualkey.get(kup, kup)
            val = compat.os_getenv_unicode(key)
            if not val:
                val = "<Missing>"
            elif key.upper() in redacted_keys:
                val = "<Redacted>"

            if row == 0:
                print("Environment variables: %s = %s" % (key, val), file=output)
            else:
                print("                       %s = %s" % (key, val), file=output)
            row += 1

    print_environment()

    if status.copying_manager_status is not None:
        print("", file=output)
        print("", file=output)
        __report_copying_manager(
            output,
            status.copying_manager_status,
            status.log_path,
            status.config_status.last_read_time,
        )

    if status.monitor_manager_status is not None:
        print("", file=output)
        print("", file=output)
        __report_monitor_manager(
            output, status.monitor_manager_status, status.config_status.last_read_time
        )


def _indent_print(str, file, indent=4):
    # type: (six.text_type, TextIO, int) -> None
    """
    Helper function to print with indents.
    :param str: Original string.
    :param file: Output file.
    :param indent: Spaces to indent.
    """
    file.write(" " * indent)
    print(str, file=file)


def __get_overall_health_check(manager_status):
    # type: (CopyingManagerStatus) -> six.text_type
    """
    Get the health check for the copying manager's thread and for its sessions.
    :return:
    """
    # show overall health check
    health_check_buffer = io.StringIO()
    if (
        manager_status.health_check_result == "Good"
        and manager_status.worker_sessions_health_check == "Good"
    ):
        # the copying manager thread and workers are good, so overall health check is good too.
        health_check_buffer.write("Good")
    else:
        if (
            manager_status.health_check_result
            and manager_status.health_check_result != "Good"
        ):
            # managers health check is something, but it's not Good, so there must be an error. Write it.
            health_check_buffer.write(manager_status.health_check_result)
        if (
            manager_status.worker_sessions_health_check
            and manager_status.worker_sessions_health_check != "Good"
        ):
            if health_check_buffer.tell() > 0:
                # if the buffer not empty, then there is a previous error, put comma.
                health_check_buffer.write(", ")
            # workers health check is something, but it's not Good, so there must be an error. Write it.
            health_check_buffer.write(manager_status.worker_sessions_health_check)

    return health_check_buffer.getvalue()


def _report_worker_session(
    output, worker_session, manager_status, agent_log_file_path, indent
):
    # type: (TextIO, CopyingManagerWorkerSessionStatus, CopyingManagerStatus, six.text_type, int) -> None
    """
    Write worker session status to the file-like object.
    :param output: file-like object.
    :param worker_session: worker session status object.
    :param agent_log_file_path:
    :param indent: Number of spaces to indent on each new line.
    :param manager_status: the manager status to produce the health check.
    :return:
    """
    _indent_print(
        "Bytes uploaded successfully:               %ld"
        % worker_session.total_bytes_uploaded,
        file=output,
        indent=indent,
    )
    _indent_print(
        "Last successful communication with Scalyr: %s"
        % scalyr_util.format_time(worker_session.last_success_time),
        file=output,
        indent=indent,
    )
    _indent_print(
        "Last attempt:                              %s"
        % scalyr_util.format_time(worker_session.last_attempt_time),
        file=output,
        indent=indent,
    )
    if worker_session.last_attempt_size is not None:
        _indent_print(
            "Last copy request size:                    %ld"
            % worker_session.last_attempt_size,
            file=output,
            indent=indent,
        )
    if worker_session.last_response is not None:
        _indent_print(
            "Last copy response size:                   %ld"
            % len(worker_session.last_response),
            file=output,
            indent=indent,
        )
        _indent_print(
            "Last copy response status:                 %s"
            % worker_session.last_response_status,
            file=output,
            indent=indent,
        )
        if worker_session.last_response_status != "success":
            _indent_print(
                "Last copy response:                        %s"
                % scalyr_util.remove_newlines_and_truncate(
                    worker_session.last_response, 1000
                ),
                file=output,
                indent=indent,
            )
    if worker_session.total_errors > 0:
        _indent_print(
            "Total responses with errors:               %d (see '%s' for details)"
            % (
                worker_session.total_errors,
                agent_log_file_path,
            ),
            file=output,
            indent=indent,
        )

    if manager_status.is_single_worker_session():
        health_check_message = __get_overall_health_check(manager_status)
    else:
        health_check_message = worker_session.health_check_result

    if health_check_message:
        # if message is not empty, write it. In other case we still don't have all health check data.
        _indent_print(
            "Health check:                              %s" % health_check_message,
            file=output,
            indent=indent,
        )
    print("", file=output)


def __report_copying_manager(output, manager_status, agent_log_file_path, read_time):
    # type: (TextIO, CopyingManagerStatus, six.text_type, float) -> None
    print("Log transmission:", file=output)
    print("=================", file=output)
    print("", file=output)
    print(
        "(these statistics cover the period from %s)"
        % scalyr_util.format_time(read_time),
        file=output,
    )
    print("", file=output)
    workers = manager_status.workers

    # if it is a default configuration, then we just print the stats of the single worker session.
    if manager_status.is_single_worker_session():
        worker_session = workers[-1].sessions[-1]
        _report_worker_session(
            output, worker_session, manager_status, agent_log_file_path, indent=0
        )
    else:
        # print some overall information from all workers.
        print(
            "Total bytes uploaded:                            %ld"
            % manager_status.total_bytes_uploaded,
            file=output,
        )

        # print the overlall health chech.
        health_check_message = __get_overall_health_check(manager_status)
        if health_check_message:
            # if message is not empty, write it. In other case we still don't have all health check data.
            _indent_print(
                "Overall health check:                            %s"
                % health_check_message,
                file=output,
                indent=0,
            )
        if manager_status.total_errors > 0:
            print(
                "Total errors occurred:                           %d"
                % manager_status.total_errors,
                file=output,
            )

        print("", file=output)

        # show every statistics for every session in every worker.
        print("Uploads statistics by worker:", file=output)
        for worker in manager_status.workers:
            if not worker.has_files:
                # skip worker if there is no log files.
                continue
            print(" Worker %s:" % worker.worker_id, file=output)
            for worker_session in worker.sessions:
                print("    Session %s:" % worker_session.session_id, file=output)
                _report_worker_session(
                    output,
                    worker_session,
                    manager_status,
                    agent_log_file_path,
                    indent=6,
                )

        # Show in which worker and session each file is located.
        _indent_print(" Log files associated with workers:", file=output, indent=0)
        for worker in manager_status.workers:
            # skip worker if there is no log files.
            if not worker.has_files:
                continue
            print("  Worker %s:" % worker.worker_id, file=output)
            for worker_session in worker.sessions:
                if len(worker.sessions) > 1:
                    print("    Session %s:" % worker_session.session_id, file=output)
                for log_processor in worker_session.log_processors:
                    _indent_print(log_processor.log_path, file=output, indent=8)

        print("", file=output)

    for matcher_status in manager_status.log_matchers:
        if not matcher_status.is_glob:
            if len(matcher_status.log_processors_status) == 0:
                # This is an absolute file path (no wildcards) and there are not matches.
                print(
                    "Path %s: no matching readable file, last checked %s"
                    % (
                        matcher_status.log_path,
                        scalyr_util.format_time(matcher_status.last_check_time),
                    ),
                    file=output,
                )
            else:
                # We have a match.. matcher_status.log_processors_status should really only have one
                # entry, but we loop anyway.
                for processor_status in matcher_status.log_processors_status:
                    output.write(
                        "Path %s: copied %ld bytes (%ld lines), %ld bytes pending, "
                        % (
                            processor_status.log_path,
                            processor_status.total_bytes_copied,
                            processor_status.total_lines_copied,
                            processor_status.total_bytes_pending,
                        )
                    )
                    if processor_status.total_bytes_skipped > 0:
                        output.write(
                            "%ld bytes skipped, " % processor_status.total_bytes_skipped
                        )
                    if processor_status.total_bytes_failed > 0:
                        output.write(
                            "%ld bytes failed, " % processor_status.total_bytes_failed
                        )
                    if processor_status.total_bytes_dropped_by_sampling > 0:
                        output.write(
                            "%ld bytes dropped by sampling (%ld lines), "
                            % (
                                processor_status.total_bytes_dropped_by_sampling,
                                processor_status.total_lines_dropped_by_sampling,
                            )
                        )

                    if processor_status.total_redactions > 0:
                        output.write(
                            "%ld redactions, " % processor_status.total_redactions
                        )
                    output.write(
                        "last checked %s"
                        % scalyr_util.format_time(processor_status.last_scan_time)
                    )
                    output.write("\n")
                    output.flush()

    need_to_add_extra_line = True
    for matcher_status in manager_status.log_matchers:
        if matcher_status.is_glob:
            if need_to_add_extra_line:
                need_to_add_extra_line = False
                print("", file=output)
            print(
                "Glob: %s:: last scanned for glob matches at %s"
                % (
                    matcher_status.log_path,
                    scalyr_util.format_time(matcher_status.last_check_time),
                ),
                file=output,
            )

            for processor_status in matcher_status.log_processors_status:
                output.write(
                    "  %s: copied %ld bytes (%ld lines), %ld bytes pending, "
                    % (
                        processor_status.log_path,
                        processor_status.total_bytes_copied,
                        processor_status.total_lines_copied,
                        processor_status.total_bytes_pending,
                    )
                )
                if processor_status.total_bytes_skipped > 0:
                    output.write(
                        "%ld bytes skipped, " % processor_status.total_bytes_skipped
                    )
                if processor_status.total_bytes_failed > 0:
                    output.write(
                        "%ld bytes failed, " % processor_status.total_bytes_failed
                    )
                if processor_status.total_bytes_dropped_by_sampling > 0:
                    output.write(
                        "%ld bytes dropped by sampling (%ld lines), "
                        % (
                            processor_status.total_bytes_dropped_by_sampling,
                            processor_status.total_lines_dropped_by_sampling,
                        )
                    )

                if processor_status.total_redactions > 0:
                    output.write("%ld redactions, " % processor_status.total_redactions)
                output.write(
                    "last checked %s"
                    % scalyr_util.format_time(processor_status.last_scan_time)
                )
                output.write("\n")
                output.flush()


def __report_monitor_manager(output, manager_status, read_time):
    print("Monitors:", file=output)
    print("=========", file=output)
    print("", file=output)
    print(
        "(these statistics cover the period from %s)"
        % scalyr_util.format_time(read_time),
        file=output,
    )
    print("", file=output)
    if manager_status.total_alive_monitors < len(manager_status.monitors_status):
        print("Running monitors:", file=output)
        padding = "  "
    else:
        padding = ""

    for entry in manager_status.monitors_status:
        if entry.is_alive:
            running_monitors_message = "%s%s: %d lines emitted, %d errors" % (
                padding,
                entry.monitor_name,
                entry.reported_lines,
                entry.errors,
            )
            if entry.stop_agent_on_failure:
                running_monitors_message = "{}, stop_agent_on_failure=true".format(
                    running_monitors_message
                )
            print(running_monitors_message, file=output)

    dead_monitors = (
        len(manager_status.monitors_status) - manager_status.total_alive_monitors
    )
    if dead_monitors > 0:
        print("", file=output)
        print("Failed monitors:", file=output)
        for entry in manager_status.monitors_status:
            if not entry.is_alive:
                print(
                    "  %s %d lines emitted, %d errors"
                    % (
                        entry.monitor_name,
                        entry.reported_lines,
                        entry.errors,
                    ),
                    file=output,
                )
