import logging
import os
from urllib.parse import parse_qs

from starlette.middleware.base import BaseHTTPMiddleware
from starlette.types import ASGIApp, Receive, Scope, Send

from nicegui import app


def setup() -> bool:
    """Setup fly.io specific settings.

    Returns True if running on fly.io, False otherwise.
    """
    if 'FLY_ALLOC_ID' not in os.environ:
        return False

    # NOTE In our global fly.io deployment we need to make sure that we connect back to the same instance.
    fly_instance_id = os.environ.get('FLY_ALLOC_ID', 'local').split('-')[0]
    app.config.socket_io_js_extra_headers['fly-force-instance-id'] = fly_instance_id  # for HTTP long polling
    app.config.socket_io_js_query_params['fly_instance_id'] = fly_instance_id  # for websocket (FlyReplayMiddleware)

    import dns.resolver  # NOTE only import on fly where we have it installed to look up if instance is still available

    @app.add_middleware
    class FlyReplayMiddleware(BaseHTTPMiddleware):
        """Replay to correct fly.io instance.

        If the wrong instance was picked by the fly.io load balancer, we use the fly-replay header
        to repeat the request again on the right instance.

        This only works if the correct instance is provided as a query_string parameter.
        """

        def __init__(self, app: ASGIApp) -> None:
            super().__init__(app)
            self.app = app
            self.app_name = os.environ.get('FLY_APP_NAME')

        async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
            query_string = scope.get('query_string', b'').decode()
            query_params = parse_qs(query_string)
            target_instance = query_params.get('fly_instance_id', [fly_instance_id])[0]

            async def send_wrapper(message):
                if target_instance != fly_instance_id and self.is_online(target_instance):
                    if message['type'] == 'websocket.close':
                        # fly.io only seems to look at the fly-replay header if websocket is accepted
                        message = {'type': 'websocket.accept'}
                    if 'headers' not in message:
                        message['headers'] = []
                    message['headers'].append([b'fly-replay', f'instance={target_instance}'.encode()])
                await send(message)
            try:
                await self.app(scope, receive, send_wrapper)
            except RuntimeError as e:
                if 'No response returned.' in str(e):
                    logging.warning(f'no response returned for {scope["path"]}')
                else:
                    logging.exception('could not handle request')

        def is_online(self, fly_instance_id: str) -> bool:
            hostname = f'{fly_instance_id}.vm.{self.app_name}.internal'
            try:
                dns.resolver.resolve(hostname, 'AAAA')
                return True
            except (dns.resolver.NoAnswer, dns.resolver.NXDOMAIN, dns.resolver.NoNameservers, dns.resolver.Timeout):
                return False

    return True
