# mautrix-twitter - A Matrix-Twitter DM puppeting bridge
# Copyright (C) 2020 Tulir Asokan
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <https://www.gnu.org/licenses/>.
from typing import Optional, Dict, AsyncIterable, Awaitable, AsyncGenerator, TYPE_CHECKING, cast
from os import path

from aiohttp import ClientSession
from yarl import URL

from mautwitdm.types import User
from mautrix.bridge import BasePuppet, async_getter_lock
from mautrix.appservice import IntentAPI
from mautrix.types import ContentURI, UserID, SyncToken, RoomID
from mautrix.util.simple_template import SimpleTemplate

from .db import Puppet as DBPuppet
from .config import Config
from . import portal as p

if TYPE_CHECKING:
    from .__main__ import TwitterBridge


class Puppet(DBPuppet, BasePuppet):
    by_twid: Dict[int, 'Puppet'] = {}
    by_custom_mxid: Dict[UserID, 'Puppet'] = {}
    hs_domain: str
    mxid_template: SimpleTemplate[int]

    config: Config

    default_mxid_intent: IntentAPI
    default_mxid: UserID

    def __init__(self, twid: int, name: Optional[str] = None, photo_url: Optional[str] = None,
                 photo_mxc: Optional[ContentURI] = None, is_registered: bool = False,
                 custom_mxid: Optional[UserID] = None, access_token: Optional[str] = None,
                 next_batch: Optional[SyncToken] = None, base_url: Optional[URL] = None) -> None:
        super().__init__(twid=twid, name=name, photo_url=photo_url, photo_mxc=photo_mxc,
                         is_registered=is_registered, custom_mxid=custom_mxid,
                         access_token=access_token, next_batch=next_batch, base_url=base_url)
        self.log = self.log.getChild(str(twid))

        self.default_mxid = self.get_mxid_from_id(twid)
        self.default_mxid_intent = self.az.intent.user(self.default_mxid)
        self.intent = self._fresh_intent()

    @classmethod
    def init_cls(cls, bridge: 'TwitterBridge') -> AsyncIterable[Awaitable[None]]:
        cls.config = bridge.config
        cls.loop = bridge.loop
        cls.mx = bridge.matrix
        cls.az = bridge.az
        cls.hs_domain = cls.config["homeserver.domain"]
        cls.mxid_template = SimpleTemplate(cls.config["bridge.username_template"], "userid",
                                           prefix="@", suffix=f":{cls.hs_domain}", type=int)
        cls.sync_with_custom_puppets = cls.config["bridge.sync_with_custom_puppets"]
        cls.homeserver_url_map = {server: URL(url) for server, url
                                  in cls.config["bridge.double_puppet_server_map"].items()}
        cls.allow_discover_url = cls.config["bridge.double_puppet_allow_discovery"]
        cls.login_shared_secret_map = {server: secret.encode("utf-8") for server, secret
                                       in cls.config["bridge.login_shared_secret_map"].items()}
        cls.login_device_name = "Twitter DM Bridge"
        return (puppet.try_start() async for puppet in cls.all_with_custom_mxid())

    def intent_for(self, portal: 'p.Portal') -> IntentAPI:
        if portal.other_user == self.twid or (self.config["bridge.backfill.invite_own_puppet"]
                                              and portal.backfill_lock.locked):
            return self.default_mxid_intent
        return self.intent

    async def update_info(self, info: User) -> None:
        update = False
        update = await self._update_name(info) or update
        update = await self._update_avatar(info.profile_image_url_https) or update
        if update:
            await self.update()

    @classmethod
    def _get_displayname(cls, info: User) -> str:
        return cls.config["bridge.displayname_template"].format(displayname=info.name, id=info.id,
                                                                username=info.screen_name)

    async def _update_name(self, info: User) -> bool:
        name = self._get_displayname(info)
        if name != self.name:
            self.name = name
            await self.default_mxid_intent.set_displayname(self.name)
            await self._update_portal_names()
            return True
        return False

    async def _update_portal_names(self) -> None:
        async for portal in p.Portal.find_private_chats_with(self.twid):
            await portal.update_name(self.name)

    async def _update_avatar(self, image_url: str) -> bool:
        if image_url != self.photo_url:
            self.photo_url = image_url
            url = URL(self.photo_url.replace("_normal.", "_400x400."))
            file_name = path.basename(url.path)
            async with ClientSession() as sess, sess.get(url) as resp:
                content_type = resp.headers["Content-Type"]
                resp_data = await resp.read()
            mxc = await self.default_mxid_intent.upload_media(data=resp_data, filename=file_name,
                                                              mime_type=content_type)
            self.photo_mxc = mxc
            await self.default_mxid_intent.set_avatar_url(mxc)
            return True
        return False

    async def default_puppet_should_leave_room(self, room_id: RoomID) -> bool:
        portal = await p.Portal.get_by_mxid(room_id)
        return portal and portal.other_user != self.twid

    # region Database getters

    def _add_to_cache(self) -> None:
        self.by_twid[self.twid] = self
        if self.custom_mxid:
            self.by_custom_mxid[self.custom_mxid] = self

    async def save(self) -> None:
        await self.update()

    @classmethod
    async def get_by_mxid(cls, mxid: UserID, create: bool = True) -> Optional['Puppet']:
        twid = cls.get_id_from_mxid(mxid)
        if twid:
            return await cls.get_by_twid(twid, create=create)
        return None

    @classmethod
    @async_getter_lock
    async def get_by_custom_mxid(cls, mxid: UserID) -> Optional['Puppet']:
        try:
            return cls.by_custom_mxid[mxid]
        except KeyError:
            pass

        puppet = cast(cls, await super().get_by_custom_mxid(mxid))
        if puppet:
            puppet._add_to_cache()
            return puppet

        return None

    @classmethod
    def get_id_from_mxid(cls, mxid: UserID) -> Optional[int]:
        return cls.mxid_template.parse(mxid)

    @classmethod
    def get_mxid_from_id(cls, twid: int) -> UserID:
        return UserID(cls.mxid_template.format_full(twid))

    @classmethod
    @async_getter_lock
    async def get_by_twid(cls, twid: int, *, create: bool = True) -> Optional['Puppet']:
        try:
            return cls.by_twid[twid]
        except KeyError:
            pass

        puppet = cast(cls, await super().get_by_twid(twid))
        if puppet is not None:
            puppet._add_to_cache()
            return puppet

        if create:
            puppet = cls(twid)
            await puppet.insert()
            puppet._add_to_cache()
            return puppet

        return None

    @classmethod
    async def all_with_custom_mxid(cls) -> AsyncGenerator['Puppet', None]:
        puppets = await super().all_with_custom_mxid()
        puppet: cls
        for index, puppet in enumerate(puppets):
            try:
                yield cls.by_twid[puppet.twid]
            except KeyError:
                puppet._add_to_cache()
                yield puppet

    # endregion
