# Copyright 2014-present MongoDB, 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
#
# https://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.

"""Represent one server the driver is connected to.

.. seealso:: This module is compatible with both the synchronous and asynchronous PyMongo APIs.
"""
from __future__ import annotations

import time
import warnings
from typing import Any, Mapping, Optional

from bson import EPOCH_NAIVE
from bson.objectid import ObjectId
from pymongo.hello import Hello
from pymongo.server_type import SERVER_TYPE
from pymongo.typings import ClusterTime, _Address


class ServerDescription:
    """Immutable representation of one server.

    :param address: A (host, port) pair
    :param hello: Optional Hello instance
    :param round_trip_time: Optional float
    :param error: Optional, the last error attempting to connect to the server
    :param round_trip_time: Optional float, the min latency from the most recent samples
    """

    __slots__ = (
        "_address",
        "_server_type",
        "_all_hosts",
        "_tags",
        "_replica_set_name",
        "_primary",
        "_max_bson_size",
        "_max_message_size",
        "_max_write_batch_size",
        "_min_wire_version",
        "_max_wire_version",
        "_round_trip_time",
        "_min_round_trip_time",
        "_me",
        "_is_writable",
        "_is_readable",
        "_ls_timeout_minutes",
        "_error",
        "_set_version",
        "_election_id",
        "_cluster_time",
        "_last_write_date",
        "_last_update_time",
        "_topology_version",
    )

    def __init__(
        self,
        address: _Address,
        hello: Optional[Hello[dict[str, Any]]] = None,
        round_trip_time: Optional[float] = None,
        error: Optional[Exception] = None,
        min_round_trip_time: float = 0.0,
    ) -> None:
        self._address = address
        if not hello:
            hello = Hello({})

        self._server_type = hello.server_type
        self._all_hosts = hello.all_hosts
        self._tags = hello.tags
        self._replica_set_name = hello.replica_set_name
        self._primary = hello.primary
        self._max_bson_size = hello.max_bson_size
        self._max_message_size = hello.max_message_size
        self._max_write_batch_size = hello.max_write_batch_size
        self._min_wire_version = hello.min_wire_version
        self._max_wire_version = hello.max_wire_version
        self._set_version = hello.set_version
        self._election_id = hello.election_id
        self._cluster_time = hello.cluster_time
        self._is_writable = hello.is_writable
        self._is_readable = hello.is_readable
        self._ls_timeout_minutes = hello.logical_session_timeout_minutes
        self._round_trip_time = round_trip_time
        self._min_round_trip_time = min_round_trip_time
        self._me = hello.me
        self._last_update_time = time.monotonic()
        self._error = error
        self._topology_version = hello.topology_version
        if error:
            details = getattr(error, "details", None)
            if isinstance(details, dict):
                self._topology_version = details.get("topologyVersion")

        self._last_write_date: Optional[float]
        if hello.last_write_date:
            # Convert from datetime to seconds.
            delta = hello.last_write_date - EPOCH_NAIVE
            self._last_write_date = delta.total_seconds()
        else:
            self._last_write_date = None

    @property
    def address(self) -> _Address:
        """The address (host, port) of this server."""
        return self._address

    @property
    def server_type(self) -> int:
        """The type of this server."""
        return self._server_type

    @property
    def server_type_name(self) -> str:
        """The server type as a human readable string.

        .. versionadded:: 3.4
        """
        return SERVER_TYPE._fields[self._server_type]

    @property
    def all_hosts(self) -> set[tuple[str, int]]:
        """List of hosts, passives, and arbiters known to this server."""
        return self._all_hosts

    @property
    def tags(self) -> Mapping[str, Any]:
        return self._tags

    @property
    def replica_set_name(self) -> Optional[str]:
        """Replica set name or None."""
        return self._replica_set_name

    @property
    def primary(self) -> Optional[tuple[str, int]]:
        """This server's opinion about who the primary is, or None."""
        return self._primary

    @property
    def max_bson_size(self) -> int:
        return self._max_bson_size

    @property
    def max_message_size(self) -> int:
        return self._max_message_size

    @property
    def max_write_batch_size(self) -> int:
        return self._max_write_batch_size

    @property
    def min_wire_version(self) -> int:
        return self._min_wire_version

    @property
    def max_wire_version(self) -> int:
        return self._max_wire_version

    @property
    def set_version(self) -> Optional[int]:
        return self._set_version

    @property
    def election_id(self) -> Optional[ObjectId]:
        return self._election_id

    @property
    def cluster_time(self) -> Optional[ClusterTime]:
        return self._cluster_time

    @property
    def election_tuple(self) -> tuple[Optional[int], Optional[ObjectId]]:
        warnings.warn(
            "'election_tuple' is deprecated, use  'set_version' and 'election_id' instead",
            DeprecationWarning,
            stacklevel=2,
        )
        return self._set_version, self._election_id

    @property
    def me(self) -> Optional[tuple[str, int]]:
        return self._me

    @property
    def logical_session_timeout_minutes(self) -> Optional[int]:
        return self._ls_timeout_minutes

    @property
    def last_write_date(self) -> Optional[float]:
        return self._last_write_date

    @property
    def last_update_time(self) -> float:
        return self._last_update_time

    @property
    def round_trip_time(self) -> Optional[float]:
        """The current average latency or None."""
        # This override is for unittesting only!
        if self._address in self._host_to_round_trip_time:
            return self._host_to_round_trip_time[self._address]

        return self._round_trip_time

    @property
    def min_round_trip_time(self) -> float:
        """The min latency from the most recent samples."""
        return self._min_round_trip_time

    @property
    def error(self) -> Optional[Exception]:
        """The last error attempting to connect to the server, or None."""
        return self._error

    @property
    def is_writable(self) -> bool:
        return self._is_writable

    @property
    def is_readable(self) -> bool:
        return self._is_readable

    @property
    def mongos(self) -> bool:
        return self._server_type == SERVER_TYPE.Mongos

    @property
    def is_server_type_known(self) -> bool:
        return self.server_type != SERVER_TYPE.Unknown

    @property
    def retryable_writes_supported(self) -> bool:
        """Checks if this server supports retryable writes."""
        return (
            self._server_type in (SERVER_TYPE.Mongos, SERVER_TYPE.RSPrimary)
        ) or self._server_type == SERVER_TYPE.LoadBalancer

    @property
    def retryable_reads_supported(self) -> bool:
        """Checks if this server supports retryable writes."""
        return self._max_wire_version >= 6

    @property
    def topology_version(self) -> Optional[Mapping[str, Any]]:
        return self._topology_version

    def to_unknown(self, error: Optional[Exception] = None) -> ServerDescription:
        unknown = ServerDescription(self.address, error=error)
        unknown._topology_version = self.topology_version
        return unknown

    def __eq__(self, other: Any) -> bool:
        if isinstance(other, ServerDescription):
            return (
                (self._address == other.address)
                and (self._server_type == other.server_type)
                and (self._min_wire_version == other.min_wire_version)
                and (self._max_wire_version == other.max_wire_version)
                and (self._me == other.me)
                and (self._all_hosts == other.all_hosts)
                and (self._tags == other.tags)
                and (self._replica_set_name == other.replica_set_name)
                and (self._set_version == other.set_version)
                and (self._election_id == other.election_id)
                and (self._primary == other.primary)
                and (self._ls_timeout_minutes == other.logical_session_timeout_minutes)
                and (self._error == other.error)
            )

        return NotImplemented

    def __ne__(self, other: Any) -> bool:
        return not self == other

    def __repr__(self) -> str:
        errmsg = ""
        if self.error:
            errmsg = f", error={self.error!r}"
        return "<{} {} server_type: {}, rtt: {}{}>".format(
            self.__class__.__name__,
            self.address,
            self.server_type_name,
            self.round_trip_time,
            errmsg,
        )

    # For unittesting only. Use under no circumstances!
    _host_to_round_trip_time: dict = {}  # type: ignore[type-arg]
