"""core/licensing.py — Quản lý bản quyền và kích hoạt.

Ưu tiên tích hợp bộ `python_client_PhanMemHopDong` (DLL-based).
Giữ fallback legacy để không làm gián đoạn môi trường cũ.
"""
import hashlib
import subprocess
import os
import sys
from pathlib import Path
from datetime import datetime, timedelta, timezone

APP_ID = "INIContract"

PLAN_LABELS = {
    "free": "FREE",
    "trial": "TRIAL (30 ngày)",
    "free_trial": "TRIAL (30 ngày)",
    "trial_30_days": "TRIAL (30 ngày)",
    "starter": "STARTER",
    "starter_30_days": "STARTER (30 ngày)",
    "starter_60_days": "STARTER (60 ngày)",
    "starter_90_days": "STARTER (90 ngày)",
    "starter_180_days": "STARTER (180 ngày)",
    "starter_1_year": "STARTER (1 năm)",
    "starter_lifetime": "STARTER (Trọn đời)",
    "pro": "PRO",
    "pro_30_days": "PRO (30 ngày)",
    "pro_60_days": "PRO (60 ngày)",
    "pro_90_days": "PRO (90 ngày)",
    "pro_180_days": "PRO (180 ngày)",
    "pro_1_year": "PRO (1 năm)",
    "pro_lifetime": "PRO (Trọn đời)",
    "office": "OFFICE",
    "office_30_days": "OFFICE (30 ngày)",
    "office_60_days": "OFFICE (60 ngày)",
    "office_90_days": "OFFICE (90 ngày)",
    "office_180_days": "OFFICE (180 ngày)",
    "office_365_days": "OFFICE (1 năm)",
    "office_1_year": "OFFICE (1 năm)",
    "office_lifetime": "OFFICE (Trọn đời)",
    "3_months": "STARTER Legacy (3 tháng)",
    "6_months": "STARTER Legacy (6 tháng)",
    "1_year": "PRO Legacy (1 năm)",
    "lifetime": "OFFICE Legacy (Trọn đời)",
}

PLAN_TIER = {
    "free": "free",
    "trial": "trial",
    "free_trial": "trial",
    "trial_30_days": "trial",
    "starter": "starter",
    "starter_": "starter",
    "3_months": "starter",
    "6_months": "starter",
    "pro": "pro",
    "pro_": "pro",
    "1_year": "pro",
    "office": "office",
    "office_": "office",
    "lifetime": "office",
}

TIER_LIMITS = {
    "free": {
        "price_monthly": 0,
        "max_contracts": 10,
        "max_templates": 2,
        "max_devices": 1,
        "has_digital_sign": True,
        "has_report": False,
        "has_backup_sync": False,
        "has_basic_permissions": False,
        "has_advanced_permissions": False,
        "has_advanced_report": False,
        "has_email_sync": False,
        "has_pdf_package": False,
        "has_audit_trail": False,
        "has_api_access": False,
        "has_sub_account": False,
        "has_industry_templates": False,
        "multi_mst": 1,
        "support": None,
    },
    "trial": {
        "price_monthly": 0,
        "max_contracts": 200,
        "max_templates": None,
        "max_devices": 3,
        "has_digital_sign": True,
        "has_report": True,
        "has_backup_sync": True,
        "has_basic_permissions": True,
        "has_advanced_permissions": True,
        "has_advanced_report": True,
        "has_email_sync": True,
        "has_pdf_package": True,
        "has_audit_trail": True,
        "has_api_access": False,
        "has_sub_account": False,
        "has_industry_templates": False,
        "multi_mst": 1,
        "support": None,
    },
    "starter": {
        "price_monthly": 49_000,
        "max_contracts": 500,
        "max_templates": None,
        "max_devices": 3,
        "has_digital_sign": True,
        "has_report": True,
        "has_backup_sync": True,
        "has_basic_permissions": True,
        "has_advanced_permissions": False,
        "has_advanced_report": False,
        "has_email_sync": False,
        "has_pdf_package": False,
        "has_audit_trail": False,
        "has_api_access": False,
        "has_sub_account": False,
        "has_industry_templates": False,
        "multi_mst": 1,
        "support": "Email+Chat",
    },
    "pro": {
        "price_monthly": 99_000,
        "max_contracts": 1500,
        "max_templates": None,
        "max_devices": 5,
        "has_digital_sign": True,
        "has_report": True,
        "has_backup_sync": True,
        "has_basic_permissions": True,
        "has_advanced_permissions": True,
        "has_advanced_report": True,
        "has_email_sync": True,
        "has_pdf_package": True,
        "has_audit_trail": True,
        "has_api_access": False,
        "has_sub_account": False,
        "has_industry_templates": False,
        "multi_mst": 2,
        "support": "Email+Chat",
    },
    "office": {
        "price_monthly": 199_000,
        "max_contracts": 5000,
        "max_templates": None,
        "max_devices": None,
        "has_digital_sign": True,
        "has_report": True,
        "has_backup_sync": True,
        "has_basic_permissions": True,
        "has_advanced_permissions": True,
        "has_advanced_report": True,
        "has_email_sync": True,
        "has_pdf_package": True,
        "has_audit_trail": True,
        "has_api_access": True,
        "has_sub_account": True,
        "has_industry_templates": True,
        "multi_mst": 5,
        "support": "SLA <4h + onboarding",
    },
}

PLAN_CONTRACT_LIMITS = {
    tier: limits["max_contracts"]
    for tier, limits in TIER_LIMITS.items()
}


def _resolve_client_root() -> Path | None:
    """Tìm thư mục python_client_PhanMemHopDong ở source và bản EXE."""
    candidates = []
    if getattr(sys, "frozen", False):
        meipass = getattr(sys, "_MEIPASS", "")
        if meipass:
            candidates.append(Path(meipass) / "python_client_PhanMemHopDong")
        candidates.append(Path(sys.executable).resolve().parent / "python_client_PhanMemHopDong")
    else:
        project_root = Path(__file__).resolve().parents[2]
        candidates.append(project_root / "python_client_PhanMemHopDong")

    for p in candidates:
        if p.exists() and p.is_dir():
            return p
    return None


def _load_new_license_stack():
    """Load bộ client DLL mới, trả về tuple module/class hoặc None."""
    root = _resolve_client_root()
    if not root:
        return None

    root_str = str(root)
    if root_str not in sys.path:
        sys.path.insert(0, root_str)

    try:
        from license_service import activate_license, load_license_key
        from customer_registration import CustomerRegistrationInput, register_customer_once
        from license_client import LicenseClient
        return activate_license, load_license_key, CustomerRegistrationInput, register_customer_once, LicenseClient
    except Exception:
        return None


def format_plan_label(plan_id: str) -> str:
    """Map plan_id sang nhãn hiển thị ngắn cho UI."""
    plan = (plan_id or "").strip().lower()
    return PLAN_LABELS.get(plan, (plan_id or "TRIAL").upper())


def get_plan_family(plan_id: str | None) -> str:
    """Map plan_id thời hạn về 5 gói gốc FREE/TRIAL/STARTER/PRO/OFFICE."""
    plan = (plan_id or "").strip().lower()
    if not plan:
        return "free"

    # Exact IDs first, then prefix families for time-based plans.
    tier = PLAN_TIER.get(plan)
    if tier:
        return tier
    if plan.startswith("starter_"):
        return "starter"
    if plan.startswith("pro_"):
        return "pro"
    if plan.startswith("office_"):
        return "office"
    if plan.startswith("trial"):
        return "trial"
    return "free"


def get_plan_limits(plan_id: str | None) -> dict:
    """Trả về giới hạn theo plan_id dưới dạng dict tier limits."""
    return dict(TIER_LIMITS[get_plan_family(plan_id)])


def _to_int(value, default=None):
    try:
        if value is None or value == "":
            return default
        return int(float(value))
    except (TypeError, ValueError):
        return default


def _first_int(data: dict, keys: tuple[str, ...], default=None):
    if not isinstance(data, dict):
        return default
    for key in keys:
        if key in data:
            value = _to_int(data.get(key), None)
            if value is not None:
                return value
    return default


def _apply_limit_overrides(base_limits: dict, context: dict | None) -> dict:
    limits = dict(base_limits)
    if not isinstance(context, dict):
        return limits

    absolute = context.get("limit_overrides") if isinstance(context.get("limit_overrides"), dict) else {}
    for key in ("max_contracts", "max_devices", "max_templates", "multi_mst"):
        if key in absolute:
            limits[key] = absolute[key]

    addon_contracts = _to_int(context.get("addon_contracts"), 0) or 0
    addon_devices = _to_int(context.get("addon_devices"), 0) or 0
    addon_mst = _to_int(context.get("addon_mst"), 0) or 0

    if addon_contracts > 0 and limits.get("max_contracts") is not None:
        limits["max_contracts"] = int(limits.get("max_contracts") or 0) + addon_contracts
    if addon_devices > 0 and limits.get("max_devices") is not None:
        limits["max_devices"] = int(limits.get("max_devices") or 0) + addon_devices
    if addon_mst > 0:
        limits["multi_mst"] = int(limits.get("multi_mst") or 1) + addon_mst

    return limits


def get_effective_plan_limits(plan_id: str | None, license_context: dict | None = None) -> dict:
    """Gioi han goi sau khi cong override/add-on doc tu server."""
    return _apply_limit_overrides(get_plan_limits(plan_id), license_context)


def get_contract_monthly_limit(plan_id: str | None, license_context: dict | None = None) -> int | None:
    """Giới hạn số HĐ theo gói. None = không giới hạn."""
    return get_effective_plan_limits(plan_id, license_context)["max_contracts"]


def can_create_contract(plan_id: str | None, current_count: int, license_context: dict | None = None) -> tuple[bool, int | None, str]:
    """Kiểm tra quota HĐ theo gói hiện tại."""
    family = get_plan_family(plan_id)
    limit = get_effective_plan_limits(plan_id, license_context)["max_contracts"]
    if limit is None:
        return True, None, family
    return current_count < limit, limit, family


def _is_trial_plan(plan_id: str) -> bool:
    plan = (plan_id or "").strip().lower()
    return plan in {"free", "trial_30_days", "free_trial", "trial"}


def _normalize_tax_code(tax_code: str | None) -> str:
    """Chuẩn hóa MST về chuỗi số để tương thích luồng activate của DLL."""
    return "".join(ch for ch in (tax_code or "").strip() if ch.isdigit())

def get_machine_id():
    """
    Tạo Machine ID duy nhất dựa trên HWID của máy tính (Windows).
    """
    stack = _load_new_license_stack()
    if stack:
        try:
            _, _, _, _, LicenseClient = stack
            return LicenseClient(APP_ID).get_machine_id()
        except Exception:
            pass

    try:
        # Lấy serial number của ổ cứng hoặc UUID bios
        cmd = 'wmic csproduct get uuid'
        uuid = subprocess.check_output(cmd, shell=True).decode().split('\n')[1].strip()
        if not uuid or "UUID" in uuid:
            # Fallback if wmic fails
            cmd = 'wmic baseboard get serialnumber'
            uuid = subprocess.check_output(cmd, shell=True).decode().split('\n')[1].strip()
        
        return hashlib.sha256(uuid.encode()).hexdigest()[:16].upper()
    except:
        return "UNKNOWN-DEVICE"

def validate_license(machine_id, license_key):
    """
    Kiểm tra tính hợp lệ của License Key.
    Công thức đơn giản: key = sha256(machine_id + "SALT").
    """
    if not license_key:
        return False

    try:
        record = _fetch_legacy_license_record(license_key=license_key, timeout=8)
        if record:
            is_active, _, _, is_expired, _, _ = _parse_license_record(record)
            return bool(is_active and not is_expired)
    except Exception:
        pass

    stack = _load_new_license_stack()
    if stack:
        try:
            activate_license, _, _, _, _ = stack
            res = activate_license(input_key=license_key, app_id=APP_ID)
            return bool(res.ok)
        except Exception:
            return False
    
    salt = "ACCOUNTANT_2026_PRO"
    expected = hashlib.sha256((machine_id + salt).encode()).hexdigest()[:20].upper()
    
    # Định dạng key: XXXX-XXXX-XXXX-XXXX-XXXX
    clean_key = license_key.replace("-", "").upper()
    return clean_key == expected

def get_trial_status(db_path):
    """
    Kiểm tra tình trạng dùng thử (giới hạn 50 chứng từ).
    """
    import sqlite3
    if not os.path.exists(db_path):
        return True, 0
        
    try:
        conn = sqlite3.connect(db_path)
        count = conn.execute("SELECT COUNT(*) FROM vouchers").fetchone()[0]
        conn.close()
        return count < 50, count
    except:
        return True, 0

def lookup_license_from_file(machine_id, file_path):
    """
    Tìm kiếm license key tương ứng với machine_id trong một file CSV.
    File CSV giả định có cột 'Machine ID' và 'License Key'.
    """
    import csv
    if not os.path.exists(file_path):
        return None
        
    try:
        with open(file_path, mode='r', encoding='utf-8') as f:
            reader = csv.DictReader(f)
            for row in reader:
                # Tìm cột có tên chứa 'Machine ID' (không phân biệt hoa thường)
                mid_key = next((k for k in row.keys() if 'Machine ID' in k), None)
                lic_key = next((k for k in row.keys() if 'License Key' in k or 'Key' in k), None)
                
                if mid_key and row[mid_key].strip().upper() == machine_id.upper():
                    return row[lic_key].strip() if lic_key else None
        return None
    except:
        return None

def lookup_license_online(machine_id, url=None):
    """
    Tải dữ liệu từ Google Sheets (dạng CSV) và tìm key cho machine_id.
    """
    import urllib.request
    import csv
    import io
    
    if not url:
        # URL mặc định từ người dùng cung cấp
        url = "https://docs.google.com/spreadsheets/d/1NJ3api5KxD41ncWYFNv4SLtsRiMwtQZzeJFZzpio7ek/export?format=csv&gid=0"
    
    try:
        # Thêm timeout và User-Agent để tránh bị block
        req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'})
        with urllib.request.urlopen(req, timeout=10) as response:
            content = response.read().decode('utf-8')
            
        reader = csv.DictReader(io.StringIO(content))
        for row in reader:
            mid_key = next((k for k in row.keys() if 'Machine ID' in k or 'Machine' in k), None)
            lic_key = next((k for k in row.keys() if 'License Key' in k or 'Key' in k), None)
            
            if mid_key and row[mid_key].strip().upper() == machine_id.upper():
                return row[lic_key].strip() if lic_key else None
        return None
    except Exception as e:
        print(f"Cloud activation error: {e}")
        return None

# ── Supabase Configuration ──────────────────────────────────
DEFAULT_SUPABASE_URL = "https://vpblekdmeijbsyphfvgi.supabase.co"
DEFAULT_SUPABASE_KEY = "sb_publishable_LXjXVM0gWhX3dQ945ZUISA_U_1N3tNV"


def _load_supabase_config_from_dll() -> tuple[str, str] | None:
    """Read the same Supabase project used by ProjectLicense_system's DLL."""
    try:
        import ctypes

        root = _resolve_client_root()
        candidates = []
        if root:
            candidates.append(root / "INI_core.dll")
        if getattr(sys, "frozen", False):
            meipass = getattr(sys, "_MEIPASS", "")
            if meipass:
                candidates.append(Path(meipass) / "INI_core.dll")
            candidates.append(Path(sys.executable).resolve().parent / "_internal" / "INI_core.dll")

        for dll_path in candidates:
            if not dll_path or not dll_path.exists():
                continue
            dll = ctypes.CDLL(str(dll_path))
            dll.get_supabase_config.argtypes = [ctypes.c_char_p, ctypes.c_int, ctypes.c_char_p, ctypes.c_int]
            dll.get_supabase_config.restype = None

            url_buf = ctypes.create_string_buffer(512)
            key_buf = ctypes.create_string_buffer(1024)
            dll.get_supabase_config(url_buf, 512, key_buf, 1024)

            url = url_buf.value.decode("utf-8", errors="replace").strip()
            key = key_buf.value.decode("utf-8", errors="replace").strip()
            if url.startswith("https://") and key:
                return url, key
    except Exception as exc:
        print(f"Supabase config from DLL warning: {exc}")
    return None


_DLL_SUPABASE_CONFIG = _load_supabase_config_from_dll()
SUPABASE_URL, SUPABASE_KEY = _DLL_SUPABASE_CONFIG or (DEFAULT_SUPABASE_URL, DEFAULT_SUPABASE_KEY)


def _supabase_headers() -> dict:
    return {
        "apikey": SUPABASE_KEY,
        "Authorization": f"Bearer {SUPABASE_KEY}",
        "Content-Type": "application/json",
        "Prefer": "return=representation",
    }


def _fetch_legacy_license_record(
    tax_id=None,
    machine_id=None,
    timeout: int = 10,
    allow_machine_lookup: bool = False,
    license_key=None,
):
    import requests

    headers = _supabase_headers()
    lookups = []
    if license_key:
        raw_key = str(license_key or "").strip()
        if raw_key:
            lookups.append(("license_key", raw_key))
    if tax_id:
        normalized_tax = _normalize_tax_code(tax_id)
        raw_tax = str(tax_id or "").strip()
        if normalized_tax:
            lookups.extend([
                ("tax_code", normalized_tax),
                ("tax_id", normalized_tax),
            ])
        if raw_tax and raw_tax != normalized_tax:
            lookups.extend([
                ("tax_code", raw_tax),
                ("tax_id", raw_tax),
            ])
    if allow_machine_lookup and machine_id:
        lookups.append(("machine_id", machine_id))

    seen = set()
    for field_name, value in lookups:
        lookup_key = (field_name, str(value))
        if lookup_key in seen:
            continue
        seen.add(lookup_key)
        try:
            value_q = requests.utils.quote(str(value), safe="")
            query_variants = [
                f"{field_name}=eq.{value_q}&app_id=eq.{APP_ID}&select=*&order=created_at.desc&limit=1",
                f"{field_name}=eq.{value_q}&select=*&order=created_at.desc&limit=1",
            ]
            for query in query_variants:
                response = requests.get(f"{SUPABASE_URL}/rest/v1/licenses?{query}", headers=headers, timeout=timeout)
                data = response.json()
                if data and isinstance(data, list):
                    return data[0]
        except Exception:
            continue
    return None


def _patch_license_record(record: dict, payload: dict, timeout: int = 8):
    if not record or not payload:
        return None

    import requests

    headers = _supabase_headers()
    lookups = []
    if record.get("id"):
        lookups.append(("id", record.get("id")))
    if record.get("license_key"):
        lookups.append(("license_key", record.get("license_key")))

    seen = set()
    for field_name, value in lookups:
        lookup_key = (field_name, str(value))
        if lookup_key in seen:
            continue
        seen.add(lookup_key)
        try:
            value_q = requests.utils.quote(str(value), safe="")
            patch_resp = requests.patch(
                f"{SUPABASE_URL}/rest/v1/licenses?{field_name}=eq.{value_q}",
                headers=headers,
                json=payload,
                timeout=timeout,
            )
            if patch_resp.status_code in (200, 204):
                try:
                    data = patch_resp.json()
                except Exception:
                    data = None
                return data
        except Exception:
            continue
    return None


def _get_nested_dict(data: dict, *keys: str) -> dict:
    current = data
    for key in keys:
        if not isinstance(current, dict):
            return {}
        current = current.get(key)
    return current if isinstance(current, dict) else {}


def _server_limit_overrides(record: dict) -> dict:
    info = record.get("info") if isinstance(record.get("info"), dict) else {}
    nested_limits = {}
    for key in ("limits", "quota", "quotas", "plan_limits"):
        if isinstance(info.get(key), dict):
            nested_limits.update(info.get(key) or {})

    source = dict(record)
    source.update(nested_limits)
    source.update(_get_nested_dict(info, "license_limits"))

    overrides = {}
    field_map = {
        "max_contracts": ("max_contracts", "contract_limit", "contracts_limit", "quota_contracts", "max_contract", "so_hop_dong"),
        "max_devices": ("max_devices", "device_limit", "devices_limit", "quota_devices", "max_device", "so_thiet_bi"),
        "max_templates": ("max_templates", "template_limit", "templates_limit", "max_template"),
        "multi_mst": ("multi_mst", "mst_limit", "tax_code_limit", "max_mst", "max_tax_codes"),
    }
    for limit_key, aliases in field_map.items():
        value = _first_int(source, aliases, None)
        if value is not None:
            overrides[limit_key] = None if value < 0 else value
    return overrides


def _addon_int_from_info(record: dict, aliases: tuple[str, ...]) -> int:
    info = record.get("info") if isinstance(record.get("info"), dict) else {}
    addon_blocks = [
        info,
        _get_nested_dict(info, "addons"),
        _get_nested_dict(info, "add_ons"),
        _get_nested_dict(info, "license_addons"),
        _get_nested_dict(info, "quota"),
        _get_nested_dict(info, "quotas"),
    ]
    total = 0
    for block in addon_blocks:
        total += _first_int(block, aliases, 0) or 0
    return total


def _fetch_server_rows(table: str, field: str, value, select: str = "*", timeout: int = 8) -> list[dict]:
    if value is None or value == "":
        return []
    try:
        import requests

        value_q = requests.utils.quote(str(value), safe="")
        response = requests.get(
            f"{SUPABASE_URL}/rest/v1/{table}?{field}=eq.{value_q}&select={select}",
            headers=_supabase_headers(),
            timeout=timeout,
        )
        if response.status_code != 200:
            return []
        data = response.json()
        return data if isinstance(data, list) else []
    except Exception:
        return []


def _fetch_license_server_context(record: dict | None) -> dict:
    if not isinstance(record, dict) or not record:
        return {}

    license_key = str(record.get("license_key") or "").strip()
    group_id = str(record.get("license_group_id") or "").strip() or license_key
    devices = _fetch_server_rows("devices", "license_key", license_key, "id,machine_id,last_check,registered_at,fee")
    addons = _fetch_server_rows("license_addons", "license_group_id", group_id, "addon_id,quantity,unit_price,total_price,created_at")

    addon_contracts = _addon_int_from_info(
        record,
        ("addon_contracts", "extra_contracts", "additional_contracts", "contract_addon", "contracts_addon"),
    )
    addon_devices = _addon_int_from_info(
        record,
        ("addon_devices", "extra_devices", "additional_devices", "device_addon", "devices_addon"),
    )
    addon_mst = _addon_int_from_info(
        record,
        ("addon_mst", "extra_mst", "additional_mst", "mst_addon", "tax_code_addon"),
    )

    paid_device_count = 0
    for device in devices:
        fee = _to_int(device.get("fee"), 0) or 0
        if fee > 0:
            paid_device_count += 1
    addon_devices += paid_device_count

    normalized_addons = []
    for addon in addons:
        addon_id = str(addon.get("addon_id") or "").strip().lower()
        quantity = max(0, _to_int(addon.get("quantity"), 0) or 0)
        if not addon_id or quantity <= 0:
            continue
        normalized_addons.append({
            "addon_id": addon_id,
            "quantity": quantity,
            "unit_price": _to_int(addon.get("unit_price"), 0) or 0,
            "total_price": _to_int(addon.get("total_price"), 0) or 0,
        })
        if addon_id in {"extra_contracts", "addon_contracts", "additional_contracts"}:
            addon_contracts += quantity * 1000
        elif "device" in addon_id or "thiet_bi" in addon_id:
            addon_devices += quantity
        elif addon_id in {"extra_mst", "addon_mst", "additional_mst"} or "mst" in addon_id:
            addon_mst += quantity

    context = {
        "license_key": license_key,
        "license_group_id": group_id,
        "current_devices": len(devices),
        "paid_device_count": paid_device_count,
        "addon_contracts": addon_contracts,
        "addon_devices": addon_devices,
        "addon_mst": addon_mst,
        "addons": normalized_addons,
        "limit_overrides": _server_limit_overrides(record),
    }
    context["effective_limits"] = get_effective_plan_limits(record.get("plan_id") or record.get("plan_type"), context)
    return context


def _attach_license_context(info: dict | None, record: dict | None, plan_id: str | None = None) -> dict:
    merged = dict(info or {})
    context = _fetch_license_server_context(record)
    if context:
        if plan_id and "effective_limits" not in context:
            context["effective_limits"] = get_effective_plan_limits(plan_id, context)
        merged["_license_context"] = context
        merged["_effective_limits"] = context.get("effective_limits") or get_effective_plan_limits(plan_id, context)
    return merged

def _parse_license_record(record):
    """Parse bản ghi Supabase theo schema license mới và schema legacy."""
    status_value = str(record.get("status", "")).strip().lower()
    if "status" in record:
        raw_active = status_value == "active"
    else:
        raw_active = bool(record.get("is_active", False))

    plan_type = (record.get("plan_id") or record.get("plan_type") or "free")
    expires_str = record.get("expire_at") or record.get("expires_at") or ""
    info = record.get("info", {})  # New: Fetch company info JSON
    notification = record.get("notification", "") # New: Fetch cloud message
    if not isinstance(info, dict):
        info = {}
    
    # Parse expiration
    is_expired = False
    expires_at = None
    if expires_str:
        try:
            clean_date = str(expires_str).replace("Z", "+00:00")
            expires_at = datetime.fromisoformat(clean_date)
            if expires_at.tzinfo is None:
                is_expired = expires_at < datetime.now()
            else:
                is_expired = expires_at <= datetime.now(timezone.utc)
        except Exception:
            is_expired = False
            
    is_active = bool(raw_active) and not is_expired
    if is_expired:
        plan_type = "free"
        expires_at = None
        notification = notification or "Gói cũ đã hết hạn, phần mềm tự chuyển về FREE theo chính sách."
        is_expired = False

    return is_active, str(plan_type).strip().lower(), expires_at, is_expired, info, notification


def _sync_legacy_license_info(tax_id=None, machine_id=None, info_data=None):
    """Đồng bộ trường info lên bảng licenses để UI luôn thấy email/số điện thoại mới."""
    if not info_data or not isinstance(info_data, dict):
        return {}

    try:
        import requests

        headers = _supabase_headers()
        record = _fetch_legacy_license_record(tax_id=tax_id, machine_id=machine_id, timeout=8)

        if not record:
            return dict(info_data)

        existing_info = record.get("info") if isinstance(record.get("info"), dict) else {}
        merged_info = dict(existing_info)
        for k, v in info_data.items():
            if v is None:
                continue
            if isinstance(v, str) and not v.strip():
                continue
            merged_info[k] = v

        payload = {}
        if tax_id:
            if "tax_code" in record:
                if record.get("tax_code") != tax_id:
                    payload["tax_code"] = tax_id
            elif record.get("tax_id") != tax_id:
                payload["tax_id"] = tax_id
        if merged_info != existing_info:
            payload["info"] = merged_info

        if payload:
            patched = _patch_license_record(record, payload, timeout=8)
            if patched and isinstance(patched, list) and isinstance(patched[0].get("info"), dict):
                return patched[0]["info"]

        return merged_info
    except Exception:
        return dict(info_data)

def check_cloud_license(tax_id=None, machine_id=None, info_data=None, license_key=None):
    normalized_tax_id = _normalize_tax_code(tax_id)
    input_license_key = str(license_key or "").strip()
    if not normalized_tax_id and not input_license_key:
        return False, "free", None, False, (info_data or {}), "", False
    """
    Kiểm tra bản quyền trên Supabase (Non-recursive). 
    """
    try:
        stack = _load_new_license_stack()
    except RuntimeError as _e:
        if "integrity check failed" in str(_e).lower():
            return False, "free", None, False, (info_data or {}), "Lỗi bảo mật: DLL bị thay thế hoặc chỉnh sửa trái phép.", True
        stack = None
    if stack:
        try:
            activate_license, load_license_key, CustomerRegistrationInput, register_customer_once, _ = stack

            key_record = _fetch_legacy_license_record(license_key=input_license_key) if input_license_key else None
            record_tax_id = ""
            if key_record:
                record_tax_id = _normalize_tax_code(key_record.get("tax_code") or key_record.get("primary_tax_code"))

            legacy_record = key_record or _fetch_legacy_license_record(normalized_tax_id or tax_id, None)

            # 1) Đăng ký khách hàng lần đầu qua DLL nếu có đủ dữ liệu công ty.
            legal_name = ""
            if info_data and isinstance(info_data, dict):
                legal_name = (info_data.get("legal_name") or "").strip()
            reg_result = None
            if normalized_tax_id and legal_name:
                try:
                    reg_payload = CustomerRegistrationInput(
                        tax_code=normalized_tax_id,
                        legal_name=legal_name,
                        address=(info_data or {}).get("address", ""),
                        email=(info_data or {}).get("email", ""),
                        phone=(info_data or {}).get("phone", ""),
                        fax=(info_data or {}).get("fax", ""),
                        website=(info_data or {}).get("website", ""),
                        representative=(info_data or {}).get("representative", ""),
                        representative_title=(info_data or {}).get("position", ""),
                        bank_account=(info_data or {}).get("bank_account", ""),
                    )
                    reg_result = register_customer_once(reg_payload)
                except Exception:
                    pass

            # 2) Ưu tiên key đã lưu local; nếu chưa có thử tax_id như key khởi tạo.
            candidate_keys = []
            # Ưu tiên MST đã chuẩn hóa, đồng thời giữ bản raw để tương thích ngược.
            raw_tax_id = (str(tax_id).strip() if tax_id else "")
            if record_tax_id:
                candidate_keys.append(record_tax_id)
            if normalized_tax_id:
                candidate_keys.append(normalized_tax_id)
            if raw_tax_id and raw_tax_id != normalized_tax_id:
                candidate_keys.append(raw_tax_id)

            # Loại trùng, giữ thứ tự ưu tiên.
            candidate_keys = [k for i, k in enumerate(candidate_keys) if k and k not in candidate_keys[:i]]

            last_result = None
            activation_errors = []
            for key in candidate_keys:
                if not key:
                    continue
                try:
                    last_result = activate_license(input_key=key, app_id=APP_ID)
                    if last_result.ok:
                        break
                except Exception as exc:
                    activation_errors.append(str(exc))
                    continue

            if last_result is None:
                if activation_errors:
                    message = "Không thể tải hoặc kích hoạt bộ bản quyền DLL. Vui lòng kiểm tra INI_core.dll trong gói build."
                    details = "; ".join(err for err in activation_errors if err)
                    if details:
                        message = f"{message} Chi tiết: {details}"
                    return False, "free", None, False, (info_data or {}), message, True
                if legacy_record:
                    record_active, record_plan, record_expiry, record_expired, record_info, record_notification = _parse_license_record(legacy_record)
                    notification = record_notification or "Bản quyền trên server đang bị khóa hoặc đã bị vô hiệu hóa."
                    record_info = _attach_license_context(record_info, legacy_record, record_plan)
                    return record_active, record_plan, record_expiry, record_expired, record_info, notification, (not record_active and not record_expired)
                if normalized_tax_id and info_data:
                    is_active, plan_type, expires_at, is_expired, info, notification = register_cloud_trial(normalized_tax_id or tax_id, None)
                    if plan_type != "ERROR":
                        return is_active, plan_type, expires_at, is_expired, (info or info_data or {}), notification, False
                return False, "free", None, False, (info_data or {}), "", False

            # 3) Chuẩn hoá về tuple legacy để GUI không cần đổi nhiều.
            plan_id = (last_result.plan_id or "free").strip().lower()
            is_active = bool(last_result.ok)
            status_code = int(getattr(last_result, "status", -1))
            is_expired = bool(status_code == 2)
            expires_at = None
            if last_result.show_days_remaining and last_result.days_remaining > 0:
                expires_at = datetime.now() + timedelta(days=last_result.days_remaining)

            notification = "" if is_active else str(last_result.status_message or "")

            # Ưu tiên dữ liệu vừa sync từ cloud; fallback sang record đăng ký hoặc info local.
            cloud_info = _sync_legacy_license_info(normalized_tax_id or tax_id, machine_id, info_data)
            record_found = bool(legacy_record)
            if legacy_record:
                record_active, record_plan, record_expiry, record_expired, record_info, record_notification = _parse_license_record(legacy_record)
                if record_active:
                    is_active = True
                    plan_id = (record_plan or plan_id or "free").strip().lower()
                    if record_expiry:
                        expires_at = record_expiry
                    is_expired = bool(record_expired)
                elif not is_active:
                    plan_id = (record_plan or plan_id or "free").strip().lower()
                    if record_expiry:
                        expires_at = record_expiry
                    is_expired = bool(record_expired or is_expired)
                if record_notification:
                    notification = record_notification
                if record_info:
                    cloud_info = record_info
            if plan_id == "free":
                is_expired = False
                expires_at = None
            if (not cloud_info) and reg_result and isinstance(getattr(reg_result, "record", None), dict):
                cloud_info = reg_result.record
            if not cloud_info:
                cloud_info = dict(info_data or {})

            is_blocked = False
            if not is_active and not is_expired:
                is_blocked = status_code in {3, 4} or (status_code == 1 and record_found)
            if not is_active and not record_found:
                plan_id = "free"

            cloud_info = _attach_license_context(cloud_info, legacy_record or key_record, plan_id)
            return is_active, plan_id, expires_at, is_expired, cloud_info, notification, is_blocked
        except RuntimeError as _e:
            # DLL integrity failure: không fallback sang legacy để tránh bypass bảo mật.
            msg = str(_e).lower()
            if "integrity check failed" in msg:
                return False, "free", None, False, (info_data or {}), "Lỗi bảo mật: DLL bị thay thế hoặc chỉnh sửa trái phép.", True
            # RuntimeError khác: fallback legacy để đảm bảo app vẫn chạy.
            pass
        except Exception:
            # Nếu stack mới lỗi runtime, fallback sang logic cũ.
            pass

    import requests
    headers = _supabase_headers()

    record = _fetch_legacy_license_record(
        tax_id=normalized_tax_id or tax_id,
        machine_id=None,
        timeout=10,
        license_key=input_license_key,
    )
    if record:
        if machine_id and info_data:
            needs_update = False
            payload = {}

            if tax_id:
                if "tax_code" in record:
                    if record.get("tax_code") != tax_id:
                        payload["tax_code"] = tax_id
                        needs_update = True
                elif record.get("tax_id") != tax_id:
                    payload["tax_id"] = tax_id
                    needs_update = True

            if info_data and record.get("info") != info_data:
                payload["info"] = info_data
                needs_update = True

            if needs_update:
                try:
                    _patch_license_record(record, payload, timeout=5)
                except Exception:
                    pass

        is_active, plan_type, expires_at, is_expired, info, notification = _parse_license_record(record)
        info = _attach_license_context(info, record, plan_type)
        is_blocked = (not is_active) and (not is_expired)
        return is_active, plan_type, expires_at, is_expired, info, notification, is_blocked

    # 3. Không thấy -> Tiến hành đăng ký gói FREE mặc định
    if normalized_tax_id and info_data:
        is_active, plan_type, expires_at, is_expired, info, notification = register_cloud_trial(normalized_tax_id or tax_id, None)
        if plan_type != "ERROR":
            return is_active, plan_type, expires_at, is_expired, info, notification, False
        return True, "free", None, False, (info_data or {}), "", False
        
    return False, "free", None, False, (info_data or {}), "", False


def ensure_device_registered(tax_id=None, license_key=None):
    """Ép gọi activate để đảm bảo thiết bị được bind lên server.

    Trả về dict để UI/log có thể hiển thị machine_id, app_id và trạng thái activate.
    """
    out = {
        "ok": False,
        "status": -1,
        "plan_id": "unknown",
        "machine_id": "",
        "app_id": APP_ID,
        "online_verified": False,
        "message": "Không có stack licensing mới",
    }

    try:
        stack = _load_new_license_stack()
    except RuntimeError as exc:
        if "integrity check failed" in str(exc).lower():
            out["message"] = "Lỗi bảo mật: DLL bị thay thế hoặc chỉnh sửa trái phép."
        else:
            out["message"] = f"Lỗi activate thiết bị: {exc}"
        return out
    if not stack:
        return out

    try:
        activate_license, load_license_key, _, _, LicenseClient = stack
        normalized_tax_id = _normalize_tax_code(tax_id)
        candidates = []
        input_license_key = str(license_key or "").strip()
        if input_license_key:
            try:
                key_record = _fetch_legacy_license_record(license_key=input_license_key, timeout=8)
                record_tax_id = _normalize_tax_code(
                    (key_record or {}).get("tax_code") or (key_record or {}).get("primary_tax_code")
                )
                if record_tax_id:
                    candidates.append(record_tax_id)
            except Exception:
                pass
        if normalized_tax_id:
            candidates.append(normalized_tax_id)
        if tax_id and str(tax_id).strip() != normalized_tax_id:
            candidates.append(str(tax_id).strip())
        candidates = [k for i, k in enumerate(candidates) if k and k not in candidates[:i]]

        if not candidates:
            out["message"] = "Không có key/MST để activate"
            return out

        last_res = None
        for k in candidates:
            try:
                last_res = activate_license(input_key=k, app_id=APP_ID)
                if last_res.ok:
                    break
            except Exception:
                continue

        if not last_res:
            out["message"] = "Activate không trả kết quả"
            return out

        client = last_res.client if getattr(last_res, "client", None) else LicenseClient(APP_ID)
        out["machine_id"] = client.get_machine_id()
        out["ok"] = bool(last_res.ok)
        out["status"] = int(last_res.status)
        out["plan_id"] = str(last_res.plan_id)
        out["message"] = str(last_res.status_message)
        try:
            res = getattr(client, "_result", None)
            if res:
                out["online_verified"] = bool(res.plan_flags & 0x04)
        except Exception:
            pass
        return out
    except RuntimeError as exc:
        msg = str(exc).lower()
        if "integrity check failed" in msg:
            out["message"] = "Lỗi bảo mật: DLL bị thay thế hoặc chỉnh sửa trái phép."
        else:
            out["message"] = f"Lỗi activate thiết bị: {exc}"
        return out
    except Exception as exc:
        out["message"] = f"Lỗi activate thiết bị: {exc}"
        return out

def register_cloud_trial(tax_id=None, machine_id=None):
    """
    Đăng ký FREE mặc định lên Supabase khi không có DLL/record.
    Hàm này KHÔNG được gọi lại check_cloud_license.
    """
    import requests
    
    url = f"{SUPABASE_URL}/rest/v1/licenses"
    headers = {
        "apikey": SUPABASE_KEY,
        "Authorization": f"Bearer {SUPABASE_KEY}",
        "Content-Type": "application/json",
        "Prefer": "return=representation"
    }
    
    payloads = []
    normalized_tax_id = _normalize_tax_code(tax_id)
    if tax_id:
        tax_value = normalized_tax_id or str(tax_id).strip()
        free_key_seed = f"{APP_ID}:{tax_value}:free"
        free_key = f"FREE-{hashlib.sha1(free_key_seed.encode('utf-8')).hexdigest()[:16].upper()}"
        payloads.append({
            "license_key": free_key,
            "app_id": APP_ID,
            "plan_id": "free",
            "primary_tax_code": tax_value,
            "tax_code": tax_value,
            "status": "active",
            "expire_at": (datetime.now(timezone.utc) + timedelta(days=9999)).isoformat(),
        })
        payloads.append({
            "tax_id": tax_value,
            "plan_type": "free",
            "is_active": False,
        })
    elif machine_id:
        payloads.append({
            "machine_id": machine_id,
            "plan_type": "free",
            "is_active": False,
        })
    
    if not payloads:
        return False, "ERROR", None, False, {}, ""

    last_error = ""
    for payload in payloads:
        try:
            response = requests.post(url, headers=headers, json=payload, timeout=10)
            
            # Nếu thành công (201) hoặc Đã tồn tại (409)
            if response.status_code in [200, 201]:
                data = response.json()
                if data and isinstance(data, list):
                    return _parse_license_record(data[0])
            elif response.status_code == 409:
                record = _fetch_legacy_license_record(
                    tax_id=normalized_tax_id or tax_id,
                    machine_id=machine_id,
                    timeout=5,
                    allow_machine_lookup=not bool(normalized_tax_id or tax_id),
                )
                if record:
                    return _parse_license_record(record)
            last_error = f"({response.status_code}) {response.text}"
        except Exception as e:
            last_error = str(e)
            continue
        
    try:
        record = _fetch_legacy_license_record(
            tax_id=normalized_tax_id or tax_id,
            machine_id=machine_id,
            timeout=5,
            allow_machine_lookup=not bool(normalized_tax_id or tax_id),
        )
        if record:
            return _parse_license_record(record)
    except Exception:
        pass

    if last_error:
        print(f"Cloud Registration failed: {last_error}")
    return False, "ERROR", None, False, {}, ""


def send_feedback_to_server(
    content: str,
    tax_id=None,
    machine_id=None,
    company_info=None,
    app_version: str = "",
    license_key=None,
) -> dict:
    """Gửi phản hồi lên server mà không phụ thuộc bản ghi bản quyền."""
    import requests

    message = (content or "").strip()
    if not message:
        return {"success": False, "message": "Nội dung phản hồi đang để trống."}

    normalized_tax_id = _normalize_tax_code(tax_id)
    company_info = company_info or {}
    sent_at = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ")

    payload_entry = {
        "content": message,
        "sent_at": sent_at,
        "tax_id": normalized_tax_id or (tax_id or ""),
        "tax_code": normalized_tax_id or (tax_id or ""),
        "machine_id": machine_id or "",
        "app_version": str(app_version or "").strip(),
        "company_name": (company_info.get("legal_name") or company_info.get("name") or "").strip(),
        "phone": (company_info.get("phone") or "").strip(),
        "email": (company_info.get("email") or "").strip(),
        "source_app": APP_ID,
        "license_key": str(license_key or "").strip(),
    }

    headers = _supabase_headers()
    errors: list[str] = []

    feedback_payload = {
        "content": message,
        "tax_code": payload_entry["tax_code"],
        "machine_id": payload_entry["machine_id"],
        "app_version": payload_entry["app_version"],
        "company_name": payload_entry["company_name"],
        "phone": payload_entry["phone"],
        "email": payload_entry["email"],
        "source_app": APP_ID,
        "license_key": payload_entry["license_key"],
        "metadata": payload_entry,
    }

    def _post_rest(table_name: str, payload: dict):
        try:
            response = requests.post(
                f"{SUPABASE_URL}/rest/v1/{table_name}",
                headers=headers,
                json=payload,
                timeout=10,
            )
            if response.status_code in (200, 201):
                return True, ""
            return False, f"{table_name} {response.status_code}: {response.text[:300]}"
        except Exception as exc:
            return False, f"{table_name}: {exc}"

    # Try the new inbox table first. If a server has an older/partial schema,
    # retry with a smaller payload before falling back to trial_logs.
    for payload in (
        feedback_payload,
        {
            "content": message,
            "tax_code": payload_entry["tax_code"],
            "machine_id": payload_entry["machine_id"],
            "app_version": payload_entry["app_version"],
            "company_name": payload_entry["company_name"],
            "phone": payload_entry["phone"],
            "email": payload_entry["email"],
            "metadata": payload_entry,
        },
        {"content": message, "metadata": payload_entry},
        {"content": message},
    ):
        ok, err = _post_rest("app_feedback", payload)
        if ok:
            return {"success": True, "message": "Đã gửi phản hồi lên server thành công."}
        if err:
            errors.append(err)

    for payload in (
        {
            "event_type": "app_feedback",
            "message": message,
            "metadata": payload_entry,
            "created_at": sent_at,
        },
        {
            "event_type": "app_feedback",
            "message": message,
            "metadata": payload_entry,
        },
        {
            "event_type": "app_feedback",
            "message": message,
        },
    ):
        ok, err = _post_rest("trial_logs", payload)
        if ok:
            return {"success": True, "message": "Đã gửi phản hồi lên server thành công."}
        if err:
            errors.append(err)

    # Last compatibility fallback for very old servers. This is silent when no
    # license row exists, because feedback must not require a paid/activated key.
    record = _fetch_legacy_license_record(
        normalized_tax_id or tax_id,
        machine_id,
        timeout=10,
        allow_machine_lookup=not bool(normalized_tax_id or tax_id),
        license_key=payload_entry["license_key"],
    )

    if record:
        existing_info = record.get("info") if isinstance(record.get("info"), dict) else {}
        history = existing_info.get("feedback_history")
        if not isinstance(history, list):
            history = []

        history.append(payload_entry)
        history = history[-20:]

        merged_info = dict(existing_info)
        merged_info["feedback_last_message"] = message
        merged_info["feedback_last_sent_at"] = sent_at
        merged_info["feedback_history"] = history

        patch_payload = {"info": merged_info}
        patched = _patch_license_record(record, patch_payload, timeout=10)
        if patched is not None:
            return {"success": True, "message": "Đã gửi phản hồi lên server thành công."}

    pending_path = ""
    try:
        import json
        from core.utils import get_app_data_path

        pending_path = get_app_data_path("feedback_pending.jsonl")
        os.makedirs(os.path.dirname(pending_path), exist_ok=True)
        with open(pending_path, "a", encoding="utf-8") as file_obj:
            file_obj.write(json.dumps(payload_entry, ensure_ascii=False) + "\n")
    except Exception:
        pending_path = ""

    joined_errors = " | ".join(errors[-4:])
    lower_errors = joined_errors.lower()
    if "failed to resolve" in lower_errors or "getaddrinfo failed" in lower_errors:
        message_text = "Chưa gửi được phản hồi vì máy chưa kết nối được tới server Supabase."
    elif "pgrst" in lower_errors or "schema cache" in lower_errors or "does not exist" in lower_errors or "404" in lower_errors:
        message_text = "Chưa gửi được phản hồi vì server chưa có bảng nhận phản hồi. Vui lòng chạy file supabase_schema/07_app_feedback.sql trên Supabase."
    else:
        message_text = "Chưa gửi được phản hồi lên server. Vui lòng kiểm tra kết nối mạng hoặc cấu hình Supabase."

    if pending_path:
        message_text += f"\n\nNội dung đã được lưu tạm tại: {pending_path}"

    return {
        "success": False,
        "message": message_text,
        "details": joined_errors,
    }
