import gzip
import logging
import os
import socket
import sys
from typing import Callable
from typing import NamedTuple
from typing import Optional

from google.protobuf.json_format import MessageToJson
from google.protobuf.message import Message
from six import BytesIO
from tornado.httpclient import AsyncHTTPClient
from tornado_retry_client import RetryClient

from .reports_pb2 import FullTracesReport
from .reports_pb2 import ReportHeader
from graphene_tornado.apollo_tooling.operation_id import (
    default_engine_reporting_signature,
)

LOGGER = logging.getLogger(__name__)

SERVICE_HEADER_DEFAULTS = {
    "hostname": socket.gethostname(),
    "agentVersion": "apollo-engine-reporting@${require('../package.json').version}",
    "runtimeVersion": "python " + ".".join(map(str, sys.version_info[0:3])),
    "uname": " ".join(os.uname()),
}

GenerateClientInfo = NamedTuple(
    "GenerateClientInfo",
    [
        ("client_name", Optional[str]),
        ("client_version", Optional[str]),
        ("client_reference_id", Optional[str]),
    ],
)

EngineReportingOptions = NamedTuple(
    "EngineReportingOptions",
    [
        ("api_key", Optional[str]),
        ("calculate_signature", Optional[Callable]),
        # ('report_interval_ms', Optional[int]),
        # ('max_uncompressed_report_size', Optional[int]),
        ("endpoint_url", Optional[str]),
        ("debug_print_reports", Optional[bool]),
        ("request_agent", Optional[bool]),
        # ('max_attempts', Optional[int]),
        # ('minimum_retry_delay_ms', Optional[int]),
        ("report_error_function", Optional[Callable]),
        # ('private_variables', Optional[List[str]]),
        # ('private_headers', Optional[List[str]]),
        # ('handle_signals', Optional[bool]),
        # ('send_reports_immediately', Optional[bool]),
        ("mask_errors_details", Optional[bool]),
        ("schema_tag", Optional[str]),
        ("generate_client_info", Optional[GenerateClientInfo]),
    ],
)
EngineReportingOptions.__new__.__defaults__ = (None,) * len(EngineReportingOptions._fields)  # type: ignore


def _serialize(message: Message) -> bytes:
    out = BytesIO()
    with gzip.GzipFile(fileobj=out, mode="w") as f:
        f.write(message.SerializeToString())
    return out.getvalue()


def _get_trace_signature(operation_name, document, query_string):
    if not document:
        return query_string
    else:
        return default_engine_reporting_signature(document, operation_name)


class EngineReportingAgent:
    def __init__(self, options: EngineReportingOptions, schema_hash: str) -> None:
        self.options = options
        self.api_key = options.api_key or os.getenv("ENGINE_API_KEY", None)

        if not self.api_key:
            raise ValueError(
                "To use EngineReportingAgent, you must specify an API key via the api_key option or the "
                + "ENGINE_API_KEY environment variable."
            )

        self.endpoint_url = (
            self.options.endpoint_url
            or "https://engine-report.apollodata.com/api/ingress/traces"
        )
        self.request_headers = {
            "user-agent": "apollo-engine-reporting",
            "x-api-key": self.api_key,
            "content-encoding": "gzip",
        }

        self._stopped = False

        self.report_header = ReportHeader()
        self.report_header.hostname = SERVICE_HEADER_DEFAULTS["hostname"]
        self.report_header.agent_version = SERVICE_HEADER_DEFAULTS["agentVersion"]
        self.report_header.runtime_version = SERVICE_HEADER_DEFAULTS["runtimeVersion"]
        self.report_header.uname = SERVICE_HEADER_DEFAULTS["uname"]
        self.report_header.schema_hash = schema_hash
        self.report_header.schema_tag = (
            options.schema_tag or os.getenv("ENGINE_SCHEMA_TAG", None) or ""
        )

        self.report = FullTracesReport(header=self.report_header)
        self.report_size = 0

    def _options(self) -> EngineReportingOptions:
        return self.options

    async def add_trace(self, operation_name, document, query_string, trace):
        operation_name = operation_name or "-"

        if self._stopped:
            return

        signature = _get_trace_signature(operation_name, document, query_string)
        stats_report_key = "# " + operation_name + "\n" + signature
        traces_per_query = self.report.traces_per_query.get(stats_report_key, None)
        if not traces_per_query:
            traces_per_query = self.report.traces_per_query[stats_report_key]
        traces_per_query.trace.extend([trace])

        await self.send_report_and_report_errors()

    async def send_report(self):
        report = self.report
        self.reset_report()

        if len(report.traces_per_query) == 0:
            return

        if self.options.debug_print_reports:
            LOGGER.info("Engine sending report: " + MessageToJson(report))

        await self.post_data(_serialize(report))

    async def post_data(self, data):
        headers = {"Content-Length": len(data)}
        headers.update(self.request_headers)

        http_client = AsyncHTTPClient()
        retry_client = RetryClient(
            http_client=http_client,
            retry_attempts=3,
            retry_start_timeout=0.5,
            retry_max_timeout=10,
            retry_factor=2,
        )

        try:
            response = await retry_client.fetch(
                self.endpoint_url,
                method="POST",
                headers=headers,
                body=data,
                raise_error=False,
            )

        finally:
            http_client.close()

        if 500 <= response.code < 600:
            raise ValueError(response.code + ": " + response.body)

        if response.code < 200 or response.code >= 300:
            raise ValueError(
                "Error sending report to Engine servers (HTTP status {}): {}".format(
                    response.code, response.body
                )
            )

        if self.options.debug_print_reports:
            LOGGER.info("Engine report: status " + response.code)

    def stop(self):
        self._stopped = True

    async def send_report_and_report_errors(self):
        try:
            await self.send_report()
        except:
            exception = sys.exc_info()[1]
            if self.options.report_error_function:
                self.options.report_error_function(exception)
            else:
                LOGGER.exception("Error sending reports to Apollo Engine")

    def reset_report(self):
        self.report = FullTracesReport(header=self.report_header)
        self.report_size: int = 0
