import os
import re
import uuid
import random
import socket
import json
import time
import string
import shutil
import logging
import base64
import requests
import argparse
from datetime import datetime, timezone
from urllib.parse import urlparse, urlencode

from bs4 import BeautifulSoup
from cryptography.hazmat.primitives import serialization, hashes
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.backends import default_backend

# 假设这些是你本地的依赖
import configure
from DrissionPage import ChromiumPage, ChromiumOptions
from vs_types import RateLimiteddError, BizLogicError
from toolkit.mihomo_tunnel import MihomoTunnel
from utils.cloudflare_bypass_for_scraping import CloudflareBypasser

# ==========================================
# 日志与常量配置
# ==========================================
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s [%(levelname)s] %(message)s',
    datefmt='%H:%M:%S'
)
logger = logging.getLogger("VFSRegistrar")

VFS_PUBLIC_KEY_PEM = """-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuupFgB+lYIOtSxrRoHzc
LmCZKJ6+oSbgqgOPzFMM0TasOeLw0NXEn1XfIzXdx75+tegNKwyIZumoh0yhubKs
t59GV321kN0iquYRHrdh3ygfDDHlS9rROQeBqRga0ncSADtbLMrBPqXJjPCoV76y
t92towriKoH75BhiazY0mghm4LjmAWrV0u/GNpV3tk9bxbtHEXGaFmxCJqjg+7x6
1e5wXLfvpj9w1QsiSWOSJxLOyICz/9ByxXycQQFdNmjnnnwco9Gt/Mi33NYH71j0
5oXIjklFC4lvJqaqSY5lS7Vwb9oCt9zX9J0Yz4z4e/3V+0jgRnWOFGofyks4FKe2
GQIDAQAB
-----END PUBLIC KEY-----"""


# ==========================================
# 辅助工具类
# ==========================================
class VFSHelper:
    @staticmethod
    def generate_mobile_number(country_code=353, e164_format=False):
        if country_code == 353:  # Ireland
            prefix = random.choice(['83', '85', '86', '87', '89'])
            number = f"{prefix}{''.join([str(random.randint(0, 9)) for _ in range(7)])}"
            return f"+353{number}" if e164_format else number
        elif country_code == 44:  # UK
            prefix_second = random.choice(['1', '2', '3', '4', '5', '7', '8', '9'])
            number = f"7{prefix_second}{''.join([str(random.randint(0, 9)) for _ in range(8)])}"
            return f"+44{number}" if e164_format else number
        elif country_code == 86:  # China
            prefixes = ["130", "131", "132", "133", "135", "136", "138", "139", "150", "158", "159", "186"]
            prefix = random.choice(prefixes)
            number = f"{prefix}{''.join([str(random.randint(0, 9)) for _ in range(8)])}"
            return f"+86{number}" if e164_format else number
        return "".join([str(random.randint(0, 9)) for _ in range(10)])

    @staticmethod
    def generate_password(length=12):
        chars = string.ascii_letters + string.digits + "@#$%"
        while True:
            pwd = ''.join(random.choices(chars, k=length))
            if (any(c.islower() for c in pwd) and 
                any(c.isupper() for c in pwd) and 
                any(c.isdigit() for c in pwd) and 
                any(c in "@#$%" for c in pwd)):
                return pwd

    @staticmethod
    def encrypt_password(password: str) -> str:
        public_key = serialization.load_pem_public_key(
            VFS_PUBLIC_KEY_PEM.encode(), backend=default_backend()
        )
        ciphertext = public_key.encrypt(
            password.encode(),
            padding.OAEP(
                mgf=padding.MGF1(algorithm=hashes.SHA256()),
                algorithm=hashes.SHA256(),
                label=None
            )
        )
        return base64.b64encode(ciphertext).decode()

    @staticmethod
    def get_client_source() -> str:
        timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S")
        payload = f"GA;{timestamp}Z"
        return VFSHelper.encrypt_password(payload)


class BrowserResponse:
    """标准化浏览器响应"""
    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:
            try:
                self._json = json.loads(self.text) if self.text else {}
            except json.JSONDecodeError:
                self._json = {}
        return self._json


# ==========================================
# API 交互与代理工具
# ==========================================
def load_proxies(pool_name):
    """从 config/proxies.json 读取对应的代理池"""
    config_path = os.path.join(os.path.dirname(__file__), 'config', 'proxies.json')
    try:
        with open(config_path, 'r', encoding='utf-8') as f:
            data = json.load(f)
            proxies = data.get(pool_name, [])
            if not proxies:
                raise ValueError(f"代理池 '{pool_name}' 为空或不存在！")
            return proxies
    except Exception as e:
        logger.error(f"读取代理配置文件失败: {e}")
        exit(1)

def upload_account_to_server(account, api_url, api_token):
    """将注册成功的账号上报到中心服务器"""
    headers = {
        'accept': 'application/json',
        'Authorization': f'Bearer {api_token}',
        'Content-Type': 'application/json'
    }

    payload = {
        "pool_name": account.get("pool_name"),
        "username": account.get("username"),
        "password": account.get("password"),
        "extra_data": {
            "country_code": account.get("country_code"),
            "mission_code": account.get("mission_code"),
            "phone_country_code": account.get("phone_country_code"),
            "phone_number": account.get("phone_number"),
            "created_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        }
    }

    try:
        logger.info(f"Uploading account {account['username']} to server...")
        resp = requests.post(api_url, json=payload, headers=headers, timeout=10)
        if resp.status_code == 200:
            logger.info(f"✅ [API Upload Success] Server responded: {resp.text}")
            return True
        else:
            logger.error(f"❌ [API Upload Failed] Status: {resp.status_code}, Body: {resp.text}")
            return False
    except Exception as e:
        logger.error(f"❌ [API Upload Error]: {e}")
        return False


# ==========================================
# 核心自动化类
# ==========================================
class VFSRegistrationBot:
    def __init__(self, vfs_url, proxy, email_api_token, master_email):
        self.vfs_url = vfs_url
        self.proxy_config = proxy
        self.email_api_token = email_api_token
        self.master_email = master_email
        
        self.page = None
        self.tunnel = None
        self.instance_id = uuid.uuid4().hex[:8]
        self.workspace = os.path.abspath(os.path.join("data/temp_browser_data", f"reg_session_{self.instance_id}"))
        self.active_proxy_url = None  # 供 request 请求复用

    def _log(self, msg):
        logger.info(f"[TLS-Reg-{self.instance_id}] {msg}")

    def _init_browser(self):
        """初始化浏览器配置"""
        co = ChromiumOptions()
        
        # 查找可用端口
        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
            s.bind(('', 0))
            port = s.getsockname()[1]
            
        co.set_local_port(port)
        
        chrome_path = configure.CHROME_PATH or os.getenv("CHROME_BIN")
        if chrome_path and os.path.exists(chrome_path):
            co.set_paths(browser_path=chrome_path)

        if self.proxy_config and self.proxy_config.get("ip"):
            p = self.proxy_config
            if p.get('username') and p.get('password'):
                self._log(f"Starting Proxy Tunnel for {p.get('ip')}...")
                exit_node = {
                    "name": "ExitNode",
                    "type": p.get('proto'),
                    "server": p.get('ip'),
                    "port": p.get('port'),
                    "username": p.get('username'),
                    "password": p.get('password')
                }
                relay_node = random.choice(configure.MIHOMO_RELAY_NODES) if configure.MIHOMO_RELAY_NODES else None
                mihomo_path = configure.MIHOMO_BIN_PATH or os.getenv("MIHOMO_BIN")
                if not mihomo_path:
                    raise BizLogicError('Mihomo path is null. Set mihomo bin path in configure or os env')
                
                self.tunnel = MihomoTunnel(mihomo_path, exit_node=exit_node, relay_node=relay_node)
                self.active_proxy_url = self.tunnel.start()
                self._log(f"Tunnel started at {self.active_proxy_url}")
                co.set_argument(f'--proxy-server={self.active_proxy_url}')
            else:
                self.active_proxy_url = f"{p.get('proto')}://{p.get('ip')}:{p.get('port')}"
                co.set_argument(f'--proxy-server={self.active_proxy_url}')
        else:
            self._log("[WARN] No proxy configured!")

        co.headless(False)
        co.set_argument('--no-sandbox')
        co.set_argument('--disable-gpu')
        co.set_argument('--disable-dev-shm-usage')
        co.set_argument('--window-size=1920,1080')
        co.set_argument('--disable-blink-features=AutomationControlled')
        
        self.page = ChromiumPage(co)
        self.page.set.timeouts(15)

    def _perform_js_fetch(self, method, url, headers=None, data=None, json_data=None, retry_count=0):
        """注入JS执行Fetch请求，绕过部分指纹检测"""
        if not self.page:
            raise BizLogicError("Browser not initialized")
        if retry_count > 3:
            raise BizLogicError("Max retries exceeded for request")

        fetch_options = {
            "method": method.upper(),
            "headers": headers or {},
            "credentials": "include"
        }
        
        if json_data:
            fetch_options['body'] = json.dumps(json_data)
            fetch_options['headers']['Content-Type'] = 'application/json'
        elif data:
            fetch_options['body'] = urlencode(data) if isinstance(data, dict) else str(data)
            fetch_options['headers']['Content-Type'] = 'application/x-www-form-urlencoded'

        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(err => ({{ status: 0, body: err.toString() }}));
        """
        
        try:
            res_dict = self.page.run_js(js_script, timeout=60)
            resp = BrowserResponse(res_dict)
            
            if resp.status_code == 200:
                return resp
            
            if resp.status_code == 403 and ("cloudflare" in resp.text.lower() or "Just a moment" in resp.text):
                logger.warning(f"Cloudflare 403 detected. Retrying ({retry_count+1})...")
                new_token = self._refresh_turnstile()
                if new_token and json_data and "captcha_api_key" in json_data:
                    json_data["captcha_api_key"] = new_token
                return self._perform_js_fetch(method, url, headers, data, json_data, retry_count + 1)
            
            if resp.status_code == 429:
                raise RateLimiteddError(f"Rate Limit: {resp.text[:100]}")
                
            return resp
        except Exception as e:
            logger.error(f"JS Execution Error: {e}")
            raise BizLogicError(f"Fetch failed: {e}")

    def _handle_cookie_banner(self):
        try:
            self.page.run_js("""
            var btn = document.getElementById('onetrust-accept-btn-handler');
            if(btn) { btn.click(); return true; }
            var banner = document.getElementById('onetrust-banner-sdk');
            if(banner) { banner.remove(); return true; }
            """)
        except:
            pass

    def _refresh_turnstile(self):
        self._log("Attempting to refresh Turnstile token...")
        try:
            self.page.run_js('try{window.turnstile.reset()}catch(e){}')
            cf_bypasser = CloudflareBypasser(self.page, log=True)
            
            for i in range(30):
                time.sleep(1)
                try:
                    ele = self.page.ele('@name=cf-turnstile-response')
                    if ele and ele.value:
                        self._log("Turnstile token obtained.")
                        return ele.value
                except:
                    pass
                if i > 5:
                    try: cf_bypasser.click_verification_button(is_dfs=False)
                    except: pass
        except Exception as e:
            logger.error(f"Turnstile refresh failed: {e}")
        return None

    def _wait_for_activation_link(self, username, max_wait_sec=60):
        self._log(f"Waiting for email to {username}...")
        start_time = time.time()

        url = "https://api.text.skin/api/email-authorizations/fetch"
        headers = {
            "Authorization": f"Bearer {self.email_api_token}",
            "Content-Type": "application/json",
            "Accept": "application/json, text/plain, */*"
        }

        while time.time() - start_time < max_wait_sec:
            try:
                utc_now_str = datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S')
                params = {
                    "email": self.master_email,
                    "sender": 'donotreply at vfsglobal.com',
                    "recipient": username,
                    "subjectKeywords": 'Welcome',
                    "bodyKeywords": 'ActivateAccount',
                    "sentDate": utc_now_str,
                    "expiry": str(60)
                }
        
                # 已经去掉 proxies=proxies 参数
                resp = requests.post(url, headers=headers, params=params, json={}, timeout=60)
                
                if resp.status_code != 200:
                    logger.warning(f"Email API returned status {resp.status_code}.")
                    time.sleep(15)
                    continue

                try:
                    result = resp.json()
                except json.JSONDecodeError:
                    time.sleep(15)
                    continue

                if result.get('code') != 0:
                    logger.debug(f"API Message: {result.get('message')}")
                else:
                    content = result.get('data', {}).get('body', "")
                    if content:
                        soup = BeautifulSoup(content, "html.parser")
                        link = soup.find("a", string="ActivateAccount")
                        if link:
                            return link["href"].replace(" ", "").replace("\n", "").replace("\r", "").strip()
            except Exception as e:
                logger.warning(f"Error fetching email: {e}")
            
            time.sleep(15)
            self._log("Checking email again...")
            
        return None

    def register(self, account):
        try:
            self._init_browser()
            self._log(f"Opening {self.vfs_url}")
            
            self.page.set.timeouts(page_load=30, script=30)
            try:
                self.page.get(self.vfs_url, retry=0, timeout=30)
            except Exception:
                logger.warning("Page load timed out. Stopping loading manually.")
                self.page.stop_loading()
            
            cf_token = None
            cf_bypasser = CloudflareBypasser(self.page, log=True)
            
            for _ in range(40):
                time.sleep(1)
                if "page-not-found" in self.page.url:
                    logger.error("❌ [BLOCKED] Redirected to 'Page Not Found'. Aborting.")
                    return False
                if "403" in self.page.title and "Just a moment" not in self.page.title:
                    logger.error("❌ [BLOCKED] 403 Forbidden detected. Aborting.")
                    return False
                
                self._handle_cookie_banner()
                
                try:
                    ele = self.page.ele('@name=cf-turnstile-response')
                    if ele and ele.value:
                        cf_token = ele.value
                        break
                except: pass
                
                try: cf_bypasser.click_verification_button(is_dfs=False)
                except: pass
            
            if not cf_token:
                raise BizLogicError("Failed to obtain initial Cloudflare token")

            post_data = {
                'emailid': account['username'],
                'password': VFSHelper.encrypt_password(account['password']),
                'confirmPassword': VFSHelper.encrypt_password(account['password']),
                'processPerDataAgreed': True,
                'intTransPerDataAgreed': True,
                'termAndConditionAgreed': True,
                'missioncode': account['mission_code'],
                'countrycode': account['country_code'],
                'languageCode': 'en',
                'dialcode': str(account['phone_country_code']),
                'contact': account['phone_number'],
                'captcha_version': 'cloudflare-v1',
                'captcha_api_key': cf_token,
                'cultureCode': 'en-US',
                'IsSpecialUser': False,
            }
            
            headers = {
                'content-type': 'application/json;charset=utf-8',
                'accept': 'application/json, text/plain, */*',
                'route': f"{account['country_code']}/en/{account['mission_code']}",
                'clientsource': VFSHelper.get_client_source(),
            }

            self._log(f"Submitting registration for {account['username']}")
            
            country_code = account.get('country_code', '').lower()
            if country_code == 'chn':
                api_domain = 'https://lift-apicn.vfsglobal.com'
            else:
                api_domain = 'https://lift-api.vfsglobal.com'
                
            register_endpoint = f"{api_domain}/user/registration"
            self._log(f"Using API Endpoint: {register_endpoint}")

            resp = self._perform_js_fetch(
                'POST', 
                register_endpoint,
                headers=headers, 
                json_data=post_data
            )
            
            self._log(f'register resp={resp.text}')
            resp_data = resp.json()
            
            if resp_data.get("code") == "200":
                self._log("Registration API success. Waiting for email...")
                activate_link = self._wait_for_activation_link(account['username'])
                
                if activate_link:
                    self._log(f"Activating account: {activate_link}")
                    activate_tab = self.page.new_tab(activate_link)
                    try:
                        success_ele = activate_tab.ele('Activation Successful', timeout=30)
                        if success_ele:
                            logger.info(f"✅ Account {account['username']} activated successfully.")
                            return True
                        else:
                            logger.error("Activation verification failed. Success text not found.")
                            return False
                    finally:
                        activate_tab.close()
                else:
                    logger.error("Timeout waiting for activation email.")
            else:
                logger.error(f"Registration failed: {resp_data}")

        except Exception as e:
            logger.error(f"Registration process exception: {e}", exc_info=True)
        return False
    
    def cleanup(self):
        self._log("Cleaning up resources...")
        if self.page:
            try: self.page.quit()
            except: pass
        if self.tunnel:
            try: self.tunnel.stop()
            except: pass
        if os.path.exists(self.workspace):
            time.sleep(1)
            shutil.rmtree(self.workspace, ignore_errors=True)


# ==========================================
# 主流程 & 命令行参数解析
# ==========================================
def parse_arguments():
    parser = argparse.ArgumentParser(description="VFS Global 自动注册机器人")
    
    # 核心参数
    parser.add_argument('--target', type=int, default=20, help="需要注册的目标账号数量")
    parser.add_argument('--vfs-url', type=str, required=True, help="VFS 注册页面的完整 URL, 如: https://visa.vfsglobal.com/chn/en/aut/register")
    parser.add_argument('--proxy-pool', type=str, default='proxy-cheap', help="使用的代理池名称 (在 proxies.json 中定义)")
    
    # 账号生成相关参数
    parser.add_argument('--account-prefix', type=str, default="cn_at", help="生成的邮箱前缀")
    parser.add_argument('--account-pool', type=str, default="cn.at.sentinel", help="上报到服务端的账号池名称")
    parser.add_argument('--phone-country', type=int, default=86, help="生成的手机号国家代码")
    parser.add_argument('--email-domain', type=str, default="text.skin", help="邮箱域名")
    
    # API 与 上报配置
    parser.add_argument('--upload-url', type=str, default="https://api.text.skin/api/account/add", help="账号上报服务器 URL")
    parser.add_argument('--upload-token', type=str, default="tok_e946329a60ff45ba807f3f41b0e8b7fc", help="账号上报服务器 API Token")
    parser.add_argument('--email-token', type=str, default="tok_e946329a60ff45ba807f3f41b0e8b7fc", help="获取邮件的 API Token")
    parser.add_argument('--master-email', type=str, default="hujiarui8@gmail.com", help="接收验证邮件的主邮箱")

    return parser.parse_args()


def generate_account_details(args):
    """根据命令行参数和 URL 生成账号数据字典"""
    # 从 URL 中提取 from_country 和 to_country
    match = re.search(r'vfsglobal\.com/([^/]+)/[^/]+/([^/]+)/register', args.vfs_url)
    if not match:
        raise ValueError(f"无法从 URL 解析国家代码: {args.vfs_url}")
        
    from_country = match.group(1)
    to_country = match.group(2)
    
    rand_suffix = ''.join(random.choices(string.ascii_lowercase + string.digits, k=6))
    username = f"{args.account_prefix}_{rand_suffix}@{args.email_domain}"
    phone = VFSHelper.generate_mobile_number(args.phone_country)
    
    return {
        'pool_name': args.account_pool,
        'country_code': from_country,
        'mission_code': to_country,
        'phone_country_code': args.phone_country,
        'phone_number': phone,
        'username': username,
        'password': VFSHelper.generate_password(),
    }


def main():
    args = parse_arguments()
    proxies = load_proxies(args.proxy_pool)
    logger.info(f"[*] 成功加载代理数量: {len(proxies)} 个")
    
    success_accounts = []
    logger.info(">>> Starting Registration Bot <<<")
    
    while len(success_accounts) < args.target:
        proxy_config = random.choice(proxies)
        account_detail = generate_account_details(args)
        
        logger.info(f"Processing Account: {account_detail['username']}")
        bot = None
        try:
            bot = VFSRegistrationBot(
                vfs_url=args.vfs_url,
                proxy=proxy_config,
                email_api_token=args.email_token,
                master_email=args.master_email
            )
            is_success = bot.register(account_detail)
            
            if is_success:
                success_accounts.append(account_detail)
                logger.info(f"Progress: {len(success_accounts)}/{args.target}")
                
                upload_account_to_server(account_detail, args.upload_url, args.upload_token)
                
                # 实时保存结果，防止中途退出丢失
                with open("registered_accounts.json", "w", encoding='utf-8') as f:
                    json.dump(success_accounts, f, indent=4, ensure_ascii=False)
            else:
                logger.warning("Registration failed. Retrying with new account details...")

            time.sleep(5)  # 避免请求过于频繁
            
        except Exception as e:
            logger.error(f"[ERROR] 注册发生致命错误 | 代理 IP: {proxy_config.get('ip')} | 异常信息: {e}")
            
        finally:
            if bot:
                bot.cleanup()

    logger.info(f">>> All tasks completed. Registered {len(success_accounts)} accounts. <<<")

if __name__ == "__main__":
    main()