#!/usr/bin/env python3
"""
botcall print daemon

Connects to botcall cloud and prints jobs to local USB printer.
"""

import asyncio
import json
import base64
import platform
import sys
import os
import click
from pathlib import Path
from dotenv import load_dotenv

# Load .env from home directory (optional - systemd sets env directly)
load_dotenv(Path.home() / ".botcall" / ".env", override=False)

API_URL = os.getenv("BOTCALL_API_URL", "https://botcall-api-production.up.railway.app")
WS_URL = os.getenv("BOTCALL_WS_URL", "wss://botcall-api-production.up.railway.app/ws/print")
API_KEY = os.getenv("BOTCALL_API_KEY")

# Printer connection (lazy loaded)
_printer = None


def get_printer():
    """Get or create printer connection using configured USB ID."""
    global _printer
    if _printer is None:
        from escpos.printer import Usb
        
        # Get printer ID from environment
        printer_id = os.getenv("BOTCALL_PRINTER")
        if not printer_id:
            raise RuntimeError("BOTCALL_PRINTER not set. Run: botcall-print install --api-key <key> --printer <vid:pid>")
        
        try:
            vid, pid = printer_id.split(":")
            vid = int(vid, 16)
            pid = int(pid, 16)
        except ValueError:
            raise RuntimeError(f"Invalid BOTCALL_PRINTER format: {printer_id}. Expected xxxx:xxxx")
        
        try:
            _printer = Usb(vid, pid)
            _printer.open()
            click.echo(f"✓ Connected to printer: {printer_id}")
        except Exception as e:
            raise RuntimeError(f"Failed to connect to printer {printer_id}: {e}")
    
    return _printer


async def print_raw_async(data: bytes) -> None:
    """Send raw ESC/POS data to printer with OS-specific handling.
    
    Recognizes delay markers (ESC _ <delayL> <delayH>) and pauses.
    """
    printer = get_printer()
    
    # Scan for delay markers and split data
    DELAY_MARKER = b'\x1b\x5f'  # ESC _
    chunks = []
    delays = []
    pos = 0
    
    while pos < len(data):
        marker_pos = data.find(DELAY_MARKER, pos)
        if marker_pos == -1:
            # No more markers, add rest of data
            chunks.append(data[pos:])
            delays.append(0)
            break
        else:
            # Add data before marker
            if marker_pos > pos:
                chunks.append(data[pos:marker_pos])
            # Parse delay (2 bytes little endian after marker)
            if marker_pos + 4 <= len(data):
                delay_ms = data[marker_pos + 2] | (data[marker_pos + 3] << 8)
                delays.append(delay_ms)
                pos = marker_pos + 4
            else:
                pos = len(data)
    
    # Send each chunk with delays
    for i, chunk in enumerate(chunks):
        if len(chunk) == 0:
            continue
            
        # Mac needs chunking to avoid buffer issues
        if platform.system() == "Darwin":
            chunk_size = 256
            mac_delay = 0.01
            for j in range(0, len(chunk), chunk_size):
                subchunk = chunk[j:j + chunk_size]
                printer._raw(subchunk)
                await asyncio.sleep(mac_delay)
        else:
            printer._raw(chunk)
        
        # Apply delay after this chunk if specified
        if i < len(delays) and delays[i] > 0:
            delay_sec = delays[i] / 1000.0
            click.echo(f"  ⏱ Cooling delay: {delays[i]}ms")
            await asyncio.sleep(delay_sec)


def print_raw(data: bytes) -> None:
    """Sync wrapper for print_raw_async - use when not in async context."""
    asyncio.get_event_loop().run_until_complete(print_raw_async(data))


async def connect_and_listen():
    """Connect to botcall and listen for print jobs."""
    import websockets
    
    if not API_KEY:
        click.echo("✗ BOTCALL_API_KEY not set. Run: botcall print setup", err=True)
        sys.exit(1)
    
    click.echo(f"Connecting to {WS_URL}...")
    
    async for websocket in websockets.connect(
        WS_URL,
        additional_headers={"Authorization": f"Bearer {API_KEY}"},
        ping_interval=20,      # Send ping every 20s
        ping_timeout=30,       # Close if no pong within 30s
        close_timeout=10,      # Don't hang on close
    ):
        try:
            click.echo("✓ Connected. Waiting for print jobs...")
            
            # Send initial heartbeat
            await websocket.send(json.dumps({
                "type": "register",
                "platform": platform.system(),
                "version": "0.2.5",
                "machineId": platform.node(),  # hostname for multi-printer support
            }))
            
            async for message in websocket:
                await handle_message(websocket, message)
                
        except websockets.ConnectionClosed:
            click.echo("Connection lost. Reconnecting in 5s...")
            await asyncio.sleep(5)
            continue


async def handle_message(ws, message: str):
    """Handle incoming message from server."""
    try:
        msg = json.loads(message)
        
        if msg["type"] == "print":
            job_id = msg["jobId"]
            data = base64.b64decode(msg["data"])
            
            click.echo(f"Printing job {job_id[:8]}...")
            
            # Notify server we're starting
            await ws.send(json.dumps({
                "type": "job_started",
                "jobId": job_id,
            }))
            
            try:
                await print_raw_async(data)
                
                # Notify server we're done
                await ws.send(json.dumps({
                    "type": "job_completed",
                    "jobId": job_id,
                }))
                click.echo(f"✓ Job {job_id[:8]} complete")
                
            except Exception as e:
                await ws.send(json.dumps({
                    "type": "job_failed",
                    "jobId": job_id,
                    "error": str(e),
                }))
                click.echo(f"✗ Job {job_id[:8]} failed: {e}", err=True)
        
        elif msg["type"] == "ping":
            await ws.send(json.dumps({"type": "pong"}))
        
        elif msg["type"] == "registered":
            click.echo(f"✓ Registered with server (user: {msg.get('userId', 'unknown')[:8]}...)")
        
        elif msg["type"] == "heartbeat_ack":
            pass  # Acknowledged, nothing to do
        
        elif msg["type"] == "error":
            click.echo(f"⚠ Server error: {msg.get('message', 'unknown')}", err=True)
            
    except Exception as e:
        click.echo(f"Error handling message: {e}", err=True)


@click.group()
def cli():
    """botcall print daemon - receive print jobs from the cloud."""
    pass


@cli.command()
@click.option("--api-key", prompt="Botcall API key", help="Your botcall API key")
def setup(api_key: str):
    """Configure the print daemon (API key only)."""
    config_dir = Path.home() / ".botcall"
    config_dir.mkdir(exist_ok=True)
    
    env_file = config_dir / ".env"
    with open(env_file, "w") as f:
        f.write(f"BOTCALL_API_KEY={api_key}\n")
    
    click.echo(f"✓ Config saved to {env_file}")
    click.echo("Run 'botcall-print daemon' to start listening for jobs.")


@cli.command()
@click.option("--api-key", prompt="Botcall API key", help="Your botcall API key")
@click.option("--printer", prompt="Printer USB ID (run 'lsusb' to find, e.g. 0416:5011)", help="Printer vendor:product ID")
def install(api_key: str, printer: str):
    """Full install: create systemd service with API key + start daemon."""
    import subprocess
    import shutil
    
    # Find the botcall-print executable
    botcall_bin = shutil.which("botcall-print")
    if not botcall_bin:
        # Check common pip install locations
        for path in [
            Path.home() / ".local" / "bin" / "botcall-print",
            Path("/usr/local/bin/botcall-print"),
        ]:
            if path.exists():
                botcall_bin = str(path)
                break
    
    if not botcall_bin:
        click.echo("✗ Could not find botcall-print executable", err=True)
        sys.exit(1)
    
    click.echo(f"✓ Found executable: {botcall_bin}")
    
    # Validate printer ID format (xxxx:xxxx)
    import re
    if not re.match(r'^[0-9a-fA-F]{4}:[0-9a-fA-F]{4}$', printer):
        click.echo(f"✗ Invalid printer ID format: {printer}", err=True)
        click.echo("  Expected format: xxxx:xxxx (e.g. 0416:5011)", err=True)
        click.echo("  Run 'lsusb' to find your printer's ID", err=True)
        sys.exit(1)
    
    # Get current user
    user = os.environ.get("USER", "pi")
    home = str(Path.home())
    
    # Create systemd service (API key and printer in environment)
    service_content = f"""[Unit]
Description=botcall print daemon
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User={user}
Environment=HOME={home}
Environment=BOTCALL_API_KEY={api_key}
Environment=BOTCALL_PRINTER={printer}
ExecStart={botcall_bin} daemon
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
"""
    
    service_path = "/etc/systemd/system/botcall-print.service"
    
    # Write service file (needs sudo)
    try:
        result = subprocess.run(
            ["sudo", "tee", service_path],
            input=service_content.encode(),
            capture_output=True
        )
        if result.returncode != 0:
            click.echo(f"✗ Failed to create service: {result.stderr.decode()}", err=True)
            sys.exit(1)
        click.echo(f"✓ Created {service_path}")
        
        # Reload systemd
        subprocess.run(["sudo", "systemctl", "daemon-reload"], check=True)
        click.echo("✓ Reloaded systemd")
        
        # Enable service
        subprocess.run(["sudo", "systemctl", "enable", "botcall-print"], check=True)
        click.echo("✓ Enabled botcall-print service")
        
        # Start service
        subprocess.run(["sudo", "systemctl", "start", "botcall-print"], check=True)
        click.echo("✓ Started botcall-print service")
        
        click.echo("\n🎉 Install complete! Check status with: sudo systemctl status botcall-print")
        
    except subprocess.CalledProcessError as e:
        click.echo(f"✗ Failed: {e}", err=True)
        sys.exit(1)
    except Exception as e:
        click.echo(f"✗ Error: {e}", err=True)
        sys.exit(1)


@cli.command()
def uninstall():
    """Stop and remove the systemd service."""
    import subprocess
    
    try:
        subprocess.run(["sudo", "systemctl", "stop", "botcall-print"], check=False)
        subprocess.run(["sudo", "systemctl", "disable", "botcall-print"], check=False)
        subprocess.run(["sudo", "rm", "-f", "/etc/systemd/system/botcall-print.service"], check=False)
        subprocess.run(["sudo", "systemctl", "daemon-reload"], check=True)
        click.echo("✓ Uninstalled botcall-print service")
    except Exception as e:
        click.echo(f"✗ Error: {e}", err=True)
        sys.exit(1)


@cli.command()
def daemon():
    """Start the print daemon."""
    click.echo("botcall print daemon v0.1.0")
    
    # Test printer connection first
    try:
        get_printer()
    except Exception as e:
        click.echo(f"✗ Printer error: {e}", err=True)
        sys.exit(1)
    
    asyncio.run(connect_and_listen())


@cli.command()
@click.argument("text")
@click.option("--printer", "-p", help="Printer USB ID (e.g. 0416:5011). If not provided, uses BOTCALL_PRINTER env var.")
def test(text: str, printer: str):
    """Test print some text."""
    # Temporarily set env var if --printer provided
    if printer:
        os.environ["BOTCALL_PRINTER"] = printer
    
    try:
        p = get_printer()
        p.text(text + "\n")
        p.cut()
        click.echo("✓ Printed!")
    except Exception as e:
        click.echo(f"✗ Error: {e}", err=True)
        sys.exit(1)


@cli.command()
def status():
    """Show daemon status."""
    config_file = Path.home() / ".botcall" / ".env"
    
    if config_file.exists():
        click.echo(f"✓ Config: {config_file}")
    else:
        click.echo("✗ Not configured. Run: botcall-print setup")
        return
    
    try:
        printer = get_printer()
        click.echo("✓ Printer: Connected")
    except Exception as e:
        click.echo(f"✗ Printer: {e}")
    
    # Check service status
    system = platform.system()
    if system == "Darwin":
        import subprocess
        result = subprocess.run(
            ["launchctl", "list", "io.botcall.print-daemon"],
            capture_output=True, text=True
        )
        if result.returncode == 0:
            click.echo("✓ Service: Running (launchd)")
        else:
            click.echo("○ Service: Not installed")
    elif system == "Linux":
        import subprocess
        result = subprocess.run(
            ["systemctl", "is-active", "botcall-print"],
            capture_output=True, text=True
        )
        if result.stdout.strip() == "active":
            click.echo("✓ Service: Running (systemd)")
        else:
            click.echo("○ Service: Not running")
    elif system == "Windows":
        click.echo("○ Service: Check Task Scheduler for 'botcall-print'")


def _get_executable_path() -> str:
    """Get the path to the botcall-print executable."""
    import shutil
    
    # Check if we're in a venv
    if sys.prefix != sys.base_prefix:
        # In a venv - use the venv's botcall-print
        if platform.system() == "Windows":
            exe = Path(sys.prefix) / "Scripts" / "botcall-print.exe"
        else:
            exe = Path(sys.prefix) / "bin" / "botcall-print"
        if exe.exists():
            return str(exe)
    
    # Try to find it in PATH
    found = shutil.which("botcall-print")
    if found:
        return found
    
    # Fallback to current Python module execution
    return f"{sys.executable} -m botcall_print"


def _install_macos() -> bool:
    """Install LaunchAgent on macOS."""
    import subprocess
    
    exe_path = _get_executable_path()
    plist_dir = Path.home() / "Library" / "LaunchAgents"
    plist_dir.mkdir(parents=True, exist_ok=True)
    plist_path = plist_dir / "io.botcall.print-daemon.plist"
    
    # Create log directory
    log_dir = Path.home() / "Library" / "Logs" / "botcall"
    log_dir.mkdir(parents=True, exist_ok=True)
    
    plist_content = f"""<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>io.botcall.print-daemon</string>
    
    <key>ProgramArguments</key>
    <array>
        <string>{exe_path}</string>
        <string>daemon</string>
    </array>
    
    <key>RunAtLoad</key>
    <true/>
    
    <key>KeepAlive</key>
    <dict>
        <key>SuccessfulExit</key>
        <false/>
        <key>NetworkState</key>
        <true/>
    </dict>
    
    <key>ThrottleInterval</key>
    <integer>5</integer>
    
    <key>StandardOutPath</key>
    <string>{log_dir}/daemon.log</string>
    
    <key>StandardErrorPath</key>
    <string>{log_dir}/daemon.log</string>
    
    <key>EnvironmentVariables</key>
    <dict>
        <key>PATH</key>
        <string>/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
    </dict>
</dict>
</plist>
"""
    
    # Unload if already loaded
    subprocess.run(["launchctl", "unload", str(plist_path)], 
                   capture_output=True, stderr=subprocess.DEVNULL)
    
    # Write plist
    plist_path.write_text(plist_content)
    click.echo(f"✓ Created {plist_path}")
    
    # Load the agent
    result = subprocess.run(["launchctl", "load", str(plist_path)], capture_output=True, text=True)
    if result.returncode != 0:
        click.echo(f"✗ Failed to load: {result.stderr}", err=True)
        return False
    
    click.echo("✓ Service loaded and started")
    click.echo(f"  Logs: {log_dir}/daemon.log")
    return True


def _install_linux() -> bool:
    """Install systemd service on Linux."""
    import subprocess
    
    exe_path = _get_executable_path()
    service_content = f"""[Unit]
Description=Botcall Print Daemon
Documentation=https://botcall.io/docs/print-daemon
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
ExecStart={exe_path} daemon
Restart=on-failure
RestartSec=5
Environment=PYTHONUNBUFFERED=1
StandardOutput=journal
StandardError=journal
SyslogIdentifier=botcall-print

[Install]
WantedBy=multi-user.target
"""
    
    # Try user service first (no sudo required)
    user_service_dir = Path.home() / ".config" / "systemd" / "user"
    user_service_dir.mkdir(parents=True, exist_ok=True)
    user_service_path = user_service_dir / "botcall-print.service"
    
    user_service_path.write_text(service_content)
    click.echo(f"✓ Created {user_service_path}")
    
    # Reload and enable
    result = subprocess.run(["systemctl", "--user", "daemon-reload"], capture_output=True)
    result = subprocess.run(["systemctl", "--user", "enable", "botcall-print"], capture_output=True, text=True)
    if result.returncode != 0:
        click.echo(f"⚠ Could not enable: {result.stderr}")
    
    result = subprocess.run(["systemctl", "--user", "start", "botcall-print"], capture_output=True, text=True)
    if result.returncode != 0:
        click.echo(f"⚠ Could not start: {result.stderr}")
        click.echo("You may need to run: loginctl enable-linger $USER")
        return False
    
    click.echo("✓ Service enabled and started")
    click.echo("  Logs: journalctl --user -u botcall-print -f")
    return True


def _install_windows() -> bool:
    """Install Task Scheduler task on Windows."""
    import subprocess
    
    exe_path = _get_executable_path()
    
    # Use Task Scheduler (no admin required for user tasks)
    task_xml = f"""<?xml version="1.0" encoding="UTF-16"?>
<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
  <RegistrationInfo>
    <Description>Botcall Print Daemon - receives print jobs from the cloud</Description>
    <URI>\\botcall-print</URI>
  </RegistrationInfo>
  <Triggers>
    <LogonTrigger>
      <Enabled>true</Enabled>
    </LogonTrigger>
  </Triggers>
  <Principals>
    <Principal id="Author">
      <LogonType>InteractiveToken</LogonType>
      <RunLevel>LeastPrivilege</RunLevel>
    </Principal>
  </Principals>
  <Settings>
    <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
    <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
    <StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
    <AllowHardTerminate>true</AllowHardTerminate>
    <StartWhenAvailable>true</StartWhenAvailable>
    <RunOnlyIfNetworkAvailable>true</RunOnlyIfNetworkAvailable>
    <AllowStartOnDemand>true</AllowStartOnDemand>
    <Enabled>true</Enabled>
    <Hidden>false</Hidden>
    <RestartOnFailure>
      <Interval>PT1M</Interval>
      <Count>3</Count>
    </RestartOnFailure>
    <ExecutionTimeLimit>PT0S</ExecutionTimeLimit>
    <Priority>7</Priority>
  </Settings>
  <Actions Context="Author">
    <Exec>
      <Command>{exe_path}</Command>
      <Arguments>daemon</Arguments>
    </Exec>
  </Actions>
</Task>
"""
    
    # Write XML to temp file
    import tempfile
    with tempfile.NamedTemporaryFile(mode='w', suffix='.xml', delete=False, encoding='utf-16') as f:
        f.write(task_xml)
        xml_path = f.name
    
    try:
        # Delete existing task if present
        subprocess.run(["schtasks", "/Delete", "/TN", "botcall-print", "/F"], 
                      capture_output=True, stderr=subprocess.DEVNULL)
        
        # Create task
        result = subprocess.run(
            ["schtasks", "/Create", "/TN", "botcall-print", "/XML", xml_path],
            capture_output=True, text=True
        )
        
        if result.returncode != 0:
            click.echo(f"✗ Failed to create task: {result.stderr}", err=True)
            return False
        
        click.echo("✓ Task Scheduler entry created")
        
        # Start the task now
        subprocess.run(["schtasks", "/Run", "/TN", "botcall-print"], capture_output=True)
        click.echo("✓ Daemon started")
        return True
        
    finally:
        Path(xml_path).unlink(missing_ok=True)


@cli.command()
def uninstall_service():
    """Remove the background service."""
    import subprocess
    
    system = platform.system()
    click.echo(f"Uninstalling botcall-print daemon on {system}...")
    
    if system == "Darwin":
        plist_path = Path.home() / "Library" / "LaunchAgents" / "io.botcall.print-daemon.plist"
        subprocess.run(["launchctl", "unload", str(plist_path)], capture_output=True)
        if plist_path.exists():
            plist_path.unlink()
            click.echo(f"✓ Removed {plist_path}")
        click.echo("✓ Service unloaded")
        
    elif system == "Linux":
        subprocess.run(["systemctl", "--user", "stop", "botcall-print"], capture_output=True)
        subprocess.run(["systemctl", "--user", "disable", "botcall-print"], capture_output=True)
        service_path = Path.home() / ".config" / "systemd" / "user" / "botcall-print.service"
        if service_path.exists():
            service_path.unlink()
            click.echo(f"✓ Removed {service_path}")
        subprocess.run(["systemctl", "--user", "daemon-reload"], capture_output=True)
        click.echo("✓ Service stopped and disabled")
        
    elif system == "Windows":
        subprocess.run(["schtasks", "/End", "/TN", "botcall-print"], capture_output=True)
        result = subprocess.run(
            ["schtasks", "/Delete", "/TN", "botcall-print", "/F"],
            capture_output=True, text=True
        )
        if result.returncode == 0:
            click.echo("✓ Task Scheduler entry removed")
        else:
            click.echo(f"⚠ Could not remove task: {result.stderr}")
    
    click.echo("\n✓ Uninstall complete")


if __name__ == "__main__":
    cli()
