try:
    import cherrypy
    from cherrypy._cpserver import Server
except ImportError:
    # to avoid sphinx build crash
    class Server:  # type: ignore
        pass

import logging

import orchestrator  # noqa
from mgr_util import build_url
from typing import Dict, List, TYPE_CHECKING, cast, Collection, Callable, NamedTuple, Optional, IO
from cephadm.services.nfs import NFSService
from cephadm.services.monitoring import AlertmanagerService, NodeExporterService, PrometheusService
import secrets
from mgr_util import verify_tls_files
import tempfile

from cephadm.services.ingress import IngressSpec
from cephadm.services.cephadmservice import CephExporterService
from cephadm.services.nvmeof import NvmeofService
from cephadm.services.service_registry import service_registry

from ceph.deployment.service_spec import SMBSpec

if TYPE_CHECKING:
    from cephadm.module import CephadmOrchestrator


def cherrypy_filter(record: logging.LogRecord) -> bool:
    blocked = [
        'TLSV1_ALERT_DECRYPT_ERROR'
    ]
    msg = record.getMessage()
    return not any([m for m in blocked if m in msg])


logging.getLogger('cherrypy.error').addFilter(cherrypy_filter)
cherrypy.log.access_log.propagate = False
logger = logging.getLogger(__name__)


class Route(NamedTuple):
    name: str
    route: str
    controller: Callable


class ServiceDiscovery:

    def __init__(self, mgr: "CephadmOrchestrator") -> None:
        self.mgr = mgr
        self.username: Optional[str] = None
        self.password: Optional[str] = None
        self.key_file: IO[bytes]
        self.cert_file: IO[bytes]

    def validate_password(self, realm: str, username: str, password: str) -> bool:
        return (password == self.password and username == self.username)

    def configure_routes(self, server: Server, enable_auth: bool) -> None:
        ROUTES = [
            Route('index', '/', server.index),
            Route('sd-config', '/prometheus/sd-config', server.get_sd_config),
            Route('rules', '/prometheus/rules', server.get_prometheus_rules),
        ]
        d = cherrypy.dispatch.RoutesDispatcher()
        for route in ROUTES:
            d.connect(**route._asdict())
        if enable_auth:
            conf = {
                '/': {
                    'request.dispatch': d,
                    'tools.auth_basic.on': True,
                    'tools.auth_basic.realm': 'localhost',
                    'tools.auth_basic.checkpassword': self.validate_password
                }
            }
        else:
            conf = {'/': {'request.dispatch': d}}
        cherrypy.tree.mount(None, '/sd', config=conf)

    def enable_auth(self) -> None:
        self.username = self.mgr.get_store('service_discovery/root/username')
        self.password = self.mgr.get_store('service_discovery/root/password')
        if not self.password or not self.username:
            self.username = 'admin'  # TODO(redo): what should be the default username
            self.password = secrets.token_urlsafe(20)
            self.mgr.set_store('service_discovery/root/password', self.password)
            self.mgr.set_store('service_discovery/root/username', self.username)

    def configure_tls(self, server: Server) -> None:
        addr = self.mgr.get_mgr_ip()
        host = self.mgr.get_hostname()
        cert, key = self.mgr.cert_mgr.generate_cert(host, addr)
        self.cert_file = tempfile.NamedTemporaryFile()
        self.cert_file.write(cert.encode('utf-8'))
        self.cert_file.flush()  # cert_tmp must not be gc'ed

        self.key_file = tempfile.NamedTemporaryFile()
        self.key_file.write(key.encode('utf-8'))
        self.key_file.flush()  # pkey_tmp must not be gc'ed

        verify_tls_files(self.cert_file.name, self.key_file.name)

        server.ssl_certificate, server.ssl_private_key = self.cert_file.name, self.key_file.name

    def configure(self, port: int, addr: str, enable_security: bool) -> None:
        # we create a new server to enforce TLS/SSL config refresh
        self.root_server = Root(self.mgr, port, addr)
        self.root_server.ssl_certificate = None
        self.root_server.ssl_private_key = None
        if enable_security:
            self.enable_auth()
            self.configure_tls(self.root_server)
        self.configure_routes(self.root_server, enable_security)


class Root(Server):

    # collapse everything to '/'
    def _cp_dispatch(self, vpath: str) -> 'Root':
        cherrypy.request.path = ''
        return self

    def stop(self) -> None:
        # we must call unsubscribe before stopping the server,
        # otherwise the port is not released and we will get
        # an exception when trying to restart it
        self.unsubscribe()
        super().stop()

    def __init__(self, mgr: "CephadmOrchestrator", port: int = 0, host: str = ''):
        self.mgr = mgr
        super().__init__()
        self.socket_port = port
        self.socket_host = host
        self.subscribe()

    @cherrypy.expose
    def index(self) -> str:
        return self.mgr.get_store('service_discovery/index') or '''<!DOCTYPE html>
<html>
<head><title>Cephadm HTTP Endpoint</title></head>
<body>
<h2>Cephadm Service Discovery Endpoints</h2>
<p><a href='prometheus/sd-config?service=mgr-prometheus'>mgr/Prometheus http sd-config</a></p>
<p><a href='prometheus/sd-config?service=alertmanager'>Alertmanager http sd-config</a></p>
<p><a href='prometheus/sd-config?service=node-exporter'>Node exporter http sd-config</a></p>
<p><a href='prometheus/sd-config?service=haproxy'>HAProxy http sd-config</a></p>
<p><a href='prometheus/sd-config?service=ceph-exporter'>Ceph exporter http sd-config</a></p>
<p><a href='prometheus/sd-config?service=nvmeof'>NVMeoF http sd-config</a></p>
<p><a href='prometheus/sd-config?service=nfs'>NFS http sd-config</a></p>
<p><a href='prometheus/sd-config?service=smb'>SMB http sd-config</a></p>
<p><a href='prometheus/rules'>Prometheus rules</a></p>
</body>
</html>'''

    @cherrypy.expose
    @cherrypy.tools.json_out()
    def get_sd_config(self, service: str) -> List[Dict[str, Collection[str]]]:
        """Return <http_sd_config> compatible prometheus config for the specified service."""
        if service == 'mgr-prometheus':
            return self.prometheus_sd_config()
        elif service == 'alertmanager':
            return self.alertmgr_sd_config()
        elif service == 'node-exporter':
            return self.node_exporter_sd_config()
        elif service == 'haproxy':
            return self.haproxy_sd_config()
        elif service == 'ceph-exporter':
            return self.ceph_exporter_sd_config()
        elif service == 'nvmeof':
            return self.nvmeof_sd_config()
        elif service == 'nfs':
            return self.nfs_sd_config()
        elif service == 'smb':
            return self.smb_sd_config()
        elif service.startswith("container"):
            return self.container_sd_config(service)
        else:
            return []

    def prometheus_sd_config(self) -> List[Dict[str, Collection[str]]]:
        """Return <http_sd_config> compatible prometheus config for prometheus service.
        Targets should be a length one list containing only the active mgr
        """
        targets = []
        mgr_daemons = self.mgr.cache.get_daemons_by_service('mgr')
        host = service_registry.get_service('mgr').get_active_daemon(mgr_daemons).hostname or ''
        fqdn = self.mgr.get_fqdn(host)
        port = self.mgr.get_module_option_ex(
            'prometheus', 'server_port', PrometheusService.DEFAULT_MGR_PROMETHEUS_PORT)
        targets.append(f'{fqdn}:{port}')
        return [{"targets": targets, "labels": {}}]

    def alertmgr_sd_config(self) -> List[Dict[str, Collection[str]]]:
        """Return <http_sd_config> compatible prometheus config for mgr alertmanager service."""
        srv_entries = []
        for dd in self.mgr.cache.get_daemons_by_service('alertmanager'):
            assert dd.hostname is not None
            addr = dd.ip if dd.ip else self.mgr.inventory.get_addr(dd.hostname)
            port = dd.ports[0] if dd.ports else AlertmanagerService.DEFAULT_SERVICE_PORT
            srv_entries.append('{}'.format(build_url(host=addr, port=port).lstrip('/')))
        return [{"targets": srv_entries, "labels": {}}]

    def node_exporter_sd_config(self) -> List[Dict[str, Collection[str]]]:
        """Return <http_sd_config> compatible prometheus config for node-exporter service."""
        srv_entries = []
        for dd in self.mgr.cache.get_daemons_by_service('node-exporter'):
            assert dd.hostname is not None
            addr = dd.ip if dd.ip else self.mgr.inventory.get_addr(dd.hostname)
            port = dd.ports[0] if dd.ports else NodeExporterService.DEFAULT_SERVICE_PORT
            srv_entries.append({
                'targets': [build_url(host=addr, port=port).lstrip('/')],
                'labels': {'instance': dd.hostname}
            })
        return srv_entries

    def haproxy_sd_config(self) -> List[Dict[str, Collection[str]]]:
        """Return <http_sd_config> compatible prometheus config for haproxy service."""
        srv_entries = []
        for dd in self.mgr.cache.get_daemons_by_type('ingress'):
            if dd.service_name() in self.mgr.spec_store:
                spec = cast(IngressSpec, self.mgr.spec_store[dd.service_name()].spec)
                assert dd.hostname is not None
                if dd.daemon_type == 'haproxy':
                    addr = self.mgr.inventory.get_addr(dd.hostname)
                    srv_entries.append({
                        'targets': [f"{build_url(host=addr, port=spec.monitor_port).lstrip('/')}"],
                        'labels': {'ingress': dd.service_name(), 'instance': dd.hostname}
                    })
        return srv_entries

    def ceph_exporter_sd_config(self) -> List[Dict[str, Collection[str]]]:
        """Return <http_sd_config> compatible prometheus config for ceph-exporter service."""
        srv_entries = []
        for dd in self.mgr.cache.get_daemons_by_service('ceph-exporter'):
            assert dd.hostname is not None
            addr = dd.ip if dd.ip else self.mgr.inventory.get_addr(dd.hostname)
            port = dd.ports[0] if dd.ports else CephExporterService.DEFAULT_SERVICE_PORT
            srv_entries.append({
                'targets': [build_url(host=addr, port=port).lstrip('/')],
                'labels': {'instance': dd.hostname}
            })
        return srv_entries

    def nvmeof_sd_config(self) -> List[Dict[str, Collection[str]]]:
        """Return <http_sd_config> compatible prometheus config for nvmeof service."""
        srv_entries = []
        for dd in self.mgr.cache.get_daemons_by_type('nvmeof'):
            assert dd.hostname is not None
            addr = dd.ip if dd.ip else self.mgr.inventory.get_addr(dd.hostname)
            port = NvmeofService.PROMETHEUS_PORT
            srv_entries.append({
                'targets': [build_url(host=addr, port=port).lstrip('/')],
                'labels': {'instance': dd.hostname}
            })
        return srv_entries

    def nfs_sd_config(self) -> List[Dict[str, Collection[str]]]:
        """Return <http_sd_config> compatible prometheus config for nfs service."""
        srv_entries = []
        for dd in self.mgr.cache.get_daemons_by_type('nfs'):
            assert dd.hostname is not None
            addr = dd.ip if dd.ip else self.mgr.inventory.get_addr(dd.hostname)
            port = NFSService.DEFAULT_EXPORTER_PORT
            srv_entries.append({
                'targets': [build_url(host=addr, port=port).lstrip('/')],
                'labels': {'instance': dd.hostname}
            })
        return srv_entries

    def smb_sd_config(self) -> List[Dict[str, Collection[str]]]:
        """Return <http_sd_config> compatible prometheus config for smb service."""
        srv_entries = []
        for dd in self.mgr.cache.get_daemons_by_type('smb'):
            assert dd.hostname is not None
            try:
                spec = cast(SMBSpec, self.mgr.spec_store[dd.service_name()].spec)
            except KeyError:
                logger.warning("no spec found for %s", dd.service_name())
                continue
            # TODO: needs updating once ip control/colocation is present
            addr = dd.ip if dd.ip else self.mgr.inventory.get_addr(dd.hostname)
            port = spec.metrics_exporter_port()
            srv_entries.append({
                'targets': [build_url(host=addr, port=port).lstrip('/')],
                'labels': {'instance': dd.hostname}
            })
        return srv_entries

    def container_sd_config(self, service: str) -> List[Dict[str, Collection[str]]]:
        """Return <http_sd_config> compatible prometheus config for a container service."""
        srv_entries = []
        for dd in self.mgr.cache.get_daemons_by_service(service):
            assert dd.hostname is not None
            addr = dd.ip if dd.ip else self.mgr.inventory.get_addr(dd.hostname)
            if not dd.ports:
                continue
            port = dd.ports[0]
            srv_entries.append({
                'targets': [build_url(host=addr, port=port).lstrip('/')],
                'labels': {'instance': dd.hostname}
            })
        return srv_entries

    @cherrypy.expose(alias='prometheus/rules')
    def get_prometheus_rules(self) -> str:
        """Return currently configured prometheus rules as Yaml."""
        cherrypy.response.headers['Content-Type'] = 'text/plain'
        with open(self.mgr.prometheus_alerts_path, 'r', encoding='utf-8') as f:
            return f.read()
