#!/usr/bin/python3
#
# Univention Portal
#
# Like what you see? Join us!
# https://www.univention.com/about-us/careers/vacancies/
#
# Copyright 2019-2023 Univention GmbH
#
# https://www.univention.de/
#
# All rights reserved.
#
# The source code of this program is made available
# under the terms of the GNU Affero General Public License version 3
# (GNU AGPL V3) as published by the Free Software Foundation.
#
# Binary versions of this program provided by Univention to you as
# well as other copyrighted, protected or trademarked materials like
# Logos, graphics, fonts, specific documentations and configurations,
# cryptographic keys etc. are subject to a license agreement between
# you and Univention and not subject to the GNU AGPL V3.
#
# In the case you use this program under the terms of the GNU AGPL V3,
# the program is provided 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 with the Debian GNU/Linux or Univention distribution in file
# /usr/share/common-licenses/AGPL-3; if not, see
# <https://www.gnu.org/licenses/>.

import ipaddress
import json
import re
from urllib.parse import urlparse

import tornado.ioloop
import tornado.web
from ldap.dn import str2dn

from univention.portal import config
from univention.portal.factory import make_portal
from univention.portal.log import get_logger, setup_logger


RE_FQDN = re.compile(r'(?=^.{4,253}$)(^((?!-)[a-zA-Z0-9-]{1,63}(?<!-)\.)+[a-zA-Z]{2,63}\.?$)')


class PortalResource(tornado.web.RequestHandler):
    def initialize(self, portals):
        self.portals = portals

    def write_error(self, status_code, **kwargs):
        if "exc_info" in kwargs:
            get_logger("server").exception("Error during service")
        return super(PortalResource, self).write_error(status_code, **kwargs)

    def find_portal(self):
        best_score = 0
        best_portal = None
        for _name, portal in self.portals.items():
            score = portal.score(self.request)
            if score > best_score:
                best_score = score
                best_portal = portal
        return best_portal

    def reverse_abs_url(self, name, args=None):
        if args is None:
            args = self.path_args
        return self.request.protocol + "://" + self.request.host + self.reverse_url(name, *args)


class LoginHandler(PortalResource):
    async def post(self, portal_name):
        portal = self.find_portal()
        await portal.login_user(self)

    async def get(self, portal_name):
        portal = self.find_portal()
        await portal.login_request(self)


class LogoutHandler(PortalResource):

    async def get(self, portal_name):
        portal = self.find_portal()
        await portal.logout_user(self)


class PortalEntriesHandler(PortalResource):
    async def get(self, portal_name):
        portal = self.find_portal()
        if not portal:
            raise tornado.web.HTTPError(404)

        user = await portal.get_user(self)

        admin_mode = False
        if self.request.headers.get("X-Univention-Portal-Admin-Mode", "no") == "yes":
            get_logger("admin").info("Admin mode requested")
            admin_mode = user.is_admin()
            if admin_mode:
                get_logger("admin").info("Admin mode granted")
            else:
                get_logger("admin").info("Admin mode rejected")

        answer = {}
        answer["cache_id"] = portal.get_cache_id()
        visible_content = portal.get_visible_content(user, admin_mode)
        answer["user_links"] = portal.get_user_links(visible_content)
        answer["menu_links"] = portal.get_menu_links(visible_content)
        answer["entries"] = portal.get_entries(visible_content)
        answer["folders"] = portal.get_folders(visible_content)
        answer["categories"] = portal.get_categories(visible_content)
        answer["portal"] = portal.get_meta(visible_content, answer["categories"])
        if not user.is_anonymous() and not admin_mode and answer["portal"].get("showUmc"):
            # this is not how the portal-server is supposed to be working
            # but we need it like that...
            umc_portal = portal._get_umc_portal()
            umc_content = umc_portal.get_visible_content(user, admin_mode)
            answer["entries"].extend(umc_portal.get_entries(umc_content))
            answer["folders"].extend(umc_portal.get_folders(umc_content))
            answer["categories"].extend(umc_portal.get_categories(umc_content))
            umc_meta = umc_portal.get_meta(umc_content, answer["categories"])
            answer["portal"]["content"].extend(umc_meta["content"])
        answer["filtered"] = not admin_mode
        answer["username"] = user.username
        answer["user_displayname"] = user.display_name
        answer["auth_mode"] = portal.auth_mode(self)
        answer["may_edit_portal"] = portal.may_be_edited(user)
        self.write(answer)


class NavigationHandler(PortalResource):

    async def get(self, portal_name):
        portal = self.find_portal()
        if not portal:
            raise tornado.web.HTTPError(404)

        self._portal_lang = self.get_query_argument("language", "en-US").replace('-', '_')
        self._portal_base = self.get_query_argument("base", self.reverse_abs_url('root', ())).rstrip('/')

        user = await portal.get_user(self)

        visible_content = portal.get_visible_content(user, False)
        categories_content = portal.get_categories(visible_content)
        meta = portal.get_meta(visible_content, categories_content)
        entries = portal.portal_cache.get_entries()
        visible_entry_dns = portal._filter_entry_dns(entries.keys(), entries, user, False)

        def get_category(category_dn):
            for category in categories_content:
                if category["dn"] == category_dn:
                    return category

        categories = []
        for category_dn, _ in meta['content']:
            category_data = get_category(category_dn)
            if not category_data:
                continue

            category = {
                "identifier": str2dn(category_dn)[0][0][1],
                "display_name": self._choose_language(category_data["display_name"]),
                "entries": [
                    self._get_entry(entries[entry_dn], entry_dn)
                    for entry_dn in category_data["entries"]
                    if entry_dn in visible_entry_dns
                ],
            }

            if not category["entries"]:
                continue
            categories.append(category)

        navigation = {
            'categories': categories,
        }
        self.write(navigation)

    def _get_entry(self, entry_data, entry_dn):
        icon_url = entry_data["logo_name"] or None
        if icon_url and icon_url.startswith('.'):  # most icons are referenced as ./portal/foo.svg
            icon_url = icon_url[1:]
        icons = {self._portal_lang: [icon_url] if icon_url else []}

        links = {}
        for link in entry_data["links"]:
            links.setdefault(link["locale"], []).append(link["value"])

        return {
            "identifier": str2dn(entry_dn)[0][0][1],
            "icon_url": self._choose_url(icons, self._portal_base + '/univention/portal'),
            "display_name": self._choose_language(entry_data["name"]),
            "link": self._choose_url(links, self._portal_base),
            "target": entry_data.get("target") or '_blank',
            "keywords": entry_data.get("keywords"),
        }

    def _choose_language(self, entry):
        return entry.get(self._portal_lang) or entry.get("en_US")

    def _choose_url(self, links, base):
        # rules:
        # - filter on the requested language otherwise fallback to en_US
        # - always fqdn before ip
        # - always https before http

        links = self._choose_language(links)
        if not links:
            return

        fqdn_links, ip_links, path_links = [], [], []
        for link in links:
            parsed = urlparse(link)
            try:
                ipaddress.ip_address(parsed.netloc)
            except ValueError:
                if RE_FQDN.search(parsed.netloc):
                    fqdn_links.append({'link': link, 'parsed': parsed})
                else:
                    path_links.append({'link': base + link, 'parsed': parsed})
            else:
                ip_links.append({'link': link, 'parsed': parsed})

        def prefer_https(links):
            for linkdict in links:
                if linkdict['parsed'].scheme == "https":
                    return linkdict["link"]

            # if we are here, we had fqdn links but none https; return the first fqdn link from list
            return links[0]["link"]

        for links in (fqdn_links, ip_links, path_links):
            if links:
                return prefer_https(links)


def get_portals():
    ret = {}
    with open("/usr/share/univention-portal/portals.json") as fd:
        portal_definitions = json.load(fd)
    for name, portal_definition in portal_definitions.items():
        get_logger("server").info(f"Building portal {name}")
        ret[name] = make_portal(portal_definition)
    return ret


def make_app():
    portals = get_portals()
    return tornado.web.Application(
        [
            tornado.web.url(r"/(.+)/login/?", LoginHandler, {"portals": portals}, name='login'),
            tornado.web.url(r"/(.+)/portal.json", PortalEntriesHandler, {"portals": portals}, name='portal'),
            tornado.web.url(r"/(.+)/navigation.json", NavigationHandler, {"portals": portals}, name='navigation'),
            tornado.web.url(r"/(.+)/logout/?", LogoutHandler, {"portals": portals}, name='logout'),
            tornado.web.url(r"/(.+)/", tornado.web.RequestHandler, name='index'),
            tornado.web.url(r"/", tornado.web.RequestHandler, name='root'),
        ],
    )


if __name__ == "__main__":
    setup_logger()
    app = make_app()
    port = config.fetch("port")
    get_logger("server").info("firing up portal server at port %s" % port)
    app.listen(port)
    tornado.ioloop.IOLoop.current().start()
