# -*- coding: utf-8 -*-
# Copyright 2014-2016 OpenMarket Ltd
#
# 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.
from typing import Optional

import attr
from nacl.signing import SigningKey

from twisted.internet import defer

from synapse.api.constants import MAX_DEPTH
from synapse.api.errors import UnsupportedRoomVersionError
from synapse.api.room_versions import (
    KNOWN_EVENT_FORMAT_VERSIONS,
    KNOWN_ROOM_VERSIONS,
    EventFormatVersions,
    RoomVersion,
)
from synapse.crypto.event_signing import add_hashes_and_signatures
from synapse.events import (
    EventBase,
    _EventInternalMetadata,
    event_type_from_format_version,
)
from synapse.types import EventID, JsonDict
from synapse.util import Clock
from synapse.util.stringutils import random_string


@attr.s(slots=True, cmp=False, frozen=True)
class EventBuilder(object):
    """A format independent event builder used to build up the event content
    before signing the event.

    (Note that while objects of this class are frozen, the
    content/unsigned/internal_metadata fields are still mutable)

    Attributes:
        room_version: Version of the target room
        room_id (str)
        type (str)
        sender (str)
        content (dict)
        unsigned (dict)
        internal_metadata (_EventInternalMetadata)

        _state (StateHandler)
        _auth (synapse.api.Auth)
        _store (DataStore)
        _clock (Clock)
        _hostname (str): The hostname of the server creating the event
        _signing_key: The signing key to use to sign the event as the server
    """

    _state = attr.ib()
    _auth = attr.ib()
    _store = attr.ib()
    _clock = attr.ib()
    _hostname = attr.ib()
    _signing_key = attr.ib()

    room_version = attr.ib(type=RoomVersion)

    room_id = attr.ib()
    type = attr.ib()
    sender = attr.ib()

    content = attr.ib(default=attr.Factory(dict))
    unsigned = attr.ib(default=attr.Factory(dict))

    # These only exist on a subset of events, so they raise AttributeError if
    # someone tries to get them when they don't exist.
    _state_key = attr.ib(default=None)
    _redacts = attr.ib(default=None)
    _origin_server_ts = attr.ib(default=None)

    internal_metadata = attr.ib(
        default=attr.Factory(lambda: _EventInternalMetadata({}))
    )

    @property
    def state_key(self):
        if self._state_key is not None:
            return self._state_key

        raise AttributeError("state_key")

    def is_state(self):
        return self._state_key is not None

    @defer.inlineCallbacks
    def build(self, prev_event_ids):
        """Transform into a fully signed and hashed event

        Args:
            prev_event_ids (list[str]): The event IDs to use as the prev events

        Returns:
            Deferred[FrozenEvent]
        """

        state_ids = yield self._state.get_current_state_ids(
            self.room_id, prev_event_ids
        )
        auth_ids = yield self._auth.compute_auth_events(self, state_ids)

        format_version = self.room_version.event_format
        if format_version == EventFormatVersions.V1:
            auth_events = yield self._store.add_event_hashes(auth_ids)
            prev_events = yield self._store.add_event_hashes(prev_event_ids)
        else:
            auth_events = auth_ids
            prev_events = prev_event_ids

        old_depth = yield self._store.get_max_depth_of(prev_event_ids)
        depth = old_depth + 1

        # we cap depth of generated events, to ensure that they are not
        # rejected by other servers (and so that they can be persisted in
        # the db)
        depth = min(depth, MAX_DEPTH)

        event_dict = {
            "auth_events": auth_events,
            "prev_events": prev_events,
            "type": self.type,
            "room_id": self.room_id,
            "sender": self.sender,
            "content": self.content,
            "unsigned": self.unsigned,
            "depth": depth,
            "prev_state": [],
        }

        if self.is_state():
            event_dict["state_key"] = self._state_key

        if self._redacts is not None:
            event_dict["redacts"] = self._redacts

        if self._origin_server_ts is not None:
            event_dict["origin_server_ts"] = self._origin_server_ts

        return create_local_event_from_event_dict(
            clock=self._clock,
            hostname=self._hostname,
            signing_key=self._signing_key,
            room_version=self.room_version,
            event_dict=event_dict,
            internal_metadata_dict=self.internal_metadata.get_dict(),
        )


class EventBuilderFactory(object):
    def __init__(self, hs):
        self.clock = hs.get_clock()
        self.hostname = hs.hostname
        self.signing_key = hs.config.signing_key[0]

        self.store = hs.get_datastore()
        self.state = hs.get_state_handler()
        self.auth = hs.get_auth()

    def new(self, room_version, key_values):
        """Generate an event builder appropriate for the given room version

        Deprecated: use for_room_version with a RoomVersion object instead

        Args:
            room_version (str): Version of the room that we're creating an event builder
                for
            key_values (dict): Fields used as the basis of the new event

        Returns:
            EventBuilder
        """
        v = KNOWN_ROOM_VERSIONS.get(room_version)
        if not v:
            # this can happen if support is withdrawn for a room version
            raise UnsupportedRoomVersionError()
        return self.for_room_version(v, key_values)

    def for_room_version(self, room_version, key_values):
        """Generate an event builder appropriate for the given room version

        Args:
            room_version (synapse.api.room_versions.RoomVersion):
                Version of the room that we're creating an event builder for
            key_values (dict): Fields used as the basis of the new event

        Returns:
            EventBuilder
        """
        return EventBuilder(
            store=self.store,
            state=self.state,
            auth=self.auth,
            clock=self.clock,
            hostname=self.hostname,
            signing_key=self.signing_key,
            room_version=room_version,
            type=key_values["type"],
            state_key=key_values.get("state_key"),
            room_id=key_values["room_id"],
            sender=key_values["sender"],
            content=key_values.get("content", {}),
            unsigned=key_values.get("unsigned", {}),
            redacts=key_values.get("redacts", None),
            origin_server_ts=key_values.get("origin_server_ts", None),
        )


def create_local_event_from_event_dict(
    clock: Clock,
    hostname: str,
    signing_key: SigningKey,
    room_version: RoomVersion,
    event_dict: JsonDict,
    internal_metadata_dict: Optional[JsonDict] = None,
) -> EventBase:
    """Takes a fully formed event dict, ensuring that fields like `origin`
    and `origin_server_ts` have correct values for a locally produced event,
    then signs and hashes it.
    """

    format_version = room_version.event_format
    if format_version not in KNOWN_EVENT_FORMAT_VERSIONS:
        raise Exception("No event format defined for version %r" % (format_version,))

    if internal_metadata_dict is None:
        internal_metadata_dict = {}

    time_now = int(clock.time_msec())

    if format_version == EventFormatVersions.V1:
        event_dict["event_id"] = _create_event_id(clock, hostname)

    event_dict["origin"] = hostname
    event_dict.setdefault("origin_server_ts", time_now)

    event_dict.setdefault("unsigned", {})
    age = event_dict["unsigned"].pop("age", 0)
    event_dict["unsigned"].setdefault("age_ts", time_now - age)

    event_dict.setdefault("signatures", {})

    add_hashes_and_signatures(room_version, event_dict, hostname, signing_key)
    return event_type_from_format_version(format_version)(
        event_dict, internal_metadata_dict=internal_metadata_dict
    )


# A counter used when generating new event IDs
_event_id_counter = 0


def _create_event_id(clock, hostname):
    """Create a new event ID

    Args:
        clock (Clock)
        hostname (str): The server name for the event ID

    Returns:
        str
    """

    global _event_id_counter

    i = str(_event_id_counter)
    _event_id_counter += 1

    local_part = str(int(clock.time())) + i + random_string(5)

    e_id = EventID(local_part, hostname)

    return e_id.to_string()
