"""
HEAD OR TAIL — Local Print Helper
=================================
Runs on each SHOP's cashier Windows PC (NOT on the Contabo server).

Why this exists:
  The game server is now on Contabo, far from the shop. A remote server can't
  reach a USB/serial thermal printer in the shop. So printing must happen on the
  cashier's own machine. This helper does exactly that, reusing your proven
  win32print ESC/POS ticket layout (AddisBet style, logo, Code128 barcode).

How it works:
  - Listens on http://127.0.0.1:9999 (local only — not exposed to the internet).
  - The cashier web page (served from Contabo) POSTs ticket data here.
  - This helper builds the ESC/POS bytes and prints silently to the thermal printer.

Setup on each shop PC (one time):
  1. Install Python 3 (python.org), check "Add to PATH".
  2. Open Command Prompt and run:  pip install pywin32
  3. Put logo.bmp (1-bit BMP) next to this file (optional; falls back to text).
  4. Set PRINTER_NAME below to your thermal printer's exact Windows name.
  5. Run:  python print_helper.py
  6. Leave it running (minimized). The cashier page will find it automatically.

To auto-start it with Windows, create a shortcut to it in the Startup folder
(Win+R -> shell:startup) or wrap it as a service later.
"""

import os, struct, datetime, json, threading, queue, traceback
from http.server import HTTPServer, BaseHTTPRequestHandler

import win32print

# ============ CONFIG ============
LISTEN_PORT  = 9999
PRINTER_NAME = "Generic / Text Only"   # <-- set to your thermal printer's exact name
HOUSE_CODE   = "HOT2025"
SHOP_NAME    = "HEAD OR TAIL"

ESC = b'\x1b'
GS  = b'\x1d'
FS  = b'\x1c'

# ================== PRINTER INIT ==================
def make_init():
    d = bytearray()
    d += ESC + b'@'
    d += ESC + b't\x00'
    d += FS  + b'.'
    d += ESC + b'a\x00'
    return bytes(d)

# ================== LOGO (GS v 0) ==================
_LOGO_CACHE = None

def _load_logo():
    logo_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "logo.bmp")
    try:
        with open(logo_path, "rb") as f: raw = f.read()
        if raw[:2] != b'BM': raise ValueError("Not BMP")
        data_offset = struct.unpack_from("<I", raw, 10)[0]
        width, raw_height = struct.unpack_from("<ii", raw, 18)
        bpp = struct.unpack_from("<H", raw, 28)[0]
        if bpp != 1: raise ValueError("Must be 1-bit")
        height = abs(raw_height)
        top_down = raw_height < 0
        row_stride = ((width + 31) // 32) * 4
        bytes_wide = (width + 7) // 8
        pixel_data = raw[data_offset:]
        rows = []
        for i in range(height):
            row = pixel_data[i*row_stride : i*row_stride+bytes_wide]
            if len(row) < bytes_wide:
                row = row + bytes(bytes_wide - len(row))
            rows.append(row)
        if not top_down: rows.reverse()
        rows = [bytes(b ^ 0xFF for b in row) for row in rows]
        blank = lambda r: all(b == 0 for b in r)
        while rows and blank(rows[0]): rows.pop(0)
        while rows and blank(rows[-1]): rows.pop()
        if not rows: return b''
        num_rows = len(rows)
        rows = [r[:bytes_wide] + bytes(max(0, bytes_wide - len(r))) for r in rows]
        out = bytearray()
        out += ESC + b'a\x01'
        out += b'\x1d\x76\x30\x03'
        out += bytes([bytes_wide & 0xFF, (bytes_wide >> 8) & 0xFF])
        out += bytes([num_rows & 0xFF, (num_rows >> 8) & 0xFF])
        for row in rows: out += row
        out += b'\n'
        out += ESC + b'a\x00'
        return bytes(out)
    except Exception:
        return b''

def get_logo():
    global _LOGO_CACHE
    if _LOGO_CACHE is None: _LOGO_CACHE = _load_logo()
    return _LOGO_CACHE

# ================== TICKET HELPERS ==================
def _txt(s): return s.encode("ascii", errors="ignore")

def _lr(left, right, width=40):
    gap = width - len(left) - len(right)
    return _txt(left + " " * max(1, gap) + right + "\n")

def _row(label, value, width=40):
    return _lr(label, f"Br {float(value):,.2f}", width)

def _center_line(text, width=40):
    pad = (width - len(text)) // 2
    return _txt(" " * max(0, pad) + text + "\n")

def _header():
    logo = get_logo()
    if logo: return logo
    d = bytearray()
    d += ESC + b'a\x01'
    d += ESC + b'!\x38'
    d += _txt(SHOP_NAME + "\n")
    d += ESC + b'!\x00'
    d += ESC + b'a\x00'
    return bytes(d)

def build_bet_ticket(ticket_id, round_num, cashier_name, picks, amount, multiplier,
                     reprint=False, redeemed=False, cancelled=False, win_amount=0):
    W = 40
    d = bytearray()
    d += make_init()
    d += _header()
    d += FS + b'.'
    d += ESC + b't\x00'
    d += ESC + b'a\x01'
    d += _txt("-" * W + "\n")
    now = datetime.datetime.now().strftime("%Y/%m/%d %H:%M")
    d += _lr(cashier_name, now, W)
    d += _lr("Ticket:" + ticket_id, "Rnd:" + str(round_num), W)
    picks_str = " ".join(p["pick"][0] + str(p["coin"]) for p in picks)
    d += _lr("Picks:", picks_str, W)
    d += _txt("-" * W + "\n")
    d += ESC + b'!\x08'
    d += _row("Stake:", amount, W)
    d += _row("Win:", round(amount * multiplier, 2), W)
    d += ESC + b'!\x00'

    if reprint:
        d += _txt("-" * W + "\n")
        d += ESC + b'!\x10'
        d += _center_line("*** COPY ***", W)
        d += ESC + b'!\x00'
        status = "Redeemed" if redeemed else ("Cancelled" if cancelled else "Active")
        d += _center_line(f"Status: {status}", W)
        if redeemed:
            d += _center_line(f"Paid: Br {win_amount:,.2f}", W)

    bc_val = (HOUSE_CODE + ticket_id + str(round_num)).upper().replace(" ", "")[:20]
    bc_payload = b'{B' + _txt(bc_val)
    d += ESC + b'a\x01'
    d += GS + b'h' + bytes([40])
    d += GS + b'w' + bytes([2])
    d += GS + b'H' + bytes([2])
    d += GS + b'f' + bytes([0])
    d += GS + b'k' + bytes([73]) + bytes([len(bc_payload)]) + bc_payload
    d += b'\n'
    d += ESC + b'a\x00'
    d += GS + b'V\x42\x00'
    return bytes(d)

def build_summary_ticket(username, cashier_id_label, start_time, end_time,
                          start_balance, deposits, bets, cancellations,
                          redeemed, commission, withdraws, end_balance, **_):
    W = 40
    d = bytearray()
    d += make_init()
    d += _header()
    d += FS + b'.'
    d += ESC + b't\x00'
    d += ESC + b'a\x01'
    d += ESC + b'!\x08'
    d += _txt("Summary\n")
    d += ESC + b'!\x00'
    d += _txt(f"{username}  {cashier_id_label}\n")
    d += _txt(f"{start_time} -\n{end_time}\n")
    d += _txt("=" * W + "\n")
    d += ESC + b'a\x00'
    d += ESC + b'!\x08'
    d += _row("Start Balance", start_balance, W)
    d += ESC + b'!\x00'
    d += _row("Deposits", deposits, W)
    d += _row("Bets", bets, W)
    d += _row("Cancellations", cancellations, W)
    d += _row("Redeemed", redeemed, W)
    d += _row("Commission", commission, W)
    d += _row("Withdraws", withdraws, W)
    d += _txt("-" * W + "\n")
    d += ESC + b'!\x08'
    d += _row("End Balance", end_balance, W)
    d += ESC + b'!\x00'
    d += b'\n'
    d += GS + b'V\x42\x00'
    return bytes(d)

# ================== PRINT QUEUE ==================
print_queue = queue.Queue()

def _raw_send(job_name, data_bytes):
    hp = win32print.OpenPrinter(PRINTER_NAME)
    try:
        win32print.StartDocPrinter(hp, 1, (job_name, None, "RAW"))
        win32print.StartPagePrinter(hp)
        win32print.WritePrinter(hp, data_bytes)
        win32print.EndPagePrinter(hp)
        win32print.EndDocPrinter(hp)
    finally:
        win32print.ClosePrinter(hp)

def print_worker():
    while True:
        job = print_queue.get()
        if job is None: break
        kind, payload = job
        try:
            if kind == "ticket":
                _raw_send(payload.get("ticket_id", "ticket"), build_bet_ticket(
                    payload["ticket_id"], payload["round_num"], payload["cashier_name"],
                    payload["picks"], payload["amount"], payload["multiplier"],
                    reprint=payload.get("reprint", False),
                    redeemed=payload.get("redeemed", False),
                    cancelled=payload.get("cancelled", False),
                    win_amount=payload.get("win_amount", 0)))
            elif kind == "summary":
                _raw_send("summary", build_summary_ticket(**payload))
        except Exception:
            traceback.print_exc()
        finally:
            print_queue.task_done()

threading.Thread(target=print_worker, daemon=True).start()

# ================== LOCAL HTTP SERVER ==================
class Handler(BaseHTTPRequestHandler):
    def log_message(self, *a): pass

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

    def do_OPTIONS(self):
        self.send_response(204); self._cors(); self.end_headers()

    def do_GET(self):
        # health check so the cashier page can detect the helper is running
        self.send_response(200); self._cors()
        self.send_header("Content-type", "application/json"); self.end_headers()
        self.wfile.write(b'{"ok":true,"helper":"head-or-tail-print"}')

    def do_POST(self):
        length = int(self.headers.get("Content-Length", 0))
        body = self.rfile.read(length) if length else b"{}"
        try:
            data = json.loads(body or b"{}")
        except Exception:
            data = {}
        kind = data.get("kind", "ticket")
        print_queue.put((kind, data))
        self.send_response(200); self._cors()
        self.send_header("Content-type", "application/json"); self.end_headers()
        self.wfile.write(b'{"queued":true}')


def main():
    print("Head or Tail print helper")
    print("Listening on http://127.0.0.1:%d" % LISTEN_PORT)
    print("Printer:", PRINTER_NAME)
    print("Leave this window open while the shop is operating.")
    HTTPServer(("127.0.0.1", LISTEN_PORT), Handler).serve_forever()


if __name__ == "__main__":
    main()
