# -*- coding: utf-8 -*-
"""HLQT — Huấn luyện QUY TRÌNH cho CSDL A (web app, cổng 8100).
Luồng: (1) nạp BẢN MÀU → agent đề nghị ĐÚNG 1 quy trình khả dĩ/sát nhất từ CSDL A
       (2) chủ nhân nhập QUY TRÌNH THỰC TẾ của bản màu đó
       (3) agent đối chiếu & đề xuất BỔ SUNG CSDL A (quy trình mới + luật mới)
       (4) chủ nhân nhập mật khẩu → hệ thống ghi vào CSDL A (backup trước).
Chạy: python server_hlqt.py   (mặc định 127.0.0.1:8100)
"""
import cgi, hashlib, hmac, html, io, json, mimetypes, os, re, secrets, shutil, sqlite3, sys, time, urllib.parse
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer

GOC = r'C:\Claw 2\Nghien cuu\Du-an-A-Quy-trinh-son'
APP = os.path.join(GOC, 'HLQT-app')
DB = os.path.join(GOC, 'CSDLA.db')
VIEC = os.path.join(APP, 'viec')
MK_FILE = os.path.join(APP, 'mat_khau.txt')
PASS_FILE = os.path.join(APP, 'pass_hlqt.txt')
NHIP = os.path.join(APP, '_nhip.json')
os.makedirs(VIEC, exist_ok=True)


def mat_khau():
    try:
        return open(MK_FILE, encoding='utf-8').read().strip() or 'hlqt'
    except Exception:
        return 'hlqt'


def pass_ghi():
    try:
        return open(PASS_FILE, encoding='utf-8').read().strip() or '19842026'
    except Exception:
        return '19842026'


def bi_mat():
    try:
        return open(os.path.join(APP, 'bi_mat.txt'), encoding='utf-8').read().strip() or 'hlqt-bi-mat'
    except Exception:
        return 'hlqt-bi-mat'


def ma_phien():
    h = hashlib.sha256(('hlqt|' + bi_mat() + '|' + time.strftime('%Y-%m-%d-%H')).encode()).hexdigest()[:24]
    return h


def noi_db():
    con = sqlite3.connect(DB)
    con.execute('''CREATE TABLE IF NOT EXISTS huan_luyen_ca (
        id INTEGER PRIMARY KEY AUTOINCREMENT, tao_luc TEXT, nguoi_nhap TEXT,
        ma_mau TEXT, he_son TEXT, loai_nen TEXT, yeu_cau TEXT, ghi_chu TEXT,
        anh_vao TEXT, anh_khac TEXT, trang_thai TEXT DEFAULT 'cho_de_nghi',
        de_nghi_luc TEXT, de_nghi_file TEXT, de_nghi_giay REAL,
        de_nghi_qt_id INTEGER, de_nghi_qt_ten TEXT, de_nghi_tom_tat TEXT, de_nghi_json TEXT,
        thuc_te_text TEXT, thuc_te_luc TEXT, thuc_te_file TEXT,
        phan_tich_luc TEXT, phan_tich_file TEXT, de_xuat_json TEXT,
        da_bo_sung INTEGER DEFAULT 0, ngay_bo_sung TEXT, qt_moi_id INTEGER, so_luat_them INTEGER,
        nguon TEXT, loi TEXT)''')
    con.execute('''CREATE TABLE IF NOT EXISTS nhat_ky_hlqt (id INTEGER PRIMARY KEY AUTOINCREMENT,
        ngay TEXT, viec TEXT, chi_tiet TEXT, nguon TEXT)''')
    con.commit()
    return con


def nhip_worker():
    try:
        d = json.load(open(NHIP, encoding='utf-8'))
        return d.get('trang_thai') or 'khong-ro', d.get('ca'), d.get('buoc') or '', d.get('luc') or ''
    except Exception:
        return 'khong-ro', None, '', ''


TT = {'cho_de_nghi': 'chờ đề nghị', 'da_de_nghi': 'đã có đề nghị', 'cho_phan_tich': 'chờ phân tích',
      'cho_xac_nhan': 'CHỜ XÁC NHẬN', 'da_bo_sung': 'đã bổ sung CSDL A', 'loi': 'LỖI'}

BASE_JS = (b'<script>document.addEventListener("DOMContentLoaded",function(){'
           b'var B=(location.pathname.indexOf("/hlqt")===0)?"/hlqt/":"/";var b=document.querySelector("base");'
           b'if(b&&b.getAttribute("href")!==B)b.href=B;});</script>')

CSS = """
:root{--nhan:#0b5c46;--vien:#dfe3e8}
*{box-sizing:border-box}body{margin:0;font:15px/1.55 'Segoe UI',Arial,sans-serif;background:#f4f6f8;color:#1b2733}
header{background:#0b3d2c;color:#fff;padding:13px 20px}header b{font-size:17px}header span{opacity:.85;font-size:13px;margin-left:8px}
main{max-width:1020px;margin:18px auto;padding:0 14px}
.the{background:#fff;border:1px solid var(--vien);border-radius:10px;padding:15px 17px;margin-bottom:15px}
label{display:block;font-weight:600;margin:9px 0 4px}
input[type=text],input[type=password],select,textarea{width:100%;padding:8px 10px;border:1px solid var(--vien);border-radius:7px;font:inherit;background:#fff}
textarea{min-height:84px;font-family:Consolas,monospace;font-size:13.5px}
button{background:var(--nhan);color:#fff;border:0;border-radius:8px;padding:11px 19px;font:inherit;font-weight:600;cursor:pointer;margin-top:12px}
button.phu{background:#eef1f4;color:#243}
small{color:#5d6b7a}
table.b{border-collapse:collapse;width:100%;margin:9px 0;font-size:14px}table.b td,table.b th{border:1px solid var(--vien);padding:6px 8px;text-align:left;vertical-align:top}
table.b th{background:#eef1f4}
.tt{display:inline-block;padding:2px 9px;border-radius:20px;font-size:12px;font-weight:700}
.cho_de_nghi{background:#fff3cd;color:#7a5b00}.da_de_nghi{background:#cfe4ff;color:#0a3d75}.cho_phan_tich{background:#e2d6ff;color:#3d1f7a}
.cho_xac_nhan{background:#ffe4cc;color:#8a4a00}.da_bo_sung{background:#d6f4e0;color:#0b5c2e}.loi{background:#ffd9d9;color:#8a1414}
img{max-width:100%;border-radius:8px}
pre{background:#f7f9fb;border:1px solid var(--vien);border-radius:8px;padding:10px;white-space:pre-wrap;font-size:13.5px}
a{color:#0b6b3a}
h1{font-size:20px}h2{font-size:17px}h3{font-size:15.5px}
blockquote{margin:8px 0;padding:6px 12px;border-left:3px solid #b9c6cf;color:#3d4b57}
"""


def TT_html(tt):
    return '<span class="tt %s">%s</span>' % (tt, TT.get(tt, tt))


def md_sang_html(md):
    """Markdown → HTML gọn (tiêu đề, đậm/nghiêng, bullet, bảng, blockquote, code)."""
    out, i = [], 0
    dong = md.split('\n')
    while i < len(dong):
        s = dong[i].rstrip()
        if s.startswith('|') and i + 1 < len(dong) and re.match(r'^\|[\s:|-]+\|$', dong[i + 1].strip()):
            hang = []
            while i < len(dong) and dong[i].strip().startswith('|'):
                if not re.match(r'^[\s:|-]+$', dong[i].strip().strip('|')):
                    hang.append([c.strip() for c in dong[i].strip().strip('|').split('|')])
                i += 1
            out.append('<table class="b">')
            for k, r in enumerate(hang):
                o = 'th' if k == 0 else 'td'
                out.append('<tr>' + ''.join('<%s>%s</%s>' % (o, dam(c), o) for c in r) + '</tr>')
            out.append('</table>')
            continue
        if s.startswith('```'):
            i += 1
            buf = []
            while i < len(dong) and not dong[i].startswith('```'):
                buf.append(dong[i]); i += 1
            i += 1
            out.append('<pre>%s</pre>' % html.escape('\n'.join(buf)))
            continue
        if s.startswith('### '):
            out.append('<h3>%s</h3>' % dam(s[4:]))
        elif s.startswith('## '):
            out.append('<h2>%s</h2>' % dam(s[3:]))
        elif s.startswith('# '):
            out.append('<h1>%s</h1>' % dam(s[2:]))
        elif s.startswith('> '):
            out.append('<blockquote>%s</blockquote>' % dam(s[2:]))
        elif re.match(r'^[-*] ', s):
            out.append('• %s<br>' % dam(s[2:]))
        elif s:
            out.append('<p>%s</p>' % dam(s))
        i += 1
    return '\n'.join(out)


def dam(t):
    t = html.escape(t)
    t = re.sub(r'\*\*(.+?)\*\*', r'<b>\1</b>', t)
    t = re.sub(r'(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)', r'<i>\1</i>', t)
    return t


# ------------------------------------------------------------------ trang
def trang_dang_nhap(loi=''):
    return """<!doctype html><html lang="vi"><meta charset="utf-8"><title>HLQT · Đăng nhập</title>
<base href="./"><style>%s</style>
<header><b>HLQT</b><span>Huấn luyện quy trình sơn — CSDL A</span></header>
<main><div class="the" style="max-width:430px">
<h3 style="margin-top:0">Đăng nhập</h3>
%s
<form method="post" action="dang-nhap"><label>Mật khẩu dùng chung</label>
<input type="password" name="mat_khau" autofocus required><button type="submit">Vào hệ thống</button></form>
<p><small>Quên mật khẩu: xem file <code>mat_khau.txt</code> trong thư mục HLQT-app.</small></p>
</div></main></html>""" % (CSS, '<p style="color:#8a1414"><b>%s</b></p>' % html.escape(loi) if loi else '')


def email_mac_dinh():
    try:
        return open(os.path.join(APP, 'email_bao.txt'), encoding='utf-8').read().strip()
    except Exception:
        return ''


def trang_chinh():
    con = noi_db()
    c = con.cursor()
    ca = c.execute('SELECT id,tao_luc,nguoi_nhap,ma_mau,trang_thai,de_nghi_qt_ten,da_bo_sung FROM huan_luyen_ca '
                   'ORDER BY id DESC LIMIT 8').fetchall()
    n_luat = c.execute('SELECT COUNT(*) FROM huan_luyen').fetchone()[0]
    n_qt = c.execute('SELECT COUNT(*) FROM quy_trinh').fetchone()[0]
    con.close()
    hang = ''.join('<tr><td>#%s</td><td>%s</td><td><a href="ca?id=%s">%s</a></td><td>%s</td><td>%s</td></tr>'
                   % (r[0], r[1], r[0], html.escape(r[3] or '(chưa ghi mã màu)'), TT_html(r[4]),
                      ('✓ ' + html.escape((r[5] or '')[:60])) if r[6] else html.escape((r[5] or '')[:60]))
                   for r in ca)
    return """<!doctype html><html lang="vi"><meta charset="utf-8"><title>HLQT — huấn luyện quy trình CSDL A</title>
<base href="./"><style>%s</style>
<header><b>HLQT</b><span>Nạp bản màu → đề nghị 1 quy trình → nhập quy trình thực tế → bổ sung CSDL A</span></header>
<main>
<div class="the"><p><small>CSDL A hiện có: <b>%d quy trình</b> · <b>%d luật huấn luyện</b> ·
<a href="danh-sach">Danh sách ca</a> · <a href="luat">Luật đã học</a> · <a href="thoat">Thoát</a></small></p></div>
<form class="the" method="post" action="tao-ca" enctype="multipart/form-data">
<h3 style="margin-top:0">1. Nạp bản màu mới</h3>
<div class="bo" style="display:flex;gap:12px;flex-wrap:wrap">
<div><label>Ảnh bản màu (bắt buộc)</label><input type="file" name="anh" accept="image/*" required></div>
<div><label>Mã màu</label><input type="text" name="ma_mau" placeholder="VD: AQ 455 / MP 764 H"></div></div>
<div class="bo" style="display:flex;gap:12px;flex-wrap:wrap">
<div><label>Hệ sơn (nếu biết)</label><input type="text" name="he_son" placeholder="VD: NC / PU / nước / UV"></div>
<div><label>Loại nền / loại gỗ</label><input type="text" name="loai_nen" placeholder="VD: MDF / sồi / tràm / veneer"></div>
<div><label>Người nhập</label><input type="text" name="nguoi_nhap" placeholder="VD: Lab – Hùng"></div></div>
<label>Email nhận thông báo (hệ thống tự báo khi agent xử lý xong)</label><input type="text" name="email" value="%s" placeholder="VD: ngoducminh84@gmail.com">
<label>Yêu cầu chất lượng / độ bóng</label><input type="text" name="yeu_cau" placeholder="VD: bóng 20%% ± 5%%, giữ vân, kháng hoá chất">
<label>Ghi chú thêm (tuỳ chọn)</label><input type="text" name="ghi_chu" placeholder="VD: ảnh chụp vuông góc ngoài xưởng, có ô giấy trắng">
<label>Ảnh phụ (tuỳ chọn, chọn nhiều được)</label><input type="file" name="anh_phu" accept="image/*" multiple>
<button type="submit">Gửi — nhờ agent đề nghị 1 quy trình</button>
<p><small>Sau khi gửi, mở trang ca để xem quy trình được đề nghị (agent chạy ~1–3 phút). Trang tự cập nhật.</small></p>
</form>
<div class="the"><h3 style="margin-top:0">Các ca gần đây</h3>
<table class="b"><tr><th>Ca</th><th>Lúc</th><th>Mã màu</th><th>Trạng thái</th><th>Quy trình được đề nghị</th></tr>%s</table></div>
</main></html>""" % (CSS, n_qt, n_luat, html.escape(email_mac_dinh()), hang or '<tr><td colspan="5">(chưa có ca nào)</td></tr>')


def trang_chi_tiet(v, q):
    chua_xong = v['trang_thai'] in ('cho_de_nghi', 'cho_phan_tich')
    nhip_tt, nhip_ca, nhip_buoc, nhip_luc = nhip_worker()
    dong_nhip = ('<b>Agent đang xử lý:</b> ca #%s · bước %s · từ %s' % (nhip_ca, nhip_buoc, nhip_luc)) \
        if nhip_tt == 'dang_chay' else '<b>Agent đang rảnh</b>'
    anh = v['anh_vao'] or ''
    anh_html = ''
    if anh and os.path.exists(anh):
        anh_html = '<p><img src="tep?p=%s" style="max-height:420px"></p>' % urllib.parse.quote(anh)
    de_nghi = ''
    if v['de_nghi_file'] and os.path.exists(v['de_nghi_file']):
        de_nghi = md_sang_html(open(v['de_nghi_file'], encoding='utf-8').read())
        de_nghi = de_nghi.replace('```json', '').replace('```', '')
    phan_tich = ''
    if v['phan_tich_file'] and os.path.exists(v['phan_tich_file']):
        phan_tich = md_sang_html(open(v['phan_tich_file'], encoding='utf-8').read())
    try:
        dx = json.loads(v['de_xuat_json'] or '{}')
    except Exception:
        dx = {}
    khoi_ghi = ''
    if v['phan_tich_file'] and not v['da_bo_sung']:
        qt = dx.get('quy_trinh_moi') or {}
        luat = dx.get('luat_moi') or []
        buoc = dx.get('buoc') or []
        kb = ''.join('<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>'
                     % (html.escape(str(b.get('cong_doan', ''))[:80]), html.escape(str(b.get('de_nghi', ''))[:160]),
                        html.escape(str(b.get('thuc_te', ''))[:160]), html.escape(str(b.get('loai', ''))[:60]))
                     for b in (dx.get('khac_biet') or []))
        kl = ''.join('<li><b>%s</b> — %s <small>(%s)</small></li>' % (html.escape(str(l.get('nhom', ''))[:60]),
                     html.escape(str(l.get('quy_tac', ''))[:300]), html.escape(str(l.get('pham_vi', ''))[:80])) for l in luat)
        khoi_ghi = ('<h3 style="margin-top:18px;color:#8a4a00">ĐỀ XUẤT GHI VÀO CSDL A — chưa ghi</h3>'
                    '<p><b>Quy trình mới:</b> %s <small>(%s · %s · %s · %d bước)</small></p>%s%s%s'
                    '<form method="post" action="ghi?id=%s" style="margin-top:10px">'
                    '<label>Nhập mật khẩu xác nhận để GHI vào CSDL A</label>'
                    '<input type="password" name="mat_khau" required style="max-width:240px">'
                    '<button type="submit">Xác nhận &amp; ghi vào CSDL A</button></form>'
                    '<p><small>Chưa nhập mật khẩu thì CSDL A giữ nguyên — agent chỉ đề xuất. Hệ thống backup CSDL A trước khi ghi.</small></p>'
                    % (html.escape(str(qt.get('ten', ''))[:200]), html.escape(str(qt.get('he_son', ''))[:40]),
                       html.escape(str(qt.get('ma_mau', ''))[:60]), html.escape(str(qt.get('loai_nen', ''))[:60]), len(buoc),
                       ('<table class="b"><tr><th>Công đoạn</th><th>Đề nghị</th><th>Thực tế</th><th>Khác biệt</th></tr>%s</table>' % kb) if kb else '',
                       ('<p><b>Luật mới rút ra:</b></p><ul>%s</ul>' % kl) if kl else '<p><small>Không có luật mới.</small></p>',
                       ('<p style="color:#8a1414"><b>Cảnh báo xung đột:</b> %s</p>' % html.escape('; '.join(map(str, dx.get('xung_dot') or [])))) if dx.get('xung_dot') else '',
                       v['id']))
    o_nhap = ''
    if v['da_bo_sung']:
        o_nhap = ('<p style="color:#0b5c2e"><b>✓ Đã bổ sung CSDL A:</b> quy trình mới <b>id %s</b> · %s luật mới · lúc %s</p>'
                  % (v['qt_moi_id'], v['so_luat_them'], v['ngay_bo_sung']))
    elif v['de_nghi_file']:
        o_nhap = ('<h3 style="margin-top:18px">2. Nhập QUY TRÌNH THỰC TẾ của bản màu này</h3>'
                  '<p><small>Dán nguyên văn quy trình mà bản màu đã thực sự được làm (các bước, loại sơn/bóng, tỉ lệ, dung môi, thời gian khô, cách thi công, số lần lót/bóng…).'
                  'Càng chi tiết, agent càng rút ra được nhiều để bổ sung CSDL A.</small></p>'
                  '<form method="post" action="nop-thuc-te?id=%s">'
                  '<textarea name="thuc_te" style="min-height:220px" placeholder="VD:\n1/ Làm ẩm + chà nhám #180\n2/ Nhuộm nền stain AQ 455 100:5 dung môi BT 07H, lau đều, khô 30 phút\n3/ Lót NC KH pha (1-1) + 10%% đen TCL3 ...">%s</textarea>'
                  '<button type="submit">%s</button></form>'
                  % (v['id'], html.escape(v['thuc_te_text'] or ''),
                     'Gửi lại cho agent phân tích' if v['phan_tich_file'] else 'Gửi cho agent phân tích & đối chiếu'))
    # khối THÔNG BÁO CHO NGƯỜI DÙNG (email) + cảnh báo sai mật khẩu
    bao = [('Đề nghị quy trình', v['bao_p']), ('Phân tích & đối chiếu', v['bao_c']), ('Ghi vào CSDL A', v['bao_ghi'])]
    dong_bao = ''.join('<li>%s: %s</li>' % (t, ('<b style="color:#0b5c2e">✔ %s</b>' % html.escape(b)) if (b or '').startswith('đã gửi')
                                            else ('<b style="color:#8a1414">%s</b>' % html.escape(b or '')) if b else '<i>chưa có</i>')
                       for t, b in bao)
    canh_bao_ui = ''
    if q.get('sai_pass'):
        canh_bao_ui += ('<p style="background:#ffe4cc;border-radius:8px;padding:8px 12px">'
                        '<b>Sai mật khẩu xác nhận — CSDL A CHƯA thay đổi.</b> Thử lại ở ô mật khẩu bên dưới.</p>')
    if q.get('da_bao'):
        canh_bao_ui += '<p style="background:#d6f4e0;border-radius:8px;padding:8px 12px"><b>Đã gửi lại thông báo qua email.</b></p>'
    khoi_bao = ('<div style="margin-top:10px;border-top:1px solid #dfe3e8;padding-top:8px">'
                '<b>Thông báo cho người dùng qua email:</b> <code>%s</code><ul>%s</ul>'
                '<form method="post" action="bao-lai?id=%s"><button type="submit" class="phu" style="padding:7px 14px">Gửi lại thông báo qua email</button></form>%s</div>'
                % (html.escape((v.get('email') or '') or email_mac_dinh() or '(chưa đặt)'), dong_bao, v['id'], canh_bao_ui))
    return """<!doctype html><html lang="vi"><meta charset="utf-8"><title>HLQT · Ca #%s</title>
<base href="./">%s<style>%s</style>
<header><b>HLQT · CA #%s</b><span>%s</span></header>
<main>
<div class="the"><p><b>Mã màu:</b> %s · <b>Hệ sơn:</b> %s · <b>Nền:</b> %s · <b>Người nhập:</b> %s · <b>Lúc:</b> %s</p>
<p><small>%s</small></p>
<p><small><b>Yêu cầu:</b> %s</small></p>
<p><small><b>Ghi chú:</b> %s</small></p>%s%s
<p><small><a href="./">← Nạp bản màu mới</a> · <a href="danh-sach">Danh sách ca</a></small></p></div>
<div class="the"><h3 style="margin-top:0">1. Quy trình ĐƯỢC ĐỀ NGHỊ (agent chọn 1 quy trình sát nhất từ CSDL A)</h3>
%s</div>
<div class="the">%s%s</div>
<div class="the">%s</div>
</main></html>""" % (v['id'], '<meta http-equiv="refresh" content="10">' if chua_xong else '', CSS, v['id'], TT_html(v['trang_thai']),
                     html.escape(v['ma_mau'] or '—'), html.escape(v['he_son'] or '—'), html.escape(v['loai_nen'] or '—'),
                     html.escape(v['nguoi_nhap'] or '—'), v['tao_luc'], dong_nhip, html.escape(v['yeu_cau'] or '—'),
                     html.escape(v['ghi_chu'] or '—'), khoi_bao, anh_html,
                     de_nghi or '<p><small>Agent đang đọc bản màu và chọn quy trình… (trang tự cập nhật mỗi 10 giây)</small></p>',
                     phan_tich or '<p><small>Chưa có phân tích — nhập quy trình thực tế ở mục 2 bên dưới.</small></p>',
                     khoi_ghi, o_nhap)


def trang_danh_sach():
    con = noi_db()
    c = con.cursor()
    rs = c.execute('SELECT id,tao_luc,ma_mau,trang_thai,de_nghi_qt_ten,da_bo_sung,qt_moi_id,so_luat_them FROM huan_luyen_ca '
                   'ORDER BY id DESC').fetchall()
    con.close()
    hang = ''.join('<tr><td>#%s</td><td>%s</td><td><a href="ca?id=%s">%s</a></td><td>%s</td><td>%s</td><td>%s</td></tr>'
                   % (r[0], r[1], r[0], html.escape(r[2] or '—'), TT_html(r[3]),
                      html.escape((r[4] or '')[:70]),
                      ('✓ QT id %s · %s luật' % (r[6], r[7])) if r[5] else '—') for r in rs)
    return """<!doctype html><html lang="vi"><meta charset="utf-8"><title>HLQT · Danh sách ca</title>
<base href="./"><style>%s</style>
<header><b>HLQT · DANH SÁCH CA HUẤN LUYỆN</b><span><a style="color:#cfe8d8" href="./">nạp bản màu mới</a></span></header>
<main><div class="the"><table class="b"><tr><th>Ca</th><th>Lúc</th><th>Mã màu</th><th>Trạng thái</th><th>Quy trình đề nghị</th><th>Đã bổ sung</th></tr>%s</table></div></main></html>""" \
        % (CSS, hang or '<tr><td colspan="6">(chưa có ca)</td></tr>')


def trang_luat():
    con = noi_db()
    c = con.cursor()
    rs = c.execute('SELECT id,ngay,nhom,quy_tac,pham_vi,trang_thai,nguon FROM huan_luyen ORDER BY id DESC').fetchall()
    con.close()
    hang = ''.join('<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>'
                   % (r[0], r[1], html.escape(r[2] or ''), html.escape((r[3] or '')[:420]), html.escape((r[4] or '')[:90]),
                      html.escape(r[5] or ''), html.escape((r[6] or '')[:90])) for r in rs)
    return """<!doctype html><html lang="vi"><meta charset="utf-8"><title>HLQT · Luật đã học</title>
<base href="./"><style>%s</style>
<header><b>HLQT · LUẬT HUẤN LUYỆN TRONG CSDL A</b><span>%d luật</span></header>
<main><div class="the"><table class="b"><tr><th>#</th><th>Ngày</th><th>Nhóm</th><th>Quy tắc</th><th>Phạm vi</th><th>Trạng thái</th><th>Nguồn</th></tr>%s</table></div>
<p><small><a href="./">← về trang chính</a></small></p></main></html>""" % (CSS, len(rs), hang)


# ------------------------------------------------------------------ HTTP
class H(BaseHTTPRequestHandler):
    server_version = 'HLQT/1.0'

    def log_message(self, *a):
        pass

    def _gui(self, ma, nd, loai='text/html; charset=utf-8'):
        # Tự chỉnh <base> theo tiền tố /hlqt (đúng cho cả /hlqt lẫn /hlqt/, cả /hlqt?x=1)
        if 'text/html' in loai and b'<base href="./">' in nd:
            nd = nd.replace(b'<base href="./">', b'<base href="./">' + BASE_JS, 1)
        self.send_response(ma)
        self.send_header('Content-Type', loai)
        self.send_header('Content-Length', str(len(nd)))
        self.send_header('Cache-Control', 'no-store, must-revalidate')
        self.send_header('Pragma', 'no-cache')
        self.end_headers()
        self.wfile.write(nd)

    def _json(self, o):
        self._gui(200, json.dumps(o, ensure_ascii=False).encode('utf-8'), 'application/json; charset=utf-8')

    def _cho_phep(self):
        c = {}
        for p in (self.headers.get('Cookie') or '').split(';'):
            if '=' in p:
                k, v = p.split('=', 1)
                c[k.strip()] = v.strip()
        return c.get('ahlqt') and hmac.compare_digest(c['ahlqt'], ma_phien())

    def do_GET(self):
        q = urllib.parse.parse_qs(urllib.parse.urlparse(self.path).query)
        duong = urllib.parse.urlparse(self.path).path
        if duong in ('/dang-nhap', '/'):
            if not self._cho_phep():
                return self._gui(200, trang_dang_nhap().encode('utf-8'))
            return self._gui(200, trang_chinh().encode('utf-8'))
        if duong == '/trang-thai':
            con = noi_db()
            r = con.execute('SELECT trang_thai,loi FROM huan_luyen_ca WHERE id=?', (q.get('id', ['0'])[0],)).fetchone()
            con.close()
            return self._json({'trang_thai': r[0] if r else 'khong-thay', 'loi': (r[1] or '')[:400] if r else ''})
        if duong == '/tep':
            p = q.get('p', [''])[0]
            p = os.path.normpath(p)
            if not p.startswith(os.path.normpath(GOC)) or not os.path.exists(p) or os.path.isdir(p):
                return self._gui(404, b'x')
            return self._gui(200, open(p, 'rb').read(), mimetypes.guess_type(p)[0] or 'application/octet-stream')
        if not self._cho_phep():
            return self._gui(200, trang_dang_nhap().encode('utf-8'))
        if duong == '/thoat':
            self.send_response(302)
            self.send_header('Set-Cookie', 'ahlqt=; Path=/; Max-Age=0')
            self.send_header('Location', './')
            self.end_headers()
            return
        if duong == '/danh-sach':
            return self._gui(200, trang_danh_sach().encode('utf-8'))
        if duong == '/luat':
            return self._gui(200, trang_luat().encode('utf-8'))
        if duong == '/ca':
            con = noi_db()
            c = con.cursor()
            c.row_factory = sqlite3.Row
            v = c.execute('SELECT * FROM huan_luyen_ca WHERE id=?', (q.get('id', ['0'])[0],)).fetchone()
            con.close()
            if not v:
                return self._gui(404, '<h3>Không thấy ca</h3>'.encode('utf-8'))
            return self._gui(200, trang_chi_tiet(dict(v), q).encode('utf-8'))
        return self._gui(404, b'<h3>Khong thay</h3>')

    def _fs(self):
        """Đọc form (kể cả multipart) — KHÔNG dùng truthiness trên FieldStorage (Python 3.11 ném TypeError)."""
        dai = int(self.headers.get('Content-Length') or 0)
        ct = self.headers.get('Content-Type', '')
        than = self.rfile.read(dai)
        return cgi.FieldStorage(fp=io.BytesIO(than), headers=self.headers,
                                environ={'REQUEST_METHOD': 'POST', 'CONTENT_TYPE': ct, 'CONTENT_LENGTH': str(dai)})

    def duong(self):
        return urllib.parse.urlparse(self.path).path

    def q(self):
        return urllib.parse.parse_qs(urllib.parse.urlparse(self.path).query)

    def do_POST(self):
        duong = urllib.parse.urlparse(self.path).path
        q = urllib.parse.parse_qs(urllib.parse.urlparse(self.path).query)
        if duong == '/dang-nhap':
            fs = self._fs()
            if fs.getvalue('mat_khau', '') == mat_khau():
                self.send_response(302)
                self.send_header('Set-Cookie', 'ahlqt=%s; Path=/; Max-Age=43200; HttpOnly; SameSite=Lax' % ma_phien())
                self.send_header('Location', './')
                self.end_headers()
                return
            return self._gui(200, trang_dang_nhap('Sai mật khẩu — thử lại.').encode('utf-8'))
        if not self._cho_phep():
            return self._gui(403, b'Chua dang nhap')

        if duong == '/tao-ca':
            fs = self._fs()
            con = noi_db()
            cur = con.execute('''INSERT INTO huan_luyen_ca (tao_luc,nguoi_nhap,ma_mau,he_son,loai_nen,yeu_cau,ghi_chu,trang_thai,nguon,email)
                                 VALUES (?,?,?,?,?,?,?,'cho_de_nghi',?,?)''',
                              (time.strftime('%Y-%m-%d %H:%M:%S'), fs.getvalue('nguoi_nhap', ''), fs.getvalue('ma_mau', ''),
                               fs.getvalue('he_son', ''), fs.getvalue('loai_nen', ''), fs.getvalue('yeu_cau', ''),
                               fs.getvalue('ghi_chu', ''), 'app HLQT (nạp bản màu)',
                               (fs.getvalue('email', '') or '').strip() or email_mac_dinh()))
            ca_id = cur.lastrowid
            con.commit()
            con.close()
            d = os.path.join(VIEC, 'C%04d_%s' % (ca_id, time.strftime('%d%m')))
            os.makedirs(os.path.join(d, 'anh_vao'), exist_ok=True)
            luu = []

            def luu_anh(fs_field, ten):
                if fs_field is None or not getattr(fs_field, 'filename', ''):
                    return None
                p = os.path.join(d, ten, os.path.basename(fs_field.filename).replace(' ', '_'))
                os.makedirs(os.path.dirname(p), exist_ok=True)
                open(p, 'wb').write(fs_field.file.read())
                return p

            chinh = luu_anh(fs['anh'], 'anh_vao') if ('anh' in fs and getattr(fs['anh'], 'filename', '')) else None
            phu = fs['anh_phu'] if ('anh_phu' in fs and getattr(fs['anh_phu'], 'filename', '')) else None
            if phu is not None:
                for f in (phu if isinstance(phu, list) else [phu]):
                    p = luu_anh(f, 'anh_phu')
                    if p:
                        luu.append(p)
            con = noi_db()
            con.execute('UPDATE huan_luyen_ca SET anh_vao=?, anh_khac=? WHERE id=?',
                        (chinh, json.dumps(luu, ensure_ascii=False), ca_id))
            con.commit()
            con.close()
            self.send_response(302)
            self.send_header('Location', './ca?id=%d' % ca_id)
            self.end_headers()
            return

        if duong == '/nop-thuc-te':
            fs = self._fs()
            ca_id = int(q.get('id', ['0'])[0])
            txt = fs.getvalue('thuc_te', '') or ''
            d = os.path.join(VIEC, 'C%04d_%s' % (ca_id, time.strftime('%d%m')))
            os.makedirs(d, exist_ok=True)
            f = os.path.join(d, 'QUY_TRINH_THUC_TE.txt')
            open(f, 'w', encoding='utf-8').write(txt)
            con = noi_db()
            con.execute("UPDATE huan_luyen_ca SET thuc_te_text=?, thuc_te_luc=?, thuc_te_file=?, trang_thai='cho_phan_tich', loi=NULL WHERE id=?",
                        (txt, time.strftime('%Y-%m-%d %H:%M:%S'), f, ca_id))
            con.commit()
            con.close()
            print('[%s] ca #%d: đã nhận quy trình thực tế (%d ký tự) — chờ agent phân tích' % (time.strftime('%H:%M:%S'), ca_id, len(txt)))
            self.send_response(302)
            self.send_header('Location', './ca?id=%d' % ca_id)
            self.end_headers()
            return

        if duong == '/bao-lai':
            fs = self._fs()
            ca_id = int(q.get('id', ['0'])[0])
            import importlib.util
            spec = importlib.util.spec_from_file_location('whlqt', os.path.join(APP, 'worker_hlqt.py'))
            m = importlib.util.module_from_spec(spec)
            spec.loader.exec_module(m)
            try:
                ok, tt = m.gui_lai_bao(ca_id)
            except Exception as e:
                ok, tt = False, 'lỗi: %s' % e
            print('[%s] ca #%d: GỬI LẠI THÔNG BÁO → %s' % (time.strftime('%H:%M:%S'), ca_id, tt), flush=True)
            self.send_response(302)
            self.send_header('Location', './ca?id=%d&da_bao=%d' % (ca_id, 1 if ok else 0))
            self.end_headers()
            return

        if duong == '/ghi':
            fs = self._fs()
            ca_id = int(q.get('id', ['0'])[0])
            if fs.getvalue('mat_khau', '').strip() != pass_ghi():
                print('[%s] ca #%d: SAI mật khẩu xác nhận ghi CSDL A' % (time.strftime('%H:%M:%S'), ca_id))
                self.send_response(302)
                self.send_header('Location', './ca?id=%d&sai_pass=1' % ca_id)
                self.end_headers()
                return
            import importlib.util
            spec = importlib.util.spec_from_file_location('whlqt', os.path.join(APP, 'worker_hlqt.py'))
            m = importlib.util.module_from_spec(spec)
            spec.loader.exec_module(m)
            n_buoc, n_luat, qt_id, canh_bao = m.ghi_vao_csdla(ca_id)
            print('[%s] ca #%d: ĐÃ GHI CSDL A — quy trình id %s · %d bước · %d luật%s'
                  % (time.strftime('%H:%M:%S'), ca_id, qt_id, n_buoc, n_luat,
                     (' | ' + '; '.join(map(str, canh_bao))) if canh_bao else ''))
            self.send_response(302)
            self.send_header('Location', './ca?id=%d&da_ghi=%d&buoc=%d&luat=%d' % (ca_id, qt_id or 0, n_buoc, n_luat))
            self.end_headers()
            return
        return self._gui(404, b'<h3>Khong thay</h3>')


def main():
    cong = int(sys.argv[1]) if len(sys.argv) > 1 else 8100
    print('HLQT server: http://127.0.0.1:%d  (CSDL A: %s)' % (cong, DB), flush=True)
    ThreadingHTTPServer(('127.0.0.1', cong), H).serve_forever()


if __name__ == '__main__':
    main()
