# -*- coding: utf-8 -*-
"""HLQT — worker: nhận ca từ app, chạy AGY 2 bước, và GHI vào CSDL A (chỉ khi chủ nhân nhập pass).

Bước P (đề nghị) : ảnh bản màu + gói kiến thức CSDL A  → AGY chọn ĐÚNG 1 quy trình khả dĩ/sát nhất.
Bước C (đối chiếu): ảnh + đề nghị + quy trình THỰC TẾ chủ nhân nhập → AGY phân tích khác biệt và
                    đề xuất bổ sung CSDL A (quy trình mới + luật mới).
Ghi CSDL A: chỉ qua server sau khi kiểm pass (hàm ghi_vao_csdla).
Chạy:  python worker_hlqt.py            (vòng lặp, chờ ca)
       python worker_hlqt.py --mot-lan  (xử lý 1 ca rồi thoát)
"""
import json, os, re, shutil, sqlite3, subprocess, sys, time

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')
KIEN_THUC = os.path.join(APP, 'kien_thuc', 'CSDLA_GOI_CHO_AGY.md')
AGY = r'C:\Users\admin\AppData\Local\agy\bin\agy.exe'
MODEL = 'gemini-3.1-pro-high'
NHIP = os.path.join(APP, '_nhip.json')
EMAIL_BAO = os.path.join(APP, 'email_bao.txt')          # email nhận thông báo mặc định
MAIL_SCRIPT = os.path.join(os.path.expandvars(r'%LOCALAPPDATA%'), 'hermes', 'skills', 'email',
                           'gui-email-kem-file', 'scripts', 'gui_mail_kem_file.py')
URL_PUBLIC = 'https://desktop-9gsdkd2.tail0ffe94.ts.net/hlqt/'
os.makedirs(VIEC, exist_ok=True)


# ---------------------------------------------------------------- BÁO CHO NGƯỜI DÙNG
def email_nhan(ca):
    """Email nhận thông báo: ưu tiên email của ca, không có thì dùng email mặc định của app."""
    e = (ca.get('email') or '').strip()
    if e and '@' in e:
        return e
    try:
        return open(EMAIL_BAO, encoding='utf-8').read().strip()
    except Exception:
        return ''


def _chuoi_buoc(j, gioi_han=10):
    ra = []
    for i, b in enumerate((j.get('buoc') or [])[:gioi_han], 1):
        ct = (b.get('chi_tiet') or b.get('buoc_nguyen_van') or '').strip()
        so = b.get('buoc_chuan_so') or '—'
        ra.append('  %s. [bước chuẩn %s] %s' % (b.get('thu_tu') or i, so, ct[:180]))
    if len(j.get('buoc') or []) > gioi_han:
        ra.append('  … còn %d bước nữa (xem trên web)' % (len(j['buoc']) - gioi_han))
    return '\n'.join(ra)


def soan_mail(ca, loai, j):
    """Trả (tiêu đề, thân thư, [tệp đính kèm]) cho từng mốc báo."""
    ma = ca.get('ma_mau') or '(chưa ghi mã màu)'
    link = '%sca?id=%s' % (URL_PUBLIC, ca['id'])
    if loai == 'p':
        td = '[HLQT] Đã có QUY TRÌNH ĐỀ NGHỊ — ca #%s · %s' % (ca['id'], ma)
        tt = ('Đã xong bước ĐỀ NGHỊ QUY TRÌNH cho bản màu bạn nạp.\n\n'
              'Ca           : #%s\nMã màu       : %s · hệ sơn: %s · nền: %s\n'
              'Quy trình chọn: id %s — %s\nĐộ khớp      : %s%%\nThời gian    : %s giây\n\n'
              'TÓM TẮT: %s\n\nCÁC BƯỚC ĐỀ NGHỊ:\n%s\n\nCẦN XÁC NHẬN:\n%s\n\n'
              '>> Mở trang ca để nhập QUY TRÌNH THỰC TẾ của bản màu này: %s\n'
              % (ca['id'], ma, ca.get('he_son') or '—', ca.get('loai_nen') or '—',
                 j.get('qt_id') or '—', (j.get('qt_ten') or '')[:120], j.get('do_khop') or '—',
                 int(ca.get('de_nghi_giay') or 0), (j.get('tom_tat') or '—')[:600],
                 _chuoi_buoc(j) or '  (không có)', '\n'.join('  - ' + str(x) for x in (j.get('can_xac_nhan') or [])) or '  - (không)',
                 link))
        kem = [ca.get('de_nghi_file')] if ca.get('de_nghi_file') else []
    elif loai == 'c':
        kb = j.get('khac_biet') or []
        luat = j.get('luat_moi') or []
        dong_kb = '\n'.join('  - %s: %s → THỰC TẾ: %s  (%s)'
                            % (b.get('cong_doan', '')[:50], str(b.get('de_nghi', ''))[:90], str(b.get('thuc_te', ''))[:90], b.get('loai', ''))
                            for b in kb[:8]) or '  (không có khác biệt)'
        dong_luat = '\n'.join('  - [%s] %s (phạm vi: %s)' % (l.get('nhom', '')[:30], str(l.get('quy_tac', ''))[:220], l.get('pham_vi', '')[:60])
                              for l in luat) or '  (không có luật mới)'
        xd = '\n'.join('  ! ' + str(x) for x in (j.get('xung_dot') or [])) or '  (không)'
        td = '[HLQT] Đã PHÂN TÍCH & ĐỐI CHIẾU xong — ca #%s · %s (%d khác biệt, %d luật mới)' % (ca['id'], ma, len(kb), len(luat))
        tt = ('Đã xong bước ĐỐI CHIẾU: quy trình thực tế bạn nhập ↔ quy trình agent đề nghị.\n\n'
              'Ca       : #%s · mã màu: %s\nKhác biệt: %d công đoạn · Luật mới rút ra: %d\n\n'
              'KHÁC BIỆT CHÍNH:\n%s\n\nLUẬT MỚI ĐỀ XUẤT GHI VÀO CSDL A:\n%s\n\nXUNG ĐỘT VỚI LUẬT CŨ:\n%s\n\n'
              'TÓM TẮT: %s\n\n>> Mở trang ca, nhập MẬT KHẨU XÁC NHẬN để ghi các nội dung trên vào CSDL A: %s\n'
              '   (Chưa nhập mật khẩu thì CSDL A giữ nguyên — agent chỉ đề xuất.)\n'
              % (ca['id'], ma, len(kb), len(luat), dong_kb, dong_luat, xd, (j.get('tom_tat') or '—')[:600], link))
        kem = [ca.get('phan_tich_file')] if ca.get('phan_tich_file') else []
    else:
        td = '[HLQT] ĐÃ GHI VÀO CSDL A — ca #%s · %s (quy trình id %s, %s luật mới)' % (
            ca['id'], ma, ca.get('qt_moi_id'), ca.get('so_luat_them'))
        tt = ('Đã ghi vào CSDL A (sau khi bạn nhập mật khẩu xác nhận).\n\n'
              'Ca            : #%s · mã màu: %s\nQuy trình mới : id %s — %s (đã gỡ tên khách, ẩn danh)\n'
              'Số bước       : %s · Luật mới thêm: %s\nThời điểm ghi : %s\n\n'
              'CSDL A đã tự dựng lại gói kiến thức ⇒ lần đề nghị sau sẽ dùng luật mới.\n\n'
              '>> Xem lại: %s\n' % (ca['id'], ma, ca.get('qt_moi_id'),
                                  (ca.get('de_nghi_qt_ten') or 'quy trình thực tế')[:100],
                                  ca.get('so_luat_them'), ca.get('so_luat_them'), ca.get('ngay_bo_sung'), link))
        kem = [p for p in (ca.get('phan_tich_file'),) if p]
    return td, tt, [k for k in kem if k and os.path.exists(k)]


def bao_mail(ca_id, loai):
    """Gửi email báo cho người dùng ở mốc 'p' (đề nghị) / 'c' (phân tích) / 'ghi' (đã ghi CSDL A)."""
    con = noi_db()
    c = con.cursor()
    cot = [x[1] for x in c.execute('PRAGMA table_info(huan_luyen_ca)')]
    r = c.execute('SELECT * FROM huan_luyen_ca WHERE id=?', (ca_id,)).fetchone()
    con.close()
    if not r:
        return False, 'không thấy ca #%s' % ca_id
    ca = dict(zip(cot, r))
    try:
        j = json.loads(ca.get('de_nghi_json') or '{}') if loai == 'p' else json.loads(ca.get('de_xuat_json') or '{}')
    except Exception:
        j = {}
    to = email_nhan(ca)
    cot_bao = {'p': 'bao_p', 'c': 'bao_c', 'ghi': 'bao_ghi'}.get(loai, 'bao_p')
    if not to:
        tt = 'chưa có email nhận thông báo (điền email ở ô tạo ca hoặc file email_bao.txt)'
        con = noi_db(); con.execute('UPDATE huan_luyen_ca SET %s=? WHERE id=?' % cot_bao, (tt, ca_id)); con.commit(); con.close()
        print('   BÁO: %s' % tt, flush=True)
        return False, tt
    if not os.path.exists(MAIL_SCRIPT):
        tt = 'thiếu script gửi mail: %s' % MAIL_SCRIPT
        con = noi_db(); con.execute('UPDATE huan_luyen_ca SET %s=? WHERE id=?' % cot_bao, (tt, ca_id)); con.commit(); con.close()
        return False, tt
    tieu_de, than, kem = soan_mail(ca, loai, j)
    argv = [sys.executable, MAIL_SCRIPT, '--to', to, '--subject', tieu_de, '--body-text', than]
    if kem:
        argv += ['--attach'] + kem
    p = subprocess.run(argv, capture_output=True, text=True, encoding='utf-8', errors='replace', cwd=APP, timeout=300)
    ra = ((p.stdout or '') + (p.stderr or '')).strip()
    d = thu_muc_ca(ca)
    open(os.path.join(d, '_log_mail_%s.txt' % loai), 'w', encoding='utf-8').write(ra[:6000])
    m = re.search(r'"?(?:id|messageId)"?\s*[:=]\s*"?([0-9a-zA-Z_-]{12,})', ra)
    id_thu = m.group(1) if m else ''
    ok = p.returncode == 0 and ('đã gửi' in ra.lower() or 'sent' in ra.lower() or id_thu)
    tt = ('đã gửi tới %s lúc %s%s' % (to, time.strftime('%H:%M %d/%m'), (' (id %s)' % id_thu) if id_thu else '')) if ok \
        else ('LỖI gửi mail: %s' % ra[-200:])
    con = noi_db()
    con.execute('UPDATE huan_luyen_ca SET %s=? WHERE id=?' % cot_bao, (tt, ca_id))
    con.execute('INSERT INTO nhat_ky_hlqt (ngay,viec,chi_tiet,nguon) VALUES (?,?,?,?)',
                (time.strftime('%Y-%m-%d %H:%M'), 'Báo cho người dùng — ca #%s mốc %s' % (ca_id, loai),
                 'Tới %s · %s · tiêu đề: %s' % (to, tt, tieu_de[:150]), 'worker HLQT'))
    con.commit()
    con.close()
    print('   BÁO (%s): %s' % (loai, tt), flush=True)
    return ok, tt


def gui_lai_bao(ca_id):
    """Bấm 'gửi lại thông báo' trên web: gửi lại theo mốc xa nhất đã có."""
    con = noi_db()
    cot = [x[1] for x in con.execute('PRAGMA table_info(huan_luyen_ca)')]
    r = con.execute('SELECT * FROM huan_luyen_ca WHERE id=?', (ca_id,)).fetchone()
    con.close()
    if not r:
        return False, 'không thấy ca'
    ca = dict(zip(cot, r))
    loai = 'ghi' if ca.get('da_bo_sung') else ('c' if ca.get('phan_tich_file') else 'p')
    return bao_mail(ca_id, loai)


# ---------------------------------------------------------------- DB
def noi_db():
    con = sqlite3.connect(DB)
    c = con.cursor()
    c.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)''')
    c.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 thu_muc_ca(ca):
    d = os.path.join(VIEC, 'C%04d_%s' % (ca['id'], time.strftime('%d%m')))
    os.makedirs(d, exist_ok=True)
    return d


def nhip(trang_thai, ca_id=None, buoc=''):
    try:
        json.dump({'luc': time.strftime('%Y-%m-%d %H:%M:%S'), 'ca': ca_id, 'buoc': buoc, 'trang_thai': trang_thai},
                  open(NHIP, 'w', encoding='utf-8'), ensure_ascii=False)
    except Exception:
        pass


# ---------------------------------------------------------------- AGY
def goi_agy(ten_file_yeu_cau, giay_toi_da=2400):
    """Gọi agy headless; prompt truyền qua FILE (ASCII argv) để giữ dấu tiếng Việt."""
    argv = [AGY, '-p', 'Read the instruction file "%s" (UTF-8, Vietnamese) and follow it exactly.' % ten_file_yeu_cau,
            '--model', MODEL, '--add-dir', GOC, '--dangerously-skip-permissions', '--output-format', 'json']
    t0 = time.time()
    r = subprocess.run(argv, capture_output=True, text=True, encoding='utf-8', errors='replace', timeout=giay_toi_da)
    giay = round(time.time() - t0, 1)
    return r, giay


def lay_json(md):
    """Lấy khối ```json ... ``` CUỐI trong file markdown."""
    khoi = re.findall(r'```json\s*(\{.*?\})\s*```', md, re.S)
    for k in reversed(khoi):
        try:
            return json.loads(k)
        except Exception:
            continue
    m = re.search(r'(\{[\s\S]*\})\s*$', md.strip())
    if m:
        try:
            return json.loads(m.group(1))
        except Exception:
            return {}
    return {}


def an_danh(text):
    """Bỏ tên khách hàng đã biết khỏi văn bản trước khi ghi CSDL A (luật ẩn danh)."""
    if not text:
        return text, []
    con = sqlite3.connect(DB)
    ten = [r[0] for r in con.execute("SELECT DISTINCT khach_hang FROM quy_trinh WHERE khach_hang IS NOT NULL "
                                     "AND length(khach_hang)>3").fetchall()]
    ten += [r[0] for r in con.execute("SELECT DISTINCT khach_hang FROM danh_muc_mau WHERE khach_hang IS NOT NULL").fetchall()]
    con.close()
    da_bo = []
    out = text
    for t in set(ten):
        t = str(t).strip()
        if len(t) < 4:
            continue
        if re.search(re.escape(t), out, re.I):
            da_bo.append(t)
            out = re.sub(re.escape(t), '「khách hàng」', out, flags=re.I)
    return out, da_bo


# ---------------------------------------------------------------- BƯỚC P
def buoc_p(ca):
    d = thu_muc_ca(ca)
    anh = ca['anh_vao'] or ''
    anh_phu = json.loads(ca['anh_khac'] or '[]')
    yeu_cau = os.path.join(d, 'YEU_CAU_DE_NGHI.txt')
    open(yeu_cau, 'w', encoding='utf-8').write("""Bạn là chuyên gia quy trình sơn gỗ nội thất của BTChem.

TÀI LIỆU PHẢI ĐỌC:
1. Gói kiến thức CSDL A (34 bước chuẩn · 118 quy trình · 29 luật huấn luyện): %s
2. Ảnh BẢN MÀU cần nhận diện: %s
%s
THÔNG TIN CA (do chủ nhân nhập):
- mã màu: %s | hệ sơn (nếu có): %s | loại nền/loại gỗ: %s
- yêu cầu chất lượng/độ bóng: %s
- ghi chú: %s

VIỆC CẦN LÀM (ghi DẦN ra file, đọc tới đâu ghi tới đó):
1. ĐỌC ẢNH BẢN MÀU: tông màu (sáng/tối, ấm/lạnh), độ trong hay đục, vân gỗ còn rõ hay bị lấp, hiệu ứng
   (giả cổ / sọc glaze / highlight / tối cạnh / lộ mộc), độ bóng (mờ / bán bóng / bóng), có khuyết tật gì.
   Nếu ảnh không đủ để kết luận thì ghi rõ "không xác định được", KHÔNG suy diễn.
2. CHỌN ĐÚNG **MỘT** QUY TRÌNH trong 118 quy trình của CSDL A là khả dĩ và SÁT NHẤT với bản màu
   (nêu rõ id + tên quy trình + vì sao khớp). Nếu cần điều chỉnh thì nêu rõ điều chỉnh gì — nhưng
   kết quả vẫn chỉ là **MỘT đề nghị duy nhất**, gồm chuỗi bước theo BỘ 34 BƯỚC CHUẨN.
3. BẢNG BƯỚC ĐỀ NGHỊ: thứ tự | bước chuẩn (số + tên) | chi tiết áp dụng (loại sơn/bóng, tỉ lệ, thời gian
   khô, định lượng, cách thi công). Chỗ nào suy luận thì ghi "(suy luận)".
4. ĐIỂM KHỚP / CHƯA CHẮC CHẮN cần chủ nhân xác nhận; các LUẬT của CSDL A đã áp dụng.

BẮT BUỘC:
- ẨN DANH: KHÔNG ghi tên khách hàng, KHÔNG ghi tên kỹ thuật viên; chỉ dùng mã màu + loại nền.
- Ghi kết quả ra ĐÚNG file: %s
- Kết thúc file bằng khối ```json``` với đúng các khoá:
  {"qt_id": <số>, "qt_ten": "...", "do_khop": <%%, số>, "tom_tat": "1-3 câu",
   "buoc": [{"thu_tu":1, "buoc_chuan_so":<số>, "ten_buoc":"...", "chi_tiet":"..."}],
   "ly_do": "...", "can_xac_nhan": ["..."], "luat_ap_dung": ["..."]}
- Dòng cuối cùng của file in đúng chữ: HOAN TAT
""" % (KIEN_THUC, anh, ('3. Ảnh phụ: ' + '; '.join(anh_phu) + '\n') if anh_phu else '',
       ca['ma_mau'] or '(không ghi)', ca['he_son'] or '', ca['loai_nen'] or '', ca['yeu_cau'] or '', ca['ghi_chu'] or '',
       os.path.join(d, 'DE_NGHI.md')))

    nhip('dang_chay', ca['id'], 'P')
    r, giay = goi_agy(yeu_cau)
    open(os.path.join(d, '_log_agy_de_nghi.txt'), 'w', encoding='utf-8').write(
        (r.stdout or '') + '\n--- STDERR ---\n' + (r.stderr or ''))
    f = os.path.join(d, 'DE_NGHI.md')
    if not os.path.exists(f):
        return False, 'AGY không tạo được DE_NGHI.md (xem _log_agy_de_nghi.txt)', giay, {}
    md = open(f, encoding='utf-8').read()
    j = lay_json(md)
    con = noi_db()
    con.execute('''UPDATE huan_luyen_ca SET trang_thai='da_de_nghi', de_nghi_luc=?, de_nghi_file=?, de_nghi_giay=?,
                   de_nghi_qt_id=?, de_nghi_qt_ten=?, de_nghi_tom_tat=?, de_nghi_json=?, loi=NULL WHERE id=?''',
                (time.strftime('%Y-%m-%d %H:%M:%S'), f, giay, j.get('qt_id'), j.get('qt_ten'),
                 (j.get('tom_tat') or '')[:1500], json.dumps(j, ensure_ascii=False), ca['id']))
    con.commit()
    con.close()
    if j:
        bao_mail(ca['id'], 'p')
    return True, '', giay, j


# ---------------------------------------------------------------- BƯỚC C
def buoc_c(ca):
    d = thu_muc_ca(ca)
    tt = os.path.join(d, 'QUY_TRINH_THUC_TE.txt')
    open(tt, 'w', encoding='utf-8').write(ca['thuc_te_text'] or '')
    yeu_cau = os.path.join(d, 'YEU_CAU_PHAN_TICH.txt')
    open(yeu_cau, 'w', encoding='utf-8').write("""Bạn là chuyên gia quy trình sơn gỗ nội thất của BTChem. Bạn vừa đề nghị 1 quy trình cho một bản màu;
nay chủ nhân cung cấp QUY TRÌNH THỰC TẾ mà bản màu đó đã được làm. Nhiệm vụ: ĐỐI CHIẾU và ĐỀ XUẤT BỔ SUNG CSDL A.

TÀI LIỆU PHẢI ĐỌC:
1. Gói kiến thức CSDL A (34 bước chuẩn · 118 quy trình · 29 luật): %s
2. Ảnh BẢN MÀU: %s
3. Đề nghị đã đưa ra: %s
4. QUY TRÌNH THỰC TẾ (do chủ nhân nhập, nguyên văn): %s
5. Thông tin ca: mã màu %s | hệ sơn %s | loại nền %s | yêu cầu %s
   (Trong JSON: trường "ma_mau" phải là mã màu thật của bản màu; nếu ca không ghi mã màu thì để chuỗi "không ghi" —
    TUYỆT ĐỐI không dùng tên thư mục hay mã ca làm mã màu.)

VIỆC CẦN LÀM (ghi DẦN ra file):
1. BẢNG ĐỐI CHIẾU theo từng công đoạn: ĐỀ NGHỊ | THỰC TẾ | loại khác biệt (giống / THÊM bước / THIẾU bước /
   khác thứ tự / khác chi tiết) | nhận xét vì sao khác.
2. ĐỌC LẠI ẢNH để xác nhận thực tế có khớp với quy trình thực tế không — tách rõ "thấy trên ảnh" và "suy đoán".
3. BÀI HỌC / LUẬT MỚI rút ra để lần sau đề nghị sát hơn: mỗi luật gồm nhóm, quy tắc (câu áp dụng được),
   phạm vi áp dụng, và nguyên văn (trích lời chủ nhân nếu có).
4. ĐỀ XUẤT GHI VÀO CSDL A:
   (a) MỘT quy trình mới: tên, hệ sơn, mã màu, loại nền, mô tả, số bước, nguồn;
   (b) danh sách bước của quy trình mới: thứ tự | nội dung nguyên văn (rút gọn) | số bước chuẩn (1–34) | ghi chú;
   (c) danh sách luật mới;
   (d) cảnh báo nếu có XUNG ĐỘT với 29 luật hiện có (nêu rõ luật nào).
   Nếu thực tế giống hệt đề nghị và không có gì mới để học thì ghi "KHÔNG CÓ GÌ MỚI" và de_nghi_ghi = false.

BẮT BUỘC:
- ẨN DANH: KHÔNG ghi tên khách hàng, KHÔNG ghi tên kỹ thuật viên.
- Ghi kết quả ra ĐÚNG file: %s
- Kết thúc file bằng khối ```json``` với đúng các khoá:
  {"khac_biet": [{"cong_doan":"...", "de_nghi":"...", "thuc_te":"...", "loai":"..."}],
   "quy_trinh_moi": {"ten":"...", "he_son":"...", "ma_mau":"...", "loai_nen":"...", "mo_ta":"...", "so_buoc":<số>},
   "buoc": [{"thu_tu":1, "buoc_nguyen_van":"...", "buoc_chuan_so":<số>, "ghi_chu":"..."}],
   "luat_moi": [{"nhom":"...", "quy_tac":"...", "pham_vi":"...", "nguyen_van":"..."}],
   "xung_dot": ["..."], "de_nghi_ghi": true/false, "tom_tat": "1-3 câu"}
- Dòng cuối cùng của file in đúng chữ: HOAN TAT
""" % (KIEN_THUC, ca['anh_vao'] or '', os.path.join(d, 'DE_NGHI.md'), tt,
       ca['ma_mau'] or '', ca['he_son'] or '', ca['loai_nen'] or '', ca['yeu_cau'] or '',
       os.path.join(d, 'PHAN_TICH.md')))

    nhip('dang_chay', ca['id'], 'C')
    r, giay = goi_agy(yeu_cau)
    open(os.path.join(d, '_log_agy_phan_tich.txt'), 'w', encoding='utf-8').write(
        (r.stdout or '') + '\n--- STDERR ---\n' + (r.stderr or ''))
    f = os.path.join(d, 'PHAN_TICH.md')
    if not os.path.exists(f):
        return False, 'AGY không tạo được PHAN_TICH.md (xem _log_agy_phan_tich.txt)', giay, {}
    md = open(f, encoding='utf-8').read()
    j = lay_json(md)
    con = noi_db()
    con.execute('''UPDATE huan_luyen_ca SET trang_thai='cho_xac_nhan', phan_tich_luc=?, phan_tich_file=?,
                   de_xuat_json=?, loi=NULL WHERE id=?''',
                (time.strftime('%Y-%m-%d %H:%M:%S'), f, json.dumps(j, ensure_ascii=False), ca['id']))
    con.commit()
    con.close()
    if j:
        bao_mail(ca['id'], 'c')
    return True, '', giay, j


# ---------------------------------------------------------------- GHI CSDL A (sau pass)
def ghi_vao_csdla(ca_id):
    con = noi_db()
    c = con.cursor()
    r = c.execute('SELECT * FROM huan_luyen_ca WHERE id=?', (ca_id,)).fetchone()
    if not r:
        con.close()
        return 0, 0, None, ['không thấy ca #%s' % ca_id]
    cot = [x[1] for x in c.execute('PRAGMA table_info(huan_luyen_ca)')]
    ca = dict(zip(cot, r))
    if ca['da_bo_sung']:
        con.close()
        return 0, 0, ca['qt_moi_id'], ['ca #%s đã bổ sung rồi' % ca_id]
    try:
        dx = json.loads(ca['de_xuat_json'] or '{}')
    except Exception:
        dx = {}
    if not dx:
        con.close()
        return 0, 0, None, ['ca #%s chưa có đề xuất (chạy bước phân tích trước)' % ca_id]

    # backup trước khi ghi
    bdir = os.path.join(GOC, '_backup')
    os.makedirs(bdir, exist_ok=True)
    bak = os.path.join(bdir, 'CSDLA_bak_%s_truoc_bo_sung_HLQT_C%04d.db' % (time.strftime('%Y%m%d_%H%M'), ca_id))
    con.commit()
    con.close()
    shutil.copy2(DB, bak)

    con = noi_db()
    c = con.cursor()
    qt = dx.get('quy_trinh_moi') or {}
    buoc = dx.get('buoc') or []
    luat = dx.get('luat_moi') or []
    canh_bao = []
    if str(dx.get('de_nghi_ghi', True)).lower() in ('false', '0', 'không', 'no'):
        canh_bao.append('AGY kết luận KHÔNG CÓ GÌ MỚI để ghi')
        con.close()
        return 0, 0, None, canh_bao

    # --- quy trình mới (ẩn danh)
    ten, bo = an_danh(str(qt.get('ten') or ('Quy trình từ ca HLQT #%d' % ca_id))[:300])
    mo_ta, bo2 = an_danh(str(qt.get('mo_ta') or '')[:4000])
    canh_bao += ['đã bỏ tên khách khỏi tên/mô tả: ' + ', '.join(sorted(set(bo + bo2)))] if (bo or bo2) else []
    # mã màu: KHÔNG nhận tên thư mục ca (C####_ddmm) — đó không phải mã màu
    ma_nhap = str(qt.get('ma_mau') or '').strip()
    if re.match(r'^C\d{4}_\d{4}$', ma_nhap) or ma_nhap == os.path.basename(thu_muc_ca(ca)):
        canh_bao.append('AGY trả mã màu là tên thư mục ca ("%s") — đã bỏ, dùng mã màu của ca' % ma_nhap)
        ma_nhap = ''
    ma_mau_ghi = (ma_nhap or (ca['ma_mau'] or '').strip() or 'không ghi')[:120]
    cur = c.execute('''INSERT INTO quy_trinh (ten,loai,khach_hang,nha_may_id,he_son,ma_mau,so_buoc,mo_ta,nguon,ngay_tai_lieu,ghi_chu)
                       VALUES (?,?,?,?,?,?,?,?,?,?,?)''',
                    (ten, 'thực tế (HLQT)', None, None, str(qt.get('he_son') or '')[:60], ma_mau_ghi,
                     len(buoc) or int(qt.get('so_buoc') or 0), mo_ta,
                     'HLQT ca #%d — ảnh bản màu + quy trình thực tế do chủ nhân nhập' % ca_id,
                     time.strftime('%Y-%m-%d'),
                     'loại nền: %s | yêu cầu: %s | đề nghị trước đó: QT id %s' % (qt.get('loai_nen') or ca['loai_nen'] or '—',
                                                                                  ca['yeu_cau'] or '—', ca['de_nghi_qt_id'] or '—')))
    qt_id = cur.lastrowid
    n_buoc = 0
    for i, b in enumerate(buoc, 1):
        nd, _ = an_danh(str(b.get('buoc_nguyen_van') or b.get('chi_tiet') or '')[:2000])
        c.execute('''INSERT INTO buoc_quy_trinh (quy_trinh_id,thu_tu,buoc_nguyen_van,buoc_chuan_so,ghi_chu)
                     VALUES (?,?,?,?,?)''',
                  (qt_id, int(b.get('thu_tu') or i), nd, b.get('buoc_chuan_so'), str(b.get('ghi_chu') or '')[:300]))
        n_buoc += 1

    # --- luật mới
    n_luat = 0
    for l in luat:
        qtac, _ = an_danh(str(l.get('quy_tac') or '')[:2000])
        pvi = str(l.get('pham_vi') or '')[:300]
        nvan = str(l.get('nguyen_van') or '')[:1000]
        if not qtac:
            continue
        c.execute('''INSERT INTO huan_luyen (ngay,nhom,nguyen_van,quy_tac,pham_vi,trang_thai,nguon,ghi_chu)
                     VALUES (?,?,?,?,?,?,?,?)''',
                  (time.strftime('%Y-%m-%d'), str(l.get('nhom') or 'HLQT')[:120], nvan, qtac, pvi,
                   'ĐANG ÁP DỤNG', 'HLQT ca #%d — đối chiếu đề nghị với quy trình thực tế' % ca_id,
                   'Bổ sung tự động sau khi chủ nhân xác nhận (pass) · bản đồi chiếu: %s' % os.path.basename(ca['phan_tich_file'] or '')))
        n_luat += 1

    c.execute('''UPDATE huan_luyen_ca SET da_bo_sung=1, ngay_bo_sung=?, qt_moi_id=?, so_luat_them=?,
                 trang_thai='da_bo_sung' WHERE id=?''',
              (time.strftime('%Y-%m-%d %H:%M:%S'), qt_id, n_luat, ca_id))
    c.execute('INSERT INTO nhat_ky_hlqt (ngay,viec,chi_tiet,nguon) VALUES (?,?,?,?)',
              (time.strftime('%Y-%m-%d %H:%M'), 'Bổ sung CSDL A từ ca HLQT #%d' % ca_id,
               'Thêm quy_trinh id %s (%d bước) + %d luật huấn luyện. Backup trước khi ghi: %s'
               % (qt_id, n_buoc, n_luat, os.path.basename(bak)), 'chủ nhân xác nhận bằng mật khẩu qua app HLQT'))
    con.commit()
    con.close()

    # báo cho người dùng: đã ghi vào CSDL A
    try:
        bao_mail(ca_id, 'ghi')
    except Exception as e:
        canh_bao.append('chưa gửi được thông báo: %s' % e)

    # dựng lại gói kiến thức (HLQT + Agent-Lab) để lần sau đề nghị sát hơn
    try:
        subprocess.run([sys.executable, os.path.join(APP, 'goi_kien_thuc_hlqt.py')], capture_output=True, timeout=300)
        subprocess.run([sys.executable, r'C:\Claw 2\Nghien cuu\Agent-Lab\dung_goi_kien_thuc.py'], capture_output=True, timeout=300)
    except Exception as e:
        canh_bao.append('chưa dựng lại được gói kiến thức: %s' % e)
    return n_buoc, n_luat, qt_id, canh_bao


# ---------------------------------------------------------------- VÒNG LẶP
def mot_lan():
    con = noi_db()
    c = con.cursor()
    cot = [x[1] for x in c.execute('PRAGMA table_info(huan_luyen_ca)')]
    r = c.execute("SELECT * FROM huan_luyen_ca WHERE trang_thai='cho_de_nghi' AND COALESCE(anh_vao,'')<>'' "
                  "AND COALESCE(de_nghi_luc,'')='' ORDER BY id LIMIT 1").fetchone()
    if r:
        ca = dict(zip(cot, r))
        con.close()
        print('→ ca #%s | %s | BƯỚC P (đề nghị quy trình)' % (ca['id'], ca['ma_mau'] or '—'), flush=True)
        ok, loi, giay, j = buoc_p(ca)
        print('   %s (%.0fs) %s' % ('xong' if ok else 'LỖI', giay, j.get('qt_ten') or loi), flush=True)
        if not ok:
            con = noi_db(); con.execute("UPDATE huan_luyen_ca SET trang_thai='loi', loi=? WHERE id=?", (loi, ca['id'])); con.commit(); con.close()
        nhip('ranh')
        return True
    r = c.execute("SELECT * FROM huan_luyen_ca WHERE trang_thai='cho_phan_tich' AND COALESCE(thuc_te_text,'')<>'' "
                  "ORDER BY id LIMIT 1").fetchone()
    if r:
        ca = dict(zip(cot, r))
        con.close()
        print('→ ca #%s | %s | BƯỚC C (đối chiếu & đề xuất bổ sung)' % (ca['id'], ca['ma_mau'] or '—'), flush=True)
        ok, loi, giay, j = buoc_c(ca)
        print('   %s (%.0fs) · khác biệt %s · luật mới %s' % ('xong' if ok else 'LỖI', giay,
              len((j.get('khac_biet') or [])), len((j.get('luat_moi') or [])), ), flush=True)
        if not ok:
            con = noi_db(); con.execute("UPDATE huan_luyen_ca SET trang_thai='loi', loi=? WHERE id=?", (loi, ca['id'])); con.commit(); con.close()
        nhip('ranh')
        return True
    con.close()
    return False


def main():
    mot = '--mot-lan' in sys.argv
    print('WORKER HLQT bắt đầu %s | model %s%s' % (time.strftime('%H:%M:%S'), MODEL, ' (1 lượt)' if mot else ''), flush=True)
    nhip('ranh')
    if mot:
        con = noi_db()
        con.execute("UPDATE huan_luyen_ca SET trang_thai='cho_phan_tich' WHERE trang_thai='da_de_nghi' AND COALESCE(thuc_te_text,'')<>''")
        con.commit(); con.close()
        mot_lan()
        return
    while True:
        try:
            if not mot_lan():
                nhip('ranh')
                time.sleep(5)
        except Exception as e:
            print('   lỗi vòng lặp:', type(e).__name__, e, flush=True)
            nhip('loi')
            time.sleep(10)


if __name__ == '__main__':
    main()
