# -*- coding: utf-8 -*-
"""LICENCE.PY — tra key của khách, đọc SỐNG từ hai hệ thống, không cache lâu.

Khách nằm ở HAI nơi khác nhau và module này phải gộp cả hai:
  1. Website (MySQL, bảng `purchase`) — key mua trực tiếp trên tradingauto.org.
  2. Máy chủ key (Redis riêng, redis.tradingauto.org:51200) — nơi các dịch
     vụ backend (TradingView, Copytrade, DCA bot, Telegram...) tự quản lý
     hạn dùng của từng key.

QUY TẮC RIÊNG TƯ BẮT BUỘC (đọc trước khi sửa bất cứ gì trong tệp này)
----------------------------------------------------------------------
`key_cua_khach()` PHẢI trả về danh sách RỖNG ngay lập tức nếu chưa biết
khách là ai (`ten_dang_nhap` rỗng) hoặc khách chưa đăng nhập (`'khach'` —
giá trị mặc định của NguCanh.nguoi_dung, xem giao_uoc.py). Việc kiểm tra
này đứng TRƯỚC MỌI kết nối mạng/CSDL trong hàm, để không có đường vòng
nào lọt qua. Không có quy tắc này, một khách vãng lai có thể dò license
key của người khác chỉ bằng cách hỏi bot.

Module này cũng KHÔNG bao giờ trả các trường nội bộ (`note`,
`active_clients`, `owner`, `created_by`...) — chỉ trả đúng các trường của
`KeyCuaKhach` trong giao_uoc.py.

Cách chạy tự kiểm (không cần CSDL/Redis thật):
    python congcu/licence.py
"""
from __future__ import annotations

import json
import logging
import os
import sys
from datetime import datetime
from typing import Dict, List, Optional, Tuple

try:
    from ..giao_uoc import KeyCuaKhach
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 giao_uoc import KeyCuaKhach


# =====================================================================
# CẤU HÌNH — đọc từ .env của website, KHÔNG viết cứng mật khẩu
# =====================================================================

# Suy tu vi tri tep: <goc>/private/services/chatbot/congcu/licence.py
#   -> len 4 bac la <goc>/private
_PRIVATE = os.path.dirname(os.path.dirname(os.path.dirname(
    os.path.dirname(os.path.abspath(__file__)))))

DUONG_DAN_ENV_UNG_VIEN = (
    os.path.join(_PRIVATE, '.env'),
    '/home/admin/web/tradingauto.org/private/.env',
    'G:/Xampp/htdocs/private/.env',
)


def _doc_tep_env(duong_dan: str) -> Dict[str, str]:
    ket_qua: Dict[str, str] = {}
    try:
        with open(duong_dan, encoding='utf-8') as tep:
            for dong in tep:
                dong = dong.strip()
                if not dong or dong.startswith('#') or '=' not in dong:
                    continue
                khoa, _, gia_tri = dong.partition('=')
                ket_qua[khoa.strip()] = gia_tri.strip().strip('"').strip("'")
    except OSError:
        return {}
    return ket_qua


def _doc_tep_env_dau_tien_co() -> Dict[str, str]:
    for duong_dan in DUONG_DAN_ENV_UNG_VIEN:
        tep = _doc_tep_env(duong_dan)
        if tep:
            return tep
    return {}


def _cau_hinh_db() -> Optional[Dict[str, str]]:
    """Biến môi trường ĐÈ LÊN tệp .env — giống hệt quy tắc trong san_pham.py."""
    tep_env = _doc_tep_env_dau_tien_co()

    def lay(khoa: str) -> Optional[str]:
        return os.environ.get(khoa) or tep_env.get(khoa)

    host, ten_db, user = lay('DB_HOST'), lay('DB_NAME'), lay('DB_USER')
    mat_khau = lay('DB_PASS') or ''
    if not (host and ten_db and user):
        return None
    return {'host': host, 'name': ten_db, 'user': user, 'pass': mat_khau}


def _mat_khau_redis() -> Optional[str]:
    """Mật khẩu máy chủ key: biến môi trường REDIS_PASSWORD trước, tệp
    .env sau. TUYỆT ĐỐI không viết cứng ở đây."""
    mk = os.environ.get('REDIS_PASSWORD')
    if mk:
        return mk
    return _doc_tep_env_dau_tien_co().get('REDIS_PASSWORD') or None


MAY_CHU_KEY_HOST = 'redis.tradingauto.org'
MAY_CHU_KEY_PORT = 51200


# =====================================================================
# DANH MỤC CÁC HASH TRÊN MÁY CHỦ KEY
# =====================================================================
# (db, tên_hash, mã_sản_phẩm, tên_trường_hạn)
#
# tên_trường_hạn:
#   'expired' -> đọc field 'expired' trong JSON, rỗng/'vv' = vĩnh viễn
#   'expires' -> đọc field 'expires' (KHÔNG PHẢI 'expired' — telelicense*
#                dùng tên trường khác các hash còn lại, dễ chép nhầm)
#   'status'  -> không xét ngày, chỉ xét field 'status' (PXlicense)
#
# Bảy mã sản phẩm dưới đây đã đối chiếu với bảng `product` thật
# (22/09/2026) — không phải phỏng đoán. Ánh xạ backend -> mã lấy từ
# key_sync/config.py: BACKENDS.
#
# Sửa mã ở đây thì phải sửa cùng lúc cả `config.py` bên key_sync, nếu
# không bot sẽ gán key vào một sản phẩm mà website không biết, rồi câu
# trả lời cho khách nói sai tên sản phẩm mà chẳng có gì báo lỗi.
NGUON_MAY_CHU_KEY: Tuple[Tuple[int, str, str, str], ...] = (
    (5, 'keytdv', 'tradingview', 'expired'),
    (3, 'keymaster', 'copytrade', 'expired'),
    (4, 'DCAlicense', 'dcabot', 'expired'),
    (4, 'PXlicense', 'tradepanel', 'status'),
    (4, 'telelicense', 'signaltrader', 'expires'),
    (4, 'telelicense:sb', 'signalbroadcaster', 'expires'),
    (4, 'MT5Mlicense', 'mt5manager', 'expired'),
)


# =====================================================================
# CHUẨN HOÁ NGÀY / TRẠNG THÁI — dùng chung cho cả web lẫn máy chủ key
# =====================================================================

def _chuan_hoa_ngay(gia_tri) -> Tuple[str, Optional[bool]]:
    """Trả (het_han, con_han). het_han rỗng nghĩa là vĩnh viễn.
    con_han = None nghĩa là không hiểu được định dạng, KHÔNG được suy
    diễn bừa (thà không biết còn hơn báo sai cho khách)."""
    if gia_tri in (None, '', 'vv'):
        return '', True

    chuoi = str(gia_tri).strip()

    # Thử timestamp unix (giây hoặc mili-giây).
    if chuoi.isdigit():
        try:
            ts = int(chuoi)
            if ts > 10**12:
                ts //= 1000
            ngay = datetime.fromtimestamp(ts)
            return ngay.strftime('%Y-%m-%d'), ngay >= datetime.now()
        except (ValueError, OSError, OverflowError):
            pass

    # Thử vài định dạng chuỗi ngày phổ biến.
    for dinh_dang in ('%Y-%m-%d %H:%M:%S', '%Y-%m-%d', '%Y/%m/%d', '%d/%m/%Y'):
        try:
            ngay = datetime.strptime(chuoi, dinh_dang)
            return ngay.strftime('%Y-%m-%d'), ngay >= datetime.now()
        except ValueError:
            continue

    logging.warning('Không hiểu định dạng ngày hết hạn %r — giữ nguyên văn, không rõ còn hạn.', gia_tri)
    return chuoi, None


def _trang_thai_con_han(gia_tri) -> Optional[bool]:
    """Dùng cho PXlicense: không có ngày hết hạn, chỉ có field status."""
    if gia_tri is None:
        return None
    s = str(gia_tri).strip().lower()
    if s in ('1', 'true', 'active', 'on', 'enabled', 'kich_hoat'):
        return True
    if s in ('0', 'false', 'inactive', 'off', 'disabled', 'revoked', 'khoa'):
        return False
    logging.warning('Không hiểu giá trị status %r trên máy chủ key.', gia_tri)
    return None


def _thuoc_ve(du_lieu: dict, ten_dang_nhap: str) -> bool:
    """Key này có thuộc về ten_dang_nhap không.

    Tên khách nằm ở HAI CHỖ KHÁC NHAU tuỳ máy chủ key, và đây là chỗ rất
    dễ trả nhầm key của người khác nếu làm ẩu:

      keytdv, keymaster, DCAlicense
          `username` (và `owner` ở DCAlicense) chứa TÊN THẬT của khách.

      PXlicense, telelicense, telelicense:sb, MT5Mlicense
          `username` / `role` là TRƯỜNG PHÂN VAI, KHÔNG phải tên khách —
          giá trị gần như luôn là đúng chữ 'user'. Ghi tên khách vào đó
          là biếu không cho licence ấy số phiên vô hạn, nên bộ đồng bộ
          cố ý đẩy tên khách sang `note` dưới dạng 'web:<username>'.

    Hệ quả: nếu khớp mù theo `username` thì với PXlicense mọi bản ghi đều
    mang 'user' — hoặc không ai khớp, hoặc TẤT CẢ cùng khớp. Cả hai đều
    hỏng, mà cái sau thì hỏng theo kiểu lộ dữ liệu.

    Vì vậy `note` được xét riêng và phải có đúng tiền tố 'web:'.
    """
    ten = ten_dang_nhap.strip().lower()

    # Tên thật — chỉ ở ba hash dùng username làm danh tính.
    for truong in ('username', 'owner'):
        gt = du_lieu.get(truong)
        if gt and str(gt).strip().lower() == ten:
            # 'admin' là cờ chống đồng bộ xoá, không phải khách.
            if str(gt).strip().lower() not in ('admin', 'user', 'demo'):
                return True

    # Tên khách do key_sync ghi kèm tiền tố: adapters.py ghi
    # 'note': 'web:' + rec['username'] cho PXlicense và telelicense.
    ghi_chu = str(du_lieu.get('note') or '').strip()
    if ghi_chu.lower().startswith('web:'):
        if ghi_chu[4:].strip().lower() == ten:
            return True

    return False


# =====================================================================
# NGUỒN 1 — WEBSITE (MySQL, bảng purchase)
# =====================================================================

def _doc_key_web(ten_dang_nhap: str) -> List[KeyCuaKhach]:
    try:
        import pymysql
    except ImportError:
        logging.warning(
            'Chưa cài thư viện pymysql nên không tra được key trên website. '
            'Cài bằng lệnh: pip install pymysql'
        )
        return []

    cau_hinh = _cau_hinh_db()
    if not cau_hinh:
        logging.warning('Không tìm thấy cấu hình CSDL — bỏ qua nguồn website khi tra key.')
        return []

    try:
        ket_noi = pymysql.connect(
            host=cau_hinh['host'],
            user=cau_hinh['user'],
            password=cau_hinh['pass'],
            database=cau_hinh['name'],
            charset='utf8mb4',
            cursorclass=pymysql.cursors.DictCursor,
            connect_timeout=5,
        )
    except Exception as loi:  # noqa: BLE001
        logging.warning('Không kết nối được CSDL để tra key website: %s', loi)
        return []

    try:
        with ket_noi:
            with ket_noi.cursor() as con_tro:
                con_tro.execute(
                    """
                    SELECT product_code, license_key, plan, time_expired
                    FROM purchase
                    WHERE username = %s
                    """,
                    (ten_dang_nhap,),
                )
                cac_hang = con_tro.fetchall()
    except Exception as loi:  # noqa: BLE001
        logging.warning('Lỗi khi tra key website cho %r: %s', ten_dang_nhap, loi)
        return []

    ket_qua: List[KeyCuaKhach] = []
    for hang in cac_hang:
        if hang.get('plan') == 'forever':
            het_han, con_han = '', True
        else:
            het_han, con_han = _chuan_hoa_ngay(hang.get('time_expired'))
        ket_qua.append(
            KeyCuaKhach(
                ma_san_pham=hang.get('product_code') or '',
                license_key=hang.get('license_key') or '',
                het_han=het_han,
                con_han=con_han,
                nguon='web',
            )
        )
    return ket_qua


def _tra_key_web(license_key: str) -> Optional[KeyCuaKhach]:
    try:
        import pymysql
    except ImportError:
        logging.warning('Chưa cài pymysql — bỏ qua nguồn website khi tra thẳng key.')
        return None

    cau_hinh = _cau_hinh_db()
    if not cau_hinh:
        return None

    try:
        ket_noi = pymysql.connect(
            host=cau_hinh['host'],
            user=cau_hinh['user'],
            password=cau_hinh['pass'],
            database=cau_hinh['name'],
            charset='utf8mb4',
            cursorclass=pymysql.cursors.DictCursor,
            connect_timeout=5,
        )
    except Exception as loi:  # noqa: BLE001
        logging.warning('Không kết nối được CSDL để tra thẳng key: %s', loi)
        return None

    try:
        with ket_noi:
            with ket_noi.cursor() as con_tro:
                con_tro.execute(
                    """
                    SELECT product_code, license_key, plan, time_expired
                    FROM purchase
                    WHERE license_key = %s
                    LIMIT 1
                    """,
                    (license_key,),
                )
                hang = con_tro.fetchone()
    except Exception as loi:  # noqa: BLE001
        logging.warning('Lỗi khi tra thẳng key %r trên website: %s', license_key, loi)
        return None

    if not hang:
        return None
    if hang.get('plan') == 'forever':
        het_han, con_han = '', True
    else:
        het_han, con_han = _chuan_hoa_ngay(hang.get('time_expired'))
    return KeyCuaKhach(
        ma_san_pham=hang.get('product_code') or '',
        license_key=hang.get('license_key') or '',
        het_han=het_han,
        con_han=con_han,
        nguon='web',
    )


# =====================================================================
# NGUỒN 2 — MÁY CHỦ KEY (Redis)
# =====================================================================

def _doc_key_may_chu(ten_dang_nhap: str) -> List[KeyCuaKhach]:
    try:
        import redis
    except ImportError:
        logging.warning(
            'Chưa cài thư viện redis nên không tra được key trên máy chủ key. '
            'Cài bằng lệnh: pip install redis'
        )
        return []

    mat_khau = _mat_khau_redis()
    ket_qua: List[KeyCuaKhach] = []

    for so_db, ten_hash, ma_san_pham, loai_han in NGUON_MAY_CHU_KEY:
        try:
            r = redis.Redis(
                host=MAY_CHU_KEY_HOST,
                port=MAY_CHU_KEY_PORT,
                db=so_db,
                password=mat_khau or None,
                decode_responses=True,
                socket_timeout=5,
                socket_connect_timeout=5,
            )
            for truong, gia_tri_json in r.hscan_iter(ten_hash):
                try:
                    du_lieu = json.loads(gia_tri_json)
                except (TypeError, ValueError):
                    continue
                if not isinstance(du_lieu, dict) or not _thuoc_ve(du_lieu, ten_dang_nhap):
                    continue

                if loai_han == 'status':
                    het_han, con_han = '', _trang_thai_con_han(du_lieu.get('status'))
                elif loai_han == 'expires':
                    het_han, con_han = _chuan_hoa_ngay(du_lieu.get('expires'))
                else:  # 'expired'
                    het_han, con_han = _chuan_hoa_ngay(du_lieu.get('expired'))

                ket_qua.append(
                    KeyCuaKhach(
                        ma_san_pham=ma_san_pham,
                        license_key=truong,
                        het_han=het_han,
                        con_han=con_han,
                        nguon='may_chu_key',
                    )
                )
        except Exception as loi:  # noqa: BLE001
            logging.warning(
                'Không đọc được hash %r (db=%s) trên máy chủ key: %s', ten_hash, so_db, loi
            )
            continue

    return ket_qua


def _tra_key_may_chu(license_key: str) -> Optional[KeyCuaKhach]:
    try:
        import redis
    except ImportError:
        logging.warning('Chưa cài redis — bỏ qua nguồn máy chủ key khi tra thẳng key.')
        return None

    mat_khau = _mat_khau_redis()
    for so_db, ten_hash, ma_san_pham, loai_han in NGUON_MAY_CHU_KEY:
        try:
            r = redis.Redis(
                host=MAY_CHU_KEY_HOST,
                port=MAY_CHU_KEY_PORT,
                db=so_db,
                password=mat_khau or None,
                decode_responses=True,
                socket_timeout=5,
                socket_connect_timeout=5,
            )
            gia_tri_json = r.hget(ten_hash, license_key)
            if not gia_tri_json:
                continue
            du_lieu = json.loads(gia_tri_json)
            if not isinstance(du_lieu, dict):
                continue

            if loai_han == 'status':
                het_han, con_han = '', _trang_thai_con_han(du_lieu.get('status'))
            elif loai_han == 'expires':
                het_han, con_han = _chuan_hoa_ngay(du_lieu.get('expires'))
            else:
                het_han, con_han = _chuan_hoa_ngay(du_lieu.get('expired'))

            return KeyCuaKhach(
                ma_san_pham=ma_san_pham,
                license_key=license_key,
                het_han=het_han,
                con_han=con_han,
                nguon='may_chu_key',
            )
        except (TypeError, ValueError):
            continue
        except Exception as loi:  # noqa: BLE001
            logging.warning(
                'Không tra được key trên hash %r (db=%s): %s', ten_hash, so_db, loi
            )
            continue
    return None


# =====================================================================
# HÀM CÔNG KHAI
# =====================================================================

def key_cua_khach(ten_dang_nhap: str) -> List[KeyCuaKhach]:
    """Toàn bộ key của một khách, gộp từ cả web và máy chủ key.

    QUY TẮC RIÊNG TƯ — kiểm tra đứng TRƯỚC MỌI truy vấn mạng/CSDL:
    khách rỗng hoặc còn ở chế độ 'khach' (chưa đăng nhập, xem
    NguCanh.nguoi_dung trong giao_uoc.py) thì KHÔNG được tra bất cứ key
    nào, kể cả rỗng danh sách cũng phải trả ngay, không đi xa hơn."""
    if not ten_dang_nhap or ten_dang_nhap == 'khach':
        return []

    ket_qua: List[KeyCuaKhach] = []
    ket_qua.extend(_doc_key_web(ten_dang_nhap))
    ket_qua.extend(_doc_key_may_chu(ten_dang_nhap))
    return ket_qua


def tra_key(license_key: str) -> Optional[KeyCuaKhach]:
    """Tra thẳng MỘT license key cụ thể (khách đã đưa đúng mã key), không
    cần biết username. Không áp quy tắc riêng tư theo username vì đây là
    tra theo key mà khách đã tự cung cấp, không phải liệt kê key người khác."""
    if not license_key:
        return None
    ket_qua = _tra_key_web(license_key)
    if ket_qua:
        return ket_qua
    return _tra_key_may_chu(license_key)


# =====================================================================
# TỰ KIỂM — không cần CSDL/Redis thật
# =====================================================================
if __name__ == '__main__':
    logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s')

    print('=== Tự kiểm licence.py (máy này không có pymysql/redis, không nối được mạng) ===')
    print()
    print('--- 1) Quy tắc riêng tư: khách rỗng / "khach" PHẢI luôn trả về [] ---')
    rong = key_cua_khach('')
    khach = key_cua_khach('khach')
    print(f"  key_cua_khach('')      -> {rong!r}")
    print(f"  key_cua_khach('khach') -> {khach!r}")
    assert rong == [], 'VI PHẠM QUY TẮC RIÊNG TƯ với chuỗi rỗng!'
    assert khach == [], "VI PHẠM QUY TẮC RIÊNG TƯ với 'khach'!"
    print('  OK — quy tắc riêng tư được tôn trọng, không cần chạm CSDL/Redis để biết điều này.')

    print()
    print('--- 2) Xuống thang êm khi thiếu thư viện / không nối được CSDL & Redis ---')
    kq = key_cua_khach('mot_khach_hang_that')
    print(f"  key_cua_khach('mot_khach_hang_that') -> {kq!r}")
    print('  (Danh sách rỗng + cảnh báo tiếng Việt ở log trên là ĐÚNG trên máy này, không phải lỗi.)')
    kq2 = tra_key('ABC-KHONG-TON-TAI-123')
    print(f"  tra_key('ABC-KHONG-TON-TAI-123') -> {kq2!r}")

    print()
    print('--- 3) Kiểm tra logic thuần (chuẩn hoá ngày / trạng thái) — không cần mạng ---')
    for gt in ['', 'vv', '2026-12-31', '2020-01-01', 1893456000, None, 'linh_tinh']:
        print(f"  _chuan_hoa_ngay({gt!r}) -> {_chuan_hoa_ngay(gt)}")
    for gt in ['1', 'active', '0', 'khoa', None, 'linh_tinh']:
        print(f"  _trang_thai_con_han({gt!r}) -> {_trang_thai_con_han(gt)}")
