# -*- coding: utf-8 -*-
"""WORKER HUẤN LUYỆN/HIỆU CHỈNH MÀU (CSDL B) — 2 bước AGY.

  Bước P (dự đoán): hỗn hợp (các mã màu CSDL B + tỉ lệ %) → AGY model mạnh dự đoán màu hỗn hợp
  Bước C (đối chiếu): dự đoán + ẢNH THỰC TẾ → AGY đọc màu thật, tính ΔE, đề xuất hiệu chỉnh
                      hệ số cường độ từng màu → ghi vào `hieu_chinh_mau` + cập nhật `mau`.

Chạy: python worker_calib.py            (vòng lặp, quét 20 giây)
      python worker_calib.py --mot-lan  (xử lý 1 lượt rồi thoát)
"""
import json, os, re, shutil, sqlite3, subprocess, sys, time, traceback

GOC = r'C:\Claw 2\Nghien cuu\Du-an-B-He-mau-Stain-Glaze\calib-app'
DB = r'C:\Claw 2\Nghien cuu\Du-an-B-He-mau-Stain-Glaze\CSDLB.db'
VIEC = os.path.join(GOC, 'viec')
AGY = r'C:\Users\admin\AppData\Local\agy\bin\agy.exe'
PROJECT = '2f54d0c6-f33f-4dae-b5b7-840339496ec7'
MODEL = 'gemini-3.1-pro-high'
PY = sys.executable
URL_PUBLIC = 'https://desktop-9gsdkd2.tail0ffe94.ts.net/calib/'
EMAIL_BAO = os.path.join(GOC, 'email_bao.txt')          # email nhận thông báo mặc định của app
MAIL_SCRIPT = os.path.join(os.path.expandvars(r'%LOCALAPPDATA%'), 'hermes', 'skills', 'email',
                           'gui-email-kem-file', 'scripts', 'gui_mail_kem_file.py')
os.makedirs(VIEC, exist_ok=True)


# ---------------------------------------------------------------- BÁO CHO NGƯỜI DÙNG
def email_nhan(v):
    e = (v.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 _tp_txt(v):
    tp = json.loads(v.get('thanh_phan') or '[]')
    return ' + '.join('%s %s%%' % (x.get('ma', ''), x.get('ti_le')) for x in tp) or '(không rõ)'


def soan_mail(v, loai, de_xuat=None):
    """Trả (tiêu đề, thân thư, [tệp đính kèm]) cho mốc 'p' / 'c' / 'ghi'."""
    hid = v['id']
    link = '%shon-hop?id=%s' % (URL_PUBLIC, hid)
    tp = _tp_txt(v)
    if loai == 'p':
        g = json.loads(v.get('du_doan_ghi_chu') or '{}')
        td = '[CALIB] Đã có DỰ ĐOÁN màu — hỗn hợp #%s (%s)' % (hid, tp)
        tt = ('Đã xong bước DỰ ĐOÁN màu cho hỗn hợp bạn nhập.\n\n'
              'Hỗn hợp      : #%s — %s (tổng %s%% trong base)\n'
              'Màu dự đoán  : %s   (L* %s / a* %s / b* %s)\n'
              'Phương pháp  : %s\nĐộ tin cậy   : %s · Sai số dự kiến: ΔE ≈ %s\n'
              'Thời gian    : %s giây\n\n'
              '>> BƯỚC TIẾP: chụp ẢNH MÀU HỖN HỢP THỰC TẾ (đúng điều kiện chuẩn: cùng thước kéo với mẫu · '
              'cùng loại base · giấy trắng cùng loại) rồi tải lên ở trang chi tiết để agent đối chiếu: %s\n'
              % (hid, tp, v.get('tong_ti_le'), v.get('du_doan_hex') or '—', v.get('du_doan_L'), v.get('du_doan_a'),
                 v.get('du_doan_b'), (g.get('phuong_phap') or '—')[:160], g.get('do_tin_cay') or '—',
                 g.get('sai_so_du_kien_deltaE') or '—', int(v.get('du_doan_giay') or 0), link))
        kem = [v.get('du_doan_file')]
    elif loai == 'c':
        de_xuat = de_xuat or []
        dong = '\n'.join('  - %s: hệ số %s → %s%s' % (
            x[2], x[3], x[4], ('  (lý do: %s)' % str(x[5])[:120]) if x[5] else '') for x in de_xuat) or '  (không có đề xuất)'
        td = '[CALIB] Đã ĐỐI CHIẾU xong — hỗn hợp #%s (ΔE %s · đề xuất hiệu chỉnh %d màu)' % (
            hid, v.get('delta_E'), len(de_xuat))
        tt = ('Đã xong bước ĐỐI CHIẾU ảnh thực tế với màu dự đoán.\n\n'
              'Hỗn hợp        : #%s — %s\nDự đoán        : %s\nMàu thực tế    : %s (L* %s / a* %s / b* %s)\n'
              'ΔE (sai lệch)  : %s\nThời gian      : %s giây\n\n'
              'ĐỀ XUẤT HIỆU CHỈNH HỆ SỐ CƯỜNG ĐỘ (chưa ghi — chờ mật khẩu):\n%s\n\n'
              'KẾT LUẬN: %s\n\n'
              '>> Mở trang chi tiết, nhập MẬT KHẨU XÁC NHẬN để ghi các hệ số này vào CSDL B: %s\n'
              '   (Chưa nhập mật khẩu thì CSDL B giữ nguyên — agent chỉ đề xuất.)\n'
              % (hid, tp, v.get('du_doan_hex') or '—', v.get('thuc_te_hex') or '—', v.get('thuc_te_L'),
                 v.get('thuc_te_a'), v.get('thuc_te_b'), v.get('delta_E'), int(v.get('doi_chieu_giay') or 0),
                 dong, (v.get('ket_luan') or '—')[:700], link))
        kem = [v.get('doi_chieu_file')]
    else:
        de_xuat = de_xuat or []
        dong = '\n'.join('  - %s: hệ số %s → %s' % (x[2], x[3], x[4]) for x in de_xuat) or '  (không có)'
        td = '[CALIB] ĐÃ GHI hệ số vào CSDL B — hỗn hợp #%s (%d màu)' % (hid, len(de_xuat))
        tt = ('Đã ghi thông số hiệu chỉnh vào CSDL B (bảng mau) sau khi bạn nhập mật khẩu xác nhận.\n\n'
              'Hỗn hợp    : #%s — %s\nΔE trước HC: %s\nThời điểm  : %s\n\n'
              'CÁC MÀU ĐÃ CẬP NHẬT HỆ SỐ:\n%s\n\n'
              'Điều kiện áp dụng: cùng thước kéo với màu mẫu · cùng loại base · giấy trắng cùng loại.\n'
              'Hệ số >1 = màu mạnh hơn dự kiến (dùng ít hơn) · <1 = yếu hơn (dùng nhiều hơn).\n\n'
              '>> Xem lại: %s · Lịch sử hiệu chỉnh: %shieu-chinh\n'
              % (hid, tp, v.get('delta_E') or '—', time.strftime('%H:%M %d/%m'), dong, link, URL_PUBLIC))
        kem = [v.get('doi_chieu_file')]
    return td, tt, [k for k in kem if k and os.path.exists(k)]


def bao_mail(con, hid, loai, de_xuat=None):
    """Gửi email báo cho người dùng ở mốc 'p' (dự đoán) / 'c' (đối chiếu) / 'ghi' (đã ghi CSDL B)."""
    con.row_factory = sqlite3.Row
    r = con.execute('SELECT * FROM hon_hop WHERE id=?', (hid,)).fetchone()
    if not r:
        return False, 'không thấy hỗn hợp #%s' % hid
    v = dict(r)
    cot = {'p': 'bao_p', 'c': 'bao_c', 'ghi': 'bao_ghi'}.get(loai, 'bao_p')
    to = email_nhan(v)
    if not to:
        tt = 'chưa có email nhận thông báo (điền ở ô tạo hỗn hợp hoặc file calib-app\\email_bao.txt)'
        con.execute('UPDATE hon_hop SET %s=? WHERE id=?' % cot, (tt, hid))
        con.commit()
        print('   BÁO: %s' % tt)
        return False, tt
    if not os.path.exists(MAIL_SCRIPT):
        tt = 'thiếu script gửi mail: %s' % MAIL_SCRIPT
        con.execute('UPDATE hon_hop SET %s=? WHERE id=?' % cot, (tt, hid))
        con.commit()
        return False, tt
    tieu_de, than, kem = soan_mail(v, loai, de_xuat)
    argv = [PY, 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=GOC, timeout=300)
    ra = ((p.stdout or '') + (p.stderr or '')).strip()
    d = v.get('thu_muc') or VIEC
    try:
        os.makedirs(d, exist_ok=True)
        open(os.path.join(d, '_log_mail_%s.txt' % loai), 'w', encoding='utf-8').write(ra[:6000])
    except Exception:
        pass
    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.execute('UPDATE hon_hop SET %s=? WHERE id=?' % cot, (tt, hid))
    con.execute('INSERT INTO nhat_ky (ngay,viec,chi_tiet,nguon) VALUES (?,?,?,?)',
                (time.strftime('%Y-%m-%d %H:%M'), 'Báo cho người dùng — hỗn hợp #%s mốc %s' % (hid, loai),
                 'Tới %s · %s · tiêu đề: %s' % (to, tt, tieu_de[:150]), 'worker CALIB'))
    con.commit()
    print('   BÁO (%s): %s' % (loai, tt))
    return ok, tt


def gui_lai_bao(hid):
    """Nút '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()
    con.row_factory = sqlite3.Row
    r = con.execute('SELECT * FROM hon_hop WHERE id=?', (hid,)).fetchone()
    if not r:
        con.close()
        return False, 'không thấy hỗn hợp'
    v = dict(r)
    de_xuat = con.execute('SELECT id,mau_id,mau_ten,gia_tri_cu,gia_tri_moi,ly_do,da_ap_dung,lab_moi,delta_E_truoc '
                          'FROM hieu_chinh_mau WHERE hon_hop_id=? ORDER BY id', (hid,)).fetchall()
    con.close()
    loai = 'ghi' if v.get('trang_thai') == 'da_hieu_chinh' else ('c' if v.get('thuc_te_hex') else 'p')
    de = [tuple(x) for x in de_xuat] if loai != 'p' else None
    con = noi_db()
    ok, tt = bao_mail(con, hid, loai, de)
    con.close()
    return ok, tt


NHP = 1


def noi_db():
    con = sqlite3.connect(DB, timeout=30)
    con.execute('''CREATE TABLE IF NOT EXISTS worker_nhip (
        id INTEGER PRIMARY KEY, dang_xu_ly TEXT, buoc TEXT, bat_dau TEXT, luc TEXT)''')
    con.commit()
    return con


def nhip(con, dang_xu_ly, buoc):
    con.execute('INSERT INTO worker_nhip (id,dang_xu_ly,buoc,bat_dau,luc) VALUES (1,?,?,?,?) '
                'ON CONFLICT(id) DO UPDATE SET dang_xu_ly=excluded.dang_xu_ly, buoc=excluded.buoc, '
                'bat_dau=excluded.bat_dau, luc=excluded.luc',
                (dang_xu_ly, buoc, time.strftime('%Y-%m-%d %H:%M:%S'), time.strftime('%Y-%m-%d %H:%M:%S')))


def doc_anh_ds():
    """Bảng thông số màu để AGY dùng khi dự đoán."""
    con = noi_db()
    c = con.cursor()
    ra = {}
    for r in c.execute('SELECT id,ten,ma,phan_loai,he_so_cuong_do,lab_hieu_chinh,so_lan_hieu_chinh,danh_gia FROM mau').fetchall():
        mau_id, ten, ma, pl, hs, lab_hc, so_lan, dg = r
        o = c.execute('SELECT nong_do,hex,L_star,a_star,b_star,ghi_chu FROM mau_bang_in WHERE mau_id=? ORDER BY id', (mau_id,)).fetchall()
        ra[ma] = {'id': mau_id, 'ten': ten, 'ma': ma, 'phan_loai': pl,
                  'he_so_cuong_do': hs if hs is not None else 1.0,
                  'lab_hieu_chinh': lab_hc or '', 'so_lan_hieu_chinh': so_lan or 0,
                  'danh_gia': dg or '', 'o_do': [dict(zip(('nong_do', 'hex', 'L', 'a', 'b', 'ghi_chu'), x)) for x in o]}
    con.close()
    return ra


def chay_agy(d, prompt, out_file, ten_log):
    cmd = [AGY, '-p', prompt, '--model', MODEL, '--effort', 'high', '--project', PROJECT,
           '--add-dir', d, '--dangerously-skip-permissions', '--output-format', 'json', '--print-timeout', '0']
    t0 = time.time()
    p = subprocess.run(cmd, capture_output=True, text=True, encoding='utf-8', errors='replace', cwd=d)
    dt = time.time() - t0
    try:
        j = json.loads(p.stdout or '{}')
        tt = json.dumps(j.get('usage'), ensure_ascii=False)
    except Exception:
        j, tt = {}, '(không parse json)'
    open(os.path.join(d, ten_log), 'w', encoding='utf-8').write(
        '=== %.1f s | rc=%s | model=%s\nusage=%s\n--- stdout (đầu) ---\n%s\n--- stderr (cuối) ---\n%s'
        % (dt, p.returncode, MODEL, tt, (p.stdout or '')[:2000], (p.stderr or '')[-1500:]))
    return dt, os.path.exists(out_file), open(out_file, encoding='utf-8').read() if os.path.exists(out_file) else ''


def json_trong(md, khoa):
    """Lấy khối JSON đầu tiên có khoá cho trước trong markdown."""
    for m in re.finditer(r'```(?:json)?\s*(\{.*?\})\s*```', md, re.S):
        try:
            o = json.loads(m.group(1))
            if khoa in o:
                return o
        except Exception:
            continue
    return None


# ---------------- BƯỚC P: DỰ ĐOÁN ----------------
def prompt_du_doan(v, ds):
    tp = json.loads(v['thanh_phan'])
    dong = []
    for x in tp:
        m = ds.get(x['ma'])
        if not m:
            continue
        o = m['o_do']
        od = '; '.join('%s: %s (L*a*b* %s/%s/%s)' % (y['nong_do'], y['hex'], y['L'], y['a'], y['b']) for y in o) or 'chưa có số đo'
        dong.append('- **%s** (mã %s, %s) — **%s%% màu trong base** · hệ số cường độ hiện tại: %s · số lần hiệu chỉnh: %s\n'
                    '    · Số đo các mức nồng độ: %s\n    · Nhận xét tài liệu: %s\n    · Hiệu chỉnh đã áp: %s'
                    % (m['ten'], m['ma'], m['phan_loai'], x['ti_le'], m['he_so_cuong_do'], m['so_lan_hieu_chinh'], od, m['danh_gia'][:200], m['lab_hieu_chinh'] or 'chưa có'))
    return ('Bạn là chuyên gia màu sắc sơn. Đây là một HỖN HỢP MÀU do phòng Lab pha từ các màu có trong CSDL nội bộ.\n\n'
            'THÀNH PHẦN: các màu pha trong base — mỗi dòng là **%% màu pha trong base** (KHÔNG cần cộng = 100%%; '
            'ví dụ: 4 nghĩa là 4%% màu đó trong base). Tổng %s%% chỉ là con số cộng lại để tham chiếu.\n%s\n\n'
            'ĐIỀU KIỆN CHUẨN (bắt buộc, đã cố định): mẫu được kéo bằng CÙNG thước kéo với màu mẫu, pha trong CÙNG loại base với mẫu, trên GIẤY TRẮNG cùng loại. \n\n'
            'Ghi chú của người pha: %s\n\n'
            'NHIỆM VỤ: **dự đoán màu của hỗn hợp** này.\n'
            '1. Trình bày cách bạn tính (mô hình: trộn tuyến tính theo không gian màu nào? Kubelka-Munk? hay trung bình có trọng số?) — nêu rõ giả định.\n'
            '2. Dùng số đo L*a*b* thực có của từng màu (ở mức nồng độ gần nhất với tỉ lệ trong hỗn hợp) — KHÔNG bịa số mới.\n'
            '3. Trình bày kết quả dự đoán: hex, L*, a*, b*, và khoảng tin cậy (ví dụ ±ΔE bao nhiêu).\n'
            '4. Nêu 3–5 yếu tố có thể làm màu thực lệch khỏi dự đoán (tương tác hoá học giữa các dye, độ mờ/độ phủ, nền giấy, sai số đọc màu từ ảnh…).\n\n'
            'YÊU CẦU ĐỊNH DẠNG: viết tệp **`%s`** (đường dẫn TUYỆT ĐỐI này, tiếng Việt, gọn) và **kết thúc bằng một khối ```json** đúng dạng:\n'
            '```json\n{"du_doan": {"hex": "#RRGGBB", "L": 0.0, "a": 0.0, "b": 0.0}, "phuong_phap": "…", "do_tin_cay": "cao/trung binh/thap", "sai_so_du_kien_deltaE": 0.0}\n```\n'
            'Kết thúc tệp bằng dòng HOAN TAT.' % (v['tong_ti_le'], '\n'.join(dong), v['ghi_chu'] or '—', os.path.join(v['thu_muc'], 'DU_DOAN.md')))


# ---------------- BƯỚC C: ĐỐI CHIẾU ----------------
def prompt_doi_chieu(v, ds, du_doan):
    tp = json.loads(v['thanh_phan'])
    dong = []
    for x in tp:
        m = ds.get(x['ma']) or {}
        dong.append('- **%s** (mã %s) · tỉ lệ %s%% · hệ số cường độ hiện tại: %s'
                    % (m.get('ten', x['ma']), x['ma'], x['ti_le'], m.get('he_so_cuong_do', 1.0)))
    return ('Bạn là chuyên gia màu sắc sơn. Có MỘT HỖN HỢP ĐÃ PHA THỰC TẾ và ẢNH MÀU THỰC TẾ của nó.\n\n'
            'ĐIỀU KIỆN CHUẨN (bắt buộc, đã cố định): mẫu được kéo bằng CÙNG thước kéo với màu mẫu, pha trong CÙNG loại base với mẫu, trên GIẤY TRẮNG cùng loại. \n\n'
            'THÀNH PHẦN (mỗi dòng là %% màu pha trong base; tổng %s%%):\n%s\n\n'
            'DỰ ĐOÁN TRƯỚC ĐÓ CỦA BẠN: hex %s · L* %s · a* %s · b* %s (phương pháp: %s)\n\n'
            'ẢNH THỰC TẾ: `anh/%s` (ảnh màu hỗn hợp do chủ nhân chụp). Hãy ĐỌC MÀU từ ảnh bằng thị giác của bạn: '
            'lấy vùng màu thực của màng/hỗn hợp, tránh nền giấy trắng, tránh vùng lóe sáng; nếu ảnh có nhiều vệt thì lấy trung bình và nói rõ.\n\n'
            'NHIỆM VỤ:\n'
            '1. Đọc màu thực tế từ ảnh: hex, L*, a*, b* + mức tin cậy + mô tả (độ trong/đục, bóng, có hạt…).\n'
            '2. Tính **ΔE** giữa dự đoán và thực tế (nêu công thức bạn dùng: CIE76 hay CIEDE2000) — trình bày cả số của từng thành phần màu để kiểm chứng được.\n'
            '3. Giải thích nguyên nhân lệch (nếu có): màu nào trong hỗn hợp đang mạnh/yếu hơn giả định, ảnh hưởng của độ phủ/độ mờ, sai số do ảnh…\n'
            '4. **ĐỀ XUẤT HIỆU CHỈNH CSDL**: với TỪNG màu trong hỗn hợp, đề xuất **hệ số cường độ mới** (số thực, 1.0 = giữ nguyên; >1 = màu này mạnh hơn giả định nên giảm tỉ lệ dùng, <1 = yếu hơn nên tăng tỉ lệ) + lý do ngắn. '
            'Nếu ΔE nhỏ (< 2) thì nói rõ KHÔNG cần hiệu chỉnh.\n\n'
            'YÊU CẦU ĐỊNH DẠNG: viết tệp **`%s`** (đường dẫn TUYỆT ĐỐI này, tiếng Việt, gọn) và **kết thúc bằng một khối ```json** đúng dạng:\n'
            '```json\n{"thuc_te": {"hex": "#RRGGBB", "L": 0.0, "a": 0.0, "b": 0.0}, "tin_cay": "cao/trung binh/thap",\n'
            ' "delta_E": 0.0, "cong_thuc_delta_E": "CIEDE2000", "ket_luan": "…",\n'
            ' "hieu_chinh": [{"ma": "MA_MÀU", "ti_le": 60.0, "he_so_cu": 1.0, "he_so_moi": 1.12,\n'
            '                 "lab_moi": {"L": 0.0, "a": 0.0, "b": 0.0, "hex": "#RRGGBB"}, "ly_do": "…"}]}\n```\n'
            'Trong đó `he_so_moi` = hệ số cường độ MỚI của màu đó (1.0 = giữ nguyên; >1 = màu mạnh hơn giả định ⇒ lần sau dùng ít lại; '
            '<1 = yếu hơn ⇒ dùng nhiều hơn), và `lab_moi` = màu của CHÍNH màu đó sau khi hiệu chỉnh (để lưu vào CSDL B).\n'
            'Kết thúc tệp bằng dòng HOAN TAT.' % (v['tong_ti_le'], '\n'.join(dong),
                                                 du_doan.get('hex', ''), du_doan.get('L', ''), du_doan.get('a', ''), du_doan.get('b', ''),
                                                 du_doan.get('phuong_phap', '')[:120], os.path.basename(v['anh_thuc_te'] or ''), os.path.join(v['thu_muc'], 'DOI_CHIEU.md')))


def xu_ly_buoc_p(v, ds):
    d = v['thu_muc']
    p = os.path.join(v['thu_muc'], 'DU_DOAN.md')
    t, ok, md = chay_agy(d, prompt_du_doan(v, ds), p, '_log_du_doan.txt')
    if not ok:
        return None, t, 'AGY không tạo được DU_DOAN.md'
    j = json_trong(md, 'du_doan')
    if not j:
        return None, t, 'không đọc được khối JSON dự đoán'
    return j, t, ''


def xu_ly_buoc_c(v, ds, du_doan):
    d = v['thu_muc']
    p = os.path.join(v['thu_muc'], 'DOI_CHIEU.md')
    t, ok, md = chay_agy(d, prompt_doi_chieu(v, ds, du_doan), p, '_log_doi_chieu.txt')
    if not ok:
        return None, t, 'AGY không tạo được DOI_CHIEU.md'
    j = json_trong(md, 'thuc_te')
    if not j:
        return None, t, 'không đọc được khối JSON đối chiếu'
    return j, t, ''


def ap_dung(con, hid):
    """GHI hiệu chỉnh của hỗn hợp `hid` vào CSDL B — CHỈ gọi sau khi đã xác nhận mật khẩu."""
    c = con.cursor()
    if not c.execute('SELECT 1 FROM hon_hop WHERE id=?', (hid,)).fetchone():
        return 0, ['không thấy hỗn hợp #%s' % hid]
    rows = c.execute('SELECT id,mau_id,mau_ten,gia_tri_cu,gia_tri_moi,lab_moi,ly_do,delta_E_truoc FROM hieu_chinh_mau '
                     'WHERE hon_hop_id=? AND da_ap_dung=0 ORDER BY id', (hid,)).fetchall()
    if not rows:
        return 0, ['không có đề xuất nào đang chờ xác nhận']
    mo = []
    for hcid, mid, ten, cu, moi, lab_moi, ly_do, de in rows:
        lab_txt = ''
        if lab_moi:
            try:
                o = json.loads(lab_moi)
                lab_txt = 'L* %s / a* %s / b* %s%s' % (o.get('L'), o.get('a'), o.get('b'),
                                                      (' · ' + o['hex']) if o.get('hex') else '')
            except Exception:
                lab_txt = str(lab_moi)[:150]
        c.execute('''UPDATE mau SET he_so_cuong_do=?, so_lan_hieu_chinh=COALESCE(so_lan_hieu_chinh,0)+1,
                     ngay_hieu_chinh=?, lab_hieu_chinh=?, ghi_chu=COALESCE(ghi_chu,'')||? WHERE id=?''',
                  (float(moi), time.strftime('%Y-%m-%d %H:%M'), lab_txt or None,
                   ' | HC %s (hỗn hợp #%s): hệ số %s→%s%s' % (time.strftime('%d/%m'), hid, cu, moi,
                                                              ('; màu sau HC: ' + lab_txt) if lab_txt else ''),
                   mid))
        c.execute('UPDATE hieu_chinh_mau SET da_ap_dung=1, ngay=? WHERE id=?', (time.strftime('%Y-%m-%d %H:%M'), hcid))
        mo.append('%s: hệ số %s→%s' % (ten, cu, moi))
    c.execute("UPDATE hon_hop SET trang_thai='da_hieu_chinh' WHERE id=?", (hid,))
    c.execute("INSERT INTO nhat_ky (ngay,viec,chi_tiet,nguon) VALUES (?,?,?,?)",
              (time.strftime('%Y-%m-%d %H:%M'), 'Áp dụng hiệu chỉnh màu từ hỗn hợp #%s' % hid,
               'Đã ghi thông số hiệu chỉnh vào CSDL B (bảng mau): ' + '; '.join(mo) +
               '. Điều kiện: cùng thước kéo · cùng base · giấy trắng cùng loại. Xác nhận bằng mật khẩu hiệu chỉnh.',
               'chủ nhân xác nhận qua app CALIB'))
    con.commit()
    # báo cho người dùng: đã ghi vào CSDL B
    try:
        de = c.execute('SELECT id,mau_id,mau_ten,gia_tri_cu,gia_tri_moi,ly_do,da_ap_dung,lab_moi,delta_E_truoc '
                       'FROM hieu_chinh_mau WHERE hon_hop_id=? AND da_ap_dung=1 ORDER BY id', (hid,)).fetchall()
        bao_mail(con, hid, 'ghi', [tuple(x) for x in de])
    except Exception as e:
        mo.append('chưa gửi được thông báo: %s' % e)
    return len([x for x in mo if 'hệ số' in x]), mo


def main():
    mot_lan = '--mot-lan' in sys.argv
    print('WORKER CALIB bắt đầu %s | model %s%s' % (time.strftime('%H:%M:%S'), MODEL, ' (1 lượt)' if mot_lan else ''))
    while True:
        con = noi_db()
        con.row_factory = sqlite3.Row
        # ưu tiên việc có ảnh thực tế (chờ đối chiếu), rồi mới tới việc chờ dự đoán
        r = con.execute("SELECT * FROM hon_hop WHERE trang_thai='da_du_doan' AND anh_thuc_te IS NOT NULL AND anh_thuc_te<>'' ORDER BY id LIMIT 1").fetchone()
        buoc = 'C'
        if not r:
            r = con.execute("SELECT * FROM hon_hop WHERE trang_thai='cho_du_doan' ORDER BY id LIMIT 1").fetchone()
            buoc = 'P'
        if not r:
            if mot_lan:
                print('không có việc chờ')
                return
            time.sleep(20)
            continue
        v = dict(r)
        nhip(con, 'Hỗn hợp #%s' % v['id'], 'P (dự đoán)' if buoc == 'P' else 'C (đối chiếu)')
        con.commit()
        ds = doc_anh_ds()
        hv = json.loads(v['thanh_phan'] or '[]')
        print('→ hỗn hợp #%s | %d thành phần | bước %s' % (v['id'], len(hv), buoc))
        bao_loai = None
        try:
            if buoc == 'P':
                j, t, loi = xu_ly_buoc_p(v, ds)
                if loi:
                    con.execute("UPDATE hon_hop SET loi=? WHERE id=?", (loi, v['id']))
                else:
                    dd = j.get('du_doan', {})
                    con.execute('''UPDATE hon_hop SET trang_thai='da_du_doan', du_doan_hex=?, du_doan_L=?, du_doan_a=?, du_doan_b=?,
                                   du_doan_ghi_chu=?, du_doan_luc=?, du_doan_file=?, du_doan_giay=?, loi=NULL WHERE id=?''',
                                (dd.get('hex'), dd.get('L'), dd.get('a'), dd.get('b'),
                                 json.dumps({k: j.get(k) for k in ('phuong_phap', 'do_tin_cay', 'sai_so_du_kien_deltaE')}, ensure_ascii=False),
                                 time.strftime('%Y-%m-%d %H:%M:%S'), os.path.join(v['thu_muc'], 'DU_DOAN.md'), round(t, 1), v['id']))
                print('   bước P: %s (%.0fs)' % ('xong' if not loi else loi, t))
                if not loi:
                    bao_loai = 'p'
            else:
                j, t, loi = xu_ly_buoc_c(v, ds, json.loads(v['du_doan_ghi_chu'] or '{}') | {'hex': v['du_doan_hex'], 'L': v['du_doan_L'], 'a': v['du_doan_a'], 'b': v['du_doan_b']})
                if loi:
                    con.execute("UPDATE hon_hop SET loi=? WHERE id=?", (loi, v['id']))
                else:
                    tt = j.get('thuc_te', {})
                    con.execute('''UPDATE hon_hop SET trang_thai='cho_xac_nhan', thuc_te_hex=?, thuc_te_L=?, thuc_te_a=?, thuc_te_b=?,
                                   delta_E=?, ket_luan=?, doi_chieu_luc=?, doi_chieu_file=?, doi_chieu_giay=?, loi=NULL WHERE id=?''',
                                (tt.get('hex'), tt.get('L'), tt.get('a'), tt.get('b'), j.get('delta_E'),
                                 (j.get('ket_luan') or '')[:1500], time.strftime('%Y-%m-%d %H:%M:%S'),
                                 os.path.join(v['thu_muc'], 'DOI_CHIEU.md'), round(t, 1), v['id']))
                    # CHỈ ĐỀ XUẤT hiệu chỉnh (da_ap_dung=0) — chưa ghi vào `mau`; chờ nhập mật khẩu xác nhận
                    n_hc = 0
                    for hc in (j.get('hieu_chinh') or []):
                        ma = (hc.get('ma') or '').strip()
                        m = ds.get(ma)
                        if not m:
                            continue
                        hs_moi = hc.get('he_so_moi')
                        if hs_moi in (None, ''):
                            continue
                        lab_moi = hc.get('lab_moi')
                        con.execute('''INSERT INTO hieu_chinh_mau (ngay,mau_id,mau_ten,hon_hop_id,tham_so,gia_tri_cu,gia_tri_moi,
                                       ly_do,delta_E_truoc,nguon,da_ap_dung,lab_moi,nguon_anh)
                                       VALUES (?,?,?,?,?,?,?,?,?,?,0,?,?)''',
                                    (time.strftime('%Y-%m-%d %H:%M'), m['id'], m['ten'], v['id'], 'he_so_cuong_do',
                                     str(m['he_so_cuong_do']), str(hs_moi), (hc.get('ly_do') or '')[:600], j.get('delta_E'),
                                     'AGY đối chiếu hỗn hợp #%s (%s)' % (v['id'], os.path.basename(v['anh_thuc_te'] or '')),
                                     json.dumps(lab_moi, ensure_ascii=False) if lab_moi else None,
                                     v['anh_thuc_te']))
                        n_hc += 1
                    print('   bước C: ΔE=%s · ĐỀ XUẤT hiệu chỉnh %d màu (chờ mật khẩu) (%.0fs)' % (j.get('delta_E'), n_hc, t))
                    bao_loai = 'c'
            con.commit()
            if bao_loai:
                de = []
                if bao_loai == 'c':
                    de = [tuple(x) for x in con.execute(
                        'SELECT id,mau_id,mau_ten,gia_tri_cu,gia_tri_moi,ly_do,da_ap_dung,lab_moi,delta_E_truoc '
                        'FROM hieu_chinh_mau WHERE hon_hop_id=? AND da_ap_dung=0 ORDER BY id', (v['id'],)).fetchall()]
                bao_mail(con, v['id'], bao_loai, de)
                con.commit()
        except Exception as e:
            con.execute("UPDATE hon_hop SET loi=? WHERE id=?", ('%s\n%s' % (e, traceback.format_exc()[-600:]), v['id']))
            con.commit()
            print('   LỖI:', e)
        nhip(con, '', '')
        con.commit()
        con.close()
        if mot_lan:
            return
        time.sleep(5)


if __name__ == '__main__':
    main()
