{ config, containerCfg, pkgs, lib, builder, name, ... }:
let
  serverCfg = config.syscfg.server;
  mediaCfg = config.syscfg.media;
  palette = serverCfg.colorScheme.palette or { };
  port = 8080;
  assetSize = 64;
  cacheMode = containerCfg.extra.cacheMode or "off";
  cacheControl =
    if cacheMode == "disk" then
      containerCfg.extra.cacheControl or "public, max-age=3600"
    else if cacheMode == "off" then
      "no-store"
    else
      throw "favicon cacheMode must be either `off` or `disk`";
  priority = toString (containerCfg.extra.priority or 2147482647);
  logoSvgFileName = builtins.baseNameOf (toString mediaCfg.logo.svg);
  logoSvgMount = "/assets/${logoSvgFileName}";
  ensureAttrSet = field: value:
    if builtins.isAttrs value then
      value
    else
      throw "favicon `${field}` must be an attribute set";
  resolveColor = value:
    if value == null then null
    else if !builtins.isString value then
      throw "favicon color values must be strings"
    else if lib.hasPrefix "#" value then
      value
    else
      let
        paletteValue = lib.attrByPath [ value ] (throw "Unknown favicon color reference `${value}`") palette;
      in
      if builtins.isString paletteValue then
        if lib.hasPrefix "#" paletteValue then
          paletteValue
        else
          "#${paletteValue}"
      else
        throw "favicon palette reference `${value}` must resolve to a string";
  normalizeProfile = profile:
    let
      normalizedProfile = ensureAttrSet "profile" profile;
      bg =
        if normalizedProfile ? bg then resolveColor normalizedProfile.bg
        else if normalizedProfile ? background then resolveColor normalizedProfile.background
        else null;
      fg =
        if normalizedProfile ? fg then resolveColor normalizedProfile.fg
        else if normalizedProfile ? foreground then resolveColor normalizedProfile.foreground
        else null;
    in
    (lib.filterAttrs (name: _: !(builtins.elem name [ "bg" "background" "fg" "foreground" ])) normalizedProfile)
    // lib.optionalAttrs (bg != null) { bg = bg; }
    // lib.optionalAttrs (fg != null) { fg = fg; };
  hostMappings = lib.mapAttrs (_: profile: normalizeProfile profile) (
    ensureAttrSet "mappings" (containerCfg.extra.mappings or { })
  );
  defaultProfile =
    if containerCfg.extra ? default then
      normalizeProfile containerCfg.extra.default
    else
      null;
    traefikAssetPathRegexp =
    "^/(.*/)?"
    + "(fav(icon)?(-[0-9]+x[0-9]+)?\\.(ico|png|svg)"
    + "|(favicon|apple-icon)(-[0-9]+)?(\\.(ico|png))?"
    + "|logo\\.(ico)"
    + "|fav([0-9]+)?\\.(ico|png)"
    + "|apple-touch-icon(-precomposed)?\\.png"
    + "|android-chrome-[0-9]+x[0-9]+\\.png"
    + "|mstile-[0-9]+x[0-9]+\\.png)$";
  pythonEnv = pkgs.python3.withPackages (ps: with ps; [
    cairosvg
    pillow
  ]);
  serverScript = pkgs.writeText "favicon-server.py" ''
    from io import BytesIO
    import hashlib
    import os
    from pathlib import Path
    import re
    import threading
    from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer

    import cairosvg
    from PIL import Image

    LOGO_PATH = ${builtins.toJSON logoSvgMount}
    LISTEN_HOST = "0.0.0.0"
    LISTEN_PORT = ${toString port}
    ASSET_SIZE = ${toString assetSize}
    CACHE_MODE = ${builtins.toJSON cacheMode}
    CACHE_CONTROL = ${builtins.toJSON cacheControl}
    CACHE_DIR = Path("/cache")

    with open(LOGO_PATH, "rb") as fh:
        LOGO_BYTES = fh.read()
    MAPPINGS = ${builtins.toJSON hostMappings}
    DEFAULT_PROFILE = ${if defaultProfile == null then "None" else builtins.toJSON defaultProfile}
    APP_DOMAIN = (${builtins.toJSON serverCfg.domain} or "").strip().lower()
    DEFAULT_COLORS = {"bg": "#111827", "fg": "#f8fafc"}
    LOGO_HASH = hashlib.sha256(LOGO_BYTES).hexdigest()
    ICON_CACHE_LOCK = threading.Lock()

    def _request_host(headers):
        host = (
            headers.get("X-Forwarded-Host")
            or headers.get("X-Original-Host")
            or headers.get("Host", "")
        )
        return (host or "").split(",", 1)[0].split(":", 1)[0].strip().lower().rstrip(".")

    def _host_candidates(host):
        candidates = []

        def add(candidate):
            if candidate and candidate not in candidates:
                candidates.append(candidate)

        add(host)
        if APP_DOMAIN:
            suffix = f".{APP_DOMAIN}"
            if host.endswith(suffix):
                add(host[: -len(suffix)].rstrip("."))
        if "." in host:
            add(host.split(".", 1)[0])

        return candidates

    def _profile_for_host(host):
        for candidate in _host_candidates(host):
            profile = MAPPINGS.get(candidate)
            if profile:
                return candidate, profile
        return None, DEFAULT_PROFILE

    def _replace_logo_fill(svg, color):
        svg, _ = re.subn(
            "fill:#3193f5",
            f"fill:{color}",
            svg,
            flags=re.IGNORECASE,
        )
        return svg

    def _colors(profile):
        profile = profile or {}
        return {
            "bg": profile.get("bg") or profile.get("background") or DEFAULT_COLORS["bg"],
            "fg": profile.get("fg") or profile.get("foreground") or DEFAULT_COLORS["fg"],
        }

    def _add_background(svg, color):
        return re.sub(
            r"(<svg\\b[^>]*>)",
            rf'\\1<circle cx="64" cy="64" r="64" fill="{color}"/>',
            svg,
            count=1,
            flags=re.IGNORECASE,
        )

    def _render_icon(colors):
        svg = LOGO_BYTES.decode("utf-8")
        svg = _replace_logo_fill(svg, colors["fg"])
        svg = _add_background(svg, colors["bg"])

        png = cairosvg.svg2png(
            bytestring=svg.encode("utf-8"),
            output_width=ASSET_SIZE,
            output_height=ASSET_SIZE,
        )
        output = BytesIO()
        with Image.open(BytesIO(png)) as image:
            with image.convert("RGBA") as rgba:
                rgba.save(output, format="ICO", sizes=[(ASSET_SIZE, ASSET_SIZE)])
        return output.getvalue()

    def _cache_path(colors):
        digest = hashlib.sha256(
            f"{ASSET_SIZE}:{LOGO_HASH}:{colors['bg']}:{colors['fg']}".encode("utf-8")
        ).hexdigest()
        return CACHE_DIR / f"{digest}.ico"

    def _payload_for(colors):
        if CACHE_MODE != "disk":
            return _render_icon(colors)

        cache_path = _cache_path(colors)
        with ICON_CACHE_LOCK:
            if cache_path.exists():
                return cache_path.read_bytes()

        payload = _render_icon(colors)
        with ICON_CACHE_LOCK:
            if cache_path.exists():
                return cache_path.read_bytes()
            CACHE_DIR.mkdir(parents=True, exist_ok=True)
            tmp_path = cache_path.with_suffix(".tmp")
            tmp_path.write_bytes(payload)
            os.replace(tmp_path, cache_path)
            return payload

    class Handler(BaseHTTPRequestHandler):
        server_version = "favicon-router/1.0"

        def _serve(self, include_body):
            host = _request_host(self.headers)
            matched_host, profile = _profile_for_host(host)
            if not profile:
                self.send_error(404, "No favicon mapping for host")
                return

            colors = _colors(profile)
            payload = _payload_for(colors)
            self.send_response(200)
            self.send_header("Content-Type", "image/x-icon")
            self.send_header("Content-Length", str(len(payload)))
            self.send_header("Cache-Control", CACHE_CONTROL)
            self.send_header("X-Favicon-Host", host or "default")
            self.send_header("X-Favicon-Mapping", matched_host or "default")
            self.end_headers()
            if include_body:
                self.wfile.write(payload)

        def do_GET(self):
            self._serve(include_body=True)

        def do_HEAD(self):
            self._serve(include_body=False)

        def log_message(self, fmt, *args):
            print("%s - - [%s] %s" % (self.address_string(), self.log_date_time_string(), fmt % args))

    if __name__ == "__main__":
        httpd = ThreadingHTTPServer((LISTEN_HOST, LISTEN_PORT), Handler)
        httpd.serve_forever()
  '';
  image = pkgs.dockerTools.streamLayeredImage {
    name = "favicon";
    tag = "1";
    contents = [
      pythonEnv
      pkgs.cacert
      pkgs.tzdata
    ];
    config = {
      Entrypoint = [ "${pythonEnv}/bin/python3" "-u" serverScript ];
      ExposedPorts = { "${toString port}/tcp" = { }; };
      WorkingDir = "/";
    };
  };
in {
  runtime = {
    paths = [
      {
        path = "${serverCfg.path.config.path}/favicon";
        mode = "0755";
        dirs = [ "cache" ];
      }
    ];

    containers = {
      server = builder.mkContainer {
        imageStream = image;
        port = port;
        extraLabels = {
          "traefik.enable" = "true";
          "traefik.http.routers.${name}.entrypoints" = "web-secure";
          "traefik.http.routers.${name}.rule" = "PathRegexp(`${traefikAssetPathRegexp}`)";
          "traefik.http.routers.${name}.priority" = priority;
          "traefik.http.routers.${name}.tls" = "true";
          "traefik.http.services.${name}.loadbalancer.server.port" = toString port;
        };
        overrides = {
          volumes = [
           "${serverCfg.path.config.path}/favicon/cache:/cache"
            "${mediaCfg.logo.svg}:${logoSvgMount}:ro"
          ];
        };
      };
    };
  };
}
