# -*- coding: utf-8 -*-
"""
AI Maketing Facebook — Auth client ONLINE-ONLY + anti-crack
- Bắt buộc kết nối license server chính thức (pin URL)
- Response license có HMAC chữ ký (từ chối fake server)
- 1 tài khoản chỉ 1 máy tại một thời điểm
- Heartbeat định kỳ; mất mạng / bị kick / chữ ký sai → không cho dùng
- Session local có integrity hash (chống sửa tay plan_expires)
"""
from __future__ import annotations

import hashlib
import hmac
import json
import os
import platform
import time
import uuid
from datetime import datetime
from typing import Any, Dict, List, Optional, Tuple
from urllib import error as urlerror
from urllib import request as urlrequest
from urllib.parse import urlparse

USERS_FILE = 'users_v10.json'  # legacy local (không dùng khi online)
SESSION_FILE = 'session_v10.json'
SETTINGS_FILE = 'app_settings_v10.json'
MACHINE_FILE = 'machine_id.txt'

PLANS = {
    'trial': {
        'label': 'Dùng thử',
        'days': 3,
        'price_vnd': 0,
        'desc': 'Dùng thử miễn phí 3 ngày',
        'tier': 'personal',
    },
    'monthly': {
        'label': 'Gói tháng',
        'days': 30,
        'price_vnd': 999000,
        'desc': '1 máy',
        'tier': 'personal',
    },
    'yearly': {
        'label': 'Gói năm',
        'days': 365,
        'price_vnd': 5999000,
        'desc': '1 máy',
        'tier': 'personal',
    },
    'yearly3': {
        'label': 'Gói 3 năm',
        'days': 365 * 3,
        'price_vnd': 14999000,
        'desc': '1 máy',
        'tier': 'personal',
    },
    'biz_monthly': {
        'label': 'Business tháng',
        'days': 30,
        'price_vnd': 4995000,
        'desc': 'Không giới hạn máy · ×5',
        'tier': 'business',
    },
    'biz_yearly': {
        'label': 'Business năm',
        'days': 365,
        'price_vnd': 29995000,
        'desc': 'Không giới hạn máy · ×5',
        'tier': 'business',
    },
    'biz_yearly3': {
        'label': 'Business 3 năm',
        'days': 365 * 3,
        'price_vnd': 74995000,
        'desc': 'Không giới hạn máy · ×5',
        'tier': 'business',
    },
}

DEFAULT_TRIAL_DAYS = 3
HOTLINE = '0379376177'
ZALO_URL = 'https://zalo.me/0379376177'
SUPPORT_EMAIL = 'aimaketing.facebook@gmail.com'
# Production license API (VPS Contabo qua HTTPS aurexislab.com)
DEFAULT_SERVER = 'https://aurexislab.com/license-api'
HEARTBEAT_SEC = 45
# Chỉ cho phép các URL license chính thức (chống trỏ fake server)
ALLOWED_SERVERS = (
    'https://aurexislab.com/license-api',
    'https://www.aurexislab.com/license-api',
)
# Chữ ký hợp lệ trong vòng 15 phút
SIG_MAX_SKEW_SEC = 900


def _now_s() -> str:
    return datetime.now().strftime('%Y-%m-%d %H:%M:%S')


def secrets_token() -> str:
    return uuid.uuid4().hex


def _license_sign_secret() -> str:
    """
    Khóa HMAC ghép từ mảnh (làm chậm reverse engineer sơ bộ).
    Phải khớp LICENSE_SIGN_SECRET trên server.
    """
    # fragments of 0b59306941b1e9910e556bdd25abf2479994543f9d49179b7a73ae98359739e6
    a = '0b59306941b1e991'
    b = '0e556bdd25abf247'
    c = bytes([0x99, 0x94, 0x54, 0x3f, 0x9d, 0x49, 0x17, 0x9b]).hex()
    d = '7a73ae98359739e6'
    raw = a + b + c + d
    # integrity of assembled key (detect trivial patch of fragments)
    expect = '70ef9fabe36afd8a'
    chk = hashlib.sha256(raw.encode('utf-8')).hexdigest()[:16]
    if chk != expect:
        # key fragments bị patch — trả secret rỗng để verify luôn fail
        return ''
    return raw


def _normalize_server(url: str) -> str:
    u = (url or '').strip().rstrip('/')
    if not u:
        return DEFAULT_SERVER
    # strip trailing path noise
    return u


def _is_allowed_server(url: str) -> bool:
    # Dev override only with env (never ship enabled)
    if os.environ.get('AIMAKETING_DEV_LICENSE') == '1':
        return True
    u = _normalize_server(url).lower()
    allowed = {_normalize_server(x).lower() for x in ALLOWED_SERVERS}
    allowed.add(_normalize_server(DEFAULT_SERVER).lower())
    return u in allowed


def _verify_license_sig(data: dict, token_hint: str = '') -> Tuple[bool, str]:
    """Xác thực HMAC từ server. Fake server / patch response → fail."""
    if not isinstance(data, dict):
        return False, 'Phản hồi license không hợp lệ'
    sig = (data.get('sig') or '').strip()
    ts = data.get('sig_ts')
    if not sig or ts is None:
        return False, 'Server license thiếu chữ ký (có thể giả mạo / bản cũ). Cập nhật server.'
    try:
        ts_i = int(ts)
    except Exception:
        return False, 'Chữ ký license hỏng (ts)'
    now = int(time.time())
    if abs(now - ts_i) > SIG_MAX_SKEW_SEC:
        return False, 'Chữ ký license hết hạn (đồng hồ máy lệch hoặc replay).'

    user = data.get('user') or {}
    token = data.get('token') or token_hint or ''
    parts = [
        str(user.get('id') or ''),
        str(user.get('username') or ''),
        str(user.get('plan') or ''),
        str(user.get('plan_expires') or ''),
        '1' if user.get('active') else '0',
        '1' if user.get('banned') else '0',
        token,
        str(ts_i),
    ]
    msg = '|'.join(parts)
    expect = hmac.new(
        _license_sign_secret().encode('utf-8'),
        msg.encode('utf-8'),
        hashlib.sha256,
    ).hexdigest()
    if not hmac.compare_digest(expect, sig):
        return False, 'Chữ ký license sai — từ chối fake server / dữ liệu bị sửa.'
    if user.get('banned'):
        return False, 'Tài khoản bị khóa'
    return True, 'OK'


def _session_integrity(token: str, user: dict, machine_id: str) -> str:
    blob = json.dumps(
        {
            't': token,
            'id': (user or {}).get('id'),
            'u': (user or {}).get('username'),
            'p': (user or {}).get('plan'),
            'e': (user or {}).get('plan_expires'),
            'a': bool((user or {}).get('active')),
            'm': machine_id,
        },
        sort_keys=True,
        ensure_ascii=False,
    )
    key = _license_sign_secret() + '|' + (machine_id or '')
    return hmac.new(key.encode('utf-8'), blob.encode('utf-8'), hashlib.sha256).hexdigest()


class AuthSystem:
    """Client kết nối license server — không cho chạy offline / fake server."""

    def __init__(self, base_dir: Optional[str] = None):
        self.base_dir = base_dir or os.path.dirname(os.path.abspath(__file__))
        try:
            os.makedirs(self.base_dir, exist_ok=True)
        except Exception:
            pass
        self.settings_path = os.path.join(self.base_dir, SETTINGS_FILE)
        self.session_path = os.path.join(self.base_dir, SESSION_FILE)
        self.machine_path = os.path.join(self.base_dir, MACHINE_FILE)
        self.settings = self._load_settings()
        raw_url = (self.settings.get('server_url') or DEFAULT_SERVER)
        self.server_url = self._pin_server(raw_url)
        self.online_required = True  # chống crack offline — không cho tắt
        self.machine_id = self._ensure_machine_id()
        self.machine_name = f"{platform.node()} | {platform.system()} {platform.release()}"
        self.token: str = ''
        self.current_user: Optional[Dict[str, Any]] = None
        self.last_error: str = ''
        self.last_heartbeat_ok: bool = False
        self.last_sig_ok: bool = False
        self.last_sig_ts: int = 0
        self._load_local_session()

    # ------------------------------------------------------------------
    # Settings / machine
    # ------------------------------------------------------------------
    def _pin_server(self, url: str) -> str:
        u = _normalize_server(url)
        if _is_allowed_server(u):
            return u
        # Tự động kéo về server chính thức — chặn fake server
        return DEFAULT_SERVER

    def _load_settings(self) -> Dict[str, Any]:
        default = {
            'theme': 'light',
            'hotline': HOTLINE,
            'trial_days_default': DEFAULT_TRIAL_DAYS,
            'server_url': DEFAULT_SERVER,
            'online_required': True,
        }
        try:
            with open(self.settings_path, 'r', encoding='utf-8') as f:
                data = json.load(f)
            if isinstance(data, dict):
                default.update(data)
        except Exception:
            pass
        # Force anti-crack flags
        default['online_required'] = True
        default['server_url'] = self._pin_server(default.get('server_url') or DEFAULT_SERVER)
        return default

    def save_settings(self) -> None:
        self.settings['online_required'] = True
        self.settings['server_url'] = self._pin_server(self.settings.get('server_url') or DEFAULT_SERVER)
        with open(self.settings_path, 'w', encoding='utf-8') as f:
            json.dump(self.settings, f, indent=2, ensure_ascii=False)

    def set_server_url(self, url: str) -> None:
        # Production: chỉ cho phép server trong allowlist
        pinned = self._pin_server(url or DEFAULT_SERVER)
        if not _is_allowed_server(url or '') and os.environ.get('AIMAKETING_DEV_LICENSE') != '1':
            pinned = DEFAULT_SERVER
        self.server_url = pinned
        self.settings['server_url'] = self.server_url
        self.save_settings()

    def _ensure_machine_id(self) -> str:
        if os.path.exists(self.machine_path):
            try:
                mid = open(self.machine_path, 'r', encoding='utf-8').read().strip()
                if len(mid) >= 16:
                    return mid
            except Exception:
                pass
        raw = f"{platform.node()}|{uuid.getnode()}|{platform.machine()}|{secrets_token()}"
        mid = hashlib.sha256(raw.encode('utf-8')).hexdigest()
        try:
            with open(self.machine_path, 'w', encoding='utf-8') as f:
                f.write(mid)
        except Exception:
            pass
        return mid

    def _save_local_session(self) -> None:
        user = self.current_user or {}
        data = {
            'token': self.token,
            'user': user,
            'machine_id': self.machine_id,
            'at': _now_s(),
            'server_url': self.server_url,
            'integrity': _session_integrity(self.token, user, self.machine_id),
            'sig_ts': self.last_sig_ts,
        }
        with open(self.session_path, 'w', encoding='utf-8') as f:
            json.dump(data, f, indent=2, ensure_ascii=False)

    def _load_local_session(self) -> None:
        try:
            with open(self.session_path, 'r', encoding='utf-8') as f:
                data = json.load(f)
            token = data.get('token') or ''
            user = data.get('user')
            mid = data.get('machine_id') or ''
            integ = data.get('integrity') or ''
            # Session copy sang máy khác / sửa file → invalid
            if mid and mid != self.machine_id:
                self.token = ''
                self.current_user = None
                return
            if token and user:
                expect = _session_integrity(token, user, self.machine_id)
                if integ and not hmac.compare_digest(expect, integ):
                    # file bị sửa tay (plan/active) — bỏ session, bắt login lại
                    self.token = ''
                    self.current_user = None
                    try:
                        os.remove(self.session_path)
                    except Exception:
                        pass
                    return
            self.token = token
            self.current_user = user
            if data.get('server_url'):
                self.server_url = self._pin_server(data['server_url'])
        except Exception:
            self.token = ''
            self.current_user = None

    def clear_session(self) -> None:
        self.token = ''
        self.current_user = None
        self.last_heartbeat_ok = False
        self.last_sig_ok = False
        self.last_sig_ts = 0
        try:
            if os.path.exists(self.session_path):
                os.remove(self.session_path)
        except Exception:
            pass

    def _accept_signed_user(self, data: dict, token: Optional[str] = None) -> Tuple[bool, str]:
        tok = token if token is not None else (data.get('token') or self.token or '')
        ok, msg = _verify_license_sig(data, token_hint=tok)
        if not ok:
            self.last_sig_ok = False
            self.last_heartbeat_ok = False
            return False, msg
        user = data.get('user') or {}
        if not user.get('active'):
            self.last_sig_ok = False
            self.last_heartbeat_ok = False
            return False, 'Gói hết hạn hoặc bị khóa'
        if token is not None and token:
            self.token = token
        elif data.get('token'):
            self.token = data['token']
        self.current_user = user
        self.last_sig_ok = True
        self.last_sig_ts = int(data.get('sig_ts') or time.time())
        self.last_heartbeat_ok = True
        self._save_local_session()
        return True, 'OK'

    # ------------------------------------------------------------------
    # HTTP
    # ------------------------------------------------------------------
    def _api(self, path: str, payload: Optional[dict] = None, method: str = 'POST', auth: bool = False) -> Tuple[bool, dict]:
        if not _is_allowed_server(self.server_url) and os.environ.get('AIMAKETING_DEV_LICENSE') != '1':
            self.server_url = DEFAULT_SERVER
        url = self.server_url + path
        # Chỉ HTTPS production (trừ dev)
        if os.environ.get('AIMAKETING_DEV_LICENSE') != '1':
            parsed = urlparse(url)
            if parsed.scheme != 'https' or not parsed.netloc.endswith('aurexislab.com'):
                return False, {
                    'ok': False,
                    'error': 'Server license không hợp lệ. Chỉ chấp nhận aurexislab.com (HTTPS).',
                }
        headers = {
            'Content-Type': 'application/json',
            'User-Agent': 'AI-Maketing-Facebook-Client/1.1',
            'X-Client-Machine': (self.machine_id or '')[:32],
        }
        if auth and self.token:
            headers['Authorization'] = f'Bearer {self.token}'
        body = None
        if payload is not None:
            body = json.dumps(payload).encode('utf-8')
        req = urlrequest.Request(url, data=body, headers=headers, method=method)
        try:
            with urlrequest.urlopen(req, timeout=12) as resp:
                raw = resp.read().decode('utf-8', errors='replace')
                data = json.loads(raw) if raw else {}
                return True, data if isinstance(data, dict) else {'ok': False, 'error': 'Phản hồi server không hợp lệ'}
        except urlerror.HTTPError as e:
            try:
                raw = e.read().decode('utf-8', errors='replace')
                data = json.loads(raw) if raw else {}
            except Exception:
                data = {'ok': False, 'error': f'HTTP {e.code}'}
            if not data.get('error'):
                data['error'] = f'HTTP {e.code}'
            data['ok'] = False
            return False, data
        except Exception as e:
            self.last_error = str(e)
            return False, {
                'ok': False,
                'error': f'Không kết nối được server license ({self.server_url}). Không cho dùng offline. Lỗi: {e}',
            }

    def check_server(self) -> Tuple[bool, str]:
        ok, data = self._api('/api/health', method='GET')
        if ok and data.get('ok'):
            if not data.get('signed_license') and data.get('sig_v') != 1:
                # old server still ok for health, but login will require sig
                pass
            return True, f"Server online · {data.get('time', '')}"
        return False, data.get('error') or 'Server offline — app không chạy được.'

    # ------------------------------------------------------------------
    # Auth API
    # ------------------------------------------------------------------
    def register(self, username: str, password: str, email: str = '', phone: str = '', full_name: str = '', trial_days=None) -> Tuple[bool, str]:
        ok, data = self._api('/api/register', {
            'username': username,
            'password': password,
            'email': email,
            'phone': phone,
            'full_name': full_name,
        })
        if not ok or not data.get('ok'):
            return False, data.get('error') or data.get('message') or 'Đăng ký thất bại'
        return True, data.get('message') or 'Đăng ký thành công'

    def login(self, username_or_email: str, password: str) -> Tuple[bool, str]:
        s_ok, s_msg = self.check_server()
        if not s_ok:
            return False, s_msg

        ok, data = self._api('/api/login', {
            'username': username_or_email,
            'password': password,
            'machine_id': self.machine_id,
            'machine_name': self.machine_name,
        })
        if not ok or not data.get('ok'):
            return False, data.get('error') or 'Đăng nhập thất bại'

        token = data.get('token') or ''
        vok, vmsg = self._accept_signed_user(data, token=token)
        if not vok:
            self.clear_session()
            return False, vmsg
        return True, data.get('message') or 'Đăng nhập OK'

    def try_restore_session(self) -> bool:
        """Khôi phục session chỉ khi heartbeat online + chữ ký OK."""
        if not self.token:
            self._load_local_session()
        if not self.token:
            return False
        ok, msg = self.heartbeat()
        if not ok:
            self.clear_session()
            self.last_error = msg
            return False
        return True

    def logout(self) -> None:
        if self.token:
            try:
                self._api('/api/logout', {}, auth=True)
            except Exception:
                pass
        self.clear_session()

    def heartbeat(self) -> Tuple[bool, str]:
        if not self.token:
            self.last_heartbeat_ok = False
            self.last_sig_ok = False
            return False, 'Chưa đăng nhập'
        ok, data = self._api('/api/heartbeat', {
            'machine_id': self.machine_id,
        }, auth=True)
        if not ok or not data.get('ok'):
            self.last_heartbeat_ok = False
            self.last_sig_ok = False
            err = data.get('error') or 'Heartbeat thất bại — mất kết nối hoặc session bị đá'
            if 'Session' in err or 'token' in err.lower() or 'máy' in err.lower() or 'không hợp lệ' in err.lower():
                self.clear_session()
            return False, err
        # Bắt buộc verify chữ ký — chống fake heartbeat
        vok, vmsg = self._accept_signed_user(data, token=self.token)
        if not vok:
            self.clear_session()
            return False, vmsg
        return True, 'OK'

    def is_license_active(self) -> bool:
        """Online + gói còn hạn + session còn sống + chữ ký hợp lệ."""
        if not self.token or not self.current_user:
            return False
        ok, _ = self.heartbeat()
        if not ok:
            return False
        return bool((self.current_user or {}).get('active')) and self.last_sig_ok and self.last_heartbeat_ok

    def quick_license_ok(self) -> bool:
        """Kiểm tra nhanh (không HTTP) — dùng kèm heartbeat gần đây."""
        if not self.token or not self.current_user:
            return False
        if not self.last_heartbeat_ok or not self.last_sig_ok:
            return False
        if not self.current_user.get('active'):
            return False
        # chữ ký quá cũ → bắt heartbeat lại
        if self.last_sig_ts and (int(time.time()) - int(self.last_sig_ts) > SIG_MAX_SKEW_SEC):
            return False
        return True

    def license_status(self, user: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
        user = user or self.current_user
        if not user:
            return {
                'active': False,
                'plan': None,
                'label': 'Chưa đăng nhập',
                'days_left': 0,
                'expires': '',
                'message': 'Vui lòng đăng nhập online.',
                'expired': True,
            }
        if self.token:
            self.heartbeat()
            user = self.current_user or user
        active = bool(user.get('active')) and self.last_heartbeat_ok and self.last_sig_ok
        return {
            'active': active,
            'expired': not active,
            'plan': user.get('plan'),
            'label': user.get('plan_label') or user.get('plan') or '',
            'days_left': user.get('days_left') or 0,
            'expires': user.get('plan_expires') or '',
            'message': (
                f"Online · {user.get('plan_label')} còn ~{user.get('days_left')} ngày (HSD {user.get('plan_expires')})"
                if active else
                f"Không dùng được: hết hạn / offline / session bị đá. Zalo {HOTLINE}"
            ),
            'price_vnd': PLANS.get(user.get('plan') or '', {}).get('price_vnd', 0),
        }

    def get_theme(self) -> str:
        if self.current_user and self.current_user.get('theme') in ('light', 'dark'):
            return self.current_user['theme']
        return self.settings.get('theme') or 'light'

    def set_theme(self, theme: str) -> None:
        theme = 'light' if theme == 'light' else 'dark'
        self.settings['theme'] = theme
        self.save_settings()
        if self.token:
            ok, data = self._api('/api/profile', {'theme': theme}, auth=True)
            if ok and data.get('user'):
                self.current_user = data['user']
                self._save_local_session()
        elif self.current_user:
            self.current_user['theme'] = theme

    def update_profile(self, full_name=None, email=None, phone=None, note=None) -> Tuple[bool, str]:
        if not self.token:
            return False, 'Chưa đăng nhập online'
        payload = {}
        if full_name is not None:
            payload['full_name'] = full_name
        if email is not None:
            payload['email'] = email
        if phone is not None:
            payload['phone'] = phone
        ok, data = self._api('/api/profile', payload, auth=True)
        if not ok or not data.get('ok'):
            return False, data.get('error') or 'Cập nhật thất bại'
        self.current_user = data.get('user') or self.current_user
        self._save_local_session()
        return True, data.get('message') or 'Đã cập nhật'

    def change_password(self, old_password: str, new_password: str) -> Tuple[bool, str]:
        if not self.token:
            return False, 'Chưa đăng nhập'
        ok, data = self._api('/api/change_password', {
            'old_password': old_password,
            'new_password': new_password,
        }, auth=True)
        if not ok or not data.get('ok'):
            return False, data.get('error') or 'Đổi mật khẩu thất bại'
        return True, data.get('message') or 'OK'

    def set_plan(self, plan: str, username: Optional[str] = None, extend: bool = True) -> Tuple[bool, str]:
        return False, 'Chỉ admin web được set gói (server). Mở /admin trên license server.'

    def admin_set_trial_days(self, days: int) -> Tuple[bool, str]:
        return False, 'Cấu hình trial trên license server (TRIAL_DAYS).'

    def public_user_view(self, user: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
        user = user or self.current_user
        if not user:
            return {}
        return dict(user)

    # ------------------------------------------------------------------
    # Cloud sync (nick FB + proxy theo user)
    # ------------------------------------------------------------------
    def sync_pull(self) -> Tuple[bool, dict]:
        if not self.token or not self.quick_license_ok():
            # still try if just logged in mid-path
            if not self.token:
                return False, {'error': 'Chưa đăng nhập'}
        ok, data = self._api('/api/sync/pull', {}, auth=True)
        if not ok or not data.get('ok'):
            return False, data if isinstance(data, dict) else {'error': 'Pull thất bại'}
        return True, data

    def sync_push(
        self,
        accounts=None,
        proxies=None,
        scenario_progress=None,
        mode: str = 'replace',
    ) -> Tuple[bool, dict]:
        if not self.token:
            return False, {'error': 'Chưa đăng nhập'}
        payload = {'mode': mode}
        if accounts is not None:
            payload['accounts'] = accounts
        if proxies is not None:
            payload['proxies'] = proxies
        if scenario_progress is not None:
            payload['scenario_progress'] = scenario_progress
        ok, data = self._api('/api/sync/push', payload, auth=True)
        if not ok or not data.get('ok'):
            return False, data if isinstance(data, dict) else {'error': 'Push thất bại'}
        return True, data

    @property
    def users(self):
        return []
