#!/usr/bin/env node
/**
 * XYZ POS Local Print Service
 * Windows executable service for detecting label printers and printing ZPL labels
 * 
 * This service runs as a background process and provides a REST API
 * for the web application to detect printers and print labels.
 * 
 * Usage:
 *   node service.js
 *   or run the compiled .exe file
 */

const http = require('http');
const url = require('url');
const { exec, spawn } = require('child_process');
const util = require('util');
const fs = require('fs');
const path = require('path');
const os = require('os');

const execPromise = util.promisify(exec);

// Configuration
const PORT = parseInt(process.env.PORT || '9101');
const HOST = process.env.HOST || 'localhost';

// Label printer brands to detect
const LABEL_PRINTER_BRANDS = [
  'zebra', 'honeywell', 'tsc', 'tspl', 'intermec', 
  'datamax', 'sato', 'printronix', 'zpl'
];

/**
 * Detect printer brand from name
 */
function detectBrand(printerName) {
  const lower = (printerName || '').toLowerCase();
  if (lower.includes('zebra')) return 'Zebra';
  if (lower.includes('honeywell')) return 'Honeywell';
  if (lower.includes('tsc') || lower.includes('tspl')) return 'TSC';
  if (lower.includes('intermec')) return 'Intermec';
  if (lower.includes('datamax')) return 'Datamax';
  if (lower.includes('sato')) return 'Sato';
  if (lower.includes('printronix')) return 'Printronix';
  if (lower.includes('zdesigner')) return 'Zebra';
  return 'Label Printer';
}

/**
 * Detect printer DPI from name or driver
 * Common DPIs: 203, 300, 600
 */
function detectDPI(printerName, driverName) {
  const combined = `${printerName} ${driverName || ''}`.toLowerCase();
  
  // Check for explicit DPI in name (e.g., "ZT231-203dpi", "203dpi")
  const dpiMatch = combined.match(/(\d+)dpi/);
  if (dpiMatch) {
    return parseInt(dpiMatch[1]);
  }
  
  // Check for model numbers that indicate DPI
  if (combined.includes('203') || combined.includes('zt231')) {
    return 203;
  }
  if (combined.includes('300') || combined.includes('zt410') || combined.includes('zt420')) {
    return 300;
  }
  if (combined.includes('600') || combined.includes('zt610')) {
    return 600;
  }
  
  // Default to 203 DPI (most common for label printers)
  return 203;
}

/**
 * Get all installed printers from Windows
 * Uses PowerShell Get-Printer command
 */
async function getInstalledPrinters() {
  try {
    // PowerShell command to get all printers
    const command = `powershell -Command "Get-Printer | Select-Object Name, DriverName, PortName | ConvertTo-Json"`;
    
    const { stdout, stderr } = await execPromise(command);
    
    if (stderr && !stdout) {
      console.error('Error getting printers:', stderr);
      return [];
    }

    // Parse JSON output
    let printers = [];
    try {
      const jsonData = JSON.parse(stdout.trim());
      printers = Array.isArray(jsonData) ? jsonData : [jsonData];
    } catch (e) {
      // If JSON parsing fails, try alternative method
      return await getPrintersAlternative();
    }

    // Filter for label printers and format
    const labelPrinters = printers
      .filter((printer) => {
        if (!printer || !printer.Name) return false;
        const name = printer.Name.toLowerCase();
        return LABEL_PRINTER_BRANDS.some(brand => name.includes(brand));
      })
      .map((printer, index) => {
        const dpi = detectDPI(printer.Name, printer.DriverName);
        return {
          id: `printer-${index}-${Date.now()}`,
          name: printer.Name,
          driver: printer.DriverName || '',
          port: printer.PortName || '',
          type: 'label',
          brand: detectBrand(printer.Name),
          dpi: dpi,
          description: `${detectBrand(printer.Name)} Label Printer (${dpi} DPI)`,
        };
      });

    return labelPrinters;
  } catch (error) {
    console.error('Error getting printers:', error.message);
    return [];
  }
}

/**
 * Alternative method to get printers using wmic
 */
async function getPrintersAlternative() {
  try {
    const command = `wmic printer get name,drivername,portname /format:csv`;
    const { stdout } = await execPromise(command);
    
    const lines = stdout.split('\n').filter(line => line.trim() && !line.startsWith('Node'));
    const printers = [];

    for (const line of lines) {
      const parts = line.split(',');
      if (parts.length >= 4) {
        const name = parts[parts.length - 3]?.trim();
        const driver = parts[parts.length - 2]?.trim();
        const port = parts[parts.length - 1]?.trim();
        
        if (name) {
          const nameLower = name.toLowerCase();
          if (LABEL_PRINTER_BRANDS.some(brand => nameLower.includes(brand))) {
            printers.push({
              id: `printer-${printers.length}`,
              name,
              driver: driver || '',
              port: port || '',
              type: 'label',
              brand: detectBrand(name),
              description: `${detectBrand(name)} Label Printer`,
            });
          }
        }
      }
    }

    return printers;
  } catch (error) {
    console.error('Error with alternative printer detection:', error.message);
    return [];
  }
}

/**
 * Print ZPL to Windows printer
 * Uses Windows copy command to send ZPL directly to printer port
 */
async function printZPLToPrinter(printerName, zplData) {
  // Escape printer name for use in PowerShell commands (handle single quotes)
  const escapedPrinterName = printerName.replace(/'/g, "''");
  
  console.log(`Starting print to: ${printerName} (escaped: ${escapedPrinterName})`);
  
  try {
    // Create temporary ZPL file
    const tempDir = os.tmpdir();
    const tempFile = path.join(tempDir, `xyz_pos_label_${Date.now()}.zpl`);
    
    // Write ZPL data to file
    fs.writeFileSync(tempFile, zplData, 'utf8');
    console.log(`Temporary ZPL file created: ${tempFile}`);
    
    // Method 1: Get printer port and send directly to port (most reliable)
    let printerPort = null;
    let printerIP = null;
    
    try {
      // Get printer port using PowerShell (properly escaped)
      const portCommand = `powershell -NoProfile -Command "(Get-Printer -Name '${escapedPrinterName}' | Select-Object -ExpandProperty PortName).ToString().Trim()"`;
      console.log(`Executing port detection: ${portCommand}`);
      
      const { stdout } = await execPromise(portCommand);
      printerPort = stdout.trim();
      console.log(`Printer port detected: '${printerPort}'`);
      
      // Check if it's a network port (IP_xxx.xxx.xxx.xxx format)
      if (printerPort && printerPort.startsWith('IP_')) {
        printerIP = printerPort.replace(/^IP_/, '');
        console.log(`Network printer detected: ${printerIP}:9100`);
      }
      
      // For USB ports, skip copy command (it doesn't work reliably)
      // Go directly to PowerShell RAW printing method
      const isUSBPort = printerPort && (printerPort.startsWith('USB') || printerPort.startsWith('LPT') || printerPort.startsWith('COM'));
      
      // Only try copy command for non-USB ports (like network shares)
      if (printerPort && printerPort.length > 0 && !printerIP && !isUSBPort) {
        try {
          const portCopyCommand = `cmd /c copy /b "${tempFile}" "${printerPort}"`;
          console.log(`Attempting direct port copy: ${portCopyCommand}`);
          
          const { stdout, stderr } = await execPromise(portCopyCommand);
          
          // Check if copy actually succeeded - it should output something like "1 file(s) copied"
          const output = (stdout || '').trim().toLowerCase();
          const hasError = stderr || output.includes('error') || output.includes('cannot');
          
          console.log(`Direct port copy output: stdout=${stdout}, stderr=${stderr}`);
          
          if (hasError || (!output.includes('copied') && !output.includes('file'))) {
            // Copy command didn't actually succeed
            throw new Error(`Copy command failed: ${stderr || stdout || 'Unknown error'}`);
          }
          
          // Clean up
          fs.unlinkSync(tempFile);
          
          console.log(`Successfully copied ZPL to port ${printerPort}`);
          
          return {
            success: true,
            message: `ZPL sent to printer port: ${printerPort}`,
            printer: printerName,
            port: printerPort,
            method: 'direct-port',
          };
        } catch (portError) {
          console.warn('Direct port copy failed:', portError.message);
          // Continue to PowerShell method
        }
      } else if (isUSBPort) {
        console.log(`USB port detected (${printerPort}), skipping copy command and using RAW printing`);
      }
    } catch (e) {
      console.warn('Could not get printer port, trying default methods:', e.message);
    }

    // Method 2: If network printer, use TCP/IP connection to port 9100
    if (printerIP) {
      try {
        const net = require('net');
        
        return new Promise((resolve, reject) => {
          const client = new net.Socket();
          const timeout = 5000; // 5 second timeout
          
          client.setTimeout(timeout);
          
          client.connect(9100, printerIP, () => {
            // Send ZPL data
            client.write(zplData, 'utf8', () => {
              client.end();
            });
          });
          
          client.on('close', () => {
            // Clean up
            if (fs.existsSync(tempFile)) {
              fs.unlinkSync(tempFile);
            }
            resolve({
              success: true,
              message: `ZPL sent to network printer: ${printerIP}:9100`,
              printer: printerName,
              ip: printerIP,
              method: 'tcp-ip',
            });
          });
          
          client.on('error', (err) => {
            client.destroy();
            reject(new Error(`Network connection failed: ${err.message}`));
          });
          
          client.on('timeout', () => {
            client.destroy();
            reject(new Error('Network connection timeout'));
          });
        });
      } catch (netError) {
        console.warn('Network printing failed, trying PowerShell method:', netError.message);
      }
    }

    // Method 3: Use PowerShell - try port first, then printer name
    // For USB printers, try to use the port directly with raw data
    try {
      // Get printer port again if we don't have it
      let portForScript = printerPort;
      if (!portForScript) {
        try {
          const portCmd = `powershell -NoProfile -Command "(Get-Printer -Name '${escapedPrinterName}' | Select-Object -ExpandProperty PortName).ToString().Trim()"`;
          const { stdout } = await execPromise(portCmd);
          portForScript = stdout.trim();
        } catch (e) {
          console.warn('Could not get port for PowerShell script');
        }
      }
      
      // Build PowerShell script using Windows print spooler API for RAW printing
      // This properly handles USB printers with raw ZPL data
      let printScript = `
$ErrorActionPreference = 'Stop'
$printerName = '${escapedPrinterName}'
$filePath = '${tempFile.replace(/\\/g, '/')}'

# Define Windows print spooler API using C#
Add-Type @"
using System;
using System.Runtime.InteropServices;
using System.Text;

public class WinSpool {
    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
    public struct DOCINFOA {
        [MarshalAs(UnmanagedType.LPStr)]
        public string pDocName;
        [MarshalAs(UnmanagedType.LPStr)]
        public string pOutputFile;
        [MarshalAs(UnmanagedType.LPStr)]
        public string pDataType;
    }
    
    [DllImport("winspool.drv", EntryPoint = "OpenPrinterA", SetLastError = true, CharSet = CharSet.Ansi, ExactSpelling = true, CallingConvention = CallingConvention.StdCall)]
    public static extern bool OpenPrinter([MarshalAs(UnmanagedType.LPStr)] string szPrinter, out IntPtr hPrinter, IntPtr pd);
    
    [DllImport("winspool.drv", EntryPoint = "ClosePrinter", SetLastError = true, ExactSpelling = true, CallingConvention = CallingConvention.StdCall)]
    public static extern bool ClosePrinter(IntPtr hPrinter);
    
    [DllImport("winspool.drv", EntryPoint = "StartDocPrinterA", SetLastError = true, CharSet = CharSet.Ansi, ExactSpelling = true, CallingConvention = CallingConvention.StdCall)]
    public static extern bool StartDocPrinter(IntPtr hPrinter, int level, ref DOCINFOA di);
    
    [DllImport("winspool.drv", EntryPoint = "EndDocPrinter", SetLastError = true, ExactSpelling = true, CallingConvention = CallingConvention.StdCall)]
    public static extern bool EndDocPrinter(IntPtr hPrinter);
    
    [DllImport("winspool.drv", EntryPoint = "StartPagePrinter", SetLastError = true, ExactSpelling = true, CallingConvention = CallingConvention.StdCall)]
    public static extern bool StartPagePrinter(IntPtr hPrinter);
    
    [DllImport("winspool.drv", EntryPoint = "EndPagePrinter", SetLastError = true, ExactSpelling = true, CallingConvention = CallingConvention.StdCall)]
    public static extern bool EndPagePrinter(IntPtr hPrinter);
    
    [DllImport("winspool.drv", EntryPoint = "WritePrinter", SetLastError = true, ExactSpelling = true, CallingConvention = CallingConvention.StdCall)]
    public static extern bool WritePrinter(IntPtr hPrinter, IntPtr pBytes, int dwCount, out int dwWritten);
}

public class RawPrint {
    public static bool SendToPrinter(string printerName, byte[] data) {
        IntPtr hPrinter = IntPtr.Zero;
        try {
            if (!WinSpool.OpenPrinter(printerName, out hPrinter, IntPtr.Zero)) {
                int error = Marshal.GetLastWin32Error();
                throw new Exception("OpenPrinter failed with error: " + error);
            }
            
            WinSpool.DOCINFOA di = new WinSpool.DOCINFOA();
            di.pDocName = "ZPL Label";
            di.pDataType = "RAW";
            
            if (!WinSpool.StartDocPrinter(hPrinter, 1, ref di)) {
                int error = Marshal.GetLastWin32Error();
                throw new Exception("StartDocPrinter failed with error: " + error);
            }
            
            if (!WinSpool.StartPagePrinter(hPrinter)) {
                int error = Marshal.GetLastWin32Error();
                WinSpool.EndDocPrinter(hPrinter);
                throw new Exception("StartPagePrinter failed with error: " + error);
            }
            
            IntPtr pBytes = Marshal.AllocHGlobal(data.Length);
            Marshal.Copy(data, 0, pBytes, data.Length);
            int dwWritten = 0;
            bool success = WinSpool.WritePrinter(hPrinter, pBytes, data.Length, out dwWritten);
            Marshal.FreeHGlobal(pBytes);
            
            if (!success || dwWritten != data.Length) {
                int error = Marshal.GetLastWin32Error();
                WinSpool.EndPagePrinter(hPrinter);
                WinSpool.EndDocPrinter(hPrinter);
                throw new Exception("WritePrinter failed. Written: " + dwWritten + ", Expected: " + data.Length + ", Error: " + error);
            }
            
            WinSpool.EndPagePrinter(hPrinter);
            WinSpool.EndDocPrinter(hPrinter);
            
            return true;
        } finally {
            if (hPrinter != IntPtr.Zero) {
                WinSpool.ClosePrinter(hPrinter);
            }
        }
    }
}
"@

try {
  # Verify printer exists
  $printer = Get-Printer -Name $printerName -ErrorAction Stop
  if (-not $printer) {
    throw "Printer '$printerName' not found"
  }
  
  # Read ZPL file as bytes
  $zplBytes = [System.IO.File]::ReadAllBytes($filePath)
  
  # Send to printer using RAW print spooler API
  $success = [RawPrint]::SendToPrinter($printerName, $zplBytes)
  
  if (-not $success) {
    throw "Failed to send data to printer"
  }
  
  Write-Output "Success: ZPL sent to printer via RAW print spooler"
} catch {
  Write-Error $_.Exception.Message
  exit 1
}
`;
      
      // Write script to temp file and execute
      const scriptFile = path.join(tempDir, `print_script_${Date.now()}.ps1`);
      fs.writeFileSync(scriptFile, printScript, 'utf8');
      console.log(`PowerShell script created: ${scriptFile}`);
      
      try {
        const scriptCommand = `powershell -NoProfile -ExecutionPolicy Bypass -File "${scriptFile}"`;
        console.log(`Executing PowerShell script: ${scriptCommand}`);
        
        const { stdout, stderr } = await execPromise(scriptCommand);
        
        console.log(`PowerShell script output - stdout: ${stdout}`);
        console.log(`PowerShell script output - stderr: ${stderr}`);
        
        // Check if it actually succeeded
        const output = (stdout || '').toLowerCase();
        if (stderr || output.includes('error') || (!output.includes('success'))) {
          throw new Error(`PowerShell script failed: ${stderr || stdout || 'No success message'}`);
        }
        
        console.log(`PowerShell print succeeded: ${stdout}`);
        
        // Clean up
        fs.unlinkSync(tempFile);
        fs.unlinkSync(scriptFile);
        
        return {
          success: true,
          message: `ZPL sent to printer: ${printerName}${portForScript ? ` (via port ${portForScript})` : ''}`,
          printer: printerName,
          port: portForScript,
          method: 'powershell-raw',
        };
      } catch (scriptError) {
        // Clean up script file
        if (fs.existsSync(scriptFile)) {
          fs.unlinkSync(scriptFile);
        }
        console.error('PowerShell script error:', scriptError);
        throw scriptError;
      }
    } catch (printError) {
      // Clean up temp file
      if (fs.existsSync(tempFile)) {
        fs.unlinkSync(tempFile);
      }
      
      console.error('Print error:', printError);
      throw new Error(`Failed to print: ${printError.message}`);
    }
  } catch (error) {
    console.error('Error printing ZPL:', error);
    throw error;
  }
}

/**
 * Send raw ZPL to printer port (direct port access)
 * This requires administrator privileges
 */
async function printZPLToPort(portName, zplData) {
  try {
    const tempDir = os.tmpdir();
    const tempFile = path.join(tempDir, `xyz_pos_label_${Date.now()}.zpl`);
    
    fs.writeFileSync(tempFile, zplData, 'utf8');
    
    // Use copy /b to send binary data directly to port
    const copyCommand = `cmd /c copy /b "${tempFile}" "${portName}"`;
    const { stdout, stderr } = await execPromise(copyCommand);
    
    // Verify the copy actually succeeded
    const output = (stdout || '').trim().toLowerCase();
    const hasError = stderr || output.includes('error') || output.includes('cannot');
    
    if (hasError || (!output.includes('copied') && !output.includes('file'))) {
      // Clean up temp file before throwing
      if (fs.existsSync(tempFile)) {
        fs.unlinkSync(tempFile);
      }
      throw new Error(`Copy command failed: ${stderr || stdout || 'Copy did not report success'}`);
    }
    
    // Clean up
    fs.unlinkSync(tempFile);
    
    console.log(`Successfully copied ZPL to port ${portName}`);
    
    return {
      success: true,
      message: `ZPL sent to port: ${portName}`,
      port: portName,
      method: 'direct-port',
    };
  } catch (error) {
    throw new Error(`Failed to print to port: ${error.message}`);
  }
}

/**
 * HTTP Server
 */
const server = http.createServer(async (req, res) => {
  const parsedUrl = url.parse(req.url, true);
  const method = req.method.toUpperCase();

  // CORS headers
  res.setHeader('Access-Control-Allow-Origin', '*');
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
  res.setHeader('Content-Type', 'application/json');

  // Handle CORS preflight
  if (method === 'OPTIONS') {
    res.writeHead(200);
    res.end();
    return;
  }

  // Health check
  if (parsedUrl.pathname === '/health' && method === 'GET') {
    res.writeHead(200);
    res.end(JSON.stringify({
      status: 'ok',
      service: 'xyz-pos-print-service',
      version: '1.0.0',
      timestamp: new Date().toISOString(),
    }));
    return;
  }

  // Get printers endpoint
  if (parsedUrl.pathname === '/printers' && method === 'GET') {
    try {
      const printers = await getInstalledPrinters();
      res.writeHead(200);
      res.end(JSON.stringify({
        success: true,
        printers,
        count: printers.length,
      }));
    } catch (error) {
      res.writeHead(500);
      res.end(JSON.stringify({
        success: false,
        error: error.message,
      }));
    }
    return;
  }

  // Print endpoint
  if (parsedUrl.pathname === '/print' && method === 'POST') {
    let body = '';
    
    req.on('data', chunk => {
      body += chunk.toString();
    });
    
    req.on('end', async () => {
      try {
        const data = JSON.parse(body);
        const { printer, zpl, port } = data;

        console.log(`Print request received - printer: ${printer || 'none'}, port: ${port || 'none'}, zpl length: ${zpl ? zpl.length : 0}`);

        if (!zpl) {
          res.writeHead(400);
          res.end(JSON.stringify({
            success: false,
            error: 'ZPL data is required',
          }));
          return;
        }

        let result;
        try {
          if (port) {
            // Print directly to port
            console.log(`Attempting to print to port: ${port}`);
            result = await printZPLToPort(port, zpl);
            console.log(`Port print result: ${JSON.stringify(result)}`);
          } else if (printer) {
            // Print to named printer
            console.log(`Attempting to print to printer: ${printer}`);
            result = await printZPLToPrinter(printer, zpl);
            console.log(`Printer print result: ${JSON.stringify(result)}`);
          } else {
            res.writeHead(400);
            res.end(JSON.stringify({
              success: false,
              error: 'Either printer name or port name is required',
            }));
            return;
          }
        } catch (printErr) {
          console.error('Print method failed:', printErr);
          throw printErr;
        }

        // Verify result has success = true
        if (result && result.success === true) {
          console.log(`Print succeeded using method: ${result.method}`);
          res.writeHead(200);
          res.end(JSON.stringify({
            success: true,
            ...result,
          }));
        } else {
          throw new Error('Print operation did not return success');
        }
      } catch (error) {
        console.error('Print error:', error);
        res.writeHead(500);
        res.end(JSON.stringify({
          success: false,
          error: error.message,
        }));
      }
    });
    return;
  }

  // 404 Not Found
  res.writeHead(404);
  res.end(JSON.stringify({
    success: false,
    error: 'Not found',
    availableEndpoints: ['/health', '/printers', '/print'],
  }));
});

// Start server
server.listen(PORT, HOST, () => {
  console.log(`
╔════════════════════════════════════════════════════════════╗
║     XYZ POS Local Print Service                            ║
║     Version 1.0.0                                          ║
╠════════════════════════════════════════════════════════════╣
║  Service running on: http://${HOST}:${PORT}              ║
║  Endpoints:                                                 ║
║    GET  /health    - Health check                          ║
║    GET  /printers  - List available label printers         ║
║    POST /print     - Print ZPL to printer                  ║
╠════════════════════════════════════════════════════════════╣
║  Press Ctrl+C to stop                                      ║
╚════════════════════════════════════════════════════════════╝
  `);
  
  // Auto-detect printers on startup
  console.log('Scanning for label printers...');
  getInstalledPrinters()
    .then(printers => {
      if (printers.length > 0) {
        console.log(`Found ${printers.length} label printer(s):`);
        printers.forEach(p => {
          console.log(`  - ${p.name} (${p.brand})`);
        });
      } else {
        console.log('No label printers detected. Make sure your printer drivers are installed.');
      }
    })
    .catch(err => {
      console.error('Error scanning printers:', err.message);
    });
});

// Handle graceful shutdown
process.on('SIGINT', () => {
  console.log('\n\nShutting down print service...');
  server.close(() => {
    console.log('Service stopped.');
    process.exit(0);
  });
});

process.on('SIGTERM', () => {
  console.log('\n\nShutting down print service...');
  server.close(() => {
    console.log('Service stopped.');
    process.exit(0);
  });
});

// Handle uncaught errors
process.on('uncaughtException', (error) => {
  console.error('Uncaught exception:', error);
  // Don't exit - keep service running
});

process.on('unhandledRejection', (reason, promise) => {
  console.error('Unhandled rejection at:', promise, 'reason:', reason);
  // Don't exit - keep service running
});
