# -*- coding: utf-8 -*-
"""NGÂN SÁCH AI — trần chi phí TOÀN CỤC theo ngày, không theo IP.

VÌ SAO cần thêm lớp này dù `chat_config.php` đã có giới hạn 12 lượt/phút
và 150 lượt/ngày: giới hạn đó tính THEO IP. Một người đổi IP (4G, VPN,
proxy xoay...) là vượt qua toàn bộ giới hạn đó — hoá đơn AI khi ấy không
có đáy, vì mỗi IP mới lại được cấp một hạn ngạch mới tinh. Bộ đếm ở đây
đếm CHI PHÍ THỰC (ước tính từ số token) cộng dồn của TOÀN BỘ hệ thống
trong một ngày, bất kể ai gọi từ IP nào — đây là lớp phòng thủ cuối, không
thay thế giới hạn theo IP mà chặn đúng lỗ hổng giới hạn theo IP để lọt.

VÌ SAO lưu vào tệp JSON thay vì DB: bộ đếm này chỉ cần sống qua một ngày,
đọc/ghi vài lần mỗi lượt chat, và không cần truy vấn phức tạp — một tệp
JSON khoá khi ghi là đủ, không đáng để kéo thêm một kết nối DB vào tiến
trình Python chạy độc lập với web PHP.
"""
from __future__ import annotations

import json
import os
import time
from datetime import datetime, timezone, timedelta
from typing import Dict, Optional, Tuple


# Múi giờ Việt Nam cố định (UTC+7) — KHÔNG dùng giờ hệ thống của tiến
# trình chạy, vì VPS có thể đặt giờ UTC trong khi "một ngày" ngân sách
# phải khớp với ngày khách hàng ở Việt Nam đang trải qua, nếu không ranh
# giới reset trần ngày sẽ lệch múi giờ và gây khó hiểu khi soi log.
_MUI_GIO_VN = timezone(timedelta(hours=7))


def _thu_muc_private() -> str:
    """Suy ra thư mục `private/` từ vị trí tệp này, không viết cứng đường
    dẫn tuyệt đối của máy chủ — VPS và máy dev có gốc dự án khác nhau.

    Vị trí tệp: private/services/chatbot/bao_ve/ngan_sach.py
    -> cha (bao_ve) -> cha (chatbot) -> cha (services) -> cha = private/
    """
    day_bao_ve = os.path.dirname(os.path.abspath(__file__))
    day_chatbot = os.path.dirname(day_bao_ve)
    day_services = os.path.dirname(day_chatbot)
    return os.path.dirname(day_services)


def _duong_dan_mac_dinh() -> str:
    return os.path.join(_thu_muc_private(), 'logs', 'chatbot', 'ngan_sach.json')


# Bảng giá ƯỚC TÍNH, đơn vị USD / 1 triệu token (vào, ra). Để hằng số riêng
# (không nhúng trong hàm) để sửa khi giá nhà cung cấp đổi mà không phải mò
# trong logic. 'khac' dùng cho nguồn bán lại rẻ (Gemini/DeepSeek qua cổng
# tương thích OpenAI) — rẻ hơn nhiều bậc so với Anthropic trực tiếp.
BANG_GIA_USD_TREN_TRIEU: Dict[str, Tuple[float, float]] = {
    'haiku': (1.0, 5.0),
    'sonnet': (3.0, 15.0),
    'opus': (5.0, 25.0),
    'khac': (0.02, 0.02),   # 500d / 1 trieu token (chu du an, 23/09/2026)
}

TRAN_NGAY_MAC_DINH_USD = 3.0


def _tu_cai_dat_web() -> bool:
    """Giá/trần lấy từ Cài đặt website (congcu/cau_hinh_ai.py) khi đi qua cổng
    bán lại — giá Anthropic gốc trong bảng trên sai tới hàng trăm lần cho cổng
    đó (Opus qua cổng ~700đ/1 triệu token, chủ dự án 24/09/2026)."""
    if os.environ.get('CHATBOT_NHA_CUNG_CAP', '').strip().lower() == 'anthropic':
        return False
    try:
        from congcu.cau_hinh_ai import co_cai_dat
        return co_cai_dat()
    except Exception:  # noqa: BLE001
        return False


def _tran_ngay_usd() -> float:
    if _tu_cai_dat_web():
        try:
            from congcu.cau_hinh_ai import tran_ngay_usd
            tran = tran_ngay_usd()
            return float('inf') if tran is None else tran
        except Exception:  # noqa: BLE001
            pass
    # Đọc mỗi lần gọi (không cache ở import-time) để đổi biến môi trường
    # lúc runtime (vd trong test) có hiệu lực ngay, không cần restart tiến trình.
    try:
        return float(os.environ.get('NGAN_SACH_NGAY_USD', TRAN_NGAY_MAC_DINH_USD))
    except (TypeError, ValueError):
        return TRAN_NGAY_MAC_DINH_USD


def _khoa_gia(ten_mo_hinh: str) -> Tuple[float, float]:
    """Suy ra mức giá từ tên mô hình bằng khớp chuỗi con.

    VÌ SAO khớp chuỗi con thay vì bảng ánh xạ tên đầy đủ: tên mô hình đổi
    theo phiên bản (vd claude-haiku-4-5-20251001, claude-sonnet-5) nhưng
    dòng sản phẩm (haiku/sonnet/opus) hiếm khi đổi tên gốc — khớp chuỗi
    con chịu được việc đổi phiên bản mà không cần sửa bảng giá liên tục.
    """
    ten = (ten_mo_hinh or '').lower()
    for khoa in ('haiku', 'sonnet', 'opus'):
        if khoa in ten:
            return khoa, BANG_GIA_USD_TREN_TRIEU[khoa]
    return 'khac', BANG_GIA_USD_TREN_TRIEU['khac']


def _ngay_hom_nay() -> str:
    return datetime.now(_MUI_GIO_VN).strftime('%Y-%m-%d')


def _trang_thai_rong() -> dict:
    return {}


def _ngay_rong() -> dict:
    return {
        'so_luot': 0,
        'token_vao': 0,
        'token_ra': 0,
        'chi_phi_usd': 0.0,
        'theo_mo_hinh': {},
    }


class _KhoaTep:
    """Khoá tệp thô sơ bằng tệp `.lock` riêng — đủ dùng để hai tiến trình
    Python (vd hai worker uvicorn) không ghi đè nhau, không cần thư viện
    ngoài (fcntl chỉ có trên POSIX, VPS chạy Linux nhưng máy dev có thể là
    Windows — tự cài `.lock` bằng O_CREAT|O_EXCL chạy được trên cả hai).

    Không chờ vô hạn: nếu không lấy được khoá sau `cho_toi_da_giay`, VẪN
    tiến hành ghi (chấp nhận rủi ro va chạm hiếm) thay vì treo cả lượt chat
    của khách — mất một lượt đếm ngân sách không nghiêm trọng bằng bot bị
    treo.
    """

    def __init__(self, duong_dan_khoa: str, cho_toi_da_giay: float = 3.0):
        self._duong_dan = duong_dan_khoa
        self._cho_toi_da = cho_toi_da_giay
        self._da_tao = False

    def __enter__(self):
        bat_dau = time.monotonic()
        while True:
            try:
                fd = os.open(self._duong_dan, os.O_CREAT | os.O_EXCL | os.O_WRONLY)
                os.close(fd)
                self._da_tao = True
                return self
            except FileExistsError:
                if time.monotonic() - bat_dau > self._cho_toi_da:
                    # Hết kiên nhẫn chờ — có thể khoá cũ bị bỏ quên do tiến
                    # trình trước crash. Vẫn tiếp tục ghi, không giữ khách chờ.
                    return self
                time.sleep(0.05)

    def __exit__(self, *_):
        if self._da_tao:
            try:
                os.remove(self._duong_dan)
            except OSError:
                pass


def _doc_toan_bo(duong_dan: str) -> dict:
    if not os.path.exists(duong_dan):
        return _trang_thai_rong()
    try:
        with open(duong_dan, 'r', encoding='utf-8') as tep:
            noi_dung = json.load(tep)
        if not isinstance(noi_dung, dict):
            return _trang_thai_rong()
        return noi_dung
    except (OSError, ValueError):
        # Tệp hỏng/đang ghi dở — coi như chưa có dữ liệu thay vì sập bot.
        # `con_ngan_sach()` KHÔNG được ném lỗi (yêu cầu bắt buộc), nên mọi
        # lỗi đọc ở đây phải tự nuốt và trả về trạng thái an toàn (rỗng).
        return _trang_thai_rong()


def _ghi_toan_bo(duong_dan: str, trang_thai: dict) -> None:
    os.makedirs(os.path.dirname(duong_dan), exist_ok=True)
    duong_dan_tam = duong_dan + '.tmp'
    with open(duong_dan_tam, 'w', encoding='utf-8') as tep:
        json.dump(trang_thai, tep, ensure_ascii=False, indent=2)
    # Ghi ra tệp tạm rồi đổi tên (atomic trên cùng ổ đĩa) — tránh trường
    # hợp một tiến trình đọc trúng lúc tệp đang được ghi dở, thấy JSON cụt.
    os.replace(duong_dan_tam, duong_dan)


def con_ngan_sach(duong_dan: Optional[str] = None) -> bool:
    """True nếu tổng chi phí ước tính HÔM NAY còn dưới trần.

    KHÔNG ĐƯỢC NÉM LỖI (yêu cầu bắt buộc) — lõi gọi hàm này để quyết định
    có gọi AI hay tự chuyển câu mời nhắn Zalo; nếu hàm này ném lỗi, lỗi đó
    sẽ làm hỏng cả lượt chat thay vì chỉ đơn giản là hết ngân sách.
    """
    try:
        duong_dan = duong_dan or _duong_dan_mac_dinh()
        trang_thai = _doc_toan_bo(duong_dan)
        ngay = trang_thai.get(_ngay_hom_nay(), _ngay_rong())
        return float(ngay.get('chi_phi_usd', 0.0)) < _tran_ngay_usd()
    except Exception:
        # Bất kỳ lỗi không lường trước nào (quyền tệp, ổ đĩa đầy...) đều
        # không được văng ra ngoài — coi như CÒN ngân sách để không tự
        # ý khoá chat của toàn bộ khách chỉ vì lỗi đọc tệp thống kê.
        return True


def ghi_nhan(so_token_vao: int, so_token_ra: int, ten_mo_hinh: str, duong_dan: Optional[str] = None) -> None:
    """Cộng dồn một lượt gọi AI vào bộ đếm ngày hôm nay, có khoá tệp."""
    duong_dan = duong_dan or _duong_dan_mac_dinh()
    duong_dan_khoa = duong_dan + '.lock'
    # Thư mục logs/chatbot không có sẵn trên máy chủ (phát hiện 23/09/2026:
    # O_CREAT của khoá tệp ném FileNotFoundError) — tự tạo.
    os.makedirs(os.path.dirname(duong_dan), exist_ok=True)

    khoa_mo_hinh, (gia_vao, gia_ra) = _khoa_gia(ten_mo_hinh)
    if _tu_cai_dat_web():
        try:
            from congcu.cau_hinh_ai import gia_usd_tren_trieu
            gia_vao = gia_ra = gia_usd_tren_trieu(ten_mo_hinh)
        except Exception:  # noqa: BLE001
            pass
    chi_phi = (so_token_vao / 1_000_000.0) * gia_vao + (so_token_ra / 1_000_000.0) * gia_ra

    with _KhoaTep(duong_dan_khoa):
        trang_thai = _doc_toan_bo(duong_dan)
        ngay_nay = _ngay_hom_nay()
        ngay = trang_thai.setdefault(ngay_nay, _ngay_rong())

        ngay['so_luot'] = ngay.get('so_luot', 0) + 1
        ngay['token_vao'] = ngay.get('token_vao', 0) + int(so_token_vao)
        ngay['token_ra'] = ngay.get('token_ra', 0) + int(so_token_ra)
        ngay['chi_phi_usd'] = round(ngay.get('chi_phi_usd', 0.0) + chi_phi, 6)

        theo_mo_hinh = ngay.setdefault('theo_mo_hinh', {})
        thong_ke_mo_hinh = theo_mo_hinh.setdefault(khoa_mo_hinh, {
            'so_luot': 0, 'token_vao': 0, 'token_ra': 0, 'chi_phi_usd': 0.0,
        })
        thong_ke_mo_hinh['so_luot'] += 1
        thong_ke_mo_hinh['token_vao'] += int(so_token_vao)
        thong_ke_mo_hinh['token_ra'] += int(so_token_ra)
        thong_ke_mo_hinh['chi_phi_usd'] = round(thong_ke_mo_hinh['chi_phi_usd'] + chi_phi, 6)

        trang_thai[ngay_nay] = ngay
        _ghi_toan_bo(duong_dan, trang_thai)


def bao_cao_hom_nay(duong_dan: Optional[str] = None) -> dict:
    """Số lượt, token, chi phí ước tính của HÔM NAY."""
    duong_dan = duong_dan or _duong_dan_mac_dinh()
    trang_thai = _doc_toan_bo(duong_dan)
    ngay_nay = _ngay_hom_nay()
    ngay = trang_thai.get(ngay_nay, _ngay_rong())
    return {
        'ngay': ngay_nay,
        'so_luot': ngay.get('so_luot', 0),
        'token_vao': ngay.get('token_vao', 0),
        'token_ra': ngay.get('token_ra', 0),
        'chi_phi_usd': round(ngay.get('chi_phi_usd', 0.0), 6),
        'tran_ngay_usd': _tran_ngay_usd(),
        'con_ngan_sach': con_ngan_sach(duong_dan),
        'theo_mo_hinh': ngay.get('theo_mo_hinh', {}),
    }


if __name__ == '__main__':
    # ---- Tự kiểm: dùng tệp riêng trong thư mục tạm, KHÔNG đụng tệp thật ----
    import tempfile

    thu_muc_tam = tempfile.mkdtemp(prefix='ngan_sach_test_')
    duong_dan_test = os.path.join(thu_muc_tam, 'ngan_sach.json')

    print("=== Ghi nhận vài lượt giả ===")
    ghi_nhan(so_token_vao=2000, so_token_ra=500, ten_mo_hinh='claude-haiku-4-5-20251001', duong_dan=duong_dan_test)
    ghi_nhan(so_token_vao=1500, so_token_ra=800, ten_mo_hinh='claude-sonnet-5', duong_dan=duong_dan_test)
    ghi_nhan(so_token_vao=3000, so_token_ra=200, ten_mo_hinh='deepseek-chat', duong_dan=duong_dan_test)

    bao_cao = bao_cao_hom_nay(duong_dan=duong_dan_test)
    print(json.dumps(bao_cao, ensure_ascii=False, indent=2))

    print("\n=== Với trần mặc định (3.0 USD), còn ngân sách? ===")
    print(con_ngan_sach(duong_dan=duong_dan_test))

    print("\n=== Đặt NGAN_SACH_NGAY_USD rất nhỏ để ép vượt trần ===")
    os.environ['NGAN_SACH_NGAY_USD'] = '0.0000001'
    print("con_ngan_sach() =", con_ngan_sach(duong_dan=duong_dan_test))
    assert con_ngan_sach(duong_dan=duong_dan_test) is False, "LỖI: phải trả False khi vượt trần"
    print("OK: vượt trần thì con_ngan_sach() trả False như mong đợi.")
    del os.environ['NGAN_SACH_NGAY_USD']

    print("\n=== Dọn tệp test ===")
    for ten_tep in os.listdir(thu_muc_tam):
        os.remove(os.path.join(thu_muc_tam, ten_tep))
    os.rmdir(thu_muc_tam)
    print("Đã xoá thư mục tạm:", thu_muc_tam, "- tồn tại?", os.path.exists(thu_muc_tam))
