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

import re
import string
import sys
from argparse import ArgumentParser, Namespace
from subprocess import DEVNULL, check_call, check_output
from typing import TYPE_CHECKING, TypeVar, overload

if TYPE_CHECKING:
    from collections.abc import Callable

DEVICE_ID_DIGITS = 6


class EmptyPrinterNameError(ValueError):
    def __init__(self) -> None:
        super().__init__("Printer name required.")


class InvalidCharacterInPrinterNameError(ValueError):
    def __init__(self) -> None:
        super().__init__("Printer name can't contain spaces or one of # ' \" / ? ")


class PrinterNameTooLongError(ValueError):
    def __init__(self, byte_size: int, max_bytes: int) -> None:
        super().__init__(
            "Printer name too long."
            f"Max length is {max_bytes} bytes, but was {byte_size} bytes."
        )


def validate_printer_name(s: str) -> str:
    if not s:
        raise EmptyPrinterNameError

    valid_chars = set(string.printable) - {" ", "#", "'", '"', "/", "?"}
    max_bytes = 127
    if not all(c in valid_chars for c in s):
        raise InvalidCharacterInPrinterNameError

    byte_size = len(bytes(s, "utf-8"))
    if byte_size > max_bytes:
        raise PrinterNameTooLongError(byte_size, max_bytes)

    return s


class InvalidDeviceIdError(ValueError):
    def __init__(self, s: str) -> None:
        super().__init__(f"Invalid device ID: {s}")


def validate_uri(s: str) -> str:
    if not s.isdigit() or len(s) != DEVICE_ID_DIGITS:
        raise InvalidDeviceIdError(s)

    return f"princh:{s}"


class InvalidDriverError(ValueError):
    def __init__(self) -> None:
        super().__init__("Driver must be 'ISO' or 'US'")


def validate_ppd(s: str) -> str:
    ppd_dir = "/usr/share/ppd/princh"
    s = s.upper()
    if s == "ISO":
        return f"{ppd_dir}/princheu.ppd"
    if s == "US":
        return f"{ppd_dir}/princhus.ppd"

    raise InvalidDriverError


class InvalidYesNoAnswerError(ValueError):
    def __init__(self) -> None:
        super().__init__("Enter either 'y' or 'n'")


def validate_yes_no(s: str, *, default: bool = False) -> bool:
    if not s:
        return default

    s = s.lower()
    if s == "n":
        return False
    if s == "y":
        return True

    raise InvalidYesNoAnswerError


T = TypeVar("T")


@overload
def get_input(prompt: str) -> str: ...
@overload
def get_input(prompt: str, validator: Callable[[str], T]) -> T: ...


def get_input(prompt: str, validator: Callable[[str], T] | None = None) -> T | str:
    while True:
        value = input(prompt).strip()
        try:
            if validator is None:
                return value
            return validator(value)
        except ValueError as e:
            print(f" {e.args[0]}")
            continue


def install_printer(
    printer_name: str,
    printer_uri: str,
    description: str,
    ppd: str,
    *,
    make_default: bool,
) -> None:
    check_call(  # noqa: S603
        [  # noqa: S607
            "lpadmin",
            "-p",
            printer_name,
            "-v",
            printer_uri,
            "-D",
            description,
            "-E",
            "-P",
            ppd,
            "-o",
            "ColorModel-default=color",
            "-o",
            "printer-is-shared=false",
        ],
        stdout=DEVNULL,
        stderr=DEVNULL,
    )
    if make_default:
        check_call(  # noqa: S603
            [  # noqa: S607
                "lpadmin",
                "-d",
                printer_name,
            ],
            stdout=DEVNULL,
            stderr=DEVNULL,
        )


def get_princh_printers() -> list[tuple[str, str]]:
    printer_re = re.compile(r"^device for (?P<name>.*): princh:(?P<device_id>.*)$")
    output = check_output(["lpstat", "-v"], encoding="utf-8")  # noqa: S607
    printers = []
    for line in output.splitlines():
        m = printer_re.match(line)
        if m:
            d = m.groupdict()
            printers.append((d["name"], d["device_id"]))

    return printers


def wizard(_args: Namespace) -> None:
    printer_name = get_input("Printer name: ", validate_printer_name)
    printer_uri = get_input("Device ID: ", validate_uri)
    description = get_input("Description: ")
    ppd = get_input("Driver [ISO/US]: ", validate_ppd)
    make_default = get_input("Make default? [y/N] ", validate_yes_no)
    install_printer(
        printer_name=printer_name,
        printer_uri=printer_uri,
        description=description,
        ppd=ppd,
        make_default=make_default,
    )


def add_printer(args: Namespace) -> None:
    install_printer(
        printer_name=args.name,
        printer_uri=args.device_id,
        description=args.description,
        ppd=args.driver,
        make_default=args.default,
    )


def show_printers(printers: list[tuple[str, str]]) -> None:
    print("Princh printers installed:")
    for name, device_id in printers:
        print(f" {device_id} {name}")


def list_printers(_args: Namespace) -> None:
    printers = get_princh_printers()
    if not printers:
        print("No Princh printers installed.")
        return
    show_printers(printers)


def remove_printers(_args: Namespace) -> None:
    printers = get_princh_printers()
    if not printers:
        print("No Princh printers installed.", file=sys.stderr)
        sys.exit(1)

    show_printers(printers)

    remove_all = get_input("Remove all? [y/N] ", validate_yes_no)
    if not remove_all:
        return

    for name, _ in printers:
        check_output(["lpadmin", "-x", name])  # noqa: S603, S607


parser = ArgumentParser()
parser.set_defaults(func=None)
subparsers = parser.add_subparsers()

wizard_parser = subparsers.add_parser("wizard")
wizard_parser.set_defaults(func=wizard)

add_parser = subparsers.add_parser("add")
add_parser.set_defaults(func=add_printer)
add_parser.add_argument(
    "--name",
    required=True,
    type=validate_printer_name,
    help="name of installed printer",
)
add_parser.add_argument(
    "--device-id", required=True, type=validate_uri, help="device id (6 digits)"
)
add_parser.add_argument(
    "--driver", required=True, type=validate_ppd, help="printer driver {ISO,US}"
)
add_parser.add_argument(
    "--description", default="", help="description of installed printer"
)
add_parser.add_argument("--default", action="store_true", help="make printer default")

list_parser = subparsers.add_parser("list")
list_parser.set_defaults(func=list_printers)

remove_parser = subparsers.add_parser("rm")
remove_parser.set_defaults(func=remove_printers)

sys_args = sys.argv[1:]
if not sys_args:
    sys_args.append("wizard")
args = parser.parse_args(sys_args)

if not args.func:
    parser.print_help()
    parser.error("Please specify a command.")

args.func(args)
