"""Built-in web admin UI for the middleware.

A dependency-free HTTP server (stdlib only) that lets a lab operator configure
the cloud connection and up to N analyzer devices from a browser, watch live
per-device status, and tail the log — no JSON editing required. Saving applies
the new config and hot-restarts the listeners via Application.reload().

Bind defaults to 127.0.0.1 (local only). Set admin.host to 0.0.0.0 in the config
to reach it from other machines on the lab network.
"""

from __future__ import annotations

import json
import logging
import os
import threading
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from typing import Any, Dict
from urllib.parse import parse_qs, urlparse

from .cloud import CloudClient
from .config import KNOWN_DRIVERS, Config

log = logging.getLogger("admin")


class AdminServer:
    """Owns the HTTP server thread bound to an Application instance."""

    def __init__(self, app: Any) -> None:
        self.app = app
        self._httpd: ThreadingHTTPServer | None = None
        self._thread: threading.Thread | None = None

    def start(self) -> None:
        handler = _make_handler(self.app)
        self._httpd = ThreadingHTTPServer((self.app.cfg.admin_host, self.app.cfg.admin_port), handler)
        self._thread = threading.Thread(target=self._httpd.serve_forever, name="admin", daemon=True)
        self._thread.start()

    def stop(self) -> None:
        if self._httpd:
            self._httpd.shutdown()
            self._httpd.server_close()
            self._httpd = None


def _tail(path: str, n: int) -> str:
    if not path or not os.path.isfile(path):
        return ""
    try:
        with open(path, "r", encoding="utf-8", errors="replace") as fh:
            return "".join(fh.readlines()[-n:])
    except OSError:
        return ""


def _make_handler(app: Any):
    class _Handler(BaseHTTPRequestHandler):
        server_version = "MoonLISMiddleware/1.0"

        def log_message(self, *args: Any) -> None:  # quiet — we have our own logging
            return

        # ----- helpers ----------------------------------------------------
        def _send(self, code: int, body: bytes, ctype: str) -> None:
            self.send_response(code)
            self.send_header("Content-Type", ctype)
            self.send_header("Content-Length", str(len(body)))
            self.send_header("Cache-Control", "no-store")
            self.end_headers()
            self.wfile.write(body)

        def _json(self, obj: Any, code: int = 200) -> None:
            self._send(code, json.dumps(obj, ensure_ascii=False).encode("utf-8"),
                       "application/json; charset=utf-8")

        def _read_body(self) -> Dict[str, Any]:
            length = int(self.headers.get("Content-Length") or 0)
            if not length:
                return {}
            raw = self.rfile.read(length)
            try:
                return json.loads(raw.decode("utf-8"))
            except (ValueError, UnicodeDecodeError):
                return {}

        # ----- auth -------------------------------------------------------
        def _authed(self) -> bool:
            """Basic-auth gate. Open when no admin password is configured (the
            UI binds to 127.0.0.1 by default); once a password is set — needed
            when the UI is exposed to the LAN for multi-station access — every
            request must present it."""
            pw = getattr(app.cfg, "admin_password", "") or ""
            if not pw:
                return True
            hdr = self.headers.get("Authorization", "")
            if hdr.startswith("Basic "):
                import base64
                try:
                    _, _, passwd = base64.b64decode(hdr[6:]).decode("utf-8").partition(":")
                    if passwd == pw:
                        return True
                except Exception:  # noqa: BLE001
                    pass
            self.send_response(401)
            self.send_header("WWW-Authenticate", 'Basic realm="Moon LIS Middleware"')
            self.send_header("Content-Length", "0")
            self.end_headers()
            return False

        # ----- routes -----------------------------------------------------
        def do_GET(self) -> None:
            if not self._authed():
                return
            route = urlparse(self.path)
            path = route.path
            if path in ("/", "/index.html"):
                self._send(200, _PAGE.encode("utf-8"), "text/html; charset=utf-8")
            elif path == "/api/status":
                self._json(app.status())
            elif path == "/api/config":
                self._json(app.cfg.to_dict())
            elif path == "/api/logs":
                n = int((parse_qs(route.query).get("n") or ["200"])[0])
                self._json({"log": _tail(app.cfg.log_file, max(1, min(n, 2000)))})
            elif path == "/api/meta":
                self._json({"drivers": KNOWN_DRIVERS})
            else:
                self._json({"error": "not found"}, 404)

        def do_POST(self) -> None:
            if not self._authed():
                return
            path = urlparse(self.path).path
            body = self._read_body()
            if path == "/api/config":
                try:
                    cfg = app.reload(body)
                    self._json({"ok": True, "messages": cfg.validation_messages()})
                except Exception as exc:  # noqa: BLE001
                    log.exception("config save failed")
                    self._json({"ok": False, "error": str(exc)}, 500)
            elif path == "/api/test-cloud":
                # Test the supplied cloud block WITHOUT saving it.
                merged = app.cfg.to_dict()
                merged["cloud"] = body.get("cloud") or merged.get("cloud")
                result = CloudClient(Config(merged)).test()
                self._json(result)
            elif path == "/api/sync":
                # Pull the device list from LIS → Machines and rebuild listeners.
                try:
                    summary = app.sync_from_cloud()
                    self._json({"ok": True, "summary": summary})
                except Exception as exc:  # noqa: BLE001
                    log.exception("cloud sync failed")
                    self._json({"ok": False, "error": str(exc)}, 500)
            else:
                self._json({"error": "not found"}, 404)

    return _Handler


# --------------------------------------------------------------------------
# The single-page dashboard (Arabic-first, RTL). Vanilla JS, no build step.
# --------------------------------------------------------------------------
_PAGE = r"""<!DOCTYPE html>
<html lang="ar" dir="rtl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Moon LIS — جسر الأجهزة</title>
<style>
  :root{--navy:#0f2a52;--teal:#0d9488;--green:#15803d;--green-bg:#dcfce7;--red:#b91c1c;
    --red-bg:#fee2e2;--amber:#b45309;--amber-bg:#fef3c7;--line:#e2e8f0;--bg:#f1f5f9;--slate:#475569;}
  *{box-sizing:border-box;margin:0;padding:0}
  body{font-family:"Segoe UI",Tahoma,Arial,sans-serif;background:var(--bg);color:#0f172a;padding:18px;line-height:1.6}
  .wrap{max-width:1080px;margin:0 auto}
  header{background:linear-gradient(135deg,var(--navy),var(--teal));color:#fff;border-radius:14px;padding:18px 22px;display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:10px}
  header h1{font-size:19px}
  header .pills{display:flex;gap:8px;flex-wrap:wrap}
  .pill{padding:4px 11px;border-radius:999px;font-size:12.5px;font-weight:700;background:rgba(255,255,255,.18)}
  .pill.ok{background:var(--green-bg);color:var(--green)}.pill.bad{background:var(--red-bg);color:var(--red)}
  .card{background:#fff;border:1px solid var(--line);border-radius:12px;padding:18px 20px;margin-top:16px}
  h2{font-size:16px;color:var(--navy);margin-bottom:12px;display:flex;justify-content:space-between;align-items:center}
  label{display:block;font-size:12.5px;color:var(--slate);margin:8px 0 3px;font-weight:600}
  input[type=text],input[type=number],input[type=password],select{width:100%;padding:8px 10px;border:1px solid #cbd5e1;border-radius:8px;font-size:13.5px;font-family:inherit}
  .row{display:flex;gap:12px;flex-wrap:wrap}.row>div{flex:1;min-width:160px}
  .chk{display:flex;align-items:center;gap:7px;margin-top:10px;font-size:13.5px}
  .chk input{width:16px;height:16px}
  button{font-family:inherit;font-size:13.5px;font-weight:700;border:none;border-radius:8px;padding:8px 15px;cursor:pointer}
  .btn{background:var(--navy);color:#fff}.btn-2{background:#e2e8f0;color:#0f172a}
  .btn-teal{background:var(--teal);color:#fff}.btn-danger{background:var(--red);color:#fff}
  .btn-sm{padding:5px 10px;font-size:12px}
  table{width:100%;border-collapse:collapse;margin-top:6px;font-size:13px}
  th,td{padding:8px 10px;text-align:right;border-bottom:1px solid var(--line);vertical-align:middle}
  th{background:#f8fafc;color:var(--navy);font-size:12px}
  .dot{display:inline-block;width:9px;height:9px;border-radius:50%;margin-inline-end:6px}
  .dot.on{background:#16a34a}.dot.off{background:#cbd5e1}.dot.err{background:#dc2626}
  .muted{color:#94a3b8;font-size:12px}
  .toast{position:fixed;inset-block-end:18px;inset-inline-start:50%;transform:translateX(50%);background:#0f172a;color:#fff;padding:10px 18px;border-radius:10px;font-size:13.5px;opacity:0;transition:.3s;pointer-events:none;z-index:50}
  .toast.show{opacity:1}
  .warn{background:var(--amber-bg);border:1px solid #fcd34d;color:var(--amber);border-radius:9px;padding:9px 12px;font-size:12.5px;margin-top:10px}
  pre{background:#0b1220;color:#cbd5e1;border-radius:9px;padding:12px;font-size:11.5px;max-height:280px;overflow:auto;direction:ltr;white-space:pre-wrap;word-break:break-word}
  dialog{border:none;border-radius:14px;padding:0;width:min(560px,94vw);box-shadow:0 20px 50px rgba(0,0,0,.3)}
  dialog::backdrop{background:rgba(15,23,42,.5)}
  .dlg-h{background:var(--navy);color:#fff;padding:13px 18px;font-weight:700}
  .dlg-b{padding:16px 18px}.dlg-f{padding:12px 18px;display:flex;gap:8px;justify-content:flex-start;border-top:1px solid var(--line)}
</style>
</head>
<body>
<div class="wrap">
  <header>
    <h1>🧪 Moon LIS — جسر الأجهزة (Middleware)</h1>
    <div class="pills" id="pills"></div>
  </header>

  <!-- Workstation identity -->
  <div class="card">
    <h2>هوية المحطة</h2>
    <label>اسم المحطة (يظهر في الكلود مع كل جهاز شغّال عليها)</label>
    <input type="text" id="ws_name" placeholder="مثلاً: فرع المعادي — محطة 1">
  </div>

  <!-- Cloud -->
  <div class="card">
    <h2>اتصال Moon ERP (السحابة)</h2>
    <div class="row">
      <div style="flex:2"><label>رابط الـAPI (base URL)</label>
        <input type="text" id="c_base" placeholder="https://moonui.elbaset.com/moon-erp-be/api"></div>
      <div><label>نوع المصادقة</label>
        <select id="c_mode"><option value="token">توكن ثابت</option><option value="login">إيميل/باسورد</option></select></div>
    </div>
    <div id="c_token_box"><label>التوكن (X-Authorization Bearer)</label><input type="text" id="c_token" placeholder="لصق التوكن"></div>
    <div id="c_login_box" class="row" style="display:none">
      <div><label>الإيميل</label><input type="text" id="c_email" placeholder="machines@moonerp.com"></div>
      <div><label>الباسورد</label><input type="password" id="c_pass"></div>
    </div>
    <div class="chk"><input type="checkbox" id="c_verify"><label style="margin:0">التحقق من شهادة SSL</label></div>
    <div style="margin-top:14px;display:flex;gap:8px">
      <button class="btn-teal" onclick="testCloud()">🔌 اختبار الاتصال</button>
      <span id="c_test" class="muted" style="align-self:center"></span>
    </div>
  </div>

  <!-- Devices -->
  <div class="card">
    <h2>الأجهزة
      <span>
        <button class="btn-teal btn-sm" onclick="syncCloud()">🔄 مزامنة من Moon ERP</button>
        <button class="btn-2 btn-sm" onclick="editDevice(-1)">+ إضافة يدوي</button>
      </span>
    </h2>
    <div class="muted" style="margin-bottom:8px">الأجهزة تُدار من Moon ERP (LIS ◂ الأجهزة) — اضغط «مزامنة» لسحبها هنا ببياناتها تلقائياً.</div>
    <div id="dev_warn"></div>
    <table>
      <thead><tr><th>تشغيل</th><th>الجهاز</th><th>Machine ID</th><th>المنفذ</th><th>الحالة</th><th>نتائج</th><th>آخر باركود</th><th></th></tr></thead>
      <tbody id="dev_rows"><tr><td colspan="8" class="muted">جارٍ التحميل…</td></tr></tbody>
    </table>
  </div>

  <!-- Save -->
  <div class="card" style="display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:10px">
    <div class="muted">الحفظ بيكتب الإعدادات ويعيد تشغيل المستمعين فوراً.</div>
    <button class="btn" onclick="saveAll()">💾 حفظ وتطبيق</button>
  </div>

  <!-- Logs -->
  <div class="card">
    <h2>السجل (Log) <button class="btn-2 btn-sm" onclick="loadLogs()">تحديث</button></h2>
    <pre id="logs">…</pre>
  </div>
</div>

<!-- Device editor -->
<dialog id="devDlg">
  <div class="dlg-h" id="dlg_title">جهاز</div>
  <div class="dlg-b">
    <div class="row">
      <div><label>الاسم</label><input type="text" id="d_name" placeholder="DH36 Hematology"></div>
      <div><label>Machine ID (من Moon ERP)</label><input type="number" id="d_mid" placeholder="7"></div>
    </div>
    <div class="row">
      <div><label>المنفذ (listen port)</label><input type="number" id="d_port" placeholder="5600"></div>
      <div><label>الـDriver</label><select id="d_driver"></select></div>
    </div>
    <div class="row">
      <div><label>عنوان الاستماع</label><input type="text" id="d_host" value="0.0.0.0"></div>
      <div><label>حقول الباركود (مفصولة بفاصلة)</label><input type="text" id="d_barcode" placeholder="OBR-3,OBR-2,SPM-2,PID-3"></div>
    </div>
    <div class="chk"><input type="checkbox" id="d_ack" checked><label style="margin:0">إرسال ACK للجهاز</label></div>
    <div class="chk"><input type="checkbox" id="d_enabled" checked><label style="margin:0">مُفعّل</label></div>
    <div style="border-top:1px dashed #cbd5e1;margin-top:10px;padding-top:10px">
      <div style="font-weight:700;color:#0f2a52;font-size:13px;margin-bottom:4px">تمرير لبرنامج تاني (Relay) — اختياري</div>
      <div class="muted" style="margin-bottom:6px">لو في برنامج تاني لازم ياخد نفس الداتا (زي UDI) — نمرّرله البايتات. سيبه فاضي لو مش محتاج.</div>
      <div class="row">
        <div><label>عنوان الوجهة (host)</label><input type="text" id="d_fwd_host" placeholder="127.0.0.1"></div>
        <div><label>بورت الوجهة</label><input type="number" id="d_fwd_port" placeholder="5600"></div>
      </div>
    </div>
  </div>
  <div class="dlg-f">
    <button class="btn" onclick="saveDevice()">حفظ الجهاز</button>
    <button class="btn-2" onclick="devDlg.close()">إلغاء</button>
  </div>
</dialog>

<div class="toast" id="toast"></div>

<script>
let cfg = null;        // full config dict
let editIdx = -1;      // device index being edited (-1 = new)
const $ = (id) => document.getElementById(id);
const devDlg = $("devDlg");

function toast(msg){ const t=$("toast"); t.textContent=msg; t.classList.add("show"); setTimeout(()=>t.classList.remove("show"),2600); }

async function loadConfig(){
  cfg = await (await fetch("/api/config")).json();
  const meta = await (await fetch("/api/meta")).json();
  $("d_driver").innerHTML = (meta.drivers||["dymind_hl7"]).map(d=>`<option>${d}</option>`).join("");
  $("ws_name").value = cfg.workstation_name||"";
  const c = cfg.cloud||{};
  $("c_base").value = c.base_url||"";
  $("c_token").value = c.token||"";
  const login = c.login||{};
  $("c_email").value = login.email||"";
  $("c_pass").value = login.password||"";
  $("c_verify").checked = c.verify_ssl!==false;
  $("c_mode").value = login.enabled ? "login" : "token";
  onModeChange();
}
function onModeChange(){
  const login = $("c_mode").value==="login";
  $("c_login_box").style.display = login?"flex":"none";
  $("c_token_box").style.display = login?"none":"block";
}
$("c_mode").addEventListener("change", onModeChange);

function collectCloud(){
  const login = $("c_mode").value==="login";
  return {
    base_url: $("c_base").value.trim(),
    token: login ? "" : $("c_token").value.trim(),
    auth_header: (cfg.cloud&&cfg.cloud.auth_header)||"X-Authorization",
    verify_ssl: $("c_verify").checked,
    timeout_seconds: (cfg.cloud&&cfg.cloud.timeout_seconds)||20,
    login: { enabled: login, email: $("c_email").value.trim(), password: $("c_pass").value }
  };
}

function editDevice(idx){
  editIdx = idx;
  const d = idx>=0 ? cfg.devices[idx] : {name:"",machine_id:"",listen_port:"",driver:"dymind_hl7",listen_host:"0.0.0.0",barcode_fields:["OBR-3","OBR-2","SPM-2","PID-3"],send_ack:true,enabled:true};
  $("dlg_title").textContent = idx>=0 ? ("تعديل: "+d.name) : "جهاز جديد";
  $("d_name").value=d.name||""; $("d_mid").value=d.machine_id||""; $("d_port").value=d.listen_port||"";
  $("d_driver").value=d.driver||"dymind_hl7"; $("d_host").value=d.listen_host||"0.0.0.0";
  $("d_barcode").value=(d.barcode_fields||[]).join(",");
  $("d_ack").checked=d.send_ack!==false; $("d_enabled").checked=d.enabled!==false;
  $("d_fwd_host").value=d.forward_host||""; $("d_fwd_port").value=d.forward_port||"";
  devDlg.showModal();
}
function saveDevice(){
  const dev = {
    name: $("d_name").value.trim()||"device",
    machine_id: parseInt($("d_mid").value)||0,
    listen_port: parseInt($("d_port").value)||0,
    driver: $("d_driver").value,
    listen_host: $("d_host").value.trim()||"0.0.0.0",
    barcode_fields: $("d_barcode").value.split(",").map(s=>s.trim()).filter(Boolean),
    value_types: ["NM","ST","SN"],
    send_ack: $("d_ack").checked,
    enabled: $("d_enabled").checked,
    forward_host: $("d_fwd_host").value.trim(),
    forward_port: parseInt($("d_fwd_port").value)||0,
  };
  if(!cfg.devices) cfg.devices=[];
  if(editIdx>=0) cfg.devices[editIdx]=dev; else cfg.devices.push(dev);
  devDlg.close();
  renderDevices(lastStatus);
  toast("اتسجّل محلياً — اضغط «حفظ وتطبيق» للتفعيل");
}
function delDevice(idx){
  if(!confirm("حذف الجهاز «"+cfg.devices[idx].name+"»؟")) return;
  cfg.devices.splice(idx,1); renderDevices(lastStatus); toast("اتشال — اضغط «حفظ وتطبيق»");
}

async function testCloud(){
  $("c_test").textContent="…جارٍ الاختبار";
  const r = await (await fetch("/api/test-cloud",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({cloud:collectCloud()})})).json();
  $("c_test").textContent = (r.ok?"✅ ":"❌ ")+r.message;
  $("c_test").style.color = r.ok?"#15803d":"#b91c1c";
}

async function syncCloud(){
  toast("…جارٍ المزامنة من Moon ERP");
  const r = await (await fetch("/api/sync",{method:"POST",headers:{"Content-Type":"application/json"},body:"{}"})).json();
  if(r.ok){ const s=r.summary||{}; toast(`✅ تمت المزامنة — جديد ${s.added||0} · محدّث ${s.updated||0} · يعمل ${s.active||0}`); await loadConfig(); loadStatus(); }
  else toast("❌ "+(r.error||"فشلت المزامنة (تأكد من اتصال السحابة)"));
}

async function saveAll(){
  cfg.cloud = collectCloud();
  cfg.workstation_name = $("ws_name").value.trim();
  const r = await (await fetch("/api/config",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(cfg)})).json();
  if(r.ok){ toast("✅ اتحفظ واتطبّق"); loadStatus(); }
  else toast("❌ "+(r.error||"فشل الحفظ"));
}

// Quick local enable/disable of a single device — saves + applies immediately.
async function toggleEnable(i, on){
  cfg.devices[i].enabled = on;
  cfg.cloud = collectCloud();
  cfg.workstation_name = $("ws_name").value.trim();
  const r = await (await fetch("/api/config",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(cfg)})).json();
  if(r.ok){ toast(on?"✅ اتفعّل":"⬜ اتوقف"); loadStatus(); }
  else { toast("❌ "+(r.error||"فشل")); cfg.devices[i].enabled = !on; }
}

let lastStatus = null;
function fmtTime(ts){ if(!ts) return "—"; const d=new Date(ts*1000); return d.toLocaleTimeString("en-GB"); }
function renderPills(s){
  const c=s.cloud||{}; const cloudOk=c.can_upload && c.has_token;
  const active=(s.devices||[]).filter(d=>d.listening).length;
  $("pills").innerHTML =
    (s.workstation?`<span class="pill">🖥️ ${s.workstation}</span>`:"")+
    `<span class="pill ${cloudOk?'ok':'bad'}">${cloudOk?'☁️ متصل':'☁️ غير متصل'}</span>`+
    `<span class="pill">⏳ في الانتظار: ${(s.buffer||{}).pending||0}</span>`+
    `<span class="pill">📟 يعمل: ${active}/${(s.devices||[]).length}</span>`;
}
function renderDevices(s){
  const live = {}; ((s&&s.devices)||[]).forEach(d=>live[d.name+"|"+d.listen_port]=d);
  const rows = (cfg.devices||[]).map((d,i)=>{
    const st = live[d.name+"|"+d.listen_port] || {};
    let dot="off",label="متوقف";
    if(st.bind_error){dot="err";label="خطأ منفذ";}
    else if(st.listening){ dot="on"; label = st.connections_open>0 ? "متصل" : "يستمع"; }
    else if(!d.enabled){dot="off";label="معطّل";}
    if(st.bind_error) label += ' <span style="color:#b91c1c" title="'+st.bind_error+'">(تعارض بورت)</span>';
    const err = st.errors? `<span style="color:#b91c1c">· أخطاء ${st.errors}</span>`:"";
    return `<tr>
      <td><input type="checkbox" style="width:18px;height:18px" ${d.enabled?"checked":""} onchange="toggleEnable(${i}, this.checked)"></td>
      <td><b>${d.name||"—"}</b><div class="muted">${d.driver}${st.relay?` · ↪ ${st.relay}`:""}</div></td>
      <td>${d.machine_id||"—"}</td><td>${d.listen_port||"—"}</td>
      <td><span class="dot ${dot}"></span>${label}${err}</td>
      <td>${st.results||0} <span class="muted">(رسائل ${st.messages||0})</span></td>
      <td>${st.last_barcode||"—"}<div class="muted">${fmtTime(st.last_message_at)}</div></td>
      <td style="white-space:nowrap"><button class="btn-2 btn-sm" onclick="editDevice(${i})">تعديل</button>
          <button class="btn-danger btn-sm" onclick="delDevice(${i})">حذف</button></td></tr>`;
  }).join("");
  $("dev_rows").innerHTML = rows || `<tr><td colspan="8" class="muted">مفيش أجهزة — اضغط «🔄 مزامنة من Moon ERP»</td></tr>`;
  const msgs = (s&&s.messages)||[];
  $("dev_warn").innerHTML = msgs.length ? `<div class="warn">⚠️ ${msgs.join(" · ")}</div>` : "";
}
async function loadStatus(){
  try{
    const s = await (await fetch("/api/status")).json();
    lastStatus = s; renderPills(s); renderDevices(s);
  }catch(e){}
}
async function loadLogs(){
  try{ const r = await (await fetch("/api/logs?n=200")).json(); $("logs").textContent = r.log||"(فارغ)"; }catch(e){}
}

(async function init(){ await loadConfig(); await loadStatus(); await loadLogs();
  setInterval(loadStatus,3000); setInterval(loadLogs,8000); })();
</script>
</body>
</html>
"""
