#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
雀巢会员俱乐部自动任务脚本
脚本作者: 3iXi
创建日期: 2025-09-19
需要依赖：httpx[http2]
抓包描述: 开启抓包，打开小程序“雀巢会员俱乐部”，进去后抓包域名https://crm.nestlechinese.com 任意请求头中的Authorization值作为环境变量值（复制Bearer后面ey开头的字符串）
环境变量：
        变量名：quechao
        变量值：Authorization的值（复制Bearer后面ey开头的字符串，不要空格）
        多账号之间用#分隔：Authorization1#Authorization2
脚本奖励：雀币、抽奖实物
"""

import os
import sys
import asyncio
import time
from datetime import datetime
from typing import List, Dict, Any, Optional

# 自动参与积分抽奖开关设置（每次抽奖扣8积分）
# True表示自动参与抽奖，False表示不参与抽奖
JoinDraw = True

# 短期抽奖活动ID，请勿修改（活动时间25.09.19-25.09.25）
DrawActivity = "SCH7TFRF0MPI"

# 领取抽奖次数的GUID，请勿修改
UPDATE_DRAW_COUNT_GUIDS = {"451BF17A8B3846DBB1D27F0E47F0B2D7", "51BF214377C74E908236D29E0DD58ABB"}

# 需要跳过的日常积分任务ID，都是无法直接完成的任务，不要修改
SKIP_TASK_GUIDS = {"38C8BBDA3DAE4CD685B270D939E5063D", "36EFECD2AD8C44278317ED567EB24DD9"}

try:
    import httpx
except ImportError:
    print("❌ 未安装httpx[http2]库，请运行以下命令安装：")
    print("pip install httpx[http2]")
    sys.exit(1)


class QueChaoBot:
    def __init__(self, jwt_token: str):
        self.jwt_token = jwt_token
        self.base_url = "https://crm.nestlechinese.com"
        self.headers = {
            "host": "crm.nestlechinese.com",
            "displayversion": "0",
            "authorization": f"Bearer {jwt_token}",
            "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36 MicroMessenger/7.0.20.1781(0x6700143B) NetType/WIFI MiniProgramEnv/Windows WindowsWechat/WMPF WindowsWechat(0x63090a13) UnifiedPCWindowsWechat(0xf2541022) XWEB/16467",
            "content-type": "application/json",
            "accept": "*/*",
            "referer": "https://servicewechat.com/wxc5db704249c9bb31/460/page-frame.html",
            "accept-encoding": "gzip, deflate, br",
            "accept-language": "zh-CN,zh;q=0.9"
        }
        self.client = None

    async def __aenter__(self):
        self.client = httpx.AsyncClient(
            base_url=self.base_url,
            headers=self.headers,
            http2=True,
            timeout=30.0
        )
        return self

    async def __aexit__(self, exc_type, exc_val, exc_tb):
        if self.client:
            await self.client.aclose()

    def check_response(self, response_data: Dict[str, Any]) -> bool:
        if response_data.get("errcode") != 200:
            print(f"❌ 请求失败: {response_data.get('errmsg', '未知错误')}")
            return False
        return True

    async def get_user_balance(self) -> Optional[int]:
        """获取用户积分余额"""
        try:
            payload = "{}"
            headers = self.headers.copy()
            headers["content-length"] = str(len(payload))
            
            response = await self.client.post(
                "/openapi/pointsservice/api/Points/getuserbalance",
                content=payload,
                headers=headers
            )
            response_data = response.json()
            
            if self.check_response(response_data):
                return response_data.get("data")
            return None
        except Exception as e:
            print(f"❌ 获取用户积分失败: {e}")
            return None

    async def daily_sign(self) -> bool:
        """每日签到"""
        current_year = datetime.now().year
        years_to_try = [current_year, current_year - 1]
        
        for year in years_to_try:
            try:
                payload = '{"rule_id":1,"goods_rule_id":1}'
                headers = self.headers.copy()
                headers["content-length"] = str(len(payload))
                
                response = await self.client.post(
                    f"/openapi/activityservice/api/sign{year}/sign",
                    content=payload,
                    headers=headers
                )
                response_data = response.json()
                
                if response_data.get("errcode") == 201:
                    print(f"⚠️ 签到错误: {response_data.get('errmsg', '未知错误')}")
                    return False
                    
                if self.check_response(response_data):
                    data = response_data.get("data", {})
                    sign_day = data.get("sign_day", 0)
                    sign_points = data.get("sign_points", 0)
                    print(f"✅ 签到成功，已签到{sign_day}天，获得{sign_points}积分")
                    return True
                    
            except Exception as e:
                print(f"❌ {year}年签到失败: {e}")
                continue
        
        print("❌ 签到失败，继续执行后续任务")
        return False

    async def get_task_list(self) -> List[Dict[str, Any]]:
        """获取日常积分任务列表"""
        try:
            payload = "{}"
            headers = self.headers.copy()
            headers["content-length"] = str(len(payload))
            
            response = await self.client.post(
                "/openapi/activityservice/api/task/getlist",
                content=payload,
                headers=headers
            )
            response_data = response.json()
            
            if self.check_response(response_data):
                tasks = response_data.get("data", [])
                uncompleted_tasks = [
                    task for task in tasks 
                    if task.get("task_status") == 0 
                    and task.get("task_guid") not in SKIP_TASK_GUIDS
                ]
                print(f"📋 未完成任务个数: {len(uncompleted_tasks)}")
                return uncompleted_tasks
            return []
        except Exception as e:
            print(f"❌ 获取任务列表失败: {e}")
            return []

    async def complete_task(self, task_guid: str, task_desc: str, max_retries: int = 3) -> bool:
        """完成任务"""
        if task_guid in SKIP_TASK_GUIDS:
            print(f"⏭️ 跳过任务【{task_desc}】")
            return False
            
        for attempt in range(max_retries):
            try:
                payload = f'{{"task_guid":"{task_guid}"}}'
                headers = self.headers.copy()
                headers["content-length"] = str(len(payload))
                
                response = await self.client.post(
                    "/openapi/activityservice/api/task/add",
                    content=payload,
                    headers=headers
                )
                response_data = response.json()
                
                if self.check_response(response_data):
                    print(f"✅ 成功完成任务【{task_desc}】")
                    return True
                else:
                    if attempt < max_retries - 1:
                        print(f"⏳ 任务提交失败，3秒后重试 ({attempt + 1}/{max_retries})")
                        await asyncio.sleep(3)
                    
            except Exception as e:
                print(f"❌ 完成任务失败: {e}")
                if attempt < max_retries - 1:
                    await asyncio.sleep(3)
        
        print(f"❌ 任务【{task_desc}】完成失败，已达到最大重试次数")
        return False

    async def get_draw_activity_info(self) -> Optional[Dict[str, Any]]:
        """获取抽奖活动信息"""
        try:
            response = await self.client.get(f"/openapi/activityservice/api/LuckyDraw/{DrawActivity}")
            response_data = response.json()
            
            if self.check_response(response_data):
                data = response_data.get("data", {})
                title = data.get("title", "")
                end_time = data.get("end_time", "")
                print(f"🎯 【{title}】活动截止日期{end_time}")
                
                if end_time:
                    try:
                        end_datetime = datetime.fromisoformat(end_time.replace('T', ' '))
                        if datetime.now() > end_datetime:
                            print("⏰ 活动已结束，跳过抽奖")
                            return None
                    except:
                        pass
                
                return data
            return None
        except Exception as e:
            print(f"❌ 获取抽奖活动信息失败: {e}")
            return None

    async def get_draw_tasks(self) -> List[Dict[str, Any]]:
        """获取抽奖任务列表"""
        all_draw_tasks = []
        
        try:
            payload = f'{{"show_channel":"{DrawActivity}","task_type":1,"gift_count":0}}'
            headers = self.headers.copy()
            headers["content-length"] = str(len(payload))
            
            response = await self.client.post(
                "/openapi/activityservice/api/task/getlistbyshowchanneltype",
                content=payload,
                headers=headers
            )
            response_data = response.json()
            
            if self.check_response(response_data):
                tasks = response_data.get("data", [])
                uncompleted_tasks = [
                    task for task in tasks 
                    if task.get("task_status") == 0 
                    and task.get("task_guid") not in SKIP_TASK_GUIDS
                ]
                all_draw_tasks.extend(uncompleted_tasks)
                print(f"📋 获取到{len(uncompleted_tasks)}个未完成的抽奖任务（type=1）")
            
            payload = f'{{"show_channel":"{DrawActivity}","task_type":0,"gift_count":1}}'
            headers["content-length"] = str(len(payload))
            
            response = await self.client.post(
                "/openapi/activityservice/api/task/getlistbyshowchanneltype",
                content=payload,
                headers=headers
            )
            response_data = response.json()
            
            if self.check_response(response_data):
                tasks = response_data.get("data", [])
                uncompleted_single_tasks = [
                    task for task in tasks 
                    if task.get("task_status") == 0 
                    and task.get("task_guid") not in SKIP_TASK_GUIDS
                ]
                all_draw_tasks.extend(uncompleted_single_tasks)
                print(f"📋 获取到{len(uncompleted_single_tasks)}个未完成的单次任务（type=0）")
            
            print(f"📋 总共获取到{len(all_draw_tasks)}个未完成的抽奖相关任务")
            return all_draw_tasks
            
        except Exception as e:
            print(f"❌ 获取抽奖任务列表失败: {e}")
            return []

    async def update_draw_count(self) -> bool:
        """更新抽奖次数"""
        success_count = 0
        
        for task_guid in UPDATE_DRAW_COUNT_GUIDS:
            try:
                payload = f'{{"task_guid":"{task_guid}"}}'
                headers = self.headers.copy()
                headers["content-length"] = str(len(payload))
                
                response = await self.client.post(
                    "/openapi/activityservice/api/task/add",
                    content=payload,
                    headers=headers
                )
                response_data = response.json()
                
                errcode = response_data.get("errcode")
                if errcode in [200, 201]:
                    print(f"✅ 领取抽奖次数提交成功{task_guid}")
                    success_count += 1
                else:
                    print(f"❌ 领取抽奖次数提交失败 (task_guid: {task_guid}): {response_data.get('errmsg', '未知错误')}")
                    
                await asyncio.sleep(1)
                    
            except Exception as e:
                print(f"❌ 更新抽奖次数异常 (task_guid: {task_guid}): {e}")
        
        return success_count > 0

    async def get_draw_count(self) -> int:
        """获取抽奖次数"""
        try:
            response = await self.client.get(f"/openapi/activityservice/api/LuckyDraw/{DrawActivity}")
            response_data = response.json()
            
            if self.check_response(response_data):
                data = response_data.get("data", {})
                count = data.get("count", 0)
                print(f"🎲 当前可抽奖{count}次")
                return count
            return 0
        except Exception as e:
            print(f"❌ 获取抽奖次数失败: {e}")
            return 0

    async def draw_lottery(self, draw_count: int) -> None:
        """进行抽奖"""
        if not JoinDraw:
            print("🚫 JoinDraw设置为False，跳过自动抽奖")
            return
        
        for i in range(draw_count):
            try:
                response = await self.client.get(
                    f"/openapi/activityservice/api/LuckyDraw/LuckyDrawByPoints/{DrawActivity}"
                )
                response_data = response.json()
                
                if self.check_response(response_data):
                    data = response_data.get("data", {})
                    title = data.get("title", "未知奖品")
                    print(f"🎉 第{i + 1}次抽奖，获得{title}")
                else:
                    print(f"❌ 第{i + 1}次抽奖失败")
                    
                if i < draw_count - 1:
                    await asyncio.sleep(1)
                    
            except Exception as e:
                print(f"❌ 第{i + 1}次抽奖异常: {e}")

    async def get_xiaoxiaole_code(self) -> None:
        """获取消消乐礼包"""
        try:
            payload = '{"rule_id":1}'
            headers = self.headers.copy()
            headers["content-length"] = str(len(payload))
            
            response = await self.client.post(
                "/openapi/activityservice/api/xiaoxiaolegame/getcode",
                content=payload,
                headers=headers
            )
            response_data = response.json()
            
            if response_data.get("errcode") == 200:
                data = response_data.get("data", {})
                print(f"✅ 获取消消乐礼包成功: {data}")
            else:
                print(f"❌ 获取消消乐礼包失败: {response_data.get('errmsg', '未知错误')}")
                
        except Exception as e:
            print(f"❌ 获取消消乐礼包异常: {e}")

    async def run(self, account_index: int) -> None:
        """运行主流程"""
        print(f"\n{'='*50}")
        print(f"开始处理账号{account_index + 1}")
        print(f"{'='*50}")
        
        # 1. 获取初始积分
        initial_balance = await self.get_user_balance()
        if initial_balance is None:
            print(f"❌ 账号{account_index + 1}Token无效")
            return
        
        print(f"✅ 账号{account_index + 1}Token有效")
        print(f"💰 今日初始积分: {initial_balance}")
        
        # 2. 每日签到
        await self.daily_sign()
        
        # 3. 获取并完成日常任务
        tasks = await self.get_task_list()
        for task in tasks:
            task_guid = task.get("task_guid", "")
            task_desc = task.get("task_sub_desc", "")
            if task_guid:
                await self.complete_task(task_guid, task_desc)
                await asyncio.sleep(1)
        
        # 4. 抽奖相关任务
        draw_info = await self.get_draw_activity_info()
        if draw_info:
            draw_tasks = await self.get_draw_tasks()
            for task in draw_tasks:
                task_guid = task.get("task_guid", "")
                task_title = task.get("task_title", "")
                if task_guid:
                    await self.complete_task(task_guid, f"抽奖任务-{task_title}")
                    await asyncio.sleep(1)
            
            # 完成抽奖任务后，更新抽奖次数
            if draw_tasks:
                print("🔄 更新抽奖次数...")
                await self.update_draw_count()
            
            # 获取抽奖次数并抽奖
            draw_count = await self.get_draw_count()
            if draw_count > 0:
                await self.draw_lottery(draw_count)
                # 抽奖操作完成后获取消消乐礼包（每天限量100份，领取不到也没事，只有游戏道具，没啥用）
                await self.get_xiaoxiaole_code()
        
        # 5. 获取最终积分并计算差值
        final_balance = await self.get_user_balance()
        if final_balance is not None:
            gained_points = final_balance - initial_balance
            print(f"📊 今日共获得{gained_points}积分，当前积分{final_balance}")
        
        print(f"✅ 账号{account_index + 1}处理完成")


def validate_jwt(jwt_token: str) -> bool:
    """验证Token格式"""
    return jwt_token.startswith('ey')


async def main():
    quechao_env = os.getenv('quechao')
    if not quechao_env:
        print("❌ 未找到名为'quechao'的环境变量")
        return
    
    jwt_tokens = [token.strip() for token in quechao_env.split('#') if token.strip()]
    
    if not jwt_tokens:
        print("❌ 环境变量'quechao'为空或格式错误")
        return
    
    print(f"🔍 检测到{len(jwt_tokens)}个账号凭证")
    
    valid_tokens = []
    for i, token in enumerate(jwt_tokens):
        if validate_jwt(token):
            valid_tokens.append(token)
        else:
            print(f"❌ 第{i + 1}个Token格式错误（不是'ey'开头），请重新填写")
    
    if not valid_tokens:
        print("❌ 没有有效的账号凭证")
        return
    
    print(f"✅ 有效账号凭证数量: {len(valid_tokens)}")
    
    for i, token in enumerate(valid_tokens):
        try:
            async with QueChaoBot(token) as bot:
                await bot.run(i)
        except Exception as e:
            print(f"❌ 账号{i + 1}处理异常: {e}")
        
        if i < len(valid_tokens) - 1:
            print(f"\n⏳ 等待3秒后处理下一个账号...")
            await asyncio.sleep(3)
    
    print(f"\n🎉 所有账号处理完成！")


if __name__ == "__main__":
    print("☕️ 雀巢自动化脚本启动")
    print(f"📅 当前时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
    print(f"🎯 抽奖活动ID: {DrawActivity}")
    print(f"🎲 自动抽奖开关: {'开启' if JoinDraw else '关闭'}")
    # print(f"🔄 更新抽奖次数任务ID: {', '.join(UPDATE_DRAW_COUNT_GUIDS)}")
    
    try:
        asyncio.run(main())
    except KeyboardInterrupt:
        print("\n⏹️ 用户中断，脚本退出")
    except Exception as e:
        print(f"\n❌ 程序异常: {e}")