import asyncio
import logging
import os

import tornado
from tornado.web import StaticFileHandler

from core.config import ALLOW_SPOTTING, WEB_SERVER_PORT, API_ONLY_MODE, LOG_WEB_REQUESTS, BASE_URL
from core.data_providers import DATA_PROVIDERS
from core.data_store import DATA_STORE
from server.handlers.api.addspot import APISpotHandler
from server.handlers.api.alerts import APIAlertsHandler, APIAlertsStreamHandler
from server.handlers.api.dxstats import APIDxStatsHandler
from server.handlers.api.lookups import APILookupCallHandler, APILookupSIGRefHandler, APILookupGridHandler
from server.handlers.api.options import APIOptionsHandler
from server.handlers.api.solar_conditions import APISolarConditionsHandler
from server.handlers.api.spots import APISpotsHandler, APISpotsStreamHandler
from server.handlers.api.status import APIStatusHandler
from server.handlers.api.v1_addspot import V1APISpotHandler
from server.handlers.api.v1_compatability import V1RedirectHandler
from server.handlers.api.v1_spots import V1APISpotsHandler, V1APISpotsStreamHandler
from server.handlers.manifesthandler import ManifestHandler
from server.handlers.metrics import PrometheusMetricsHandler
from server.handlers.pagetemplate import PageTemplateHandler
from server.sse_broadcaster import SSEBroadcaster

_HERE = os.path.dirname(__file__ or "")


class WebServer:
    """Provides the public-facing web server."""

    def __init__(self):
        """Constructor"""

        self._data_store = DATA_STORE
        self._data_providers = DATA_PROVIDERS
        self._spot_broadcaster = SSEBroadcaster()
        self._alert_broadcaster = SSEBroadcaster()
        self._port = WEB_SERVER_PORT
        self._api_only_mode = API_ONLY_MODE
        self._shutdown_event = asyncio.Event()
        self.web_server_metrics = {
            "last_page_access_time": None,
            "last_api_access_time": None,
            "page_access_counter": 0,
            "api_access_counter": 0,
            "status": "Starting"
        }

    def setup(self):
        # Listen for new spots and alerts being added to the cache, so we can notify SSE clients immediately
        DATA_STORE.spots.add_listener(self._spot_broadcaster.publish)
        DATA_STORE.alerts.add_listener(self._alert_broadcaster.publish)

    def start(self):
        """Start the web server"""

        asyncio.run(self._start_inner())

    def stop(self):
        """Stop the web server"""

        self._shutdown_event.set()

    async def _start_inner(self):
        """Start method (async). Sets up the Tornado application."""

        # Bind the SSE broadcasters to the web server's loop, so they fire correctly
        self._spot_broadcaster.bind_to_web_server_loop()
        self._alert_broadcaster.bind_to_web_server_loop()

        # Prepare a list of common arguments that are passed in to every API & page handler. This is just a basic thing
        # to avoid copy-pasting the same thing to every route declaration below.
        handler_opts = {"web_server_metrics": self.web_server_metrics}

        # API endpoints are always enabled
        api_routes = [
            (r"/api/v2/spots", APISpotsHandler, {"spots": self._data_store.spots, **handler_opts}),
            (r"/api/v2/alerts", APIAlertsHandler, {"alerts": self._data_store.alerts, **handler_opts}),
            (r"/api/v2/spots/stream", APISpotsStreamHandler,
             {"sse_spot_broadcaster": self._spot_broadcaster, **handler_opts}),
            (r"/api/v2/alerts/stream", APIAlertsStreamHandler,
             {"sse_alert_broadcaster": self._alert_broadcaster, **handler_opts}),
            (r"/api/v2/solar", APISolarConditionsHandler, {"solar_conditions": self._data_store.solar_conditions,
                                                           **handler_opts}),
            (r"/api/v2/dxstats", APIDxStatsHandler, {"spots": self._data_store.spots, **handler_opts}),
            (r"/api/v2/options", APIOptionsHandler, {"status_data": self._data_store.status_data, **handler_opts}),
            (r"/api/v2/status", APIStatusHandler, {"status_data": self._data_store.status_data, **handler_opts}),
            (r"/api/v2/lookup/call", APILookupCallHandler, {**handler_opts}),
            (r"/api/v2/lookup/sigref", APILookupSIGRefHandler, {**handler_opts}),
            (r"/api/v2/lookup/grid", APILookupGridHandler, {**handler_opts}),
            (r"/api/v2/spot", APISpotHandler,
             {"spots": self._data_store.spots, "spot_providers": self._data_providers, **handler_opts}),
        ]

        # v1 API redirects. Most v1 enpoints are unchanged in v2, and get an HTTP 308 redirect to the v2 API. The ones
        # that have the major breaking changes get a bespoke handler.
        v1_compat_routes = [
            (r"/api/v1/spots", V1APISpotsHandler, {"spots": self._data_store.spots, **handler_opts}),
            (r"/api/v1/spots/stream", V1APISpotsStreamHandler,
             {"sse_spot_broadcaster": self._spot_broadcaster, **handler_opts}),
            (r"/api/v1/spot", V1APISpotHandler),
            (r"/api/v1/(.*)", V1RedirectHandler),
        ]

        # If in API-only mode, serve a basic homepage; in normal mode, serve the usual UI routes
        if self._api_only_mode:
            logging.info("API-only mode is enabled. Web UI will not be served.")
            ui_routes = [
                (r"/", PageTemplateHandler, {"template_name": "api_only_home", **handler_opts})
            ]
        else:
            ui_routes = [
                (r"/", PageTemplateHandler, {"template_name": "spots", **handler_opts}),
                (r"/map", PageTemplateHandler, {"template_name": "map", **handler_opts}),
                (r"/bands", PageTemplateHandler, {"template_name": "bands", **handler_opts}),
                (r"/alerts", PageTemplateHandler, {"template_name": "alerts", **handler_opts}),
                (r"/conditions", PageTemplateHandler, {"template_name": "conditions", **handler_opts}),
                (r"/status", PageTemplateHandler, {"template_name": "status", **handler_opts}),
                (r"/about", PageTemplateHandler, {"template_name": "about", **handler_opts})
            ]
            # Only allow the Add Spot page if spotting is allowed
            if ALLOW_SPOTTING:
                ui_routes += [(r"/add-spot", PageTemplateHandler, {"template_name": "add_spot", **handler_opts})]

        # API docs, Prometheus metrics, webapp manifest and static assets are always available regardless of API-only
        # mode.
        misc_routes = [
            (r"/apidocs", PageTemplateHandler, {"template_name": "apidocs", **handler_opts}),
            (r"/metrics", PrometheusMetricsHandler),
            (r"/manifest.webmanifest", ManifestHandler),
            # If e.g. nginx is configured as a reverse proxy with a hard-coded path to static files, as per the README,
            # this will never have to handle anything, but having it here allows Spothole to work without nginx for
            # testing.
            (r"/static/(.*)", StaticFileHandler, {"path": os.path.join(_HERE, "../static")})
        ]

        app = tornado.web.Application(api_routes + v1_compat_routes + ui_routes + misc_routes,
                                      template_path=os.path.join(_HERE, "../templates"),
                                      log_function=request_log,
                                      debug=False)
        app.listen(self._port, xheaders=True)
        logging.info("Web server running on port " + str(WEB_SERVER_PORT))
        logging.info("You can access your copy of Spothole at " + BASE_URL)
        await self._shutdown_event.wait()


def request_log(handler):
    """Custom log function to provide more data about requests when enabled, and to provide the ability to turn off
    web request logging altogetether."""

    if LOG_WEB_REQUESTS:
        if handler.get_status() < 500:
            log_method = logging.info
        else:
            log_method = logging.warning

        request = handler.request
        client_ip = request.remote_ip
        referrer = request.headers.get("Referer", "-")
        user_agent = request.headers.get("User-Agent", "-")

        log_method(
            f'{client_ip} - "{request.method} {request.uri}" '
            f'{handler.get_status()} {request.request_time():.2f}ms | '
            f'Ref: {referrer} | UA: {user_agent}'
        )


# Global object
WEB_SERVER = WebServer()
