import time
import json
import random
import re
import os
import uuid
import shutil
import socket
from datetime import datetime
from typing import List, Dict, Optional, Any, Callable
from urllib.parse import urljoin, urlparse, urlencode, parse_qs
from concurrent.futures import ThreadPoolExecutor
from DrissionPage import ChromiumPage, ChromiumOptions

import configure
from vs_plg import IVSPlg
from vs_types import VSPlgConfig, AppointmentType, VSQueryResult, VSBookResult, AvailabilityStatus, TimeSlot, DateAvailability, NotFoundError, PermissionDeniedError, RateLimiteddError, SessionExpiredOrInvalidError, BizLogicError 
from utils.cloudflare_bypass_for_scraping import CloudflareBypasser
from toolkit.mihomo_tunnel import MihomoTunnel
from utils.mouse import HumanMouse, MOUSE_PROFILES
from utils.keyboard import HumanKeyboard
from utils.fingerprint_utils import FingerprintGenerator


class BrowserResponse:
    """模拟 requests.Response"""
    def __init__(self, result_dict):
        result_dict = result_dict or {}
        self.status_code = result_dict.get('status', 0)
        self.text = result_dict.get('body', '')
        self.headers = result_dict.get('headers', {})
        self.url = result_dict.get('url', '')
        self._json = None

    def json(self):
        if self._json is None:
            if not self.text:
                return {}
            try:
                self._json = json.loads(self.text)
            except:
                self._json = {}
        return self._json

class TlsPlugin(IVSPlg):
    """
    TLSContact 签证预约插件 (DrissionPage 版)
    """

    def __init__(self, group_id: str):
        self.group_id = group_id
        self.config: Optional[VSPlgConfig] = None
        self.free_config: Dict[str, Any] = {}
        self.is_healthy = True
        self.logger = None
        
        self.mouse = None
        self.keyboard = None
        self.page: Optional[ChromiumPage] = None    
        self.travel_group: Optional[Dict] = None

        self.instance_id = uuid.uuid4().hex[:8]
        self.root_workspace = os.path.abspath(os.path.join("data/temp_browser_data", f"{self.group_id}.{self.instance_id}"))
        self.user_data_path = os.path.join(self.root_workspace, "user_data")
    
        if not os.path.exists(self.root_workspace):
            os.makedirs(self.root_workspace)
        
        self.tunnel = None
        self.session_create_time: float = 0
    
    def set_log(self, logger: Callable[[str], None]):
        self.logger = logger
    
    def _log(self, message):
        if self.logger:
            self.logger(f'[TlsPlugin] [{self.group_id}] [{self.instance_id}] {message}')
        else:
            print(f'[TlsPlugin] [{self.group_id}] [{self.instance_id}] {message}')

    def set_config(self, config: VSPlgConfig):
        self.config = config
        self.free_config = config.free_config or {}
        
    def keep_alive(self):
        try:
            self.page.refresh()
            self.page.wait.load_start(timeout=2)
            self.page.wait.doc_loaded()
            time.sleep(random.uniform(1, 3))
            self._check_page_is_session_expired_or_invalid('Book your appointment', html = self.page.html)
            self.simulate_random_human_mouse_move()
        except SessionExpiredOrInvalidError as e:
            self.is_healthy = False
        except Exception as e:
            self._log(f"Unexpected error in keep_alive: {e}")
            
    def simulate_random_human_mouse_move(self, min_x=100, max_x=800, min_y=100, max_y=800, min_points=1, max_points=2):
        """
        在指定区域内模拟人类随机移动鼠标并点击数次。
        
        :param min_x: X坐标最小范围
        :param max_x: X坐标最大范围
        :param min_y: Y坐标最小范围
        :param max_y: Y坐标最大范围
        :param min_point: 随便移动的最少次数
        :param max_point: 随便移动的最多次数
        """
        move_cnt = random.randint(min_points, max_points)
        self._log(f"Starting random human simulation: will move {move_cnt} times in the area.")
        for i in range(move_cnt):
            rand_x = random.randint(min_x, max_x)
            rand_y = random.randint(min_y, max_y)
            self._log(f"[{i+1}/{move_cnt}] Moving mouse to ({rand_x}, {rand_y})")
            self.mouse.move(rand_x, rand_y, humanize=True)
        self._log("Random human move simulation completed.")

    def health_check(self) -> bool:
        if not self.is_healthy:
            return False
        if self.page is None:
            return False
        try:
            if not self.page.run_js("return 1;"):
                return False
        except:
            return False
            
        if self.config.session_max_life > 0:
            current_time = time.time()
            elapsed_time = current_time - self.session_create_time
            if elapsed_time > self.config.session_max_life:
                self._log(f"Session expired.")
                return False
        return True
    
    def _save_screenshot(self, name_prefix):
        try:
            timestamp = int(time.time())
            filename = f"{self.instance_id}_{name_prefix}_{timestamp}.jpg"
            save_path = os.path.join("data", filename)
            os.makedirs("data", exist_ok=True)
            self.page.get_screenshot(path=save_path, full_page=False)
            self._log(f"Screenshot saved to {save_path}")
        except Exception as e:
            self._log(f"Failed to save screenshot: {e}")

    def create_session(self):
        """
        全浏览器会话创建：过盾 -> JS注入登录 -> 状态机自动路由导航 -> 到达目标页
        """
        self._log(f"Initializing Session (ID: {self.instance_id})...")
        captcha_future = None
        captcha_executor = ThreadPoolExecutor(max_workers=1)
        login_captcha_cfg = self.free_config.get("login_captcha", {})
        if login_captcha_cfg.get('solve_advance'):
            login_page = login_captcha_cfg.get("page_url")
            site_key = login_captcha_cfg.get("site_key")
            task_type = login_captcha_cfg.get("task")
            self._log(f"🚀 Early starting background Captcha solve for sitekey={site_key}")
            rc_params = {
                "type": task_type,
                "page": login_page,
                "siteKey": site_key, 
                "apiToken": self.free_config.get("capsolver_key", "")
            }
            captcha_future = captcha_executor.submit(self._solve_recaptcha, rc_params)
        co = ChromiumOptions()
        
        def get_free_port():
            with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
                s.bind(('', 0))
                return s.getsockname()[1]
        
        debug_port = get_free_port()
        self._log(f"Assigned Debug Port: {debug_port}")
        self._log(f"Account id={self.config.account.id}, proxy id={self.config.proxy.id}")
    
        co.set_local_port(debug_port)       
        co.set_user_data_path(self.user_data_path)
        
        chrome_path = configure.CHROME_PATH
        if not chrome_path:
            chrome_path = os.getenv("CHROME_BIN")
        if chrome_path and os.path.exists(chrome_path):
            co.set_paths(browser_path=chrome_path)
        
        if self.config.proxy and self.config.proxy.ip:
            p = self.config.proxy
            if p.username and p.password:
                self._log(f"Starting Proxy Tunnel for {p.ip}...")
                exit_node = {
                    "name": "ExitNode",
                    "type": p.proto,
                    "server": p.ip,
                    "port": p.port,
                    "username": p.username,
                    "password": p.password
                }
                relay_node = None
                if configure.MIHOMO_RELAY_NODES:
                    relay_node = random.choice(configure.MIHOMO_RELAY_NODES)
                mihomo_path = configure.MIHOMO_BIN_PATH
                if not mihomo_path:
                    mihomo_path = os.getenv("MIHOMO_BIN")
                if not mihomo_path:
                    raise BizLogicError(message='Mihomo path is null, You need set mihomo bin path in configure or os env')
                self.tunnel = MihomoTunnel(mihomo_path, exit_node=exit_node, relay_node=relay_node)
                local_proxy = self.tunnel.start()
                self._log(f"Tunnel started at {local_proxy}")
                co.set_argument(f'--proxy-server={local_proxy}')
            else:
                proxy_str = f"{p.proto}://{p.ip}:{p.port}"
                co.set_argument(f'--proxy-server={proxy_str}')
        else:
            self._log("[WARN] No proxy configured!")

        specific_fp = FingerprintGenerator().generate(self.config.account.username)
        fp_seed = specific_fp.get("seed")
        fp_platform = specific_fp.get("platform")
        fp_brand = specific_fp.get("brand")
        self._log(f'browser fingerprint seed={fp_seed}')

        co.headless(False)
        co.set_argument('--no-sandbox')
        co.set_argument('--disable-dev-shm-usage')
        co.set_argument('--window-size=1920,1080')
        co.set_argument('--disable-blink-features=AutomationControlled')
        # co.set_argument('--ignore-gpu-blocklist')
        # co.set_argument('--enable-webgl')
        # co.set_argument('--use-gl=angle')
        # co.set_argument('--use-angle=swiftshader')
        co.set_argument(f"--fingerprint={fp_seed}")
        co.set_argument(f"--fingerprint-platform={fp_platform}")
        co.set_argument(f"--fingerprint-brand={fp_brand}")
        
        self.page = ChromiumPage(co)
        if self.config.debug:
            self.page.get('https://example.com')
            js_script = """
            function getFingerprint() {
                let webglVendor = 'Unknown';
                let webglRenderer = 'Unknown';
                try {
                    let canvas = document.createElement('canvas');
                    let gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
                    if (gl) {
                        let debugInfo = gl.getExtension('WEBGL_debug_renderer_info');
                        if (debugInfo) {
                            webglVendor = gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL);
                            webglRenderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL);
                        }
                    }
                } catch(e) {}

                return {
                    "User-Agent": navigator.userAgent,
                    "Platform": navigator.userAgentData ? navigator.userAgentData.platform : navigator.platform,
                    "Brands": navigator.userAgentData ? navigator.userAgentData.brands.map(b => b.brand).join(', ') : 'Not Supported',
                    "CPU Cores": navigator.hardwareConcurrency,
                    "Language": navigator.language,
                    "Timezone": Intl.DateTimeFormat().resolvedOptions().timeZone,
                    "WebGL Vendor": webglVendor,
                    "WebGL Renderer": webglRenderer
                };
            }
            return getFingerprint();
            """
            fp_data = self.page.run_js(js_script)
            self._log("================ 预检浏览器指纹数据 ================")
            self._log(json.dumps(fp_data, indent=4, ensure_ascii=False))
            self._log("====================================================")

        # --- 初始化访问与过盾 ---
        tls_url = self.free_config.get('tls_url', '')
        self._log(f"Navigating: {tls_url}")
        self.page.get(tls_url)
        time.sleep(5)
        if 'Attention Required! | Cloudflare' in self.page.title and 'Sorry, you have been blocked' in self.page.html:
            self._log(f'Block by cloudflare, try refresh...')
            self.page.refresh()
            self.page.wait.load_start(timeout=2)
            self.page.wait.doc_loaded()
        cf_bypasser = CloudflareBypasser(self.page, log=self.config.debug)
        if not cf_bypasser.bypass(max_retry=6):
            raise BizLogicError("Cloudflare bypass timeout")
        time.sleep(3)
        cf_bypasser.handle_waiting_room()
        
        self._log("Init humanize tools...")
        
        profile_name = random.choice(list(MOUSE_PROFILES.keys()))
        self._log(f"[HumanMouse] current mouse profiles: {profile_name}") 
        
        self.mouse = HumanMouse(self.page, timing=MOUSE_PROFILES[profile_name], debug=self.config.debug)
        self.keyboard = HumanKeyboard(self.page)
        viewport_width = self.page.rect.viewport_size[0]
        viewport_height = self.page.rect.viewport_size[1]
        init_x = random.randint(10, viewport_width - 10)
        init_y = random.randint(10, viewport_height - 10)
        self.mouse.move(init_x, init_y) 

        max_steps = 15
        stuck_counter = 0
        last_url = ""
        session_created = False
        has_submitted_login = False
        other_langs = ['fr-fr', 'ar-ar', 'cb-ph', 'id-id', 'km-kh', 'mk-mk', 'ru-ru', 'sq-al', 'th-th', 'tl-ph', 'uz-uz', 'vi-vn', 'zh-cn', 'hy-am', 'zh-hk']
        
        for step in range(max_steps):
            self.page.wait.doc_loaded()
            time.sleep(1)
            
            current_url = self.page.url
            current_title = self.page.title.lower()
            current_html_content = self.page.html
            self._log(f"--- [Router Step {step+1}] Current URL: {current_url} ---")
            
            if current_url == last_url:
                stuck_counter += 1
            else:
                last_url = current_url
                stuck_counter = 0
            
             # 在某个页面卡住3轮以上,直接重试
            if stuck_counter >= 3:
                self._log("[WARN] Page stucked, try to refresh...")
                self.page.refresh()
                self.page.wait.load_start(timeout=5)
                stuck_counter = 0
                continue
            
            server_error_indicators = [
                "502 Bad Gateway",
                "503 Service Temporarily Unavailable",
            ]
            
            # 网络出现故障,直接重试
            if any(err in current_html_content for err in server_error_indicators):
                self._log(f"[WARN] Server network error, try to refresh (Step: {step})...")
                time.sleep(2)
                self.page.refresh()
                self.page.wait.load_start(timeout=5)
                continue
            
            # 特征1: 必须是 <p> 标签，且包含指定文案 (绝对不会匹配到 <script> 标签里的字典)
            spa_error_p = self.page.ele('xpath://p[contains(text(), "It looks like something went wrong")]', timeout=0.1)
            # 特征2: 必须是 <div> 标签，且直接包含 "Error code:" 文本
            error_code_div = self.page.ele('xpath://div[contains(text(), "Error code:")]', timeout=0.1)
            if spa_error_p or error_code_div:
                extracted_code = "Unknown"
                if error_code_div:
                    span_ele = error_code_div.ele('tag:span', timeout=0.1)
                    if span_ele:
                        extracted_code = span_ele.text
                self._log(f"[WARN] Frontend application error page detected (Error Code: {extracted_code}). Triggering fallback page refresh (Step: {step})...")                
                time.sleep(2)
                self.page.refresh()
                self.page.wait.load_start(timeout=5)
                continue
            
            cloudflare_blocked_indicators = [
                "Sorry, you have been blocked" in current_html_content,
                "You are being rate limited" in current_html_content,
                "Cloudflare Ray ID" in current_html_content
            ]
            if any(cloudflare_blocked_indicators):
                raise BizLogicError(message="Blocked by Cloudflare WAF. Need to change IP or browser fingerprint.")
            
            # 遇到五秒盾先绕盾
            if "just a moment" in current_title:
                cf_bypasser.bypass(max_retry=3)
                time.sleep(3)
                continue
            
            if "waitting room" in current_title:
                cf_bypasser.handle_waiting_room()
                time.sleep(3)
                continue
            
            # 如果语言不匹配, 切换语言到英语
            matched_lang = next((lang for lang in other_langs if lang in current_url), None)
            if matched_lang:
                current_url = current_url.replace(matched_lang, 'en-us')
                self.page.get(current_url)
                self.page.wait.load_start(timeout=3)
                continue
                    
            # 到达终极目标页面 (成功退出条件)
            if "appointment-booking" in current_url or self.page.ele('tag:button@text():Book your appointment', timeout=1):
                btn_selector = 'tag:button@text():Book your appointment'            
                if self.page.wait.ele_displayed(btn_selector, timeout=10):
                    self._handle_cookie_dialog()
                    self.session_create_time = time.time()
                    self._log("✅ Login & Navigation Success! Reached appointment-booking.")
                    session_created = True
                    break
            
            # 页面发生react渲染错误
            if 'a client-side exception has occurred while loading' in current_html_content:
                self.page.refresh()
                self.page.wait.load_start(timeout=3)
                continue
            
            # 改签的情况
            if '/workflow/application-summary' in current_url:
                change_btn_sel = 'tag:button@@text():Change'
                self.page.wait.ele_displayed(change_btn_sel, timeout=10)
                self.page.ele(change_btn_sel).click(by_js=True)
                confirm_btn_sel = 'tag:button@@text():Yes'
                self.page.wait.ele_displayed(confirm_btn_sel, timeout=5)
                self.page.ele(confirm_btn_sel).click(by_js=True)
                self.page.wait.load_start(timeout=5)
                continue
            
            # 遇到没有申请人的拦截页 (致命错误退出条件)
            no_applicant_indicators = [
                "Add a new applicant" in current_html_content,
                "You have not yet added an applicant" in current_html_content,
                "applicants-information" in current_url
            ]
            if any(no_applicant_indicators):
                raise BizLogicError(message="No applicant added. Cannot proceed to booking.") 
            
            # 首页/登录入口页 -> 需要点击进入登录
            if tls_url in current_url:
                if self.page.ele("tag:a@@href:login", timeout=1) and not self.page.ele('tag:label@@text():Email', timeout=1):
                    self._log("State: Login Portal. Clicking login link...")
                    login_link = self.page.ele("tag:a@@href:login")
                    self.mouse.human_click_ele(login_link)
                    
                    self.page.wait.load_start(timeout=3)
                    continue

                if self.page.ele("tag:svg@@data-testid=user-button", timeout=1):
                    self._log("State: Already login, logout now...")
                    user_btn = self.page.ele("tag:svg@@data-testid=user-button")
                    self.mouse.human_click_ele(user_btn)
                    time.sleep(1.5)
                    logout_btn = self.page.ele("#logout")
                    self.mouse.human_click_ele(logout_btn)
                    
                    self.page.wait.load_start(timeout=3)
                    self.page.get(tls_url)
                    self.page.wait.load_start(timeout=3)
                    continue
            
            # 真正的登录表单页
            if self.page.ele('tag:label@@text():Email', timeout=1) and not has_submitted_login:
                self._log("State: Login Form. Processing credentials and Captcha...")
                
                recaptchav2_token = ""
                if not captcha_future and (self.page.ele('.g-recaptcha') or self.page.ele('xpath://iframe[contains(@src, "recaptcha")]')):
                    rec_iframe = self.page.ele('xpath://iframe[contains(@src, "recaptcha")]')
                    rec_iframe_src = rec_iframe.attr('src')
                    rec_parsed = urlparse(rec_iframe_src)
                    rec_params = parse_qs(rec_parsed.query)
                    rec_sitekey = rec_params.get("k", [None])[0]
                    rec_size = rec_params.get("size", [None])[0]
                    
                    if 'normal' == rec_size:
                        self._log(f"Found dynamic sitekey={rec_sitekey}. Starting async Captcha solver...")
                        rc_params = {
                            "type": "ReCaptchaV2TaskProxyLess",
                            "page": current_url,
                            "siteKey": rec_sitekey, 
                            "apiToken": self.free_config.get("capsolver_key", "")
                        }
                        captcha_future = captcha_executor.submit(self._solve_recaptcha, rc_params)

                username = self.config.account.username
                password = self.config.account.password
                
                input_ele = self.page.ele('tag:label@@text():Email').next()
                self.mouse.human_click_ele(input_ele)
                time.sleep(random.uniform(0.2, 0.6))
                self.keyboard.type_text(username, humanize=True)
                time.sleep(random.uniform(0.5, 1.2)) 
            
                input_ele = self.page.ele('tag:label@@text():Password').next()
                self.mouse.human_click_ele(input_ele)
                time.sleep(random.uniform(0.2, 0.6))
                self.keyboard.type_text(password, humanize=True)
                
                # 注入 Token
                if captcha_future:
                    self._log("Waiting for background Captcha result...")
                    try:
                        # 设一个合理的超时，防止死锁
                        recaptchav2_token = captcha_future.result(timeout=120) 
                        self._log("Background Captcha solved successfully!")
                    except Exception as e:
                        raise BizLogicError(f"Captcha solving failed or timed out: {e}")
                
                    # 注入 Token
                    if recaptchav2_token:
                        inject_js = f"var g = document.getElementById('g-recaptcha-response'); if(g) {{ g.value = '{recaptchav2_token}'; }}"
                        self.page.run_js(inject_js)
                        time.sleep(random.uniform(0.5, 1.0))
                
                self._log("Submitting Login...")
                login_btn = self.page.ele('tag:button@@text():Login')
                self.mouse.human_click_ele(login_btn)
                has_submitted_login = True
                
                self.page.wait.load_start(timeout=5)
                continue
            
            # Travel Groups 页面
            if "travel-groups" in current_url:
                self._log("State: Travel Groups. Selecting targeted group...")
                groups = self._parse_travel_groups(current_html_content)
                location = self.free_config.get('location')
                self.travel_group = next((g for g in groups if location in g['location']), None)
                
                if not self.travel_group or not self.travel_group.get("submitted"):
                    self._save_screenshot("group_not_found")
                    raise NotFoundError(f"Group not found for {location}")
            
                formgroup_id = self.travel_group.get('group_number')
                btn_selector = f'tag:button@@name=formGroupId@@value={formgroup_id}'
                
                if self.page.wait.eles_loaded(btn_selector, timeout=10):
                    buttons = self.page.eles(btn_selector)
                    select_btn = next((btn for btn in reversed(buttons) if btn.rect.size[0] > 0 and btn.rect.size[1] > 0), None)
                    
                    if select_btn:
                        time.sleep(random.uniform(0.5, 1.2))
                        self.mouse.human_click_ele(select_btn)
                        self.page.wait.load_start(timeout=3)
                        continue
                    else:
                        self._log("[WARN] Select button found but not visible.")
                else:
                    self._log(f"[WARN] Wait timeout for group button {formgroup_id}")
            
            # 中间过渡页，需点击 "Book Appointment" 继续往下走
            if self.page.ele('#book-appointment-btn', timeout=1):
                self._log("State: Intermediate Dashboard. Clicking Book Appointment button...")
                self.mouse.human_click_ele(self.page.ele('#book-appointment-btn'))
                
                self.page.wait.load_start(timeout=3)
                continue
            
            # 登录失败校验 或 未知加载状态
            if "login-actions" in current_url and has_submitted_login:
                self._log("Waiting on login-actions... (Might be authenticating or invalid credentials)")
                time.sleep(2)
                if self.page.ele('text:Invalid username or password', timeout=1): # 假设网页上有错误提示
                    raise BizLogicError(message="Login Failed! Invalid credentials or Captcha rejected.")
                continue
            
            self._log("State: Transitioning or Unknown. Waiting 2 seconds...")
            time.sleep(2)
            
            # 如果处于某些特定的复杂页面且久久不动，尝试主动刷新
            if step > 0 and step % 4 == 0: 
                self._log("[WARN] Unknown state persisted for too long. Attempting fallback refresh...")
                self.page.refresh()
                self.page.wait.load_start(timeout=3)
                
        if not session_created:
            raise BizLogicError(f"Failed to reach appointment-booking after {max_steps} navigation steps. Stuck at: {self.page.url}")

    def query(self, apt_type: AppointmentType) -> VSQueryResult:
        res = VSQueryResult()
        res.success = False
        slots = []
        self._log(f"Executing silent JS fetch...") 
        resp = self._perform_request("GET", self.page.url, retry_count=0)
        self._check_page_is_session_expired_or_invalid('Book your appointment', resp.text)
        slots = self._parse_appointment_slots(resp.text)
        
        if slots:
            res.success = True
            earliest_date = slots[0]["date"]
            earliest_dt = datetime.strptime(earliest_date, "%Y-%m-%d")
            res.availability_status = AvailabilityStatus.Available
            res.earliest_date = earliest_dt
            date_map: dict[datetime, list[TimeSlot]] = {}
            for s in slots:
                date_str = s["date"]
                dt = datetime.strptime(date_str, "%Y-%m-%d")
                date_map.setdefault(dt, []).append(
                    TimeSlot(time=s["time"], label=str(s.get("label", "")))
                )
            res.availability = [DateAvailability(date=d, times=slots) for d, slots in date_map.items()]
            self._log(f"Slot Found! earliest_date={earliest_date}, size={len(slots)}")
        else:
            self._log("No slots available.")
            res.success = False
            res.availability_status = AvailabilityStatus.NoneAvailable
            
        # TODO(TEST): 临时测试预约提交
        if configure.TLS_TEST_BOOK_AFTER_QUERY:
            test_date = "2026-06-10"
            test_time = "09:00"
            test_label = ""
            test_dt = datetime.strptime(test_date, "%Y-%m-%d")
            query_res = VSQueryResult()
            query_res.success = True
            query_res.availability_status = AvailabilityStatus.Available
            query_res.earliest_date = test_dt
            query_res.availability = [
                DateAvailability(
                    date=test_dt,
                    times=[TimeSlot(time=test_time, label=test_label)]
                )
            ]
            self._log(f"[TEST] using fixed June slot: {test_date} {test_time} {test_label}")
            test_userinput = {
                "support_pta": False,
                "expected_end_date": "2100-01-01",
                "expected_start_date": "2000-01-01"
            }
            try:
                self.book(query_res, test_userinput)
            except Exception as e:
                self._log(f"[TEST] book() after query failed: {e}")
            self.is_healthy = False
        return res

    def book(self, slot_info: VSQueryResult, user_inputs: Dict = None) -> VSBookResult:
        res = VSBookResult()
        res.success = False
        
        exp_start = user_inputs.get('expected_start_date', '')
        exp_end = user_inputs.get('expected_end_date', '')
        support_pta = user_inputs.get('support_pta', True)

        target_labels = ['']
        if support_pta:
            target_labels.append('pta')

        available_dates_str =[
            da.date.strftime("%Y-%m-%d")
            for da in slot_info.availability if da.date
        ]
        
        valid_dates_list = self._filter_dates(available_dates_str, exp_start, exp_end)
        if not valid_dates_list:
            raise NotFoundError(message="No dates match user constraints")
        
        all_possible_slots =[]
        for da in slot_info.availability:
            if not da.date:
                continue
                
            date_str = da.date.strftime("%Y-%m-%d")
            if date_str in valid_dates_list:
                for t in da.times:
                    if t.label in target_labels:
                        all_possible_slots.append({
                            "date": date_str,
                            "time_obj": t,
                            "label": t.label
                        })
        
        if not all_possible_slots:
            raise NotFoundError(message="No suitable slot found (after label filtering)")

        selected_slot = random.choice(all_possible_slots)
        selected_date = selected_slot["date"]
        selected_time = selected_slot["time_obj"]
        selected_label = selected_slot["label"]

        self._log(f"Found {len(all_possible_slots)} valid slots. selected slot: {selected_date} {selected_time.time} {selected_label}")
        self.page.listen.start('/workflow/appointment-booking', method='POST')
        js_update_form = f"""
        try {{
            const buttons = Array.from(document.querySelectorAll('button[type="submit"]'));
            const submitBtn = buttons.find(btn => {{
                return btn.textContent.trim().toLowerCase().includes('book your appointment');
            }});
            if (!submitBtn) return 'Submit button not found';
            const form = submitBtn.closest('form');
            if (!form) return 'Correct form not found';
            function setReactValue(input, value) {{
                if (!input) return;
                input.value = value;
            }}
            setReactValue(form.querySelector('input[name="date"]'), '{selected_date}');
            setReactValue(form.querySelector('input[name="time"]'), '{selected_time.time}');
            setReactValue(form.querySelector('input[name="appointmentLabel"]'), '{selected_label}');
            submitBtn.removeAttribute('disabled');
            submitBtn.classList.remove('opacity-50', 'cursor-not-allowed'); 
            return 'form_updated';
        }} catch (e) {{
            return e.toString();
        }}
        """
        update_res = self.page.run_js(js_update_form)
        self._log(f"Form update triggered: {update_res}")
        
        if update_res != 'form_updated':
            raise BizLogicError(message=f"Failed to update form: {update_res}")
        
        submit_btn = self.page.ele('tag:button@@type=submit@@text():Book your appointment')
        if not submit_btn:
            raise BizLogicError(message="Submit button not found for mouse click")
            
        self._log("Moving mouse to submit button and clicking")
        self.mouse.human_click_ele(submit_btn)
            
        packet = self.page.listen.wait(timeout=10)
        if not packet:
            raise BizLogicError(message='Listening data failed')
        
        self.page.listen.stop()
        self._log(f"URL: {packet.url}")
        self._log(f"POST Body: {packet.request.postData}")
        self._log(f"POST Resp: {packet.response.body}")

        self._log("Waiting for Next.js to process the form submission...")
        for _ in range(10):
            try:
                current_page_url = self.page.url
                current_page_html = self.page.html
                appointment_confirmation_indicators = [
                    "order-summary" in current_page_url,
                    "partner-services" in current_page_url,
                    "appointment-confirmation" in current_page_url,
                    "Change my appointment" in current_page_html,
                    "Book a new appointment" in current_page_html,
                ]
                
                if any(appointment_confirmation_indicators):
                    self._log(f"✅ BOOKING SUCCESS! Redirected to: {current_page_url}")
                    res.success = True
                    res.label = selected_label
                    res.book_date = selected_date
                    res.book_time = selected_time.time
                    self._save_screenshot("book_slot_success")
                    break
                
                toast_selector = 'tag:div@role=alert'
                toast_ele = self.page.ele(toast_selector, timeout=0.5)
                if toast_ele:
                    error_msg = toast_ele.text
                    self._log(f"❌ BOOKING FAILED! Detected popup: {error_msg}")
                    break
                time.sleep(0.5)
            except Exception:
                pass
        return res

    def _perform_request(self, method, url, headers=None, data=None, json_data=None, params=None, retry_count=0):
        """
        在浏览器上下文中注入 JS 执行 Fetch
        """
        if not self.page:
            raise BizLogicError("Browser not initialized")

        if params:
            from urllib.parse import urlencode
            if '?' in url:
                url += '&' + urlencode(params)
            else:
                url += '?' + urlencode(params)

        fetch_options = {
            "method": method.upper(),
            "headers": headers or {},
            "credentials": "include"
        }

        # Body 处理
        if json_data:
            fetch_options['body'] = json.dumps(json_data)
            fetch_options['headers']['Content-Type'] = 'application/json'
        elif data:
             if isinstance(data, dict):
                from urllib.parse import urlencode
                fetch_options['body'] = urlencode(data)
                fetch_options['headers']['Content-Type'] = 'application/x-www-form-urlencoded'
             else:
                 fetch_options['body'] = data

        js_script = f"""
        const url = "{url}";
        const options = {json.dumps(fetch_options)};
        
        return fetch(url, options)
            .then(async response => {{
                const text = await response.text();
                const headers = {{}};
                response.headers.forEach((value, key) => headers[key] = value);
                
                return {{
                    status: response.status,
                    body: text,
                    headers: headers,
                    url: response.url
                }};
            }})
            .catch(error => {{
                return {{
                    status: 0,
                    body: error.toString(),
                    headers: {{}},
                    url: url
                }};
            }});
        """
        
        res_dict = self.page.run_js(js_script, timeout=30)
        resp = BrowserResponse(res_dict)
        
        if resp.status_code == 200:
            return resp
            
        elif resp.status_code == 401:
            self.is_healthy = False
            raise SessionExpiredOrInvalidError()
            
        # 将 403 和 429 合并，共享绕盾重试逻辑
        elif resp.status_code in (403, 429):
            if retry_count < 2:
                # 动态打印是 403 还是 429
                self._log(f"HTTP {resp.status_code} Detected. Cloudflare block or rate limit? Attempting refresh (Try {retry_count+1}/2)...")
                if self._refresh_firewall_session():
                    self._log("Firewall session refreshed. Retrying request...")
                    return self._perform_request(method, url, headers, data, json_data, params, retry_count+1)
                else:
                    self._log("Failed to refresh firewall session.")    
            
            # 如果重试 2 次仍然失败，或者刷新防火墙失败，则按原来的逻辑抛出各自的异常
            if resp.status_code == 403:
                raise PermissionDeniedError(f"HTTP 403: {resp.text[:100]}")
            else:
                self.is_healthy = False
                # 注意：保留了原代码中 RateLimiteddError 的拼写（双d）
                raise RateLimiteddError(f"HTTP 429: {resp.text[:100]}")
                
        else:
            if resp.status_code == 0:
                 raise BizLogicError(f"Network Error: {resp.text}")
            raise BizLogicError(message=f"HTTP Error {resp.status_code}: {resp.text[:100]}")
    
    def _refresh_firewall_session(self) -> bool:
        """
        主动刷新页面以触发 Cloudflare 挑战并尝试通过
        """
        try:
            self._log("Refreshing page to trigger Cloudflare...")
            self.page.refresh()
            time.sleep(5)
            cf = CloudflareBypasser(self.page, log=self.config.debug)
            success = cf.bypass(max_retry=6)
            
            if success:
                title = self.page.title.lower()
                if "access denied" in title:
                    return False
                time.sleep(2)
                return True
            
            return False
        except Exception as e:
            self._log(f"Error during firewall refresh: {e}")
            return False

    def _solve_recaptcha(self, params) -> str:
        """调用 VSCloudApi 解决 ReCaptcha"""
        key = params.get("apiToken")
        if not key:
            raise NotFoundError("Api-token required")
        
        submit_url = "https://api.capsolver.com/createTask"
        task = {
            "type": params.get("type"),
            "websiteURL": params.get("page"),
            "websiteKey": params.get("siteKey"),
        }
        if params.get("action"):
            task["pageAction"] = params.get("action")
            
        if params.get("proxy"):
            p = self.config.proxy
            task["proxyType"] = p.proto
            task["proxyAddress"] = p.ip
            task["proxyPort"] = p.port
            if p.username:
                task["proxyLogin"] = p.username
                task["proxyPassword"] = p.password
        
        payload = {"clientKey": key, "task": task}
        import requests as req
        r = req.post(submit_url, json=payload, timeout=20)
        if r.status_code != 200:
            raise BizLogicError(message="Failed to submit capsolver task")
        
        task_id = r.json().get("taskId")
        for _ in range(20):
            r = req.post("https://api.capsolver.com/getTaskResult", json={"clientKey": key, "taskId": task_id}, timeout=20)
            if r.status_code == 200:
                d = r.json()
                if d.get("status") == "ready":
                    return d["solution"]["gRecaptchaResponse"]
            time.sleep(3)
        raise BizLogicError(message="Capsolver task timeout")

    def _parse_travel_groups(self, html_content) -> List[Dict]:
        groups = []
        js_pattern = r'\\"travelGroups\\":\s*(\[.*?\]),\\"availableCountriesToCreateGroups'
        js_match = re.search(js_pattern, html_content, re.DOTALL)
        if js_match:
            json_str = js_match.group(1).replace(r'\"', '"')
            data = json.loads(json_str)
            for g in data:
                groups.append({
                    'group_name': g.get('groupName'),
                    'group_number': g.get('formGroupId'),
                    'location': g.get('vacName'),
                    'submitted': g.get('submitted')
                })
        else:
            self._log('Parsed travel group page, but not found travelGroups')
        return groups

    def _parse_appointment_slots(self, html_content) -> List[Dict]:
        slots = []
        pattern = r'"availableAppointments\\":\s*(\[.*\]),\\"showFlexiAppointment'
        match = re.search(pattern, html_content, re.DOTALL)
        
        if match:
            json_str = match.group(1).replace(r'\"', '"')
            data = json.loads(json_str)
            for day in data:
                d_str = day.get('day')
                for s in day.get('slots', []):
                    labels = s.get('labels', [])
                    lbl = None
                    if 'pta' in labels: lbl = 'pta'
                    elif 'ptaw' in labels: lbl = 'ptaw'
                    elif '' in labels: lbl = ''
                    if lbl is not None:
                        slots.append({
                            'date': d_str,
                            'time': s.get('time'),
                            'label': lbl
                        })
        return slots
    
    def _handle_cookie_dialog(self, timeout: int = 3) -> bool:
        """
        检测并处理 Osano Cookie 弹窗
        """
        try:
            dialog = self.page.ele('@aria-label=Cookie Consent Banner', timeout=timeout)
            if not dialog:
                return False
            target_btn = dialog.ele('tag:button@text():Save')
            if not target_btn:
                target_btn = dialog.ele('tag:button@text():Accept')
            if target_btn:
                self.mouse.human_click_ele(target_btn)
                self._log(f"Handle cookie window success")
                return True
            else:
                self._log(f"Found cookie window, but button not found")
                return False
        except Exception as e:
            self._log(f"Handle cookie window exception: {e}")
            return False
  
    def _check_page_is_session_expired_or_invalid(self, keyword, html: str) -> bool:
        if not html:
            self.is_healthy = False
            raise SessionExpiredOrInvalidError()
        
        html_lower = html.lower()
        if keyword.lower() not in html_lower: 
            session_expire_or_invalid_indicators = [
                'redirected automatically' in html_lower,
                'login' in html_lower and 'password' in html_lower,
                'session expired' in html_lower
            ]
            if any(session_expire_or_invalid_indicators):
                self.is_healthy = False
                raise SessionExpiredOrInvalidError()
            
    def _filter_dates(self, dates: List[str], start_str: str, end_str: str) -> List[str]:
        if not start_str or not end_str:
            return dates
        valid_dates = []
        s_date = datetime.strptime(start_str[:10], "%Y-%m-%d")
        e_date = datetime.strptime(end_str[:10], "%Y-%m-%d")
        for date_str in dates:
            curr_date = datetime.strptime(date_str, "%Y-%m-%d")
            if s_date <= curr_date <= e_date:
                valid_dates.append(date_str)
        random.shuffle(valid_dates)
        return valid_dates
    
    # --- 资源清理核心方法 ---
    def cleanup(self):
        """
        销毁浏览器并彻底删除临时文件
        """
        if self.page:
            try:
                self.page.quit(force=True)
            except Exception:
                pass
            self.page = None
        
        if os.path.exists(self.root_workspace):
            for _ in range(3):
                try:
                    time.sleep(0.2)
                    shutil.rmtree(self.root_workspace, ignore_errors=True)
                    break
                except Exception as e:
                    self._log(f"Cleanup retry: {e}")
                    time.sleep(0.5)
            
            if os.path.exists(self.root_workspace):
                 self._log(f"[WARN] Failed to fully remove workspace: {self.root_workspace}")
        if self.tunnel:
            try: self.tunnel.stop()
            except: pass
            self.tunnel = None
        
    def __del__(self):
        """
        析构函数：当对象被垃圾回收时自动调用
        """
        self.cleanup()