"""
ChefSoft Print Agent v2.7 - BITMAP MODE (80mm)
========================================
Для Xprinter XP-T80Q с GBK прошивкой.
Печать через растровое изображение — поддерживает любой язык.

Поддержка сетевых (Ethernet/Wi-Fi) и USB принтеров.

Установка:
  pip install pillow
  pip install pywin32        # только для USB-принтеров (Windows)
  python print_agent.py

КОНФИГУРАЦИЯ ПРИНТЕРОВ — в файле print_config.json (рядом с этим скриптом).
Если файл отсутствует — используются значения по умолчанию (см. DEFAULT_PRINTERS).

Формат print_config.json:
{
  "kitchen": {"type": "network", "host": "192.168.0.120", "port": 9100},
  "mangal":  {"type": "network", "host": "192.168.0.110", "port": 9100},
  "bar":     {"type": "network", "host": "192.168.0.150", "port": 9100},
  "admin":   {"type": "usb", "name": "xPrinter"}
}

type: "network" — печать по IP:port (Ethernet/Wi-Fi)
type: "usb"     — печать через Windows-спулер (USB), "name" = имя принтера в Windows
"""

import http.server
import socketserver
import json
import os
import socket
from datetime import datetime

try:
    from PIL import Image, ImageDraw, ImageFont
    PIL_OK = True
except ImportError:
    PIL_OK = False
    print("[WARN] Pillow не установлен! Запустите: pip install pillow")

# USB-печать через Windows-спулер (только Windows)
try:
    import win32print
    USB_OK = True
except ImportError:
    USB_OK = False
    print("[INFO] pywin32 не установлен — USB-принтеры недоступны. Для USB: pip install pywin32")

CONFIG_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "print_config.json")

DEFAULT_PRINTERS = {
    "kitchen": {"type": "network", "host": "192.168.0.120", "port": 9100},
    "mangal":  {"type": "network", "host": "192.168.0.110", "port": 9100},
    "bar":     {"type": "network", "host": "192.168.0.150", "port": 9100},
    "admin":   {"type": "usb", "name": "xPrinter"},
}


def load_printers():
    """Загружает конфиг из print_config.json или использует DEFAULT_PRINTERS."""
    if os.path.exists(CONFIG_PATH):
        try:
            with open(CONFIG_PATH, "r", encoding="utf-8") as f:
                cfg = json.load(f)
                print(f"[OK] Конфиг загружен: {CONFIG_PATH}")
                return cfg
        except Exception as e:
            print(f"[WARN] Ошибка чтения {CONFIG_PATH}: {e}. Используем значения по умолчанию.")
    return DEFAULT_PRINTERS


PRINTERS = load_printers()

PORT = 9101
PAPER_WIDTH_PX = 576
LEFT_MARGIN = 6   # small left margin
FONT_SIZE = 32


def get_font(size):
    font_paths = [
        "C:/Windows/Fonts/arial.ttf",
        "C:/Windows/Fonts/arialbd.ttf",
        "C:/Windows/Fonts/cour.ttf",
        "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
        "/System/Library/Fonts/Arial.ttf",
    ]
    for path in font_paths:
        try:
            return ImageFont.truetype(path, size)
        except Exception:
            continue
    return ImageFont.load_default()


def get_bold_font(size):
    font_paths = [
        "C:/Windows/Fonts/arialbd.ttf",
        "C:/Windows/Fonts/arial.ttf",
        "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
        "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
        "/System/Library/Fonts/Arial Bold.ttf",
        "/System/Library/Fonts/Arial.ttf",
    ]
    for path in font_paths:
        try:
            return ImageFont.truetype(path, size)
        except Exception:
            continue
    return ImageFont.load_default()


def image_to_escpos(img):
    """Конвертируем PIL Image в ESC/POS растровые команды (GS v 0)"""
    img = img.convert('1')
    width, height = img.size
    byte_width = (width + 7) // 8

    buf = bytearray()
    buf += b'\x1b\x40'  # Init

    nL = byte_width & 0xFF
    nH = (byte_width >> 8) & 0xFF
    dL = height & 0xFF
    dH = (height >> 8) & 0xFF
    buf += b'\x1d\x76\x30' + bytes([0, nL, nH, dL, dH])

    pixels = img.load()
    for y in range(height):
        for bx in range(byte_width):
            byte = 0
            for bit in range(8):
                x = bx * 8 + bit
                if x < width:
                    if pixels[x, y] == 0:
                        byte |= (0x80 >> bit)
            buf.append(byte)

    buf += b'\n\n\n'
    buf += b'\x1d\x56\x42\x00'  # Cut
    return bytes(buf)


def text_width(font, text):
    try:
        bbox = font.getbbox(text)
        return bbox[2] - bbox[0]
    except:
        return len(text) * FONT_SIZE


def draw_centered(draw, y, text, font, width=PAPER_WIDTH_PX):
    tw = text_width(font, text)
    x = max(LEFT_MARGIN, (width - tw) // 2)
    draw.text((x, y), text, font=font, fill=0)


def build_kitchen_receipt(title, items, order):
    """
    Чек для кухни/мангала — только для повара.
    Заголовок + инфо о столе/официанте по центру.
    Блюда: название и количество на одной строке.
    """
    font_title  = get_bold_font(FONT_SIZE + 16)
    font_info   = get_bold_font(FONT_SIZE + 6)
    font_item   = get_bold_font(FONT_SIZE + 8)
    font_time   = get_font(FONT_SIZE - 4)

    PAD = 8

    def h(font, text="X"):
        try:
            return font.getbbox(text)[3] + PAD
        except:
            return FONT_SIZE + PAD

    lines = []

    # Заголовок
    lines.append(('center', title, font_title, h(font_title, title) + 4))

    # Стол и зона по центру
    table_str = f"{order.get('table_name', '')}  ·  {order.get('zone', '')}"
    lines.append(('center', table_str, font_info, h(font_info, table_str)))

    # Официант по центру
    waiter_str = f"Официант: {order.get('waiter_name', '')}"
    lines.append(('center', waiter_str, font_info, h(font_info, waiter_str) + 2))

    # Разделитель
    lines.append(('divider', None, None, 14))

    # Блюда — название × кол-во на одной строке
    for item in items:
        name = item.get('name', '')
        qty = item.get('quantity', 1)
        grams = item.get('grams', None)
        sold_by_weight = item.get('sold_by_weight', False)
        if sold_by_weight and grams:
            item_str = f"{name}  ·  {grams}г  ×{qty}"
        else:
            item_str = f"{name}  ×{qty}"
        lines.append(('text', item_str, font_item, h(font_item, item_str) + 2))

    # Разделитель
    lines.append(('divider', None, None, 10))

    # Время
    time_str = datetime.now().strftime('%d.%m.%Y  %H:%M')
    lines.append(('center', time_str, font_time, h(font_time, time_str)))

    total_h = sum(r[3] for r in lines) + 30

    img = Image.new('1', (PAPER_WIDTH_PX, total_h), 1)
    draw = ImageDraw.Draw(img)

    y = 12
    for kind, text, font, lh in lines:
        if kind == 'divider':
            draw.line([(LEFT_MARGIN, y + 4), (PAPER_WIDTH_PX - LEFT_MARGIN, y + 4)], fill=0, width=2)
        elif kind == 'center':
            draw_centered(draw, y, text, font)
        else:
            draw.text((LEFT_MARGIN, y), text, font=font, fill=0)
        y += lh

    return image_to_escpos(img)


def build_full_receipt(order, settings=None):
    """Полный клиентский чек для администратора."""
    settings = settings or {}
    restaurant = settings.get('restaurant_name', 'Имрон')
    service_pct = settings.get('service_charge_percent', 0)

    font_title   = get_bold_font(FONT_SIZE + 12)
    font_info    = get_font(FONT_SIZE)
    font_item    = get_bold_font(FONT_SIZE + 4)
    font_price   = get_bold_font(FONT_SIZE + 2)
    font_total   = get_bold_font(FONT_SIZE + 10)
    font_small   = get_font(FONT_SIZE - 2)

    PAD = 10

    def lh(font, text="X"):
        try:
            return font.getbbox(text)[3] + PAD
        except:
            return FONT_SIZE + PAD

    lines = []

    lines.append(('center', restaurant, font_title, lh(font_title, restaurant) + 6))
    lines.append(('divider', None, None, 12))

    table_str = f"Стол: {order.get('table_name', '')} / {order.get('zone', '')}"
    lines.append(('text', table_str, font_info, lh(font_info, table_str)))
    waiter_str = f"Официант: {order.get('waiter_name', '')}"
    lines.append(('text', waiter_str, font_info, lh(font_info, waiter_str)))
    lines.append(('divider', None, None, 12))

    subtotal = 0
    for item in (order.get('items') or []):
        name = item.get('name', '')
        price = item.get('price', 0)
        qty = item.get('quantity', 1)
        total = item.get('total', price * qty)
        grams = item.get('grams', None)
        sold_by_weight = item.get('sold_by_weight', False)
        weight_grams = item.get('weight_grams', 100)
        subtotal += total
        lines.append(('text', name, font_item, lh(font_item, name)))
        if sold_by_weight and grams:
            detail_str = f"  {grams}г × {price} смн/{weight_grams}г = {total} смн"
        else:
            detail_str = f"  {qty} шт × {price} смн = {total} смн"
        lines.append(('text', detail_str, font_price, lh(font_price, detail_str)))

    # Источник истины — только order.service_charge_amount.
    # Если обслуживание не было добавлено в корзине (0/None) — строку НЕ печатаем,
    # даже если в настройках указан процент. Это исключает спорные ситуации с гостями.
    service_amount = order.get('service_charge_amount') or 0
    if service_amount > 0:
        if service_pct > 0:
            disp_pct = service_pct
        elif subtotal > 0:
            disp_pct = round(service_amount * 100 / subtotal)
        else:
            disp_pct = 0
    else:
        disp_pct = 0
    stored_total = order.get('total_amount')
    grand_total = stored_total if (stored_total and stored_total > 0) else subtotal + service_amount

    lines.append(('divider', None, None, 12))
    lines.append(('text', f"Сумма: {subtotal} смн", font_info, lh(font_info) + 4))
    if service_amount:
        lines.append(('text', f"Обслуживание ({disp_pct}%): {service_amount} смн", font_info, lh(font_info) + 2))

    total_str = f"ИТОГО: {grand_total} смн"
    lines.append(('center', total_str, font_total, lh(font_total, total_str) + 8))
    lines.append(('center', 'Спасибо за визит!', font_info, lh(font_info) + 4))
    time_str = datetime.now().strftime('%d.%m.%Y  %H:%M')
    lines.append(('center', time_str, font_small, lh(font_small, time_str)))

    total_h = sum(r[3] for r in lines) + 30
    img = Image.new('1', (PAPER_WIDTH_PX, total_h), 1)
    draw = ImageDraw.Draw(img)

    y = 10
    for kind, text, font, row_h in lines:
        if kind == 'divider':
            draw.line([(LEFT_MARGIN, y + 4), (PAPER_WIDTH_PX - LEFT_MARGIN, y + 4)], fill=0, width=2)
        elif kind == 'center':
            draw_centered(draw, y, text, font)
        else:
            draw.text((LEFT_MARGIN, y), text, font=font, fill=0)
        y += row_h

    return image_to_escpos(img)


def send_to_network(host, port, data):
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        s.settimeout(5)
        s.connect((host, port))
        s.sendall(data)


def send_to_usb(printer_name, data):
    """Печать на USB-принтер через Windows-спулер (RAW-режим)."""
    if not USB_OK:
        raise Exception("pywin32 не установлен. Для USB-принтера запустите: pip install pywin32")
    hPrinter = win32print.OpenPrinter(printer_name)
    try:
        hJob = win32print.StartDocPrinter(hPrinter, 1, ("ChefSoft Receipt", None, "RAW"))
        win32print.StartPagePrinter(hPrinter)
        win32print.WritePrinter(hPrinter, data)
        win32print.EndPagePrinter(hPrinter)
        win32print.EndDocPrinter(hPrinter)
    finally:
        win32print.ClosePrinter(hPrinter)


def send_to_printer(target, data):
    """Универсальная отправка: network или usb — по конфигу принтера."""
    cfg = PRINTERS.get(target)
    if not cfg:
        raise Exception(f"Принтер '{target}' не настроен в print_config.json")
    ptype = cfg.get("type", "network")
    if ptype == "usb":
        name = cfg.get("name", "")
        send_to_usb(name, data)
        return f"usb:{name}"
    else:
        host = cfg.get("host", "")
        port = int(cfg.get("port", 9100))
        send_to_network(host, port, data)
        return f"{host}:{port}"


def printer_label(target):
    """Краткое описание принтера для логов."""
    cfg = PRINTERS.get(target, {})
    if cfg.get("type") == "usb":
        return f"usb:{cfg.get('name', '?')}"
    return f"{cfg.get('host', '?')}:{cfg.get('port', 9100)}"


class PrintHandler(http.server.BaseHTTPRequestHandler):
    def log_message(self, format, *args):
        print(f"[{self.address_string()}] {format % args}")

    def do_OPTIONS(self):
        try:
            self.send_response(200)
            self._cors()
            self.end_headers()
        except (ConnectionAbortedError, ConnectionResetError, BrokenPipeError) as e:
            print(f"[WARN] do_OPTIONS: клиент разорвал соединение ({e})")

    def do_POST(self):
        try:
            if not PIL_OK:
                raise Exception("Pillow не установлен. Запустите: pip install pillow")

            length = int(self.headers.get('Content-Length', 0))
            body = self.rfile.read(length)
            data = json.loads(body)

            order = data.get('order', {})
            menu_items = data.get('menuItems', [])
            settings = data.get('settings', {})
            mode = data.get('mode', 'kitchen')
            target_printer_override = data.get('target_printer', None)

            results = []
            errors = []

            if mode == 'full_receipt':
                target = target_printer_override or 'admin'
                if target not in PRINTERS:
                    target = 'admin'
                try:
                    receipt = build_full_receipt(order, settings)
                    where = send_to_printer(target, receipt)
                    results.append(f"{target}: ok")
                    print(f"[OK] Full receipt -> {target} ({where})")
                except Exception as e:
                    errors.append(f"full_receipt ({target}): {e}")
                    print(f"[ERR] Full receipt ({target}): {e}")
            else:
                kitchen_items = []
                mangal_items = []
                bar_items = []

                for item in order.get('items', []):
                    mid = item.get('menu_item_id')
                    mi = next((m for m in menu_items if m.get('id') == mid), None)
                    if mi:
                        target = mi.get('printer_target', 'kitchen')
                    else:
                        # Если блюдо не найдено в меню — не печатаем (безопаснее)
                        print(f"[WARN] Блюдо не найдено в меню: {item.get('name')} (id={mid}), пропускаем")
                        continue

                    # Явно пропускаем все блюда с printer_target = "none"
                    if target == 'none':
                        print(f"[SKIP] {item.get('name')} — без печати (none)")
                        continue
                    elif target == 'mangal':
                        mangal_items.append(item)
                    elif target == 'bar':
                        bar_items.append(item)
                    else:
                        kitchen_items.append(item)

                if kitchen_items:
                    try:
                        receipt = build_kitchen_receipt("КУХНЯ", kitchen_items, order)
                        where = send_to_printer('kitchen', receipt)
                        results.append("kitchen: ok")
                        print(f"[OK] КУХНЯ -> {where} ({len(kitchen_items)} блюд)")
                    except Exception as e:
                        errors.append(f"kitchen: {e}")
                        print(f"[ERR] КУХНЯ: {e}")

                if mangal_items:
                    try:
                        receipt = build_kitchen_receipt("МАНГАЛ", mangal_items, order)
                        where = send_to_printer('mangal', receipt)
                        results.append("mangal: ok")
                        print(f"[OK] МАНГАЛ -> {where} ({len(mangal_items)} блюд)")
                    except Exception as e:
                        errors.append(f"mangal: {e}")
                        print(f"[ERR] МАНГАЛ: {e}")

                if bar_items:
                    try:
                        receipt = build_kitchen_receipt("БАР", bar_items, order)
                        where = send_to_printer('bar', receipt)
                        results.append("bar: ok")
                        print(f"[OK] БАР -> {where} ({len(bar_items)} блюд)")
                    except Exception as e:
                        errors.append(f"bar: {e}")
                        print(f"[ERR] БАР: {e}")

                if not kitchen_items and not mangal_items and not bar_items:
                    print(f"[INFO] Нет блюд для печати (все без печати или не найдены)")
                    results.append("no_print_items: skipped")

            self.send_response(200)
            self._cors()
            self.send_header('Content-Type', 'application/json')
            self.end_headers()
            self.wfile.write(json.dumps({"ok": True, "results": results, "errors": errors}).encode())

        except Exception as e:
            print(f"[ERR] Handler: {e}")
            self.send_response(500)
            self._cors()
            self.send_header('Content-Type', 'application/json')
            self.end_headers()
            self.wfile.write(json.dumps({"ok": False, "error": str(e)}).encode())

    def do_GET(self):
        self.send_response(200)
        self._cors()
        self.send_header('Content-Type', 'application/json')
        self.end_headers()
        self.wfile.write(json.dumps({
            "status": "ChefSoft Print Agent v2.7",
            "PIL": PIL_OK,
            "USB": USB_OK,
            "printers": PRINTERS
        }).encode())

    def _cors(self):
        self.send_header('Access-Control-Allow-Origin', '*')
        self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
        self.send_header('Access-Control-Allow-Headers', 'Content-Type')


if __name__ == '__main__':
    # ThreadingHTTPServer — каждое подключение в своём потоке.
    # Иначе медленная печать на сетевой принтер (таймаут 5с) блокирует
    # все остальные запросы (включая CORS-preflight с телефонов),
    # и браузер прерывает запрос → "The user aborted a request".
    server = http.server.ThreadingHTTPServer(('0.0.0.0', PORT), PrintHandler)
    server.daemon_threads = True
    print(f"ChefSoft Print Agent v2.7 запущен на порту {PORT} (многопоточный)")
    print(f"Pillow: {'OK' if PIL_OK else 'НЕ УСТАНОВЛЕН — запустите pip install pillow'}")
    print(f"pywin32 (USB): {'OK' if USB_OK else 'НЕ УСТАНОВЛЕН — для USB: pip install pywin32'}")
    print(f"Конфиг: {CONFIG_PATH}")
    print(f"Принтеры: {PRINTERS}")
    server.serve_forever()
