#!/usr/bin/env python3
import sys

if sys.version_info < (3, 8):
    raise RuntimeError("At least Python 3.8 is required")

import logging, asyncio
from mastodon import Mastodon
from asyncio.events import AbstractEventLoop
from threading import Lock, Event
from asyncio import Task
from .mast import MastodonPoster, StreamEvent
from .config import ClientConfiguration
from typing import Optional, Dict, Any


class StreamingClient(object):
    def __init__(self, config: ClientConfiguration):
        self.config = config
        self.mastodon = self.create_mastodon(self.config)
        self.loop: Optional[AbstractEventLoop] = None
        self.loop_task: Optional[Task] = None
        self.stopped = Event()
        self.lock = Lock()

    @classmethod
    def create_mastodon(self, config: ClientConfiguration) -> MastodonPoster:
        return MastodonPoster(
            config.api_base_url,
            ClientConfiguration.ensure_set(config.client_id, "client_id"),
            ClientConfiguration.ensure_set(config.client_secret, "client_secret"),
            config.access_token,
            list(config.scopes),
        )

    async def on_connect(self) -> None:
        pass

    async def on_message(self, event: StreamEvent) -> None:
        raise NotImplementedError(f"Unhandled event: {event}")

    async def run(self, stream: str):
        while True:
            try:
                await self.on_connect()
                async for message in self.mastodon.stream(stream):
                    try:
                        await self.on_message(message)
                    except:
                        logging.exception(f"Failed to handle {message}")

                    if self.stopped.is_set():
                        return
            except:
                if self.stopped.wait(60):
                    break
                logging.exception("Reconnecting after exception")

    def start(self, stream: str):
        with self.lock:
            self.loop = asyncio.new_event_loop()
            self.task = self.loop.create_task(self.run(stream))
            loop = self.loop

        try:
            loop.run_forever()
        finally:
            self.stopped.set()

    def stop(self):
        with self.lock:
            if self.loop is None or self.task is None:
                raise RuntimeError("Client has not been started.")

            self.stopped.set()
            self.task.cancel()
            self.loop.call_soon_threadsafe(self.loop.stop)
            self.loop = None
            self.task = None

    def get_loop(self) -> AbstractEventLoop:
        with self.lock:
            if self.loop is None:
                raise RuntimeError("Loop is not running")
            return self.loop

    def login(self) -> None:
        self.config.access_token = self.mastodon.setup_token()
        self.mastodon = self.create_mastodon(self.config)

    @classmethod
    def register(cls, config: ClientConfiguration) -> None:
        config.client_id, config.client_secret = MastodonPoster.get_secrets_from_server(
            config.api_base_url, config.app_name, config.scopes
        )


def run_test_client() -> None:
    logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
    from argparse import ArgumentParser
    from .config import CPConfiguration
    from hashlib import sha3_512
    from copy import deepcopy
    from pathlib import Path
    from pprint import pformat

    class TestConfig(CPConfiguration):
        BASE_CONFIG = deepcopy(CPConfiguration.BASE_CONFIG)
        BASE_CONFIG["Mastodon"][
            "scopes"
        ] = "read:statuses read:notifications write:statuses read:accounts"
        BASE_CONFIG["Mastodon"]["app_name"] = "plmast_test"

    class TestClient(StreamingClient):
        @staticmethod
        def sha3(s: str) -> str:
            digester = sha3_512()
            digester.update(s.encode("utf8"))
            return digester.hexdigest()

        async def post_reply_to(self, payload: Dict[str, Any]) -> None:
            post_id = payload["id"]
            post_content = payload["content"]
            self.mastodon.reply(self.sha3(post_content), in_reply_to=payload)

        async def on_message(self, event: StreamEvent) -> None:
            logging.info("%s: %s" % (event.event, pformat(event.payload)))
            if event.event != "update":
                return

            try:
                if event.payload["account"]["id"] == self.mastodon.user_id:
                    return
            except KeyError:
                return

            return await self.post_reply_to(event.payload)

    aparser = ArgumentParser(usage="%(prog)s -c config.ini")
    aparser.add_argument(
        "--config",
        "-c",
        dest="config",
        type=Path,
        metavar="config.ini",
        required=True,
        help="Configuration parser",
    )
    args = aparser.parse_args()

    config = TestConfig(args.config)
    try:
        try:
            if not all([config.api_base_url, config.app_name]):
                raise ValueError(f"api_base_url and app_name have not been set")
        except:
            config.mark_modified()
            raise

        if not all([config.client_id, config.client_secret]):
            logging.info(f"Acquiring client secrets from {config.api_base_url}")
            StreamingClient.register(config)

        client = TestClient(config)
        if not config.access_token:
            logging.info(f"Logging in to {config.api_base_url}")
            client.login()

        logging.info("Starting a notification stream")
        client.start("user")

    finally:
        config.save()
