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

import dataclasses
import json
import os
import os.path
import re
import sys
import traceback
import urllib.request
from dataclasses import dataclass
from datetime import datetime, timezone
from pathlib import Path
from pwd import getpwnam
from tempfile import mkdtemp

DAEMON_PORT = 23166

# Sandboxing might prevent us from using the normal pcp log file
PCP_LOG_PATH_CANDIDATES = [
    Path("/var/log/princh/pcp/pcp.log"),
    Path("/tmp/princh-pcp.log"),  # noqa: S108
]

PCP_LOG_FILE = None
for path in PCP_LOG_PATH_CANDIDATES:
    try:
        old_umask = os.umask(0)
        path.parent.mkdir(parents=True, exist_ok=True)
        f = path.open("a")
        os.umask(old_umask)
        PCP_LOG_FILE = f
    except OSError:  # noqa:  PERF203
        pass


# 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)
    if PCP_LOG_FILE is None:
        return

    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:]


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


@dataclass
class Message:
    username: str
    user_id: int
    printer_id: str
    pdf_file_path: str
    title: str
    job_id: str
    options: str
    copies: str


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", "Sending job to daemon.")
    body = bytes(
        json.dumps(
            dataclasses.asdict(
                Message(
                    username=user,
                    user_id=user_info.pw_uid,
                    printer_id=printer_id,
                    pdf_file_path=str(pdf_file_path),
                    title=title,
                    job_id=job_id,
                    options=options,
                    copies=copies,
                )
            )
        ),
        "ascii",
    )

    req = urllib.request.Request(
        f"http://localhost:{DAEMON_PORT}/", data=body, method="POST"
    )
    req.add_header("content-length", str(len(body)))
    with urllib.request.urlopen(req) as response:  # noqa: S310
        if response.status != 200:  # noqa: PLR2004
            log("ERROR", f"Daemon returned status code {response.status}")


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)
