from __future__ import annotations

import base64
import hashlib
import hmac
import json
import os
import platform
import re
import shutil
import subprocess
import uuid
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Any, Dict, Optional
from core.config import get_app_data_directory, logger
from core.license_types import LicensePayload


class TenantManager:
    APP_DIR_NAME = "Golder"
    LICENSE_FILE_NAME = "license.json"
    LICENSE_ALT_FILE_NAME = ".golder_lic"
    PROFILE_SYNC_STATE_FILE_NAME = "profile_sync_bootstrap.json"
    RUNTIME_STATE_FILE_NAME = "runtime.state"
    DEFAULT_VPS_HOST = "213.142.148.53"
    DEFAULT_VPS_PORT = 3306
    LICENSE_VERIFY_URL = "https://app.golder.biz/api/license/verify.php"
    DESKTOP_LOGOUT_URL = "https://app.golder.biz/api/session/desktop_logout.php"
    HARDWARE_BINDING_ALGORITHM = "GolderHWID-v1"
    HARDWARE_BINDING_VERSION = b"ghb1"
    OFFLINE_MODE_ALGORITHM = "GolderOfflineCode-v1"
    OFFLINE_MODE_VERSION = b"gof1"
    LICENSE_FILE_VERSION = b"glf1"
    RUNTIME_STATE_VERSION = b"grs1"
    TIME_CHEAT_GRACE_SECONDS = 120
    LEGACY_PLAN_MAP = {
        "1": "A-SOLO",
        "2": "A-OFFICE",
        "3": "A-COMPLEX",
    }
    PLAN_FEATURES = {
        "L-SOLO": {"sync": False, "web_mobile": False, "remote_db": False, "backup": False},
        "L-SYNC": {"sync": True, "web_mobile": True, "remote_db": True, "backup": True},
        "A-SOLO": {"sync": False, "web_mobile": False, "remote_db": False, "backup": False},
        "A-OFFICE": {"sync": True, "web_mobile": False, "remote_db": True, "backup": True},
        "A-COMPLEX": {"sync": True, "web_mobile": True, "remote_db": True, "backup": True},
    }

    def __init__(self, license_path: Optional[Path] = None) -> None:
        self.license_path = Path(license_path) if license_path else self._get_default_license_path()
        self.alt_license_path = self._get_alt_license_path()
        self.license_path.parent.mkdir(parents=True, exist_ok=True)
        self._last_validation_error = ""
        self._hardware_id_cache = ""
        self._legacy_hardware_id_cache = ""
        if license_path is None:
            self._migrate_legacy_state_if_needed()

    def _get_default_license_path(self) -> Path:
        # Keep tenant state in the same app-data root used by the rest of the app.
        return Path(get_app_data_directory()) / self.LICENSE_FILE_NAME

    def _get_alt_license_path(self) -> Path:
        return self.license_path.parent / self.LICENSE_ALT_FILE_NAME

    def _get_legacy_base_dir(self) -> Path:
        appdata = os.getenv("APPDATA")
        if appdata:
            return Path(appdata) / self.APP_DIR_NAME
        return Path.home() / "AppData" / "Roaming" / self.APP_DIR_NAME

    def _migrate_legacy_state_if_needed(self) -> None:
        legacy_base_dir = self._get_legacy_base_dir()
        new_base_dir = self.license_path.parent

        if legacy_base_dir.resolve() == new_base_dir.resolve():
            return
        if not legacy_base_dir.exists():
            return

        files_to_migrate = (
            self.LICENSE_FILE_NAME,
            self.LICENSE_ALT_FILE_NAME,
            "device.id",
            self.PROFILE_SYNC_STATE_FILE_NAME,
            self.RUNTIME_STATE_FILE_NAME,
        )
        for file_name in files_to_migrate:
            src = legacy_base_dir / file_name
            dst = new_base_dir / file_name
            if not src.exists() or dst.exists():
                continue
            try:
                src.replace(dst)
            except Exception:
                try:
                    shutil.copy2(src, dst)
                except Exception as exc:
                    logger.warning(f"[Tenant] Legacy state could not be migrated ({src} -> {dst}): {exc}")

    def load_license(self) -> Dict[str, Any]:
        for loader in (self._read_plain_license_file, self._read_encrypted_license_file):
            raw = loader()
            if isinstance(raw, dict) and raw:
                return self._normalize_license_record(raw)
        return {}

    def _read_plain_license_file(self) -> Dict[str, Any]:
        if not self.license_path.exists():
            return {}
        try:
            content = self.license_path.read_text(encoding="utf-8")
            raw = json.loads(content)
            return dict(raw) if isinstance(raw, dict) else {}
        except Exception as exc:
            logger.warning(f"[Tenant] License file could not be read: {exc}")
            return {}

    def _read_encrypted_license_file(self) -> Dict[str, Any]:
        if not self.alt_license_path.exists():
            return {}
        try:
            token = self.alt_license_path.read_text(encoding="utf-8").strip()
            decrypted = self._decrypt_local_blob(
                token,
                version=self.LICENSE_FILE_VERSION,
                purpose="license-file",
            )
            if not decrypted:
                return {}
            raw = json.loads(decrypted.decode("utf-8"))
            return dict(raw) if isinstance(raw, dict) else {}
        except Exception as exc:
            logger.warning(f"[Tenant] Encrypted license file could not be read: {exc}")
            return {}

    def _normalize_license_record(self, raw: Dict[str, Any]) -> Dict[str, Any]:
        data = dict(raw or {})
        plan_type = self._normalize_plan_type(data.get("plan_type"))
        desktop_expiry = str(
            data.get("desktop_expiry_date")
            or data.get("license_expiry_date")
            or data.get("expiry_date")
            or ""
        ).strip()
        sync_expiry = str(data.get("sync_expiry_date") or "").strip()
        if not sync_expiry and self._plan_has_sync(plan_type):
            sync_expiry = desktop_expiry

        data["plan_type"] = plan_type
        data["desktop_expiry_date"] = desktop_expiry
        data["sync_expiry_date"] = sync_expiry
        data["license_expiry_date"] = desktop_expiry
        data["expiry_date"] = desktop_expiry
        data["sync_status"] = self._resolve_sync_status(data)
        data["web_mobile_enabled"] = 1 if self._resolve_web_mobile_enabled(data) else 0
        data["backup_enabled"] = 1 if self._resolve_backup_enabled(data) else 0
        return data

    def _write_license_files(self, payload: Dict[str, Any]) -> None:
        serialized = json.dumps(payload, ensure_ascii=False, indent=2)
        self.license_path.write_text(serialized, encoding="utf-8")
        encrypted = self._encrypt_local_blob(
            serialized.encode("utf-8"),
            version=self.LICENSE_FILE_VERSION,
            purpose="license-file",
        )
        if encrypted:
            try:
                self.alt_license_path.write_text(encrypted, encoding="utf-8")
            except Exception as exc:
                logger.warning(f"[Tenant] Encrypted license mirror could not be written: {exc}")

    def clear_license(self) -> None:
        """Clears persisted tenant/cloud license credentials."""
        for path in (self.license_path, self.alt_license_path):
            try:
                if path.exists():
                    path.unlink()
            except Exception as exc:
                logger.warning(f"[Tenant] License file could not be removed ({path}): {exc}")

    def get_hardware_id(self) -> str:
        """
        Returns a stable device identifier used for server-side device limits.
        """
        hw_id = self._hardware_id_cache or self._compute_hardware_id()
        self._hardware_id_cache = hw_id
        hw_file = self.license_path.parent / "device.id"

        if hw_id:
            try:
                stored_hw_id = hw_file.read_text(encoding="utf-8").strip() if hw_file.exists() else ""
            except Exception:
                stored_hw_id = ""
            if stored_hw_id != hw_id:
                try:
                    hw_file.write_text(hw_id, encoding="utf-8")
                except Exception:
                    pass
            return hw_id

        if hw_file.exists():
            try:
                return hw_file.read_text(encoding="utf-8").strip()
            except Exception:
                pass
        return ""

    def save_license(
        self,
        plan_type: str,
        db_host: str,
        db_name: str,
        db_user: str,
        db_pass: str,
        max_devices: int = 1,
        max_pc_devices: int = 1,
        max_web_sessions: int = 0,
        is_ai_enabled: int = 0,
        last_login_date: Optional[str] = None,
        customer_username: str = "",
        customer_email: str = "",
        customer_phone: str = "",
        customer_id: int = 0,
        license_expiry_date: str = "",
        desktop_expiry_date: str = "",
        sync_expiry_date: str = "",
        sync_status: str = "",
        web_mobile_enabled: Optional[bool] = None,
        license_payload: str = "",
        license_signature: str = "",
        license_key_id: str = "",
        license_algorithm: str = "Ed25519",
        license_public_key: str = "",
        api_token: str = "",
        api_token_expires_at: str = "",
        offline_mode: bool = False,
        offline_mode_activated_at: str = "",
        offline_mode_algorithm: str = "",
        ) -> None:
        normalized_pc = max(1, int(max_pc_devices or max_devices or 1))
        normalized_web = max(0, int(max_web_sessions or 0))
        normalized_plan = self._normalize_plan_type(plan_type) or "A-SOLO"
        desktop_expiry = str(desktop_expiry_date or license_expiry_date or "").strip()
        sync_expiry = str(sync_expiry_date or "").strip()
        current_hardware_id = self.get_hardware_id()
        local_hardware_binding = self._encrypt_hardware_binding(current_hardware_id)
        payload: Dict[str, Any] = {
            "plan_type": normalized_plan,
            "db_host": str(db_host).strip(),
            "db_name": str(db_name).strip(),
            "db_user": str(db_user).strip(),
            "db_pass": str(db_pass),
            "max_devices": normalized_pc,  # backward compatibility
            "max_pc_devices": normalized_pc,
            "max_web_sessions": normalized_web,
            "is_ai_enabled": int(is_ai_enabled or 0),
            "last_login_date": str(last_login_date or datetime.utcnow().date().isoformat()),
            "customer_username": str(customer_username or "").strip(),
            "customer_email": str(customer_email or "").strip(),
            "customer_phone": str(customer_phone or "").strip(),
            "customer_id": max(0, int(customer_id or 0)),
            "desktop_expiry_date": desktop_expiry,
            "sync_expiry_date": sync_expiry,
            "license_expiry_date": desktop_expiry,
            "expiry_date": desktop_expiry,
            "license_payload": str(license_payload or ""),
            "license_signature": str(license_signature or ""),
            "license_key_id": str(license_key_id or ""),
            "license_algorithm": str(license_algorithm or "Ed25519"),
            "license_public_key": str(license_public_key or ""),
            "local_hardware_binding": local_hardware_binding,
            "local_hardware_binding_algorithm": (
                self.HARDWARE_BINDING_ALGORITHM if local_hardware_binding else ""
            ),
            "api_token": str(api_token or ""),
            "api_token_expires_at": str(api_token_expires_at or ""),
            "offline_mode": bool(offline_mode),
            "offline_mode_activated_at": str(offline_mode_activated_at or ""),
            "offline_mode_algorithm": (
                str(offline_mode_algorithm or self.OFFLINE_MODE_ALGORITHM).strip()
                if offline_mode
                else ""
            ),
        }
        payload["sync_status"] = (
            str(sync_status).strip().lower()
            if str(sync_status or "").strip()
            else self._resolve_sync_status(payload)
        )
        if web_mobile_enabled is None:
            payload["web_mobile_enabled"] = 1 if self._resolve_web_mobile_enabled(payload) else 0
        else:
            payload["web_mobile_enabled"] = 1 if self._coerce_bool(web_mobile_enabled) else 0
        payload["backup_enabled"] = 1 if self._resolve_backup_enabled(payload) else 0
        self._write_license_files(payload)

    def save_from_api(self, api_data: Dict[str, Any]) -> None:
        db_host = str(api_data.get("db_host") or "").strip()
        if db_host in {"", "127.0.0.1", "localhost", "::1"}:
            db_host = self.DEFAULT_VPS_HOST
        plan_type = self._normalize_plan_type(api_data.get("plan_type"))
        max_pc_devices = self._safe_int(
            api_data.get("max_pc_devices", api_data.get("max_devices", 1)),
            fallback=1,
            minimum=1,
        )
        max_web_sessions = self._safe_int(api_data.get("max_web_sessions", 0), fallback=0, minimum=0)
        is_ai_enabled = self._safe_int(api_data.get("is_ai_enabled", 0), fallback=0, minimum=0)

        payload_text = str(api_data.get("license_payload") or "").strip()
        signature = str(api_data.get("license_signature") or "").strip()
        public_key = str(api_data.get("license_public_key") or "").strip()
        key_id = str(api_data.get("license_key_id") or "").strip()
        api_token = str(api_data.get("api_token") or "").strip()
        algorithm = str(api_data.get("license_algorithm") or "Ed25519").strip() or "Ed25519"
        payload_obj = self._parse_license_payload(payload_text)

        if not payload_text or not signature or not public_key:
            raise RuntimeError("Sunucudan lisans imzasi eksik geldi.")
        if not self._verify_license_signature(payload_text, signature, public_key, algorithm):
            remote_check = self._verify_license_signature_remote(
                payload_text,
                signature,
                key_id=key_id,
                api_token=api_token,
            )
            if not remote_check.get("ok"):
                self._last_validation_error = (
                    str(remote_check.get("message") or "").strip()
                    or "Lisans imza dogrulamasi basarisiz."
                )
                return False
                detail = str(remote_check.get("message") or "").strip()
                if not self._has_local_license_verifier():
                    if detail:
                        raise RuntimeError(
                            f"Lisans imzasi yerelde dogrulanamadi ve sunucu kontrolu basarisiz: {detail}"
                        )
                    raise RuntimeError("Lisans imza dogrulamasi icin cryptography veya PyNaCl gerekli.")
                raise RuntimeError("Lisans imza dogrulamasi basarisiz.")

            fresh_license = remote_check.get("fresh_license")
            if isinstance(fresh_license, dict):
                payload_text = str(fresh_license.get("license_payload") or payload_text).strip()
                signature = str(fresh_license.get("license_signature") or signature).strip()
                public_key = str(fresh_license.get("license_public_key") or public_key).strip()
                key_id = str(fresh_license.get("license_key_id") or key_id).strip()
                algorithm = str(fresh_license.get("license_algorithm") or algorithm).strip() or algorithm
                payload_obj = self._parse_license_payload(payload_text)
        if key_id and payload_obj.key_id and key_id != payload_obj.key_id:
            raise RuntimeError("Lisans key_id uyumsuz.")
        payload_plan = self._normalize_plan_type(payload_obj.plan_type)
        if not plan_type:
            plan_type = payload_plan or "A-SOLO"
        if payload_plan and payload_plan != plan_type:
            raise RuntimeError("Lisans plani sunucu cevabi ile uyumsuz.")

        desktop_expiry = str(
            api_data.get("desktop_expiry_date")
            or api_data.get("license_expiry_date")
            or api_data.get("expiry_date")
            or payload_obj.desktop_expiry_date
            or payload_obj.expiry_date
            or ""
        ).strip()
        sync_expiry = str(
            api_data.get("sync_expiry_date")
            or payload_obj.sync_expiry_date
            or ""
        ).strip()
        if self._is_expired(desktop_expiry):
            raise RuntimeError("Lisans suresi dolmus.")
        if payload_obj.license_status and str(payload_obj.license_status).lower() not in {"active", ""}:
            raise RuntimeError("Lisans aktif degil.")
        if self._has_signed_hardware_binding(payload_obj) and not self._license_matches_current_hardware(payload_obj):
            raise RuntimeError("Lisans bu bilgisayara ait degil.")

        normalized_payload = {
            "plan_type": plan_type,
            "desktop_expiry_date": desktop_expiry,
            "sync_expiry_date": sync_expiry,
            "sync_status": str(api_data.get("sync_status") or payload_obj.sync_status or "").strip().lower(),
            "web_mobile_enabled": api_data.get("web_mobile_enabled", payload_obj.web_mobile_enabled),
            "offline_mode": False,
        }
        sync_status = normalized_payload["sync_status"] or self._resolve_sync_status(normalized_payload)
        web_mobile_enabled = self._coerce_bool(normalized_payload["web_mobile_enabled"])
        if sync_status != "active":
            web_mobile_enabled = False

        self.save_license(
            plan_type=plan_type,
            db_host=db_host,
            db_name=str(api_data.get("db_name", "")),
            db_user=str(api_data.get("db_user", "")),
            db_pass=str(api_data.get("db_pass", "")),
            max_devices=max_pc_devices,
            max_pc_devices=max_pc_devices,
            max_web_sessions=max_web_sessions,
            is_ai_enabled=is_ai_enabled,
            last_login_date=datetime.utcnow().date().isoformat(),
            customer_username=str(api_data.get("customer_username", "")),
            customer_email=str(api_data.get("customer_email", "")),
            customer_phone=str(api_data.get("customer_phone", "")),
            customer_id=self._safe_int(
                api_data.get("customer_id", payload_obj.customer_id),
                fallback=payload_obj.customer_id,
                minimum=0,
            ),
            license_expiry_date=desktop_expiry,
            desktop_expiry_date=desktop_expiry,
            sync_expiry_date=sync_expiry,
            sync_status=sync_status,
            web_mobile_enabled=web_mobile_enabled,
            license_payload=payload_text,
            license_signature=signature,
            license_key_id=key_id,
            license_algorithm=algorithm,
            license_public_key=public_key,
            api_token=api_token,
            api_token_expires_at=str(api_data.get("api_token_expires_at", "") or ""),
        )
        self._last_validation_error = ""

    def get_plan_type(self) -> Optional[str]:
        plan_type = self._normalize_plan_type(self.load_license().get("plan_type", ""))
        return plan_type or None

    def get_last_validation_error(self) -> str:
        return str(self._last_validation_error or "").strip()

    def has_sync_entitlement(self, data: Optional[Dict[str, Any]] = None) -> bool:
        license_data = data if isinstance(data, dict) else self.load_license()
        return self._plan_has_sync(self._normalize_plan_type(license_data.get("plan_type")))

    def is_web_mobile_enabled(self, data: Optional[Dict[str, Any]] = None) -> bool:
        license_data = data if isinstance(data, dict) else self.load_license()
        return self._resolve_web_mobile_enabled(license_data)

    def can_use_backup_features(self, data: Optional[Dict[str, Any]] = None) -> bool:
        license_data = data if isinstance(data, dict) else self.load_license()
        return self._resolve_backup_enabled(license_data)

    def get_desktop_expiry_date(self, data: Optional[Dict[str, Any]] = None) -> str:
        license_data = data if isinstance(data, dict) else self.load_license()
        return str(
            license_data.get("desktop_expiry_date")
            or license_data.get("license_expiry_date")
            or license_data.get("expiry_date")
            or ""
        ).strip()

    def get_sync_expiry_date(self, data: Optional[Dict[str, Any]] = None) -> str:
        license_data = data if isinstance(data, dict) else self.load_license()
        return str(license_data.get("sync_expiry_date") or "").strip()

    def get_sync_status(self, data: Optional[Dict[str, Any]] = None) -> str:
        license_data = data if isinstance(data, dict) else self.load_license()
        return self._resolve_sync_status(license_data)

    def is_sync_expired(self, data: Optional[Dict[str, Any]] = None) -> bool:
        return self.get_sync_status(data) == "expired"

    def is_desktop_expired(self, data: Optional[Dict[str, Any]] = None) -> bool:
        return self._is_expired(self.get_desktop_expiry_date(data))

    def is_offline_mode_enabled(self) -> bool:
        return self._is_offline_license_valid(self.load_license())

    def get_license_expiry_display(self) -> str:
        if self.is_offline_mode_enabled():
            return "Sinirsiz (Offline)"
        expiry = self.get_desktop_expiry_date()
        return expiry or "Suresiz"

    def get_sync_expiry_display(self) -> str:
        status = self.get_sync_status()
        if status == "disabled":
            return "Kapali"
        expiry = self.get_sync_expiry_date()
        if not expiry:
            return "Suresiz"
        return expiry

    def generate_offline_mode_code(self, device_id: Optional[str] = None) -> str:
        normalized = str(device_id or self.get_hardware_id() or "").strip()
        if not normalized:
            return ""

        enc_key, mac_key = self._offline_mode_keys()
        nonce = os.urandom(16)
        plaintext = normalized.encode("utf-8")
        ciphertext = self._license_binding_xor(plaintext, enc_key, nonce)
        mac = hmac.new(
            mac_key,
            self.OFFLINE_MODE_VERSION + nonce + ciphertext,
            hashlib.sha256,
        ).digest()
        return self._base64url_encode(self.OFFLINE_MODE_VERSION + nonce + ciphertext + mac)

    def decode_offline_mode_code(self, value: str) -> str:
        raw = self._base64url_decode(value)
        min_length = len(self.OFFLINE_MODE_VERSION) + 16 + 32
        if len(raw) < min_length or not raw.startswith(self.OFFLINE_MODE_VERSION):
            return ""

        nonce_start = len(self.OFFLINE_MODE_VERSION)
        nonce_end = nonce_start + 16
        nonce = raw[nonce_start:nonce_end]
        ciphertext = raw[nonce_end:-32]
        received_mac = raw[-32:]

        enc_key, mac_key = self._offline_mode_keys()
        expected_mac = hmac.new(
            mac_key,
            self.OFFLINE_MODE_VERSION + nonce + ciphertext,
            hashlib.sha256,
        ).digest()
        if not hmac.compare_digest(expected_mac, received_mac):
            return ""

        try:
            return self._license_binding_xor(ciphertext, enc_key, nonce).decode("utf-8").strip()
        except Exception:
            return ""

    def validate_offline_mode_code(self, value: str, device_id: Optional[str] = None) -> bool:
        expected = str(device_id or self.get_hardware_id() or "").strip()
        decoded = self.decode_offline_mode_code(value)
        if not expected or not decoded:
            return False
        return hmac.compare_digest(decoded, expected)

    def activate_offline_mode(self, offline_code: str) -> Dict[str, Any]:
        normalized_code = str(offline_code or "").strip()
        if not normalized_code:
            raise RuntimeError("Offline sifresi bos olamaz.")

        device_id = self.get_hardware_id()
        if not device_id:
            raise RuntimeError("Device ID okunamadi.")
        if not self.validate_offline_mode_code(normalized_code, device_id=device_id):
            raise RuntimeError("Offline sifresi bu cihaz icin gecersiz.")

        existing = self.load_license()
        existing_max_devices = self._safe_int(
            existing.get("max_devices", existing.get("max_pc_devices", 1)),
            fallback=1,
            minimum=1,
        )
        existing_max_pc_devices = self._safe_int(
            existing.get("max_pc_devices", existing_max_devices),
            fallback=existing_max_devices,
            minimum=1,
        )
        self.save_license(
            plan_type=self._normalize_plan_type(existing.get("plan_type")),
            db_host=str(existing.get("db_host") or "").strip(),
            db_name=str(existing.get("db_name") or "").strip(),
            db_user=str(existing.get("db_user") or "").strip(),
            db_pass=str(existing.get("db_pass") or ""),
            max_devices=existing_max_devices,
            max_pc_devices=existing_max_pc_devices,
            max_web_sessions=self._safe_int(existing.get("max_web_sessions", 0), fallback=0, minimum=0),
            is_ai_enabled=self._safe_int(existing.get("is_ai_enabled", 0), fallback=0, minimum=0),
            last_login_date=datetime.utcnow().date().isoformat(),
            customer_username=str(existing.get("customer_username") or "").strip(),
            customer_email=str(existing.get("customer_email") or "").strip(),
            customer_phone=str(existing.get("customer_phone") or "").strip(),
            customer_id=self._safe_int(existing.get("customer_id", 0), fallback=0, minimum=0),
            license_expiry_date=self.get_desktop_expiry_date(existing),
            desktop_expiry_date=self.get_desktop_expiry_date(existing),
            sync_expiry_date=self.get_sync_expiry_date(existing),
            sync_status=self.get_sync_status(existing),
            web_mobile_enabled=self.is_web_mobile_enabled(existing),
            license_payload=str(existing.get("license_payload") or ""),
            license_signature=str(existing.get("license_signature") or ""),
            license_key_id=str(existing.get("license_key_id") or ""),
            license_algorithm=str(existing.get("license_algorithm") or "Ed25519"),
            license_public_key=str(existing.get("license_public_key") or ""),
            api_token="",
            api_token_expires_at="",
            offline_mode=True,
            offline_mode_activated_at=datetime.utcnow().replace(microsecond=0).isoformat() + "Z",
            offline_mode_algorithm=self.OFFLINE_MODE_ALGORITHM,
        )
        return self.load_license()

    def is_profile_sync_enabled(self) -> bool:
        data = self.load_license()
        if not data or self.is_offline_mode_enabled():
            return False
        if not self.has_sync_entitlement(data):
            return False
        return self.get_sync_status(data) == "active"

    def get_customer_id(self) -> int:
        data = self.load_license()
        stored_customer_id = self._safe_int(data.get("customer_id", 0), fallback=0, minimum=0)
        if stored_customer_id > 0:
            return stored_customer_id
        payload = self._parse_license_payload(str(data.get("license_payload") or "").strip())
        return max(0, int(payload.customer_id or 0))

    def profile_sync_scope_key(self) -> str:
        data = self.load_license()
        customer_id = self.get_customer_id()
        db_name = str(data.get("db_name") or "").strip().lower()
        username = str(data.get("customer_username") or "").strip().lower()
        parts = []
        if customer_id > 0:
            parts.append(f"customer:{customer_id}")
        if db_name:
            parts.append(f"db:{db_name}")
        if username:
            parts.append(f"user:{username}")
        return "|".join(parts) or "default"

    def _profile_sync_state_path(self) -> Path:
        return self.license_path.parent / self.PROFILE_SYNC_STATE_FILE_NAME

    def _load_profile_sync_state(self) -> Dict[str, Any]:
        path = self._profile_sync_state_path()
        if not path.exists():
            return {}
        try:
            raw = json.loads(path.read_text(encoding="utf-8"))
        except Exception as exc:
            logger.warning(f"[Tenant] Profile sync state could not be read: {exc}")
            return {}
        return raw if isinstance(raw, dict) else {}

    def _save_profile_sync_state(self, state: Dict[str, Any]) -> None:
        path = self._profile_sync_state_path()
        try:
            path.write_text(json.dumps(state, ensure_ascii=False, indent=2), encoding="utf-8")
        except Exception as exc:
            logger.warning(f"[Tenant] Profile sync state could not be written: {exc}")

    def has_completed_profile_sync_bootstrap(self) -> bool:
        state = self._load_profile_sync_state()
        scope_key = self.profile_sync_scope_key()
        return bool(((state.get(scope_key) or {}) if isinstance(state.get(scope_key), dict) else {}).get("completed"))

    def mark_profile_sync_bootstrap_completed(self, completed: bool = True) -> None:
        state = self._load_profile_sync_state()
        scope_key = self.profile_sync_scope_key()
        state[scope_key] = {
            "completed": bool(completed),
            "updated_at": datetime.utcnow().replace(microsecond=0).isoformat() + "Z",
        }
        self._save_profile_sync_state(state)

    def get_remote_db_config(self) -> Optional[Dict[str, Any]]:
        data = self.load_license()
        if not data or self.is_offline_mode_enabled():
            return None
        plan_type = self._normalize_plan_type(data.get("plan_type"))
        if not self._plan_requires_remote_db(plan_type):
            return None
        if self.get_sync_status(data) != "active":
            return None

        db_host = str(data.get("db_host", "")).strip()
        db_name = str(data.get("db_name", "")).strip()
        db_user = str(data.get("db_user", "")).strip()
        db_pass = str(data.get("db_pass", ""))

        if not all([db_host, db_name, db_user, db_pass]):
            return None

        return {
            "host": db_host,
            "database": db_name,
            "user": db_user,
            "password": db_pass,
            "port": self.DEFAULT_VPS_PORT,
        }

    def is_license_valid(self) -> bool:
        self._last_validation_error = ""
        data = self.load_license()
        if not data:
            self._last_validation_error = "Yerel lisans dosyasi bulunamadi."
            return False

        if self._is_offline_license_valid(data):
            if self._is_time_cheat_detected():
                self._last_validation_error = "Sistem saati geri alinmis gorunuyor. Lutfen tarih ve saati kontrol edin."
                return False
            if self.is_desktop_expired(data):
                self._last_validation_error = "Masaustu lisans suresi dolmus."
                return False
            return True

        plan_type = self._normalize_plan_type(data.get("plan_type", ""))
        if plan_type not in self.PLAN_FEATURES:
            self._last_validation_error = "Lisans plani gecersiz."
            return False

        payload_text = str(data.get("license_payload", "") or "").strip()
        signature = str(data.get("license_signature", "") or "").strip()
        public_key = str(data.get("license_public_key", "") or "").strip()
        key_id = str(data.get("license_key_id", "") or "").strip()
        algorithm = str(data.get("license_algorithm", "Ed25519") or "Ed25519").strip() or "Ed25519"

        if not payload_text or not signature or not public_key:
            self._last_validation_error = "Imzali lisans sertifikasi eksik."
            return False

        if not self._verify_license_signature(payload_text, signature, public_key, algorithm):
            self._last_validation_error = "Yerel imzali lisans sertifikasi dogrulanamadi."
            return False
            remote_check = self._verify_license_signature_remote(
                payload_text,
                signature,
                key_id=key_id,
                api_token=str(data.get("api_token", "") or "").strip(),
            )
            if not remote_check.get("ok"):
                # EĞER İNTERNET KESİNTİSİ VARSA YEREL LİSANSA GÜVEN VE UYGULAMAYI AÇ
                if remote_check.get("is_network_error"):
                    logger.warning("Lisans doğrulama sunucusuna ulaşılamadı. İnternet yok. Çevrimdışı devam ediliyor.")
                else:
                    return False
            else:
                fresh_license = remote_check.get("fresh_license")
                if isinstance(fresh_license, dict):
                    payload_text = str(fresh_license.get("license_payload") or payload_text).strip()
                    signature = str(fresh_license.get("license_signature") or signature).strip()
                    public_key = str(fresh_license.get("license_public_key") or public_key).strip()
                    key_id = str(fresh_license.get("license_key_id") or key_id).strip()
                    algorithm = str(fresh_license.get("license_algorithm") or algorithm).strip() or algorithm

        payload = self._parse_license_payload(payload_text)
        payload_plan = self._normalize_plan_type(payload.plan_type)
        if payload_plan and payload_plan != plan_type:
            self._last_validation_error = "Lisans plani sertifika ile uyusmuyor."
            return False
        if payload.license_status and str(payload.license_status).lower() not in {"active", ""}:
            self._last_validation_error = "Lisans aktif degil."
            return False
        if not self._is_current_hardware_authorized(data, payload):
            self._last_validation_error = "Bu lisans baska bir bilgisayara kilitli."
            return False
        if self._is_time_cheat_detected():
            self._last_validation_error = "Sistem saati geri alinmis gorunuyor. Lutfen tarih ve saati kontrol edin."
            return False

        desktop_expiry = self.get_desktop_expiry_date(data) or payload.desktop_expiry_date or payload.expiry_date
        if self._is_expired(desktop_expiry):
            self._last_validation_error = "Masaustu lisans suresi dolmus."
            return False

        return True

    def get_api_token(self) -> Optional[str]:
        data = self.load_license()
        if not data or self.is_offline_mode_enabled():
            return None
        if self.get_sync_status(data) != "active":
            return None
        token = str(data.get("api_token", "") or "").strip()
        if not token:
            return None
        token_expiry = str(data.get("api_token_expires_at", "") or "").strip()
        if token_expiry and self._is_expired(token_expiry):
            return None
        return token

    def logout_current_desktop_session(self) -> bool:
        token = self.get_api_token()
        if not token:
            return True

        try:
            import requests
        except Exception as exc:
            raise RuntimeError(f"requests modulu eksik: {exc}") from exc

        hardware_id = self.get_hardware_id()
        response = None
        payload: Any = None
        try:
            response = requests.post(
                self.DESKTOP_LOGOUT_URL,
                json={"hardware_id": hardware_id},
                headers={"Authorization": f"Bearer {token}"},
                timeout=15,
            )
            try:
                payload = response.json()
            except ValueError:
                payload = None
        except Exception as exc:
            raise RuntimeError(f"Sunucuya cikis istegi gonderilemedi: {exc}") from exc

        if response is not None and response.status_code == 401:
            return True
        if not response.ok:
            message = ""
            if isinstance(payload, dict):
                message = str(payload.get("message") or payload.get("code") or "").strip()
            raise RuntimeError(message or f"Sunucu {response.status_code} hatasi dondurdu.")

        if not isinstance(payload, dict):
            raise RuntimeError("Sunucudan gecersiz cikis yaniti geldi.")
        if str(payload.get("status") or "").strip().lower() not in {"ok", "success"}:
            raise RuntimeError(str(payload.get("message") or "Masaustu oturumu kapatilamadi."))
        return True

    def _safe_int(self, value: Any, fallback: int = 0, minimum: int = 0) -> int:
        try:
            parsed = int(value)
        except (TypeError, ValueError):
            parsed = int(fallback)
        return max(int(minimum), parsed)

    def _coerce_bool(self, value: Any) -> bool:
        if isinstance(value, bool):
            return value
        if isinstance(value, (int, float)):
            return int(value) != 0
        return str(value or "").strip().lower() in {"1", "true", "yes", "on"}

    def _normalize_plan_type(self, plan_type: Any) -> str:
        raw = str(plan_type or "").strip().upper()
        if not raw:
            return ""
        if raw in self.LEGACY_PLAN_MAP:
            return self.LEGACY_PLAN_MAP[raw]
        return raw

    def _plan_has_sync(self, plan_type: str) -> bool:
        normalized = self._normalize_plan_type(plan_type)
        return bool((self.PLAN_FEATURES.get(normalized) or {}).get("sync"))

    def _plan_supports_web_mobile(self, plan_type: str) -> bool:
        normalized = self._normalize_plan_type(plan_type)
        return bool((self.PLAN_FEATURES.get(normalized) or {}).get("web_mobile"))

    def _plan_requires_remote_db(self, plan_type: str) -> bool:
        normalized = self._normalize_plan_type(plan_type)
        return bool((self.PLAN_FEATURES.get(normalized) or {}).get("remote_db"))

    def _plan_supports_backup(self, plan_type: str) -> bool:
        normalized = self._normalize_plan_type(plan_type)
        return bool((self.PLAN_FEATURES.get(normalized) or {}).get("backup"))

    def _compute_hardware_id(self) -> str:
        system_name = platform.system().strip().lower()
        parts = []
        if system_name == "windows":
            parts.extend(
                [
                    self._get_windows_baseboard_serial(),
                    self._get_windows_disk_serial(),
                ]
            )
        elif system_name == "darwin":
            parts.extend(
                [
                    self._get_macos_board_serial(),
                    self._get_macos_disk_serial(),
                ]
            )

        normalized_parts = [
            re.sub(r"\s+", "", str(part or "").strip()).upper()
            for part in parts
            if str(part or "").strip()
        ]

        if len(normalized_parts) < 2:
            node = platform.node().strip()
            mac = str(uuid.getnode()).strip()
            fallback = f"{system_name}|{node}|{mac}"
            normalized_parts.append(fallback)

        raw = "|".join([system_name] + normalized_parts)
        return hashlib.sha256(raw.encode("utf-8")).hexdigest()[:64]

    def _compute_legacy_hardware_id(self) -> str:
        if self._legacy_hardware_id_cache:
            return self._legacy_hardware_id_cache
        node = platform.node()
        mac = uuid.getnode()
        raw = f"{node}-{mac}"
        self._legacy_hardware_id_cache = hashlib.sha256(raw.encode("utf-8")).hexdigest()[:32]
        return self._legacy_hardware_id_cache

    def _hardware_id_candidates(self) -> list[str]:
        candidates = []
        for candidate in (
            self.get_hardware_id(),
            self._compute_legacy_hardware_id(),
        ):
            normalized = str(candidate or "").strip()
            if normalized and normalized not in candidates:
                candidates.append(normalized)
        return candidates

    def _run_command(self, args: list[str]) -> str:
        try:
            completed = subprocess.run(
                args,
                capture_output=True,
                text=True,
                timeout=8,
                check=False,
            )
        except Exception:
            return ""
        output = "\n".join(
            [str(completed.stdout or "").strip(), str(completed.stderr or "").strip()]
        ).strip()
        return output

    def _clean_hardware_value(self, value: str) -> str:
        cleaned = str(value or "").strip()
        cleaned = cleaned.replace("\x00", "").strip()
        if not cleaned:
            return ""
        lowered = cleaned.lower()
        if lowered in {"none", "unknown", "to be filled by o.e.m.", "default string", "system serial number"}:
            return ""
        return cleaned

    def _first_nonempty_line(self, output: str) -> str:
        for line in str(output or "").splitlines():
            cleaned = self._clean_hardware_value(line)
            if cleaned:
                return cleaned
        return ""

    def _regex_search(self, text: str, pattern: str) -> str:
        match = re.search(pattern, str(text or ""), flags=re.IGNORECASE | re.MULTILINE)
        if not match:
            return ""
        return self._clean_hardware_value(match.group(1))

    def _get_windows_baseboard_serial(self) -> str:
        commands = [
            [
                "powershell",
                "-NoProfile",
                "-Command",
                "(Get-CimInstance Win32_BaseBoard | Select-Object -First 1 -ExpandProperty SerialNumber)",
            ],
            ["wmic", "baseboard", "get", "serialnumber"],
        ]
        for command in commands:
            value = self._first_nonempty_line(self._run_command(command))
            if value and "serialnumber" not in value.lower():
                return value
        return ""

    def _get_windows_disk_serial(self) -> str:
        commands = [
            [
                "powershell",
                "-NoProfile",
                "-Command",
                "($v = Get-CimInstance Win32_PhysicalMedia | Where-Object { $_.SerialNumber } | "
                "Select-Object -First 1 -ExpandProperty SerialNumber); if ($v) { $v }",
            ],
            [
                "powershell",
                "-NoProfile",
                "-Command",
                "(Get-CimInstance Win32_DiskDrive | Select-Object -First 1 -ExpandProperty SerialNumber)",
            ],
            ["wmic", "diskdrive", "get", "serialnumber"],
        ]
        for command in commands:
            value = self._first_nonempty_line(self._run_command(command))
            if value and "serialnumber" not in value.lower():
                return value
        return ""

    def _get_macos_board_serial(self) -> str:
        output = self._run_command(
            ["ioreg", "-d2", "-c", "IOPlatformExpertDevice"]
        )
        value = self._regex_search(output, r'"IOPlatformSerialNumber"\s*=\s*"([^"]+)"')
        if value:
            return value
        value = self._regex_search(output, r'"IOPlatformUUID"\s*=\s*"([^"]+)"')
        if value:
            return value
        profile = self._run_command(["system_profiler", "SPHardwareDataType"])
        value = self._regex_search(profile, r"Serial Number \(system\):\s*(.+)")
        if value:
            return value
        return self._regex_search(profile, r"Hardware UUID:\s*(.+)")

    def _get_macos_disk_serial(self) -> str:
        for data_type in ("SPNVMeDataType", "SPSerialATADataType", "SPStorageDataType"):
            output = self._run_command(["system_profiler", data_type])
            value = self._regex_search(output, r"Serial Number:\s*(.+)")
            if value:
                return value
        diskutil = self._run_command(["diskutil", "info", "/"])
        value = self._regex_search(diskutil, r"Disk / Partition UUID:\s*(.+)")
        if value:
            return value
        return self._regex_search(diskutil, r"Volume UUID:\s*(.+)")

    def _parse_license_payload(self, payload_json: str) -> LicensePayload:
        if not payload_json:
            return LicensePayload()
        try:
            raw = json.loads(payload_json)
        except Exception:
            return LicensePayload()
        if not isinstance(raw, dict):
            return LicensePayload()
        return LicensePayload(
            customer_id=self._safe_int(raw.get("customer_id"), fallback=0, minimum=0),
            username=str(raw.get("username") or ""),
            plan_type=self._normalize_plan_type(raw.get("plan_type")),
            max_pc_devices=self._safe_int(raw.get("max_pc_devices", 1), fallback=1, minimum=1),
            max_web_sessions=self._safe_int(raw.get("max_web_sessions", 0), fallback=0, minimum=0),
            is_ai_enabled=self._safe_int(raw.get("is_ai_enabled", 0), fallback=0, minimum=0),
            expiry_date=str(raw.get("expiry_date") or ""),
            desktop_expiry_date=str(raw.get("desktop_expiry_date") or raw.get("expiry_date") or ""),
            sync_expiry_date=str(raw.get("sync_expiry_date") or ""),
            sync_status=str(raw.get("sync_status") or ""),
            web_mobile_enabled=1 if self._coerce_bool(raw.get("web_mobile_enabled")) else 0,
            license_status=str(raw.get("license_status") or "active"),
            hardware_binding=str(raw.get("hardware_binding") or ""),
            hardware_binding_algorithm=str(
                raw.get("hardware_binding_algorithm")
                or raw.get("hardware_binding_alg")
                or ""
            ),
            issued_at=str(raw.get("issued_at") or ""),
            nonce=str(raw.get("nonce") or ""),
            key_id=str(raw.get("key_id") or raw.get("license_key_id") or ""),
            algorithm=str(raw.get("algorithm") or "Ed25519"),
        )

    def _resolve_sync_status(self, data: Dict[str, Any]) -> str:
        if not isinstance(data, dict):
            return "disabled"
        if self._coerce_bool(data.get("offline_mode")):
            return "disabled"
        plan_type = self._normalize_plan_type(data.get("plan_type"))
        if not self._plan_has_sync(plan_type):
            return "disabled"
        explicit = str(data.get("sync_status") or "").strip().lower()
        if explicit in {"active", "expired", "disabled"}:
            if explicit == "active" and self._is_expired(str(data.get("sync_expiry_date") or "").strip()):
                return "expired"
            return explicit
        if self._is_expired(str(data.get("sync_expiry_date") or "").strip()):
            return "expired"
        return "active"

    def _resolve_web_mobile_enabled(self, data: Dict[str, Any]) -> bool:
        if not isinstance(data, dict):
            return False
        if self._coerce_bool(data.get("offline_mode")):
            return False
        plan_type = self._normalize_plan_type(data.get("plan_type"))
        if not self._plan_supports_web_mobile(plan_type):
            return False
        if self._resolve_sync_status(data) != "active":
            return False
        explicit = data.get("web_mobile_enabled")
        if explicit is None or str(explicit).strip() == "":
            return True
        return self._coerce_bool(explicit)

    def _resolve_backup_enabled(self, data: Dict[str, Any]) -> bool:
        if not isinstance(data, dict):
            return False
        if self._coerce_bool(data.get("offline_mode")):
            return False
        plan_type = self._normalize_plan_type(data.get("plan_type"))
        if not self._plan_supports_backup(plan_type):
            return False
        return self._resolve_sync_status(data) == "active"

    def _is_truthy(self, value: Any) -> bool:
        if isinstance(value, bool):
            return value
        return str(value or "").strip().lower() in {"1", "true", "yes", "on"}

    def _is_offline_license_valid(self, data: Optional[Dict[str, Any]] = None) -> bool:
        payload = data if isinstance(data, dict) else self.load_license()
        if not payload or not self._is_truthy(payload.get("offline_mode")):
            return False

        algorithm = str(payload.get("offline_mode_algorithm") or "").strip()
        if algorithm and algorithm != self.OFFLINE_MODE_ALGORITHM:
            return False
        return self._local_license_matches_current_hardware(payload)

    def _offline_mode_keys(self) -> tuple[bytes, bytes]:
        root = hashlib.sha256(
            os.getenv("GOLDER_OFFLINE_MODE_KEY", "golder-offline-mode-v1").encode("utf-8")
        ).digest()
        enc_key = hmac.new(root, b"enc", hashlib.sha256).digest()
        mac_key = hmac.new(root, b"mac", hashlib.sha256).digest()
        return enc_key, mac_key

    def _license_binding_keys(self) -> tuple[bytes, bytes]:
        root = hashlib.sha256(
            os.getenv("GOLDER_HW_BINDING_KEY", "golder-hardware-binding-v1").encode("utf-8")
        ).digest()
        enc_key = hmac.new(root, b"enc", hashlib.sha256).digest()
        mac_key = hmac.new(root, b"mac", hashlib.sha256).digest()
        return enc_key, mac_key

    def _local_blob_keys(self, purpose: str) -> tuple[bytes, bytes]:
        seed = hashlib.sha256(
            os.getenv("GOLDER_LOCAL_STATE_KEY", "golder-local-state-v1").encode("utf-8")
        ).digest()
        purpose_bytes = str(purpose or "default").encode("utf-8")
        enc_key = hmac.new(seed, b"enc:" + purpose_bytes, hashlib.sha256).digest()
        mac_key = hmac.new(seed, b"mac:" + purpose_bytes, hashlib.sha256).digest()
        return enc_key, mac_key

    def _license_binding_xor(self, data: bytes, key: bytes, nonce: bytes) -> bytes:
        if not data:
            return b""

        stream = bytearray()
        counter = 0
        while len(stream) < len(data):
            block = hashlib.sha256(key + nonce + counter.to_bytes(4, "big")).digest()
            stream.extend(block)
            counter += 1

        return bytes(chunk ^ stream[idx] for idx, chunk in enumerate(data))

    def _base64url_decode(self, value: str) -> bytes:
        normalized = str(value or "").strip()
        if not normalized:
            return b""
        padding = "=" * ((4 - len(normalized) % 4) % 4)
        try:
            return base64.urlsafe_b64decode(normalized + padding)
        except Exception:
            return b""

    def _base64url_encode(self, value: bytes) -> str:
        if not value:
            return ""
        return base64.urlsafe_b64encode(value).decode("ascii").rstrip("=")

    def _encrypt_local_blob(self, payload: bytes, version: bytes, purpose: str) -> str:
        if not payload:
            return ""
        enc_key, mac_key = self._local_blob_keys(purpose)
        nonce = os.urandom(16)
        ciphertext = self._license_binding_xor(payload, enc_key, nonce)
        mac = hmac.new(mac_key, version + nonce + ciphertext, hashlib.sha256).digest()
        return self._base64url_encode(version + nonce + ciphertext + mac)

    def _decrypt_local_blob(self, token: str, version: bytes, purpose: str) -> bytes:
        raw = self._base64url_decode(token)
        min_length = len(version) + 16 + 32
        if len(raw) < min_length or not raw.startswith(version):
            return b""
        nonce_start = len(version)
        nonce_end = nonce_start + 16
        nonce = raw[nonce_start:nonce_end]
        ciphertext = raw[nonce_end:-32]
        received_mac = raw[-32:]
        enc_key, mac_key = self._local_blob_keys(purpose)
        expected_mac = hmac.new(mac_key, version + nonce + ciphertext, hashlib.sha256).digest()
        if not hmac.compare_digest(expected_mac, received_mac):
            return b""
        try:
            return self._license_binding_xor(ciphertext, enc_key, nonce)
        except Exception:
            return b""

    def _encrypt_hardware_binding(self, hardware_id: str) -> str:
        normalized = str(hardware_id or "").strip()
        if not normalized:
            return ""

        enc_key, mac_key = self._license_binding_keys()
        nonce = os.urandom(16)
        plaintext = normalized.encode("utf-8")
        ciphertext = self._license_binding_xor(plaintext, enc_key, nonce)
        mac = hmac.new(
            mac_key,
            self.HARDWARE_BINDING_VERSION + nonce + ciphertext,
            hashlib.sha256,
        ).digest()
        return self._base64url_encode(self.HARDWARE_BINDING_VERSION + nonce + ciphertext + mac)

    def _decrypt_hardware_binding(self, value: str) -> str:
        raw = self._base64url_decode(value)
        min_length = len(self.HARDWARE_BINDING_VERSION) + 16 + 32
        if len(raw) < min_length or not raw.startswith(self.HARDWARE_BINDING_VERSION):
            return ""

        nonce_start = len(self.HARDWARE_BINDING_VERSION)
        nonce_end = nonce_start + 16
        nonce = raw[nonce_start:nonce_end]
        ciphertext = raw[nonce_end:-32]
        received_mac = raw[-32:]

        enc_key, mac_key = self._license_binding_keys()
        expected_mac = hmac.new(
            mac_key,
            self.HARDWARE_BINDING_VERSION + nonce + ciphertext,
            hashlib.sha256,
        ).digest()
        if not hmac.compare_digest(expected_mac, received_mac):
            return ""

        try:
            return self._license_binding_xor(ciphertext, enc_key, nonce).decode("utf-8").strip()
        except Exception:
            return ""

    def _license_matches_current_hardware(self, payload: LicensePayload) -> bool:
        binding = str(payload.hardware_binding or "").strip()
        if not binding:
            return False

        algorithm = str(payload.hardware_binding_algorithm or "").strip()
        if algorithm and algorithm != self.HARDWARE_BINDING_ALGORITHM:
            return False

        bound_hardware_id = self._decrypt_hardware_binding(binding)
        if not bound_hardware_id:
            return False
        for candidate in self._hardware_id_candidates():
            if hmac.compare_digest(bound_hardware_id, candidate):
                return True
        return False

    def _has_signed_hardware_binding(self, payload: LicensePayload) -> bool:
        return bool(str(payload.hardware_binding or "").strip())

    def _local_license_matches_current_hardware(self, data: Dict[str, Any]) -> bool:
        binding = str(data.get("local_hardware_binding") or "").strip()
        if not binding:
            return False

        algorithm = str(data.get("local_hardware_binding_algorithm") or "").strip()
        if algorithm and algorithm != self.HARDWARE_BINDING_ALGORITHM:
            return False

        bound_hardware_id = self._decrypt_hardware_binding(binding)
        if not bound_hardware_id:
            return False
        for candidate in self._hardware_id_candidates():
            if hmac.compare_digest(bound_hardware_id, candidate):
                return True
        return False

    def _is_current_hardware_authorized(self, data: Dict[str, Any], payload: LicensePayload) -> bool:
        if self._has_signed_hardware_binding(payload):
            return self._license_matches_current_hardware(payload)
        return self._local_license_matches_current_hardware(data)

    def _has_local_license_verifier(self) -> bool:
        try:
            from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey  # noqa: F401

            return True
        except Exception:
            pass

        try:
            from nacl.signing import VerifyKey  # noqa: F401

            return True
        except Exception:
            return False

    def _verify_license_signature_remote(
        self,
        payload_json: str,
        signature_b64: str,
        key_id: str = "",
        api_token: str = "",
    ) -> Dict[str, Any]:
        if not payload_json or not signature_b64 or not api_token:
            return {"ok": False, "message": "remote verification prerequisites are missing"}

        try:
            import requests
        except Exception:
            return {"ok": False, "message": "requests module is missing"}

        request_payload: Dict[str, Any] = {
            "license_payload": payload_json,
            "license_signature": signature_b64,
        }
        if key_id:
            request_payload["license_key_id"] = key_id

        try:
            response = requests.post(
                self.LICENSE_VERIFY_URL,
                json=request_payload,
                headers={"Authorization": f"Bearer {api_token}"},
                timeout=15,
            )
            response.raise_for_status()
            payload = response.json()
        except requests.exceptions.RequestException as exc:
            # İNTERNET YOKSA VEYA BAĞLANTI KOPTUYSA BURASI ÇALIŞIR
            return {"ok": False, "message": str(exc), "is_network_error": True}
        except Exception as exc:
            return {"ok": False, "message": str(exc), "is_network_error": False}

        if not isinstance(payload, dict):
            return {"ok": False, "message": "remote verification returned invalid JSON"}
        if str(payload.get("status") or "").strip().lower() not in {"ok", "success"}:
            return {"ok": False, "message": str(payload.get("message") or "remote verification failed")}
        if not bool(payload.get("valid")):
            return {"ok": False, "message": "license is not active on server"}
        if not bool(payload.get("signature_valid")):
            return {"ok": False, "message": "server rejected license signature"}

        fresh_license = payload.get("fresh_license")
        return {
            "ok": True,
            "fresh_license": dict(fresh_license) if isinstance(fresh_license, dict) else {},
        }

    def _verify_license_signature(
        self,
        payload_json: str,
        signature_b64: str,
        public_key_b64: str,
        algorithm: str = "Ed25519",
    ) -> bool:
        if algorithm.strip().lower() != "ed25519":
            return False
        try:
            signature = base64.b64decode(signature_b64, validate=True)
            public_key = base64.b64decode(public_key_b64, validate=True)
        except Exception:
            return False
        if not payload_json or not signature or not public_key:
            return False
        payload_bytes = payload_json.encode("utf-8")

        try:
            from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey

            Ed25519PublicKey.from_public_bytes(public_key).verify(signature, payload_bytes)
            return True
        except Exception:
            pass

        try:
            from nacl.signing import VerifyKey

            VerifyKey(public_key).verify(payload_bytes, signature)
            return True
        except Exception:
            return False

    def _runtime_state_path(self) -> Path:
        return self.license_path.parent / self.RUNTIME_STATE_FILE_NAME

    def _read_runtime_state(self) -> Dict[str, Any]:
        path = self._runtime_state_path()
        if not path.exists():
            return {}
        try:
            token = path.read_text(encoding="utf-8").strip()
            decrypted = self._decrypt_local_blob(
                token,
                version=self.RUNTIME_STATE_VERSION,
                purpose="runtime-state",
            )
            if not decrypted:
                return {}
            raw = json.loads(decrypted.decode("utf-8"))
            return dict(raw) if isinstance(raw, dict) else {}
        except Exception as exc:
            logger.warning(f"[Tenant] Runtime state could not be read: {exc}")
            return {}

    def _write_runtime_state(self, payload: Dict[str, Any]) -> None:
        path = self._runtime_state_path()
        try:
            encrypted = self._encrypt_local_blob(
                json.dumps(payload, ensure_ascii=False, separators=(",", ":")).encode("utf-8"),
                version=self.RUNTIME_STATE_VERSION,
                purpose="runtime-state",
            )
            if encrypted:
                path.write_text(encrypted, encoding="utf-8")
        except Exception as exc:
            logger.warning(f"[Tenant] Runtime state could not be written: {exc}")

    def record_last_run_time(self, now: Optional[datetime] = None) -> None:
        current = now or datetime.utcnow()
        payload = {
            "last_run_time": current.replace(microsecond=0).isoformat() + "Z",
            "hardware_id": self.get_hardware_id(),
        }
        self._write_runtime_state(payload)

    def _is_time_cheat_detected(self) -> bool:
        state = self._read_runtime_state()
        saved_hardware_id = str(state.get("hardware_id") or "").strip()
        current_hardware_id = self.get_hardware_id()
        if saved_hardware_id and current_hardware_id and saved_hardware_id != current_hardware_id:
            return False
        last_run = self._parse_datetime(str(state.get("last_run_time") or "").strip(), assume_utc=True)
        if last_run is None:
            return False
        return datetime.utcnow() + timedelta(seconds=self.TIME_CHEAT_GRACE_SECONDS) < last_run

    def _parse_datetime(self, value: str, assume_utc: bool = False) -> Optional[datetime]:
        raw = str(value or "").strip()
        if not raw:
            return None

        for fmt in ("%Y-%m-%d %H:%M:%S", "%Y-%m-%d"):
            try:
                parsed = datetime.strptime(raw, fmt)
                if fmt == "%Y-%m-%d":
                    return parsed.replace(hour=23, minute=59, second=59)
                return parsed
            except ValueError:
                pass

        normalized = raw.replace("Z", "+00:00")
        try:
            parsed = datetime.fromisoformat(normalized)
        except ValueError:
            return None

        if parsed.tzinfo is not None:
            if assume_utc:
                parsed = parsed.astimezone(timezone.utc).replace(tzinfo=None)
            else:
                parsed = parsed.astimezone().replace(tzinfo=None)
        return parsed

    def _is_expired(self, expiry_value: str) -> bool:
        expiry = self._parse_datetime(str(expiry_value or "").strip())
        if expiry is None:
            return False
        return expiry < datetime.now()




