# 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.
import logging
from typing import TYPE_CHECKING, Any, Collection, Dict, List, Optional, Tuple, Union

import attr
from signedjson.types import SigningKey

from synapse.api.constants import MAX_DEPTH
from synapse.api.room_versions import (
    KNOWN_EVENT_FORMAT_VERSIONS,
    EventFormatVersions,
    RoomVersion,
)
from synapse.crypto.event_signing import add_hashes_and_signatures
from synapse.event_auth import auth_types_for_event
from synapse.events import EventBase, _EventInternalMetadata, make_event_from_dict
from synapse.state import StateHandler
from synapse.storage.databases.main import DataStore
from synapse.types import EventID, JsonDict
from synapse.types.state import StateFilter
from synapse.util import Clock
from synapse.util.stringutils import random_string

if TYPE_CHECKING:
    from synapse.handlers.event_auth import EventAuthHandler
    from synapse.server import HomeServer

logger = logging.getLogger(__name__)


@attr.s(slots=True, cmp=False, frozen=True, auto_attribs=True)
class EventBuilder:
    """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
        type
        sender
        content
        unsigned
        internal_metadata

        _state
        _auth
        _store
        _clock
        _hostname: The hostname of the server creating the event
        _signing_key: The signing key to use to sign the event as the server
    """

    _state: StateHandler
    _event_auth_handler: "EventAuthHandler"
    _store: DataStore
    _clock: Clock
    _hostname: str
    _signing_key: SigningKey

    room_version: RoomVersion

    room_id: str
    type: str
    sender: str

    content: JsonDict = attr.Factory(dict)
    unsigned: JsonDict = 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: Optional[str] = None
    _redacts: Optional[str] = None
    _origin_server_ts: Optional[int] = None

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

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

        raise AttributeError("state_key")

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

    async def build(
        self,
        prev_event_ids: Collection[str],
        auth_event_ids: Optional[List[str]],
        depth: Optional[int] = None,
    ) -> EventBase:
        """Transform into a fully signed and hashed event

        Args:
            prev_event_ids: The event IDs to use as the prev events
            auth_event_ids: The event IDs to use as the auth events.
                Should normally be set to None, which will cause them to be calculated
                based on the room state at the prev_events.
            depth: Override the depth used to order the event in the DAG.
                Should normally be set to None, which will cause the depth to be calculated
                based on the prev_events.

        Returns:
            The signed and hashed event.
        """
        if auth_event_ids is None:
            state_ids = await self._state.compute_state_after_events(
                self.room_id,
                prev_event_ids,
                state_filter=StateFilter.from_types(
                    auth_types_for_event(self.room_version, self)
                ),
                await_full_state=False,
            )
            auth_event_ids = self._event_auth_handler.compute_auth_events(
                self, state_ids
            )

        format_version = self.room_version.event_format
        # The types of auth/prev events changes between event versions.
        prev_events: Union[Collection[str], List[Tuple[str, Dict[str, str]]]]
        auth_events: Union[List[str], List[Tuple[str, Dict[str, str]]]]
        if format_version == EventFormatVersions.ROOM_V1_V2:
            auth_events = await self._store.add_event_hashes(auth_event_ids)
            prev_events = await self._store.add_event_hashes(prev_event_ids)
        else:
            auth_events = auth_event_ids
            prev_events = prev_event_ids

        # Otherwise, progress the depth as normal
        if depth is None:
            (
                _,
                most_recent_prev_event_depth,
            ) = await self._store.get_max_depth_of(prev_event_ids)

            depth = most_recent_prev_event_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: Dict[str, Any] = {
            "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,
        }

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

        # MSC2174 moves the redacts property to the content, it is invalid to
        # provide it as a top-level property.
        if self._redacts is not None and not self.room_version.msc2176_redaction_rules:
            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:
    def __init__(self, hs: "HomeServer"):
        self.clock = hs.get_clock()
        self.hostname = hs.hostname
        self.signing_key = hs.signing_key

        self.store = hs.get_datastores().main
        self.state = hs.get_state_handler()
        self._event_auth_handler = hs.get_event_auth_handler()

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

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

        Returns:
            EventBuilder
        """
        return EventBuilder(
            store=self.store,
            state=self.state,
            event_auth_handler=self._event_auth_handler,
            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.ROOM_V1_V2:
        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 make_event_from_dict(
        event_dict, room_version, internal_metadata_dict=internal_metadata_dict
    )


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


def _create_event_id(clock: Clock, hostname: str) -> str:
    """Create a new event ID

    Args:
        clock
        hostname: The server name for the event ID

    Returns:
        The new event ID
    """

    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()
