<?php
declare(strict_types=1);

// ── Config ────────────────────────────────────────────────────────────────
define('BASE_DIR',      __DIR__);
define('TEMPLATE_DIR',  BASE_DIR . '/templates');
define('GENERATED_DIR', BASE_DIR . '/generated');
define('MAX_TEXT',      2000);
define('MAX_UPLOAD_MB', 10);

// ── Helpers ───────────────────────────────────────────────────────────────

function json_response(array $data, int $code = 200): never {
    http_response_code($code);
    header('Content-Type: application/json; charset=utf-8');
    echo json_encode($data, JSON_UNESCAPED_UNICODE);
    exit;
}

function safe_name(string $s): string {
    return preg_replace('/[^A-Za-z0-9_\-]/u', '_', $s);
}

function ensure_dir(string $d): void {
    if (!is_dir($d)) mkdir($d, 0755, true);
}

// ── Template helpers ──────────────────────────────────────────────────────

function all_templates(): array {
    $paths = glob(TEMPLATE_DIR . '/*.docx') ?: [];
    $out = [];
    foreach ($paths as $p) {
        $stem = pathinfo($p, PATHINFO_FILENAME);
        if (str_starts_with(strtolower(basename($p)), '~$')) continue;
        [$alias, $note] = str_contains($stem, '-')
            ? array_map('trim', explode('-', $stem, 2))
            : [$stem, ''];
        $out[] = ['path' => $p, 'alias' => $alias, 'note' => $note, 'filename' => basename($p)];
    }
    usort($out, fn($a,$b) => strcasecmp($a['alias'], $b['alias']));
    return $out;
}

function resolve_template(string $name): ?string {
    foreach (all_templates() as $t) {
        if ($t['alias'] === $name || $t['filename'] === $name || pathinfo($t['filename'], PATHINFO_FILENAME) === $name)
            return $t['path'];
    }
    return null;
}

function is_routeid(string $path): bool {
    return strtolower(explode('-', pathinfo($path, PATHINFO_FILENAME), 2)[0]) === 'routeid';
}

// ── QR code (pure PHP, no library needed) ────────────────────────────────
// Uses Google Charts API for QR (offline alternative: install endroid/qr-code via composer)

function make_qr_png(string $value, int $size = 200): string {
    // Use qrencode if available, else fallback to GD pixel art
    if (shell_exec('which qrencode 2>/dev/null')) {
        $tmp = tempnam(sys_get_temp_dir(), 'qr_') . '.png';
        $margin = 2;
        $cmd = sprintf('qrencode -o %s -s 12 -m %d %s 2>/dev/null',
            escapeshellarg($tmp), $margin, escapeshellarg($value));
        exec($cmd, $out, $rc);
        if ($rc === 0 && file_exists($tmp)) {
            $data = file_get_contents($tmp);
            unlink($tmp);
            return $data;
        }
    }
    // Fallback: 1x1 white PNG
    $im = imagecreatetruecolor($size, $size);
    $white = imagecolorallocate($im, 255, 255, 255);
    imagefill($im, 0, 0, $white);
    ob_start(); imagepng($im); $data = ob_get_clean();
    imagedestroy($im);
    return $data;
}

// ── DOCX manipulation ─────────────────────────────────────────────────────

function word_text_fragment(string $text): string {
    $parts = array_map(fn($p) => htmlspecialchars($p, ENT_XML1, 'UTF-8'), explode("\n", $text));
    return implode('</w:t><w:br/><w:t>', $parts);
}

function replace_text_nodes(string $docXml, string $textValue): string {
    preg_match_all('#(<w:t(?:\s[^>]*)?>)(.*?)(</w:t>)#s', $docXml, $m, PREG_OFFSET_CAPTURE);
    if (empty($m[0])) throw new RuntimeException('模板中没有文本节点');

    $contents = array_map(fn($x) => html_entity_decode($x[0], ENT_XML1, 'UTF-8'), $m[2]);
    $full = implode('', $contents);

    if (preg_match('/text/i', $full, $pm, PREG_OFFSET_CAPTURE)) {
        $start = $pm[0][1]; $end = $start + strlen($pm[0][0]);
        $cursor = 0; $ranges = [];
        foreach ($contents as $c) { $l = strlen($c); $ranges[] = [$cursor, $cursor+$l]; $cursor += $l; }

        $new = [];
        foreach ($contents as $i => $c) {
            [$ns, $ne] = $ranges[$i];
            if ($ne <= $start || $ns >= $end) { $new[] = word_text_fragment($c); continue; }
            $r = '';
            if ($ns <= $start && $start < $ne) { $r .= substr($c, 0, $start-$ns); $r .= $textValue; }
            if ($ns < $end && $end <= $ne)       { $r .= substr($c, $end-$ns); }
            $new[] = word_text_fragment($r);
        }
    } else {
        // fallback: replace first non-empty node
        $new = []; $found = false;
        foreach ($contents as $c) {
            if (!$found && trim($c) !== '') { $found = true; $new[] = word_text_fragment($textValue); }
            else $new[] = word_text_fragment($c);
        }
        if (!$found) throw new RuntimeException("模板没有可替换的文本");
    }

    // rebuild
    $result = ''; $cursor = 0;
    foreach ($m[0] as $idx => $match) {
        $cStart = $m[2][$idx][1]; $cEnd = $cStart + strlen($m[2][$idx][0]);
        $result .= substr($docXml, $cursor, $cStart - $cursor);
        $result .= $new[$idx];
        $cursor = $cEnd;
    }
    return $result . substr($docXml, $cursor);
}

function template_wants_vcenter(string $docXml): bool {
    $dom = new DOMDocument(); $dom->loadXML($docXml, LIBXML_NOERROR);
    $ns = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main';
    foreach ($dom->getElementsByTagNameNS($ns, 'vAlign') as $n)
        if ($n->getAttributeNS($ns, 'val') === 'center') return true;
    return false;
}

function enforce_vcenter(string $docXml): string {
    $dom = new DOMDocument(); $dom->loadXML($docXml, LIBXML_NOERROR|LIBXML_NOWARNING);
    $ns = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main';
    $xp = new DOMXPath($dom); $xp->registerNamespace('w', $ns);

    $body = $xp->query('//w:body')->item(0);
    $sect = $xp->query('//w:body/w:sectPr')->item(0);
    if (!$body || !$sect) return $docXml;

    $pgSz  = $xp->query('//w:sectPr/w:pgSz')->item(0);
    $pgMar = $xp->query('//w:sectPr/w:pgMar')->item(0);
    $W  = (int)($pgSz  ? $pgSz->getAttributeNS($ns,'w')      : 2835);
    $H  = (int)($pgSz  ? $pgSz->getAttributeNS($ns,'h')      : 1134);
    $mL = (int)($pgMar ? $pgMar->getAttributeNS($ns,'left')   : 0);
    $mR = (int)($pgMar ? $pgMar->getAttributeNS($ns,'right')  : 0);
    $mT = (int)($pgMar ? $pgMar->getAttributeNS($ns,'top')    : 0);
    $mB = (int)($pgMar ? $pgMar->getAttributeNS($ns,'bottom') : 0);
    $w  = max(1,$W-$mL-$mR); $h = max(1,$H-$mT-$mB);

    $kids = [];
    foreach ($body->childNodes as $c) if ($c !== $sect) $kids[] = $c;
    if (!$kids) return $docXml;

    // horizontal center each paragraph
    foreach ($kids as $k) {
        if ($k->localName !== 'p') continue;
        $pPr = null;
        foreach ($k->childNodes as $ch) if ($ch->localName==='pPr'){$pPr=$ch;break;}
        if (!$pPr){$pPr=$dom->createElementNS($ns,'w:pPr');$k->insertBefore($pPr,$k->firstChild);}
        $jc=null; foreach($pPr->childNodes as $ch) if($ch->localName==='jc'){$jc=$ch;break;}
        if(!$jc){$jc=$dom->createElementNS($ns,'w:jc');$pPr->appendChild($jc);}
        $jc->setAttributeNS($ns,'w:val','center');
    }

    // wrap in table for vertical centering
    $e=fn($tag)=>$dom->createElementNS($ns,'w:'.$tag);
    $a=fn($el,$k,$v)=>$el->setAttributeNS($ns,'w:'.$k,$v);
    $tbl=$e('tbl'); $tblPr=$e('tblPr');
    $tw=$e('tblW'); $a($tw,'w',(string)$w); $a($tw,'type','dxa'); $tblPr->appendChild($tw);
    $jc=$e('jc'); $a($jc,'val','center'); $tblPr->appendChild($jc);
    $brd=$e('tblBorders');
    foreach(['top','left','bottom','right','insideH','insideV'] as $s){
        $b=$e($s);$a($b,'val','nil');$brd->appendChild($b);
    }
    $tblPr->appendChild($brd);
    $cm=$e('tblCellMar');
    foreach(['top','left','bottom','right'] as $s){
        $mb=$e($s);$a($mb,'w','0');$a($mb,'type','dxa');$cm->appendChild($mb);
    }
    $tblPr->appendChild($cm); $tbl->appendChild($tblPr);
    $grid=$e('tblGrid'); $gc=$e('gridCol'); $a($gc,'w',(string)$w); $grid->appendChild($gc); $tbl->appendChild($grid);
    $tr=$e('tr'); $trPr=$e('trPr'); $trH=$e('trHeight'); $a($trH,'val',(string)$h); $a($trH,'hRule','exact');
    $trPr->appendChild($trH); $tr->appendChild($trPr);
    $tc=$e('tc'); $tcPr=$e('tcPr');
    $tcw=$e('tcW'); $a($tcw,'w',(string)$w); $a($tcw,'type','dxa'); $tcPr->appendChild($tcw);
    $va=$e('vAlign'); $a($va,'val','center'); $tcPr->appendChild($va);
    $tc->appendChild($tcPr);
    foreach($kids as $k){$body->removeChild($k);$tc->appendChild($k);}
    $tr->appendChild($tc); $tbl->appendChild($tr);
    $body->insertBefore($tbl,$sect);
    return $dom->saveXML();
}

function build_docx(string $tplPath, string $outputPath, string $rawText): array {
    $zip = new ZipArchive();
    if ($zip->open($tplPath) !== true) throw new RuntimeException('无法打开模板');

    $docXml  = $zip->getFromName('word/document.xml');
    $relsXml = $zip->getFromName('word/_rels/document.xml.rels');
    if ($docXml === false) throw new RuntimeException('模板损坏');

    $isRoute   = is_routeid($tplPath);
    $qrValue   = $isRoute ? strtolower($rawText) : $rawText;
    $textValue = $isRoute ? strtoupper($rawText) : $rawText;

    // Find QR image target
    $imgTarget = null; $qrBytes = null;
    if ($relsXml !== false) {
        $dom = new DOMDocument(); $dom->loadXML($docXml, LIBXML_NOERROR);
        $blips = $dom->getElementsByTagNameNS('http://schemas.openxmlformats.org/drawingml/2006/main','blip');
        if ($blips->length > 0) {
            $relId = $blips->item(0)->getAttributeNS('http://schemas.openxmlformats.org/officeDocument/2006/relationships','embed');
            $rdom = new DOMDocument(); $rdom->loadXML($relsXml, LIBXML_NOERROR);
            foreach ($rdom->getElementsByTagNameNS('http://schemas.openxmlformats.org/package/2006/relationships','Relationship') as $r) {
                if ($r->getAttribute('Id') === $relId) {
                    $imgTarget = 'word/' . ltrim($r->getAttribute('Target'),'/');
                    break;
                }
            }
        }
    }

    if ($imgTarget !== null) {
        $oldBytes = $zip->getFromName($imgTarget);
        $sz = $oldBytes ? getimagesizefromstring($oldBytes) : null;
        $qrBytes = make_qr_png($qrValue, $sz ? (int)$sz[0] : 200);
    }

    $newDoc = replace_text_nodes($docXml, $textValue);
    if ($imgTarget === null && template_wants_vcenter($docXml))
        $newDoc = enforce_vcenter($newDoc);

    ensure_dir(dirname($outputPath));
    $out = new ZipArchive();
    $out->open($outputPath, ZipArchive::CREATE | ZipArchive::OVERWRITE);
    for ($i = 0; $i < $zip->numFiles; $i++) {
        $name = $zip->getNameIndex($i);
        if ($name === 'word/document.xml')       $out->addFromString($name, $newDoc);
        elseif ($imgTarget && $name===$imgTarget) $out->addFromString($name, $qrBytes);
        else { $d=$zip->getFromIndex($i); if($d!==false) $out->addFromString($name,$d); }
    }
    $zip->close(); $out->close();

    return ['qr_value'=>$imgTarget?$qrValue:'','text_value'=>$textValue,'has_qr'=>$imgTarget!==null];
}

function docx_to_pdf(string $docx, string $outDir): string {
    $soffice = trim(shell_exec('which soffice 2>/dev/null') ?: shell_exec('which libreoffice 2>/dev/null') ?: '');
    if (!$soffice) throw new RuntimeException('未找到 LibreOffice，请先安装');
    $cmd = sprintf('%s --headless --convert-to pdf --outdir %s %s 2>&1',
        escapeshellarg($soffice), escapeshellarg($outDir), escapeshellarg($docx));
    exec($cmd, $o, $rc);
    $pdf = $outDir . '/' . pathinfo($docx, PATHINFO_FILENAME) . '.pdf';
    if ($rc !== 0 || !file_exists($pdf))
        throw new RuntimeException('LibreOffice 转换失败: ' . implode("\n",$o));
    return $pdf;
}

function pdf_pages(string $pdf): int {
    $out = shell_exec('pdfinfo ' . escapeshellarg($pdf) . ' 2>/dev/null');
    if (preg_match('/^Pages:\s*(\d+)/m', (string)$out, $m)) return (int)$m[1];
    throw new RuntimeException('pdfinfo 未返回页数');
}

function pdf_to_png(string $pdf, string $outPrefix): string {
    $cmd = sprintf('pdftoppm -png -singlefile -r 150 %s %s 2>&1',
        escapeshellarg($pdf), escapeshellarg($outPrefix));
    exec($cmd, $o, $rc);
    $png = $outPrefix . '.png';
    if ($rc!==0||!file_exists($png)) throw new RuntimeException('预览生成失败: '.implode("\n",$o));
    return $png;
}

function do_print(string $pdf, string $printer=''): string {
    $lp = trim(shell_exec('which lp 2>/dev/null') ?: '');
    if (!$lp) throw new RuntimeException('未找到 lp 命令，请安装 cups-client');
    $cmd = $printer ? "$lp -d ".escapeshellarg($printer)." ".escapeshellarg($pdf)
                    : "$lp ".escapeshellarg($pdf);
    $out = shell_exec($cmd . ' 2>&1');
    return trim((string)$out);
}

function list_printers(): array {
    $raw = shell_exec('lpstat -a 2>/dev/null') ?: '';
    $printers = [];
    foreach (explode("\n", $raw) as $line) {
        if (preg_match('/^(\S+)\s/', $line, $m)) $printers[] = $m[1];
    }
    return array_unique($printers);
}

// ── API router ────────────────────────────────────────────────────────────

$action = $_GET['action'] ?? $_POST['action'] ?? '';

if ($action === 'list_templates') {
    $tpls = array_map(fn($t) => ['alias'=>$t['alias'],'note'=>$t['note'],'filename'=>$t['filename']], all_templates());
    json_response(['ok'=>true,'templates'=>$tpls]);
}

if ($action === 'upload_template') {
    $f = $_FILES['file'] ?? null;
    if (!$f || $f['error'] !== UPLOAD_ERR_OK) json_response(['ok'=>false,'error'=>'上传失败'],400);
    if (strtolower(pathinfo($f['name'],PATHINFO_EXTENSION)) !== 'docx')
        json_response(['ok'=>false,'error'=>'只支持 .docx 文件'],400);
    if ($f['size'] > MAX_UPLOAD_MB*1024*1024)
        json_response(['ok'=>false,'error'=>'文件过大（最大 '.MAX_UPLOAD_MB.'MB）'],400);
    ensure_dir(TEMPLATE_DIR);
    $dest = TEMPLATE_DIR . '/' . basename($f['name']);
    if (!move_uploaded_file($f['tmp_name'], $dest))
        json_response(['ok'=>false,'error'=>'保存失败'],500);
    json_response(['ok'=>true,'filename'=>basename($dest)]);
}

if ($action === 'delete_template') {
    $filename = basename($_POST['filename'] ?? '');
    $path = TEMPLATE_DIR . '/' . $filename;
    if (!$filename || !file_exists($path)) json_response(['ok'=>false,'error'=>'文件不存在'],404);
    unlink($path);
    json_response(['ok'=>true]);
}

if ($action === 'list_printers') {
    json_response(['ok'=>true,'printers'=>list_printers()]);
}

if ($action === 'preview') {
    $tplName = trim($_POST['template'] ?? '');
    $text    = trim($_POST['text'] ?? '');
    if (!$tplName || !$text) json_response(['ok'=>false,'error'=>'参数缺失'],400);
    $tplPath = resolve_template($tplName);
    if (!$tplPath) json_response(['ok'=>false,'error'=>'找不到模板'],404);
    if (mb_strlen($text) > MAX_TEXT) json_response(['ok'=>false,'error'=>'内容过长'],400);

    $tmp = sys_get_temp_dir().'/lp_prev_'.uniqid();
    mkdir($tmp);
    try {
        $docx = $tmp.'/out.docx';
        build_docx($tplPath, $docx, $text);
        $pdf  = docx_to_pdf($docx, $tmp);
        $pages = pdf_pages($pdf);
        if ($pages > 1) {
            array_map('unlink', glob($tmp.'/*')); rmdir($tmp);
            json_response(['ok'=>false,'error'=>"内容过长，生成了 {$pages} 页，请缩短内容"],422);
        }
        $png = pdf_to_png($pdf, $tmp.'/prev');
        $b64 = base64_encode(file_get_contents($png));
        array_map('unlink', glob($tmp.'/*')); rmdir($tmp);
        json_response(['ok'=>true,'image'=>'data:image/png;base64,'.$b64,'pages'=>$pages]);
    } catch (Throwable $e) {
        @array_map('unlink', glob($tmp.'/*')); @rmdir($tmp);
        json_response(['ok'=>false,'error'=>$e->getMessage()],500);
    }
}

if ($action === 'print') {
    $tplName = trim($_POST['template'] ?? '');
    $text    = trim($_POST['text'] ?? '');
    $printer = trim($_POST['printer'] ?? '');
    if (!$tplName || !$text) json_response(['ok'=>false,'error'=>'参数缺失'],400);
    $tplPath = resolve_template($tplName);
    if (!$tplPath) json_response(['ok'=>false,'error'=>'找不到模板'],404);

    $isRoute = is_routeid($tplPath);
    $textVal = $isRoute ? strtoupper($text) : $text;
    $qrVal   = $isRoute ? strtolower($text) : '';

    ensure_dir(GENERATED_DIR);
    $stem    = safe_name($tplName).'_'.safe_name(substr($textVal,0,30)).'_'.time();
    $docxOut = GENERATED_DIR.'/'.$stem.'.docx';

    $tmp = sys_get_temp_dir().'/lp_print_'.uniqid();
    mkdir($tmp);
    try {
        $meta = build_docx($tplPath, $docxOut, $text);
        $pdf  = docx_to_pdf($docxOut, $tmp);
        $pages = pdf_pages($pdf);
        if ($pages > 1) {
            array_map('unlink', glob($tmp.'/*')); rmdir($tmp);
            json_response(['ok'=>false,'error'=>"内容过长（{$pages} 页），请缩短后重试"],422);
        }
        $cupsOut = do_print($pdf, $printer);
        array_map('unlink', glob($tmp.'/*')); rmdir($tmp);
        json_response(['ok'=>true,
            'message'    => '已发送到打印机',
            'cups'       => $cupsOut,
            'text_value' => $meta['text_value'],
            'qr_value'   => $meta['qr_value'],
            'has_qr'     => $meta['has_qr'],
            'output'     => basename($docxOut),
        ]);
    } catch (Throwable $e) {
        @array_map('unlink', glob($tmp.'/*')); @rmdir($tmp);
        json_response(['ok'=>false,'error'=>$e->getMessage()],500);
    }
}

// ── HTML frontend ─────────────────────────────────────────────────────────
?><!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>标签打印系统</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&family=Noto+Sans+SC:wght@300;400;500&display=swap" rel="stylesheet">
<style>
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
:root{
  --bg:#0e0f11;--bg2:#16181c;--bg3:#1e2026;--bg4:#262830;
  --border:#2e3038;--border2:#3a3d48;
  --text:#e8eaf0;--text2:#9ea3b0;--text3:#5a5f6e;
  --accent:#00d4aa;--accent2:#00a882;--accent-dim:rgba(0,212,170,.12);
  --red:#ff4d4d;--red-dim:rgba(255,77,77,.12);
  --yellow:#f5c842;--yellow-dim:rgba(245,200,66,.1);
  --mono:'IBM Plex Mono',monospace;
  --sans:'Noto Sans SC','PingFang SC',sans-serif;
  --radius:6px;--radius-lg:10px;
}
body{background:var(--bg);color:var(--text);font-family:var(--sans);font-weight:300;min-height:100vh;line-height:1.6}

/* Layout */
.app{display:grid;grid-template-columns:260px 1fr;min-height:100vh}
.sidebar{background:var(--bg2);border-right:1px solid var(--border);padding:0;display:flex;flex-direction:column}
.main{padding:32px 40px;overflow-y:auto}

/* Sidebar */
.sidebar-logo{padding:24px 20px 20px;border-bottom:1px solid var(--border)}
.logo-mark{font-family:var(--mono);font-size:11px;letter-spacing:.2em;color:var(--accent);text-transform:uppercase;margin-bottom:4px}
.logo-title{font-size:18px;font-weight:500;letter-spacing:-.02em}
.sidebar-nav{padding:16px 12px;flex:1}
.nav-section{margin-bottom:8px}
.nav-label{font-family:var(--mono);font-size:10px;letter-spacing:.15em;color:var(--text3);text-transform:uppercase;padding:6px 8px;margin-bottom:2px}
.nav-item{display:flex;align-items:center;gap:10px;padding:9px 10px;border-radius:var(--radius);cursor:pointer;font-size:13.5px;color:var(--text2);transition:all .15s;border:none;background:none;width:100%;text-align:left}
.nav-item:hover{background:var(--bg3);color:var(--text)}
.nav-item.active{background:var(--accent-dim);color:var(--accent)}
.nav-item svg{width:16px;height:16px;flex-shrink:0;opacity:.7}
.nav-item.active svg{opacity:1}
.sidebar-footer{padding:16px 20px;border-top:1px solid var(--border);font-family:var(--mono);font-size:10px;color:var(--text3)}

/* Main panels */
.panel{display:none}
.panel.active{display:block}
.panel-header{margin-bottom:28px}
.panel-title{font-size:22px;font-weight:500;letter-spacing:-.03em;margin-bottom:6px}
.panel-sub{font-size:13px;color:var(--text2)}

/* Cards */
.card{background:var(--bg2);border:1px solid var(--border);border-radius:var(--radius-lg);padding:20px 22px;margin-bottom:16px}
.card-title{font-family:var(--mono);font-size:11px;letter-spacing:.12em;text-transform:uppercase;color:var(--text3);margin-bottom:14px}

/* Form */
.field{margin-bottom:16px}
.field label{display:block;font-size:12px;color:var(--text2);margin-bottom:6px;font-family:var(--mono);letter-spacing:.05em}
.field input,.field select,.field textarea{
  width:100%;background:var(--bg3);border:1px solid var(--border);border-radius:var(--radius);
  color:var(--text);font-family:var(--sans);font-size:14px;padding:9px 12px;
  transition:border-color .15s;outline:none;
}
.field textarea{resize:vertical;min-height:100px;line-height:1.6;font-size:13.5px}
.field input:focus,.field select:focus,.field textarea:focus{border-color:var(--accent)}
.field select option{background:var(--bg3)}

/* Buttons */
.btn{display:inline-flex;align-items:center;gap:7px;padding:9px 18px;border-radius:var(--radius);
  font-size:13.5px;font-weight:400;cursor:pointer;border:1px solid transparent;transition:all .15s}
.btn-primary{background:var(--accent);color:#000;border-color:var(--accent)}
.btn-primary:hover{background:var(--accent2)}
.btn-ghost{background:transparent;color:var(--text2);border-color:var(--border)}
.btn-ghost:hover{border-color:var(--border2);color:var(--text);background:var(--bg3)}
.btn-danger{background:transparent;color:var(--red);border-color:var(--red-dim)}
.btn-danger:hover{background:var(--red-dim)}
.btn:disabled{opacity:.4;cursor:not-allowed}
.btn svg{width:14px;height:14px}
.btn-row{display:flex;gap:10px;flex-wrap:wrap}

/* Print workflow */
.workflow{display:grid;grid-template-columns:1fr 1fr;gap:20px}
.preview-box{background:var(--bg3);border:1px solid var(--border);border-radius:var(--radius-lg);
  min-height:200px;display:flex;align-items:center;justify-content:center;overflow:hidden;position:relative}
.preview-box img{max-width:100%;max-height:420px;object-fit:contain;display:block}
.preview-placeholder{text-align:center;color:var(--text3);font-size:13px;font-family:var(--mono)}
.preview-placeholder svg{width:32px;height:32px;margin:0 auto 10px;display:block;opacity:.3}

/* Template list */
.tpl-grid{display:grid;gap:10px}
.tpl-item{display:flex;align-items:center;gap:12px;background:var(--bg3);border:1px solid var(--border);
  border-radius:var(--radius);padding:12px 14px;transition:border-color .15s}
.tpl-item:hover{border-color:var(--border2)}
.tpl-alias{font-family:var(--mono);font-size:13px;color:var(--accent);min-width:90px}
.tpl-note{font-size:13px;color:var(--text2);flex:1}
.tpl-file{font-family:var(--mono);font-size:11px;color:var(--text3)}
.tpl-actions{display:flex;gap:6px;margin-left:auto}

/* Upload area */
.upload-area{border:1.5px dashed var(--border2);border-radius:var(--radius-lg);padding:32px;
  text-align:center;cursor:pointer;transition:all .15s;position:relative}
.upload-area:hover,.upload-area.drag{border-color:var(--accent);background:var(--accent-dim)}
.upload-area input{position:absolute;inset:0;opacity:0;cursor:pointer;width:100%;height:100%}
.upload-icon{margin:0 auto 10px;display:block;width:36px;height:36px;color:var(--text3)}
.upload-area:hover .upload-icon,.upload-area.drag .upload-icon{color:var(--accent)}
.upload-text{font-size:13px;color:var(--text2)}
.upload-hint{font-size:11px;color:var(--text3);margin-top:4px;font-family:var(--mono)}

/* Toast */
.toast-wrap{position:fixed;bottom:24px;right:24px;display:flex;flex-direction:column;gap:8px;z-index:999}
.toast{padding:11px 16px;border-radius:var(--radius);font-size:13px;max-width:340px;
  backdrop-filter:blur(8px);animation:toastIn .2s ease;display:flex;align-items:center;gap:8px}
.toast-ok{background:rgba(0,212,170,.15);border:1px solid rgba(0,212,170,.3);color:var(--accent)}
.toast-err{background:rgba(255,77,77,.15);border:1px solid rgba(255,77,77,.3);color:var(--red)}
.toast-info{background:rgba(245,200,66,.1);border:1px solid rgba(245,200,66,.25);color:var(--yellow)}
@keyframes toastIn{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}

/* Result */
.result-box{background:var(--bg3);border:1px solid var(--border);border-radius:var(--radius);
  padding:14px 16px;font-family:var(--mono);font-size:12.5px;line-height:1.8;color:var(--text2)}
.result-box .key{color:var(--text3)}
.result-box .val{color:var(--accent)}

/* Spinner */
.spin{display:inline-block;width:14px;height:14px;border:2px solid currentColor;border-top-color:transparent;
  border-radius:50%;animation:spin .6s linear infinite}
@keyframes spin{to{transform:rotate(360deg)}}

/* Divider */
.divider{height:1px;background:var(--border);margin:20px 0}

/* Badge */
.badge{display:inline-block;padding:2px 8px;border-radius:3px;font-family:var(--mono);font-size:11px}
.badge-green{background:rgba(0,212,170,.15);color:var(--accent)}
.badge-gray{background:var(--bg4);color:var(--text3)}

/* Responsive */
@media(max-width:768px){
  .app{grid-template-columns:1fr}
  .sidebar{display:none}
  .main{padding:20px}
  .workflow{grid-template-columns:1fr}
}
</style>
</head>
<body>
<div class="app">

<!-- Sidebar -->
<nav class="sidebar">
  <div class="sidebar-logo">
    <div class="logo-mark">Label System</div>
    <div class="logo-title">标签打印</div>
  </div>
  <div class="sidebar-nav">
    <div class="nav-section">
      <div class="nav-label">操作</div>
      <button class="nav-item active" data-panel="print" onclick="showPanel('print',this)">
        <svg fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path d="M6 9V2h12v7M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"/><rect x="6" y="14" width="12" height="8" rx="1"/></svg>
        打印标签
      </button>
      <button class="nav-item" data-panel="templates" onclick="showPanel('templates',this)">
        <svg fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14,2 14,8 20,8"/></svg>
        模板管理
      </button>
    </div>
  </div>
  <div class="sidebar-footer">v1.0 · PHP Label Print</div>
</nav>

<!-- Main -->
<main class="main">

  <!-- Print panel -->
  <div id="panel-print" class="panel active">
    <div class="panel-header">
      <div class="panel-title">打印标签</div>
      <div class="panel-sub">选择模板，输入内容，预览后打印</div>
    </div>

    <div class="workflow">
      <!-- Left: form -->
      <div>
        <div class="card">
          <div class="card-title">打印参数</div>
          <div class="field">
            <label>模板</label>
            <select id="sel-template" onchange="onTemplateChange()">
              <option value="">— 加载中 —</option>
            </select>
          </div>
          <div id="tpl-info" style="margin-bottom:14px;display:none">
            <span class="badge badge-gray" id="tpl-alias-badge"></span>
            <span style="font-size:12px;color:var(--text3);margin-left:8px" id="tpl-note-badge"></span>
          </div>
          <div class="field">
            <label>打印内容 <span style="color:var(--text3);font-size:10px">（支持换行）</span></label>
            <textarea id="print-text" placeholder="输入要打印的内容..."></textarea>
          </div>
          <div class="field">
            <label>打印机 <span style="color:var(--text3);font-size:10px">（留空使用系统默认）</span></label>
            <select id="sel-printer">
              <option value="">系统默认打印机</option>
            </select>
          </div>
          <div class="btn-row">
            <button class="btn btn-ghost" id="btn-preview" onclick="doPreview()">
              <svg fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
              预览
            </button>
            <button class="btn btn-primary" id="btn-print" onclick="doPrint()" disabled>
              <svg fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path d="M6 9V2h12v7M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"/><rect x="6" y="14" width="12" height="8" rx="1"/></svg>
              打印
            </button>
          </div>
        </div>

        <div id="print-result" style="display:none" class="card">
          <div class="card-title">打印结果</div>
          <div class="result-box" id="result-content"></div>
        </div>
      </div>

      <!-- Right: preview -->
      <div>
        <div class="card" style="padding:16px">
          <div class="card-title">预览</div>
          <div class="preview-box" id="preview-box">
            <div class="preview-placeholder">
              <svg fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18M9 21V9"/></svg>
              点击「预览」查看效果
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>

  <!-- Templates panel -->
  <div id="panel-templates" class="panel">
    <div class="panel-header">
      <div class="panel-title">模板管理</div>
      <div class="panel-sub">管理 Word (.docx) 打印模板</div>
    </div>

    <div class="card">
      <div class="card-title">上传新模板</div>
      <div class="upload-area" id="upload-area">
        <input type="file" accept=".docx" id="upload-input" onchange="uploadTemplate(this)">
        <svg class="upload-icon" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17,8 12,3 7,8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>
        <div class="upload-text">点击或拖拽上传模板文件</div>
        <div class="upload-hint">仅支持 .docx · 最大 <?= MAX_UPLOAD_MB ?>MB</div>
        <div class="upload-hint" style="margin-top:6px;color:var(--text2)">命名规则：<code style="color:var(--accent)">模板名-备注.docx</code>　例：<code style="color:var(--accent)">routeid-路由器id.docx</code></div>
      </div>
    </div>

    <div class="card">
      <div class="card-title" style="margin-bottom:16px;display:flex;justify-content:space-between;align-items:center">
        <span>已有模板</span>
        <button class="btn btn-ghost" style="padding:5px 12px;font-size:12px" onclick="loadTemplates()">
          <svg fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24" style="width:12px;height:12px"><path d="M23 4v6h-6M1 20v-6h6"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/></svg>
          刷新
        </button>
      </div>
      <div id="tpl-list" class="tpl-grid">
        <div style="color:var(--text3);font-size:13px;font-family:var(--mono)">加载中...</div>
      </div>
    </div>
  </div>

</main>
</div>

<!-- Toast container -->
<div class="toast-wrap" id="toasts"></div>

<script>
const $ = id => document.getElementById(id)

// ── Toast ────────────────────────────────────────────────────────────────
function toast(msg, type='info', dur=3500) {
  const wrap = $('toasts')
  const el = document.createElement('div')
  el.className = `toast toast-${type==='ok'?'ok':type==='err'?'err':'info'}`
  const icons = {ok:'✓',err:'✕',info:'◆'}
  el.innerHTML = `<span>${icons[type]||'◆'}</span><span>${msg}</span>`
  wrap.appendChild(el)
  setTimeout(() => { el.style.opacity='0'; el.style.transform='translateY(8px)'; el.style.transition='all .3s'; setTimeout(()=>el.remove(),300) }, dur)
}

// ── Panel nav ────────────────────────────────────────────────────────────
function showPanel(id, btn) {
  document.querySelectorAll('.panel').forEach(p => p.classList.remove('active'))
  document.querySelectorAll('.nav-item').forEach(b => b.classList.remove('active'))
  $('panel-'+id).classList.add('active')
  btn.classList.add('active')
  if (id==='templates') loadTemplates()
}

// ── Templates ────────────────────────────────────────────────────────────
let templates = []

async function loadTemplates() {
  const res = await fetch('?action=list_templates')
  const data = await res.json()
  templates = data.templates || []
  renderTemplateList()
  populateTemplateSelect()
}

function renderTemplateList() {
  const el = $('tpl-list')
  if (!templates.length) {
    el.innerHTML = '<div style="color:var(--text3);font-size:13px;font-family:var(--mono);padding:8px 0">暂无模板，请先上传</div>'
    return
  }
  el.innerHTML = templates.map(t => `
    <div class="tpl-item">
      <span class="tpl-alias">${t.alias}</span>
      <span class="tpl-note">${t.note || '<span style="color:var(--text3)">—</span>'}</span>
      <span class="tpl-file">${t.filename}</span>
      <div class="tpl-actions">
        <button class="btn btn-danger" style="padding:5px 10px;font-size:12px" onclick="deleteTemplate('${t.filename}')">删除</button>
      </div>
    </div>
  `).join('')
}

function populateTemplateSelect() {
  const sel = $('sel-template')
  const cur = sel.value
  sel.innerHTML = '<option value="">— 选择模板 —</option>' +
    templates.map(t => `<option value="${t.alias}">${t.alias}${t.note?' — '+t.note:''}</option>`).join('')
  if (cur) sel.value = cur
}

function onTemplateChange() {
  const alias = $('sel-template').value
  const t = templates.find(x => x.alias===alias)
  const info = $('tpl-info')
  if (t) {
    $('tpl-alias-badge').textContent = t.alias
    $('tpl-note-badge').textContent = t.note || ''
    info.style.display = ''
    // reset preview and print button when template changes
    resetPreview()
  } else {
    info.style.display = 'none'
    resetPreview()
  }
}

function resetPreview() {
  $('preview-box').innerHTML = `<div class="preview-placeholder">
    <svg fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24" style="width:32px;height:32px;margin:0 auto 10px;display:block;opacity:.3"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18M9 21V9"/></svg>
    点击「预览」查看效果</div>`
  $('btn-print').disabled = true
  $('print-result').style.display = 'none'
}

async function uploadTemplate(input) {
  const file = input.files[0]
  if (!file) return
  const fd = new FormData()
  fd.append('action','upload_template')
  fd.append('file', file)
  const area = $('upload-area')
  area.style.opacity = '.6'
  try {
    const res = await fetch('', {method:'POST', body:fd})
    const data = await res.json()
    if (data.ok) { toast('模板上传成功：'+data.filename,'ok'); await loadTemplates() }
    else toast('上传失败：'+data.error,'err')
  } catch(e) { toast('上传出错','err') }
  area.style.opacity = '1'
  input.value = ''
}

async function deleteTemplate(filename) {
  if (!confirm('确认删除模板 ' + filename + '？')) return
  const fd = new FormData()
  fd.append('action','delete_template')
  fd.append('filename',filename)
  const res = await fetch('', {method:'POST', body:fd})
  const data = await res.json()
  if (data.ok) { toast('已删除','ok'); await loadTemplates() }
  else toast('删除失败：'+data.error,'err')
}

// ── Drag & drop upload ────────────────────────────────────────────────────
const area = $('upload-area')
area.addEventListener('dragover', e => { e.preventDefault(); area.classList.add('drag') })
area.addEventListener('dragleave', () => area.classList.remove('drag'))
area.addEventListener('drop', e => {
  e.preventDefault(); area.classList.remove('drag')
  const file = e.dataTransfer?.files[0]
  if (file) { const dt = new DataTransfer(); dt.items.add(file); $('upload-input').files = dt.files; uploadTemplate($('upload-input')) }
})

// ── Printers ─────────────────────────────────────────────────────────────
async function loadPrinters() {
  try {
    const res = await fetch('?action=list_printers')
    const data = await res.json()
    const sel = $('sel-printer')
    const printers = data.printers || []
    sel.innerHTML = '<option value="">系统默认打印机</option>' +
      printers.map(p => `<option value="${p}">${p}</option>`).join('')
  } catch(e) {}
}

// ── Preview ───────────────────────────────────────────────────────────────
async function doPreview() {
  const tpl = $('sel-template').value
  const text = $('print-text').value.trim()
  if (!tpl) { toast('请先选择模板','info'); return }
  if (!text) { toast('请输入打印内容','info'); return }

  const btn = $('btn-preview')
  btn.disabled = true
  btn.innerHTML = '<span class="spin"></span> 生成中...'
  $('preview-box').innerHTML = '<div class="preview-placeholder"><span class="spin" style="width:24px;height:24px;border-width:2.5px"></span></div>'

  const fd = new FormData()
  fd.append('action','preview')
  fd.append('template',tpl)
  fd.append('text',text)

  try {
    const res = await fetch('', {method:'POST', body:fd})
    const data = await res.json()
    if (data.ok) {
      $('preview-box').innerHTML = `<img src="${data.image}" alt="预览">`
      $('btn-print').disabled = false
      toast('预览生成成功','ok',2000)
    } else {
      $('preview-box').innerHTML = `<div class="preview-placeholder" style="color:var(--red);padding:20px">${data.error}</div>`
      $('btn-print').disabled = true
      toast(data.error,'err')
    }
  } catch(e) {
    toast('预览请求失败','err')
    $('preview-box').innerHTML = '<div class="preview-placeholder">预览失败</div>'
  }
  btn.disabled = false
  btn.innerHTML = `<svg fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24" style="width:14px;height:14px"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg> 预览`
}

// ── Print ─────────────────────────────────────────────────────────────────
async function doPrint() {
  const tpl     = $('sel-template').value
  const text    = $('print-text').value.trim()
  const printer = $('sel-printer').value
  if (!tpl || !text) { toast('模板或内容不能为空','info'); return }

  const btn = $('btn-print')
  btn.disabled = true
  btn.innerHTML = '<span class="spin"></span> 打印中...'

  const fd = new FormData()
  fd.append('action','print')
  fd.append('template',tpl)
  fd.append('text',text)
  fd.append('printer',printer)

  try {
    const res = await fetch('', {method:'POST', body:fd})
    const data = await res.json()
    const box = $('print-result')
    const content = $('result-content')
    if (data.ok) {
      toast('已发送到打印机','ok')
      box.style.display = ''
      content.innerHTML = [
        `<span class="key">状态　</span><span class="val">✓ ${data.message}</span>`,
        `<span class="key">模板　</span><span class="val">${tpl}</span>`,
        data.has_qr ? `<span class="key">二维码</span><span class="val">${data.qr_value}</span>` : '',
        `<span class="key">文字　</span><span class="val">${data.text_value}</span>`,
        `<span class="key">CUPS　</span><span class="val">${data.cups}</span>`,
        `<span class="key">文件　</span><span class="val">${data.output}</span>`,
      ].filter(Boolean).join('<br>')
    } else {
      toast(data.error,'err',5000)
      box.style.display = ''
      content.innerHTML = `<span style="color:var(--red)">✕ ${data.error}</span>`
    }
  } catch(e) { toast('打印请求失败','err') }

  btn.disabled = false
  btn.innerHTML = `<svg fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24" style="width:14px;height:14px"><path d="M6 9V2h12v7M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"/><rect x="6" y="14" width="12" height="8" rx="1"/></svg> 打印`
}

// ── Init ──────────────────────────────────────────────────────────────────
loadTemplates()
loadPrinters()
</script>
</body>
</html>
