#!/usr/bin/python3
from __future__ import annotations

import os
import os.path
import re
import subprocess
import sys
import traceback
from collections import Counter
from datetime import datetime, timezone
from pathlib import Path
from pwd import getpwnam
from tempfile import mkdtemp

PCP_LOG_PATH = Path("/var/log/princh/pcp/pcp.log")

old_umask = os.umask(0)
PCP_LOG_PATH.parent.mkdir(parents=True, exist_ok=True)
PCP_LOG_FILE = PCP_LOG_PATH.open("a")
os.umask(old_umask)


# Backends communicate with the CUPS system mostly through stderr.
# Read more at:
#     https://web.archive.org/web/20160729181800/http://cups.org/documentation.php/doc-1.7/api-filter.html#MESSAGES
def log(level: str, message: str) -> None:
    print(level + ":", message, file=sys.stderr)
    try:
        now_str = (
            datetime.now(tz=timezone.utc)
            .isoformat(sep="T", timespec="milliseconds")
            .replace("+00:00", "")
            + "Z"
        )
        level_str = level.lower()[:4]
        print(
            f"{now_str} [{level_str}][backnd]: \t{message}",
            file=PCP_LOG_FILE,
            flush=True,
        )
    except OSError:
        pass


def strip_pjl_header(content: bytes) -> bytes:
    """
    The file we get from cups might contain a PJL header before the PDF file,
    which we want to remove.
    Example:
        b'\x1b%-12345X@PJL\n'
        b'@PJL JOB NAME = "Example Domain" DISPLAY = "1553 tyilo Example Domain"\n'
        b'@PJL SET USERNAME = "tyilo"\n'
        b'@PJL SET DUPLEX=OFF\n'
        b'@PJL SET PAPER=A4\n'
        b'@PJL SET RESOLUTION=600\n'
        b'@PJL ENTER LANGUAGE = PDF\n'
        b'%PDF-1.5\n'
        b'%\xbf\xf7\xa2\xfe\n'
        b'% This file was generated by pdftopdf\n'
        ...
    """
    pdf_start = content.find(b"%PDF-")
    if pdf_start == -1:
        log("WARNING", "Couldn't find PDF header in file")
        return content

    return content[pdf_start:]


def get_gui_env_vars(user_id: int) -> dict[bytes, str]:
    keys = {b"DISPLAY", b"XAUTHORITY", b"WAYLAND_DISPLAY", b"DBUS_SESSION_BUS_ADDRESS"}
    counts: dict[bytes, Counter[str]] = {k: Counter() for k in keys}
    for process in Path("/proc").iterdir():
        environ_path = process / "environ"
        try:
            uid = environ_path.stat().st_uid
            if uid != user_id:
                continue
            env_kvs = environ_path.read_bytes().split(b"\0")
        except OSError:
            continue
        for entry in env_kvs:
            key, _, value = entry.partition(b"=")

            if key in keys:
                try:
                    str_value = str(value, "ascii")
                except ValueError:
                    continue

                counts[key][str_value] += 1

    env = {}
    for k, count in counts.items():
        most_common = count.most_common(1)
        if most_common:
            env[k] = most_common[0][0]

    return env


class InvalidDeviceUriError(Exception):
    def __init__(self, device_uri: str) -> None:
        super().__init__(f"Invalid device URI: {device_uri}")


def main(args: list[str]) -> None:
    # Command line arguments are many and varied.
    if len(args) not in (5, 6):
        log("ERROR", "Unexpected number of command line arguments.")
        sys.exit(1)

    job_id, user, title, copies, options, *remaining_args = args

    # CUPS provides the file we are to print in one of two ways. We will see
    # either a sixth command line argument, a file name; or we can read the file
    # off stdin. Either way, we spool it to a file and use Ghostscript to build
    # a PDF.

    if remaining_args:
        content = Path(remaining_args[0]).read_bytes()
    else:
        content = sys.stdin.buffer.read()

    content = strip_pjl_header(content)

    """
    The DEVICE_URI environment variable is set. It lists the printer we are
    targeting.
    The format is: 'princh:<printer_id>'
    Example: 'princh:123456'
    """

    device_uri = os.getenv("DEVICE_URI", "")
    m = re.match(r"princh:(?P<printer>\d{6,})", device_uri)

    if not m:
        raise InvalidDeviceUriError(device_uri)

    printer_id = m.group("printer")

    # spool the pdf data from cups to a pdf file!

    os.umask(0o0000)
    pdf_dir = Path(mkdtemp(prefix="princh_", dir="/tmp"))
    pdf_dir.mkdir(parents=True, exist_ok=True)
    pdf_file_path = pdf_dir / f"{job_id}.pdf"

    log("INFO", f"Spooling to: {pdf_file_path}")

    Path(pdf_file_path).write_bytes(content)

    log("INFO", "Spooling done.")

    log("INFO", "Looking for printing user.")

    user_info = getpwnam(user)
    log("INFO", f"Found user: {user}: uid={user_info.pw_uid}, gid={user_info.pw_gid}")

    pdf_file_path.chmod(0o0400)
    os.chown(pdf_file_path, user_info.pw_uid, user_info.pw_gid)
    pdf_dir.chmod(0o0700)
    os.chown(pdf_dir, user_info.pw_uid, user_info.pw_gid)

    log("INFO", "Attempting to deliver print job.")

    env = get_gui_env_vars(user_info.pw_uid)
    log("INFO", f"Found env: {env}")
    subprocess.check_call(  # noqa: S603
        [  # noqa: S607
            "sudo",
            "-u",
            user,
            "ELECTRON_RUN_AS_NODE=1",
            "--",
            "/opt/princh/pcp/princh-cloud-printer",
            "/opt/princh/pcp/client.js",
            printer_id,
            pdf_file_path,
            user,
            title,
            job_id,
            options,
            copies,
        ],
        env=env,
    )


if __name__ == "__main__":
    try:
        main(sys.argv[1:])
    except:  # noqa: E722
        for line in traceback.format_exc().split("\n"):
            log("CRIT", line)
        sys.exit(1)
