# Key Sync Service - Entry Point
# Đồng bộ key từ website xuống Redis của máy chủ key.
#
# Sửa 16/09/2026:
#   - Danh mục sản phẩm lấy từ /api/products thay vì viết cứng trong
#     config.py. Thêm sản phẩm ở trang quản trị là dịch vụ tự nhận ra ở
#     vòng kế tiếp, không phải sửa file, không phải khởi động lại — mà
#     khởi động lại thì đứt long-poll của mọi EA đang kết nối.
#   - Key thiếu hạn sử dụng thì TỪ CHỐI ghi. Quy ước của copytrade coi
#     thiếu `expired` là vĩnh viễn; một lỗi API sẽ biến cả rổ key hết hạn
#     thành vĩnh viễn mà không ai biết.

import requests
import json
import time
import logging
import os
import sys
import urllib3
from datetime import datetime

try:
    from .config import (BACKENDS, TOOLS_FALLBACK, API_BASE, LOG_FILE,
                         UPDATE_INTERVAL, FORCE_SYNC_INTERVAL)
    from .crypto import decrypt_api_response
    from .sync_engine import SyncEngine
    from .adapters import chuyen_doi, truong_cua
    from . import sig_engine
except ImportError:
    from config import (BACKENDS, TOOLS_FALLBACK, API_BASE, LOG_FILE,
                        UPDATE_INTERVAL, FORCE_SYNC_INTERVAL)
    from crypto import decrypt_api_response
    from sync_engine import SyncEngine
    from adapters import chuyen_doi, truong_cua
    import sig_engine

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

# Duong dan log: config.LOG_FILE -> bien moi truong KEY_SYNC_LOG -> mac dinh.
# Tren VPS dich vu chay duoi ProtectSystem=strict nen chi ghi duoc vao
# /var/log/tradingauto; de duong dan tuong doi la dich vu chet ngay khi
# khoi dong. Ghi vao file that bai thi van chay, chi mat log file.
_log_file = os.environ.get('KEY_SYNC_LOG') or LOG_FILE
_handlers = [logging.StreamHandler(sys.stdout)]
try:
    _handlers.insert(0, logging.FileHandler(_log_file, encoding='utf-8'))
except OSError as _e:
    print(f"[canh bao] khong ghi duoc log vao {_log_file}: {_e}")

logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s',
    handlers=_handlers,
)
logger = logging.getLogger(__name__)

engines = {}          # ten may chu key -> SyncEngine
catalogue = {}        # ma san pham -> {title, backend, fields}
_catalogue_stamp = 0  # lan cuoi lay duoc danh muc


def call_api(path, params=None):
    """Gọi một API của website và giải mã. Lỗi thì trả None."""
    url = API_BASE.rstrip('/') + path
    r = requests.get(url, params=params or {}, verify=False, timeout=10)
    if r.status_code != 200:
        raise RuntimeError('HTTP %s' % r.status_code)
    data = json.loads(decrypt_api_response(r.text))
    if not isinstance(data, dict):
        raise RuntimeError('không phải JSON object')
    return data


def load_catalogue():
    """Lấy danh mục sản phẩm từ website. Thất bại thì giữ danh mục cũ."""
    global catalogue, _catalogue_stamp
    try:
        data = call_api('/api/products')
        if data.get('status') != 'true' or 'data' not in data:
            raise RuntimeError(data.get('msg', 'không có sản phẩm'))

        moi = {}
        for item in data['data']:
            backend = item.get('backend', '')
            if sig_engine.la_backend_sig(backend):
                # VipTrend: keysign:{key} field <kenh> - bo dong bo rieng (sig_engine.py)
                moi[item['code']] = {'title': item.get('title', item['code']), 'backend': backend, 'fields': []}
                continue
            if backend not in BACKENDS:
                logger.warning(
                    "[%s] bỏ qua: chưa khai backend %r trong config.BACKENDS "
                    "(không đoán db, xem chú thích trong config.py)",
                    item.get('title', item.get('code')), backend)
                continue
            moi[item['code']] = {
                'title':   item.get('title', item['code']),
                'backend': backend,
                'fields':  item.get('fields', ['expired', 'username']),
            }

        if not moi:
            raise RuntimeError('không sản phẩm nào có backend đã khai')

        catalogue = moi
        _catalogue_stamp = time.time()
        return True

    except Exception as e:
        if catalogue:
            logger.warning("Không lấy được /api/products (%s) — giữ danh mục cũ, %d sản phẩm",
                           e, len(catalogue))
        else:
            logger.warning("Không lấy được /api/products (%s) — dùng danh mục dự phòng", e)
            catalogue = dict(TOOLS_FALLBACK)
        return False


def group_by_backend():
    """Gom sản phẩm theo máy chủ key.

    Một hash Redis là của MỘT máy chủ key, mà nhiều sản phẩm có thể dùng
    chung một máy chủ (tele:st và tele:sb cùng ghi vào telelicense). Nếu
    đồng bộ theo từng sản phẩm thì mỗi bản đồng bộ chỉ thấy key của sản
    phẩm mình, coi key của sản phẩm kia là "không còn" và xoá đi — hai
    bên xoá lẫn nhau mỗi vòng. Nên đơn vị đồng bộ phải là máy chủ key.
    """
    nhom = {}
    for cfg in catalogue.values():
        be = cfg['backend']
        if be not in BACKENDS:
            continue
        c = BACKENDS[be]
        # Khoá gom là CẶP (db, hash), không phải tên backend. Hai tên
        # backend khác nhau vẫn có thể trỏ vào cùng một hash, mà cùng
        # hash thì vẫn xoá key của nhau như thường.
        khoa = '%s/%s' % (c['db'], c['key_hash'])
        # Trường nào được ghi là do BỘ CHUYỂN ĐỔI của hash đó quyết
        # định, không phải do website khai. Website không biết máy chủ
        # key đọc tên trường gì.
        truong = truong_cua(c['key_hash'])
        if truong is None:
            logger.warning("[%s] bỏ qua: chưa có bộ chuyển đổi cho hash %r "
                           "(xem adapters.py) — không ghi dạng chung vào máy "
                           "chủ không hiểu nó", cfg['title'], c['key_hash'])
            continue
        g = nhom.setdefault(khoa, {'db': c['db'], 'key_hash': c['key_hash'],
                                   'backends': [], 'titles': [], 'fields': truong})
        if be not in g['backends']:
            g['backends'].append(be)
        g['titles'].append(cfg['title'])
    return nhom


def sync_engines(nhom):
    """Tạo engine cho mỗi hash Redis, bỏ engine của hash không còn dùng."""
    for khoa, g in nhom.items():
        ten = g['key_hash'] + ' (' + ', '.join(g['titles']) + ')'
        cu = engines.get(khoa)
        if cu and cu.fields == g['fields']:
            cu.tool_name = ten
            continue
        eng = SyncEngine(
            tool_name=ten,
            db=g['db'],
            key_hash=g['key_hash'],
            fields=g['fields'],
            force_sync_interval=FORCE_SYNC_INTERVAL,
        )
        eng.load_cache_from_redis()
        eng.backends = list(g['backends'])
        engines[khoa] = eng
        logger.info("Hash %s (db %s): fields=%s — backend: %s — sản phẩm: %s",
                    g['key_hash'], g['db'], g['fields'],
                    ', '.join(g['backends']), ', '.join(g['titles']))

    for khoa in list(engines):
        if khoa not in nhom:
            logger.info("Hash %s không còn sản phẩm nào — ngừng đồng bộ", khoa)
            del engines[khoa]


def fetch_and_sync_sig():
    """VipTrend: quyen kenh keysign:{key} field <kenh> (sig_engine.py). Loi API -> giu nguyen may key."""
    ds = sorted({c['backend'] for c in catalogue.values() if sig_engine.la_backend_sig(c['backend'])})
    if not ds:
        return
    try:
        data = call_api('/api/keys', {'backend': ','.join(ds)})
        if data.get('status') != 'true' or 'data' not in data:
            logger.error("[keysign] API lỗi (%s) — giữ nguyên Redis", data.get('msg', 'không rõ'))
            return
        sig_engine.dong_bo(data['data'])
    except requests.exceptions.RequestException as e:
        logger.error("[keysign] lỗi kết nối API: %s", e)
    except Exception as e:
        logger.error("[keysign] lỗi: %s", e)


def fetch_and_sync(engine):
    """Lấy key của MỘT hash Redis (gộp mọi backend/sản phẩm dùng nó) và ghi."""
    ten = engine.tool_name
    try:
        data = call_api('/api/keys', {'backend': ','.join(engine.backends)})

        if data.get('status') != 'true' or 'data' not in data:
            # status=false là LỖI thật (truy vấn hỏng, CSDL chập), không
            # phải "không có key nào" — /api/keys trả status=true kèm
            # danh sách rỗng cho trường hợp đó. Gặp lỗi thì giữ nguyên
            # Redis: ghi rỗng xuống là mọi khách mất key cùng lúc.
            logger.error("[%s] API lỗi (%s) — giữ nguyên Redis",
                         ten, data.get('msg', 'không rõ'))
            return

        data_dict = {}
        bo_qua = 0
        for item in data['data']:
            key = str(item['key_active'])

            expired_val = item.get('expired') or item.get('time_expired')
            if expired_val is None or str(expired_val).strip() == '':
                bo_qua += 1
                continue
            if str(expired_val).isdigit():
                expired_val = datetime.fromtimestamp(int(expired_val)).strftime('%Y-%m-%d')
            item = dict(item)
            item['expired'] = expired_val

            value = chuyen_doi(engine.key_hash, item)
            if value is None:
                bo_qua += 1
                continue
            data_dict[key] = value

        if bo_qua:
            logger.error("[%s] %d key KHÔNG có hạn sử dụng — từ chối ghi. "
                         "Thiếu `expired` bị hiểu là vĩnh viễn nên không ghi bừa.",
                         ten, bo_qua)

        engine.save_to_redis(data_dict)

    except requests.exceptions.RequestException as e:
        logger.error("[%s] lỗi kết nối API: %s", ten, e)
    except Exception as e:
        logger.error("[%s] lỗi: %s", ten, e)


def run():
    print("=" * 56)
    print("  TRADING AUTO - KEY SYNC")
    print(f"  Website : {API_BASE}")
    print(f"  Chu ky  : {UPDATE_INTERVAL}s")
    print("=" * 56)

    load_catalogue()
    sync_engines(group_by_backend())

    while True:
        try:
            now = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
            logger.info(f"[{now}] Đang kiểm tra...")

            # Danh mục có thể đổi bất cứ lúc nào (admin thêm sản phẩm)
            load_catalogue()
            sync_engines(group_by_backend())

            for eng in list(engines.values()):
                fetch_and_sync(eng)
            fetch_and_sync_sig()

            time.sleep(UPDATE_INTERVAL)

        except KeyboardInterrupt:
            logger.info("Dừng service...")
            break
        except Exception as e:
            logger.error(f"Lỗi vòng lặp chính: {e}")
            time.sleep(5)


if __name__ == "__main__":
    run()
