# -*- coding: utf-8 -*-
"""ZALO_CN.PY — tài khoản Zalo CÁ NHÂN (phụ) để phát bản tin vào nhóm Zalo (02/10/2026).

VÌ SAO
------
Chủ dự án: "phần zalo tôi có sẵn module rồi, bạn chỉ cần gắn vào là đc, zalo này là tk phụ chuyên support tư vấn
khách hàng ngoài tk chính thức của tôi". Module: zalo_kit/ (trích từ thansa-os) gọi zalo-agent-cli@1.6.2, bên dưới
là zca-js — API Zalo KHÔNG CHÍNH THỨC, tài khoản CÓ THỂ BỊ KHOÁ. Vì vậy ở đây CHỈ GỬI (bản tin vào nhóm đã chọn),
không đọc tin, không tự trả lời; mỗi nhóm tối đa một bản tin mỗi lượt, các nhóm cách nhau GIAN_CACH_GIAY.

CÀI ĐẶT TRÊN MÁY WEB
-------------------
- Node 20: /opt/node20 (bản chính thức nodejs.org, đã đối SHA256).
- CLI: private/services/zalo_cli (npm --prefix, ghim 1.6.2, dấu .zalo-kit-installed).
- Phiên đăng nhập: /home/admin/.zalo-kit/homes/<thư mục riêng> (quyền 700 — ai có thư mục này là dùng được tài khoản).
- chat_cau_hinh (theo khong_gian): zl_home (thư mục phiên), zl_ten (tên tài khoản), zl_nhom (JSON [{"id","ten"}]
  nhóm nhận bản tin), zl_bat ('1' bật gửi).

CLI:  python kenh/zalo_cn.py dang-nhap      — QR gửi về Telegram riêng của chủ shop, chờ quét (tối đa 5 phút)
      python kenh/zalo_cn.py nhom           — liệt kê nhóm của tài khoản
      python kenh/zalo_cn.py thu <id nhóm>  — gửi MỘT tin chữ thử vào nhóm
"""
from __future__ import annotations

import asyncio
import base64
import json
import logging
import os
import sys
import time
from typing import Any, Dict, List, Optional, Tuple

try:
    from congcu import csdl
    from kenh.phieu import _cau_hinh_tele
except ImportError:
    _THU_MUC_CHA = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
    if _THU_MUC_CHA not in sys.path:
        sys.path.insert(0, _THU_MUC_CHA)
    from congcu import csdl  # type: ignore
    from kenh.phieu import _cau_hinh_tele  # type: ignore

_log = logging.getLogger('chatbot.kenh.zalo_cn')

NODE_BIN = '/opt/node20/bin'
THU_MUC_CLI = os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', '..', 'zalo_cli'))
THU_MUC_PHIEN = '/home/admin/.zalo-kit/homes'
GIAN_CACH_GIAY = 40
KG = 'tradingauto'


def _cli():
    if NODE_BIN not in os.environ.get('PATH', '').split(os.pathsep) and os.path.isdir(NODE_BIN):
        os.environ['PATH'] = NODE_BIN + os.pathsep + os.environ.get('PATH', '')
    from zalo_kit import ZaloCli
    return ZaloCli(install_dir=THU_MUC_CLI, auto_install=False)


def cau_hinh(kg: str = KG, _lay_ket_noi=None) -> Dict[str, str]:
    def _doc(ct):
        ct.execute("SELECT khoa, gia_tri FROM chat_cau_hinh WHERE khong_gian = %s AND khoa LIKE 'zl\\_%%'", (kg,))
        return {r['khoa']: str(r['gia_tri'] or '') for r in (ct.fetchall() or [])}
    return csdl.voi_ct(_lay_ket_noi, _doc)


def _luu(kg: str, khoa: str, gia_tri: str, _lay_ket_noi=None) -> None:
    csdl.voi_ct(_lay_ket_noi, lambda ct: ct.execute(
        """INSERT INTO chat_cau_hinh (khong_gian, khoa, gia_tri, cap_nhat) VALUES (%s, %s, %s, NOW())
           ON DUPLICATE KEY UPDATE gia_tri = VALUES(gia_tri), cap_nhat = NOW()""", (kg, khoa, gia_tri)))


def _tele_anh(png: bytes, chu: str) -> bool:
    """Gửi ảnh (QR) về chat RIÊNG của chủ shop (bảng system)."""
    token, chat = csdl.voi_ct(None, _cau_hinh_tele)
    if not token or not chat:
        return False
    import httpx
    r = httpx.post(f'https://api.telegram.org/bot{token}/sendPhoto', data={'chat_id': chat, 'caption': chu},
                   files={'photo': ('qr.png', png, 'image/png')}, timeout=20)
    return bool(r.json().get('ok'))


# =====================================================================
# ĐĂNG NHẬP / NHÓM / GỬI
# =====================================================================

def dang_nhap(kg: str = KG, cho_giay: int = 300) -> Tuple[bool, str]:
    from zalo_kit import QrLogin
    os.makedirs(THU_MUC_PHIEN, mode=0o700, exist_ok=True)

    def _xong(home: str, info: dict) -> Optional[str]:
        try:
            _luu(kg, 'zl_home', home)
            _luu(kg, 'zl_ten', str(info.get('label') or ''))
            return None
        except Exception as e:  # noqa: BLE001
            return str(e)
    lg = QrLogin(THU_MUC_PHIEN, _cli(), on_success=_xong)
    het, lan, loi = time.time() + cho_giay, 0, ''
    # zalo-agent-cli THOÁT (mã 1) khi QR hết hạn (~1–2 phút) -> mở phiên mới, gửi QR mới, tới khi hết cho_giay.
    while time.time() < het:
        bd = lg.start('ho-tro')
        if not bd.get('ok'):
            return False, bd.get('error') or 'Không mở được đăng nhập.'
        sid, da_gui_qr, lan = bd['sid'], '', lan + 1
        while time.time() < het:
            st = lg.status(sid)
            if st['state'] == 'qr' and st['qr'] and st['qr'] != da_gui_qr:
                png = base64.b64decode(st['qr'].split(',', 1)[1])
                ok = _tele_anh(png, f'Mã QR lần {lan}: quét NGAY bằng app Zalo của TÀI KHOẢN PHỤ (hỗ trợ khách) để hệ '
                                    'thống phát bản tin vào nhóm Zalo. Mã hết hạn sau khoảng 1 phút, hết hạn thì mã mới tự gửi.')
                print(f'Đã gửi QR lần {lan} về Telegram.' if ok else 'KHÔNG gửi được QR về Telegram.', flush=True)
                da_gui_qr = st['qr']
            if st['state'] == 'done':
                return True, f"Đăng nhập xong: {st.get('label')}"
            if st['state'] == 'error':
                loi = st.get('error') or 'Đăng nhập lỗi.'
                print(f'Lần {lan}: {loi}', flush=True)
                break
            time.sleep(2)
        lg.cancel(sid)
        if 'chưa hoàn tất' not in loi:          # lỗi khác "hết hạn QR" thì dừng, không mở lại
            return False, loi
    return False, 'Hết giờ chờ quét QR. ' + loi


def ds_nhom(home: str) -> Tuple[bool, List[Dict[str, Any]], str]:
    ok, data, loi = asyncio.run(_cli().run(home, ['group', 'list'], timeout=90))
    if not ok:
        return False, [], loi
    ds = data if isinstance(data, list) else (data.get('groups') or data.get('data') or []) if isinstance(data, dict) else []
    ra = []
    for g in ds:
        if isinstance(g, dict):
            ra.append({'id': str(g.get('threadId') or g.get('groupId') or g.get('id') or ''),
                       'ten': str(g.get('name') or g.get('groupName') or ''),
                       'so_tv': g.get('totalMember') or g.get('memberCount') or g.get('members')})
    return True, ra, ''


def gui_nhom(home: str, nhom_id: str, chu: str, anh: Optional[str] = None) -> Tuple[bool, str]:
    """Gửi vào MỘT nhóm: có ảnh thì ảnh kèm chú thích, không thì tin chữ. Hết giờ = KHÔNG RÕ đã đi chưa ->
    người gọi KHÔNG gửi lại (zalo_kit.is_timeout)."""
    cli = _cli()
    if anh:
        ok, _d, loi = asyncio.run(cli.run(home, ['msg', 'send-image'], [nhom_id, anh], ['-t', '1', '-m', chu], timeout=120))
    else:
        ok, _d, loi = asyncio.run(cli.run(home, ['msg', 'send'], [nhom_id, chu], ['-t', '1'], timeout=90))
    return ok, loi


def phat_ban_tin(chu: str, anh: Optional[str] = None, kg: str = KG, _ngu=time.sleep) -> List[Tuple[str, bool, str]]:
    """Phát một bản tin vào các nhóm đã chọn (zl_nhom), lần lượt, cách nhau GIAN_CACH_GIAY. Tắt (zl_bat != '1') thì
    không gửi gì. Trả [(tên nhóm, ok, lỗi)]."""
    cfg = cau_hinh(kg)
    if cfg.get('zl_bat') != '1' or not cfg.get('zl_home'):
        return []
    try:
        nhom = json.loads(cfg.get('zl_nhom') or '[]')
    except ValueError:
        nhom = []
    ra = []
    for i, g in enumerate(nhom):
        if i:
            _ngu(GIAN_CACH_GIAY)
        ok, loi = gui_nhom(cfg['zl_home'], str(g['id']), chu, anh)
        ra.append((str(g.get('ten') or g['id']), ok, loi))
        if not ok:
            _log.warning('Zalo nhóm %s lỗi: %s', g.get('ten'), loi)
    return ra


# =====================================================================
# CHO TRANG ADMIN (public/admin/zalo.php qua cổng /zalo/lenh) — chủ dự án 02/10: "phần zalo và đấu nối, login
# logout thêm vào 1 tab trong admin". Đăng nhập KHÔNG chặn: trang hỏi trạng thái mỗi 2 giây, QR hiện trên trang.
# =====================================================================

_LG = None
_LG_KHOA = __import__('threading').Lock()


def _qr_login(kg: str):
    global _LG
    with _LG_KHOA:
        if _LG is None:
            from zalo_kit import QrLogin

            def _xong(home: str, info: dict) -> Optional[str]:
                try:
                    cu = cau_hinh(kg).get('zl_home') or ''
                    _luu(kg, 'zl_home', home)
                    _luu(kg, 'zl_ten', str(info.get('label') or ''))
                    if cu and cu != home:
                        _xoa_phien(cu)                  # đổi tài khoản: bỏ phiên cũ
                    return None
                except Exception as e:  # noqa: BLE001
                    return str(e)
            os.makedirs(THU_MUC_PHIEN, mode=0o700, exist_ok=True)
            _LG = QrLogin(THU_MUC_PHIEN, _cli(), on_success=_xong)
        return _LG


def _xoa_phien(home: str) -> None:
    import shutil
    goc = os.path.realpath(THU_MUC_PHIEN)
    duong = os.path.realpath(home or '')
    if duong.startswith(goc + os.sep) and os.path.isdir(duong):
        shutil.rmtree(duong, ignore_errors=True)


def _don_phien_thua(kg: str, tuoi_giay: int = 900) -> None:
    """Thư mục phiên của các lần đăng nhập dở (không phải zl_home) cũ hơn tuoi_giay -> xoá."""
    hien = os.path.realpath(cau_hinh(kg).get('zl_home') or '')
    if not os.path.isdir(THU_MUC_PHIEN):
        return
    for ten in os.listdir(THU_MUC_PHIEN):
        d = os.path.join(THU_MUC_PHIEN, ten)
        if os.path.realpath(d) != hien and time.time() - os.path.getmtime(d) > tuoi_giay:
            _xoa_phien(d)


def lenh_web(lenh: str, du: Dict[str, Any], kg: str = KG) -> Dict[str, Any]:
    """Một cửa cho trang admin. Trả dict có 'ok'."""
    cfg = cau_hinh(kg)
    home = cfg.get('zl_home') or ''
    if lenh == 'trang-thai':
        try:
            nhom = json.loads(cfg.get('zl_nhom') or '[]')
        except ValueError:
            nhom = []
        return {'ok': True, 'dang_nhap': bool(home and os.path.isdir(home)), 'ten': cfg.get('zl_ten', ''),
                'bat': cfg.get('zl_bat') == '1', 'nhom': nhom, 'co_node': os.path.isdir(NODE_BIN)}
    if lenh == 'dang-nhap':
        _don_phien_thua(kg)
        bd = _qr_login(kg).start('ho-tro')
        return {'ok': bool(bd.get('ok')), 'sid': bd.get('sid', ''), 'loi': bd.get('error', '')}
    if lenh == 'qr':
        st = _qr_login(kg).status(str(du.get('sid') or ''))
        het_han = st.get('state') == 'error' and 'chưa hoàn tất' in str(st.get('error') or '')
        return {'ok': True, 'state': st.get('state'), 'qr': st.get('qr') or '', 'loi': st.get('error') or '',
                'het_han': het_han, 'ten': st.get('label') or ''}
    if lenh == 'dang-xuat':
        if home:
            try:
                asyncio.run(_cli().run(home, ['logout'], timeout=60))
            except Exception:  # noqa: BLE001
                pass
            _xoa_phien(home)
        for k, v in (('zl_home', ''), ('zl_ten', ''), ('zl_bat', '0')):
            _luu(kg, k, v)
        return {'ok': True}
    if not home:
        return {'ok': False, 'loi': 'Chưa đăng nhập Zalo.'}
    if lenh == 'kiem':
        ok, data, loi = asyncio.run(_cli().run(home, ['whoami'], timeout=60))
        ten = ''
        if ok and isinstance(data, dict):
            ten = str(data.get('displayName') or data.get('name') or data.get('zaloName') or '')
        return {'ok': ok, 'ten': ten, 'loi': loi}
    if lenh == 'nhom':
        ok, ds, loi = ds_nhom(home)
        if ok:
            _luu(kg, 'zl_nhom_ds', json.dumps(ds, ensure_ascii=False))
        return {'ok': ok, 'nhom': ds, 'loi': loi}
    if lenh == 'thu':
        ok, loi = gui_nhom(home, str(du.get('nhom') or ''), 'Tin thử từ hệ thống TradingAuto (kiểm tra kết nối bản tin).')
        return {'ok': ok, 'loi': loi}
    return {'ok': False, 'loi': 'Lệnh lạ.'}


if __name__ == '__main__':
    logging.basicConfig(level=logging.INFO)
    lenh = sys.argv[1] if len(sys.argv) > 1 else ''
    if lenh == 'dang-nhap':
        ok, tb = dang_nhap()
        print(('OK ' if ok else 'LỖI ') + tb)
    elif lenh == 'nhom':
        home = cau_hinh().get('zl_home')
        ok, ds, loi = ds_nhom(home) if home else (False, [], 'chưa đăng nhập')
        print('LỖI ' + loi if not ok else '\n'.join(f"{g['id']}\t{g['so_tv']}\t{g['ten']}" for g in ds))
    elif lenh == 'thu' and len(sys.argv) > 2:
        home = cau_hinh().get('zl_home')
        print(gui_nhom(home, sys.argv[2], 'Tin thử từ hệ thống TradingAuto.') if home else 'chưa đăng nhập')
    else:
        print(__doc__)
