"""
TikTok 私信机器人模块
通过 Playwright 自动化控制网页版 TikTok，实现私信监控和自动回复。

使用方法:
    bot = TikTokBot()
    bot.login()          # 打开浏览器让用户扫码登录
    bot.start_monitoring()  # 开始监控新私信
    bot.stop_monitoring()   # 停止监控
"""

import os, json, time, threading, re, asyncio
from datetime import datetime
from typing import Optional, Callable

# ── 常量 ──
CONTEXT_DIR = os.path.join(os.path.dirname(__file__), "tiktok_session")
COOKIE_FILE = os.path.join(CONTEXT_DIR, "cookies.json")
STORAGE_FILE = os.path.join(CONTEXT_DIR, "storage_state.json")

# 中文星期
WEEKDAYS = ["周一","周二","周三","周四","周五","周六","周日"]


def _ts() -> str:
    return datetime.now().strftime("%H:%M:%S")

def _fmt_time(ts: float) -> str:
    dt = datetime.fromtimestamp(ts)
    if dt.date() == datetime.now().date():
        return dt.strftime("%H:%M")
    return dt.strftime(f"%m/%d %H:%M")


class TikTokBot:
    """TikTok 网页私信机器人"""

    def __init__(self):
        self._loop: Optional[asyncio.AbstractEventLoop] = None
        self._browser = None
        self._context = None
        self._page: Optional = None
        self._monitor_thread: Optional[threading.Thread] = None
        self._running = False
        self._stop_ev = threading.Event()
        self._message_queue: list[dict] = []
        self._conversations: list[dict] = []
        self._on_new_message: Optional[Callable] = None
        self._on_log: Optional[Callable] = None
        self._lock = threading.Lock()
        self._reply_rules: list[dict] = []
        self._auto_reply_enabled = False
        self._logged_in = False

        # 确保目录存在
        os.makedirs(CONTEXT_DIR, exist_ok=True)

    # ── 日志 ──
    def _log(self, msg: str):
        if self._on_log:
            self._on_log(f"[{_ts()}] {msg}")

    # ── Playwright 生命周期 ──
    async def _ensure_browser(self):
        """确保浏览器已启动"""
        if self._page:
            return True
        try:
            from playwright.async_api import async_playwright
            self._pw = await async_playwright().start()

            # 如果已有保存的 session，恢复
            if os.path.exists(STORAGE_FILE):
                self._context = await self._pw.chromium.launch_persistent_context(
                    user_data_dir=CONTEXT_DIR,
                    headless=False,
                    viewport={'width': 1280, 'height': 800},
                    no_viewport=True,
                    locale="zh-CN",
                    timezone_id="Asia/Shanghai",
                    user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36',
                )
                # 恢复 storage state
                try:
                    with open(STORAGE_FILE, 'r') as f:
                        state = json.load(f)
                    await self._context.add_cookies(state.get('cookies', []))
                    await self._context.add_init_script("""
                        Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
                        Object.defineProperty(navigator, 'plugins', { get: () => [1,2,3,4,5] });
                    """)
                except:
                    pass
            else:
                self._context = await self._pw.chromium.launch_persistent_context(
                    user_data_dir=CONTEXT_DIR,
                    headless=False,
                    viewport={'width': 1280, 'height': 800},
                    no_viewport=True,
                    locale="zh-CN",
                    timezone_id="Asia/Shanghai",
                    user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36',
                )
                await self._context.add_init_script("""
                    Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
                    Object.defineProperty(navigator, 'plugins', { get: () => [1,2,3,4,5] });
                """)

            pages = self._context.pages
            self._page = pages[0] if pages else await self._context.new_page()
            return True
        except Exception as e:
            self._log(f"❌ 浏览器启动失败: {e}")
            return False

    # ── 登录 ──
    async def _do_login(self):
        """打开 TikTok 等待用户登录"""
        if not await self._ensure_browser():
            return False
        try:
            await self._page.goto("https://www.tiktok.com", timeout=30000)
            await self._page.wait_for_load_state("domcontentloaded")

            self._log("🟡 请在打开的浏览器中扫码或账号登录 TikTok")
            self._log("   登录完成后，请在此应用点击「确认已登录」按钮")

            # 轮询等待登录状态（检测 cookie 中的 sessionid）
            for _ in range(120):  # 最多等 2 分钟
                cookies = await self._context.cookies()
                for c in cookies:
                    if c['name'] in ('sessionid', 'sid_tt', 'sessionid_ss'):
                        self._logged_in = True
                        self._save_session()
                        self._log("✅ 登录成功！")
                        return True
                await asyncio.sleep(1)

            self._log("⏰ 登录超时，请重试")
            return False
        except Exception as e:
            self._log(f"❌ 登录出错: {e}")
            return False

    def login(self, timeout: int = 120) -> bool:
        """登录 TikTok（同步方法）"""
        async def _run():
            return await self._do_login()

        try:
            result = asyncio.run(_run())
            return result
        except RuntimeError:
            # event loop already running
            loop = asyncio.new_event_loop()
            asyncio.set_event_loop(loop)
            result = loop.run_until_complete(self._do_login())
            loop.close()
            return result
        except Exception as e:
            self._log(f"❌ 登录异常: {e}")
            return False

    async def check_login_status(self) -> bool:
        """检查是否已登录"""
        if self._logged_in:
            return True
        if not await self._ensure_browser():
            return False
        try:
            cookies = await self._context.cookies()
            for c in cookies:
                if c['name'] in ('sessionid', 'sid_tt', 'sessionid_ss'):
                    self._logged_in = True
                    return True
        except:
            pass
        return False

    def is_logged_in(self) -> bool:
        """同步检查登录状态"""
        try:
            loop = asyncio.new_event_loop()
            asyncio.set_event_loop(loop)
            result = loop.run_until_complete(self.check_login_status())
            loop.close()
            return result
        except:
            return self._logged_in

    def _save_session(self):
        """保存 session"""
        try:
            import asyncio
            async def save():
                if self._context:
                    state = await self._context.storage_state()
                    with open(STORAGE_FILE, 'w') as f:
                        json.dump(state, f)
                    self._log("💾 Session 已保存")
            asyncio.run(save())
        except:
            pass

    # ── 获取私信 ──
    async def _go_to_messages(self):
        """打开私信页面"""
        if not await self._ensure_browser():
            return False
        try:
            await self._page.goto("https://www.tiktok.com/messages", timeout=30000)
            await self._page.wait_for_load_state("domcontentloaded")
            await asyncio.sleep(2)
            return True
        except Exception as e:
            self._log(f"❌ 打开私信失败: {e}")
            return False

    async def _get_conversations(self) -> list[dict]:
        """获取会话列表"""
        convos = []
        if not await self._go_to_messages():
            return convos

        try:
            # 尝试多种选择器
            selectors = [
                '[data-e2e="messages-conversation-item"]',
                '[class*="MessageConversationItem"]',
                '[class*="ConversationItem"]',
                '[class*="message-item"]',
                'div[class*="conversation"]',
            ]

            items = []
            for sel in selectors:
                items = await self._page.query_selector_all(sel)
                if items:
                    break

            if not items:
                # 尝试更通用的查找
                items = await self._page.query_selector_all('[class*="item"]')

            for item in items[:20]:
                try:
                    text = await item.inner_text()
                    lines = [l.strip() for l in text.split('\n') if l.strip()]
                    convos.append({
                        'name': lines[0] if lines else "未知",
                        'preview': lines[1] if len(lines) > 1 else "",
                        'time': lines[-1] if len(lines) > 2 else "",
                        'element': item,
                    })
                except:
                    continue

            self._conversations = convos
            self._log(f"📨 获取到 {len(convos)} 个会话")
        except Exception as e:
            self._log(f"⚠ 获取会话失败: {e}")

        return convos

    def fetch_conversations(self) -> list[dict]:
        """同步获取会话列表"""
        try:
            loop = asyncio.new_event_loop()
            asyncio.set_event_loop(loop)
            result = loop.run_until_complete(self._get_conversations())
            loop.close()
            return result
        except:
            return self._conversations

    # ── 获取新消息 ──
    async def _check_new_messages(self) -> list[dict]:
        """检查是否有新消息"""
        new_msgs = []
        if not self._logged_in:
            return new_msgs
        try:
            await self._go_to_messages()
            # 检查未读标记
            unread_selectors = [
                '[data-e2e="unread-count"]',
                '[class*="UnreadBadge"]',
                '[class*="unread"]',
            ]
            for sel in unread_selectors:
                badges = await self._page.query_selector_all(sel)
                for badge in badges:
                    text = await badge.inner_text()
                    try:
                        count = int(text)
                        if count > 0:
                            new_msgs.append({'unread': count})
                    except:
                        new_msgs.append({'unread': text})
                if badges:
                    break
        except:
            pass
        return new_msgs

    # ── 发送回复 ──
    async def _send_reply(self, conversation_idx: int, message: str) -> bool:
        """回复指定会话"""
        if not self._conversations or conversation_idx >= len(self._conversations):
            return False
        try:
            conv = self._conversations[conversation_idx]
            # 点击会话
            el = conv.get('element')
            if el:
                await el.click()
                await asyncio.sleep(1)

            # 输入消息
            input_selectors = [
                '[data-e2e="message-input"]',
                '[contenteditable="true"]',
                'div[class*="input"]',
                'textarea',
                '[class*="DraftEditor"]',
            ]
            chat_input = None
            for sel in input_selectors:
                chat_input = await self._page.query_selector(sel)
                if chat_input:
                    break

            if chat_input:
                await chat_input.fill(message)
                await asyncio.sleep(0.5)

                # 发送
                send_btn = await self._page.query_selector('[data-e2e="send-message"]')
                if send_btn:
                    await send_btn.click()
                else:
                    await chat_input.press("Enter")

                self._log(f"📤 已回复 {conv.get('name','')}: {message[:30]}...")
                return True
        except Exception as e:
            self._log(f"❌ 发送回复失败: {e}")
        return False

    def reply(self, conversation_idx: int, message: str) -> bool:
        """同步发送回复"""
        try:
            loop = asyncio.new_event_loop()
            asyncio.set_event_loop(loop)
            result = loop.run_until_complete(self._send_reply(conversation_idx, message))
            loop.close()
            return result
        except:
            return False

    # ── 自动回复规则 ──
    def set_reply_rules(self, rules: list[dict]):
        """设置自动回复规则
        rules: [{'keyword': '你好', 'reply': '你好呀！', 'match': 'exact|contains|regex'}, ...]
        """
        self._reply_rules = rules
        self._log(f"📋 已设置 {len(rules)} 条回复规则")

    def set_auto_reply(self, enabled: bool):
        self._auto_reply_enabled = enabled
        self._log(f"{'🟢' if enabled else '🔴'} 自动回复{'已开启' if enabled else '已关闭'}")

    def _match_reply(self, text: str) -> Optional[str]:
        """根据规则匹配回复内容"""
        for rule in self._reply_rules:
            keyword = rule.get('keyword', '')
            reply = rule.get('reply', '')
            match = rule.get('match', 'contains')

            if not keyword or not reply:
                continue

            if match == 'exact' and text == keyword:
                return reply
            elif match == 'regex':
                try:
                    if re.search(keyword, text):
                        return reply
                except:
                    continue
            else:  # contains
                if keyword in text:
                    return reply
        return None

    # ── 监控线程 ──
    def start_monitoring(self, interval: float = 5.0):
        """开始后台监控私信"""
        if self._running:
            return
        self._running = True
        self._stop_ev.clear()
        self._monitor_thread = threading.Thread(
            target=self._monitor_loop,
            args=(interval,),
            daemon=True
        )
        self._monitor_thread.start()
        self._log(f"🔍 私信监控已启动 (间隔 {interval}s)")

    def stop_monitoring(self):
        self._running = False
        self._stop_ev.set()
        self._log("⏹ 私信监控已停止")

    def set_on_new_message(self, callback: Callable):
        self._on_new_message = callback

    def set_on_log(self, callback: Callable):
        self._on_log = callback

    def _monitor_loop(self, interval: float):
        """监控循环（在后台线程运行）"""
        import asyncio
        last_check = 0

        while not self._stop_ev.is_set():
            if not self._logged_in:
                # 检查登录状态
                try:
                    loop = asyncio.new_event_loop()
                    asyncio.set_event_loop(loop)
                    logged = loop.run_until_complete(self.check_login_status())
                    loop.close()
                except:
                    pass

            if self._logged_in:
                try:
                    loop = asyncio.new_event_loop()
                    asyncio.set_event_loop(loop)
                    new_msgs = loop.run_until_complete(self._check_new_messages())
                    loop.close()

                    if new_msgs and self._on_new_message:
                        self._on_new_message(new_msgs)

                    # 自动回复
                    if self._auto_reply_enabled and new_msgs:
                        self._process_auto_reply()
                except:
                    pass

            self._stop_ev.wait(interval)

    def _process_auto_reply(self):
        """处理自动回复"""
        if not self._reply_rules:
            return
        # 获取最新会话
        convos = self.fetch_conversations()
        for i, conv in enumerate(convos):
            preview = conv.get('preview', '')
            if preview and self._match_reply(preview):
                reply = self._match_reply(preview)
                if reply:
                    self.reply(i, reply)

    # ── 清理 ──
    async def _cleanup(self):
        """清理资源"""
        if self._context:
            try:
                await self._context.close()
            except:
                pass
            self._context = None
        if self._pw:
            try:
                await self._pw.stop()
            except:
                pass
            self._pw = None
        self._page = None

    def close(self):
        self.stop_monitoring()
        try:
            loop = asyncio.new_event_loop()
            asyncio.set_event_loop(loop)
            loop.run_until_complete(self._cleanup())
            loop.close()
        except:
            pass

    # ── 获取消息历史 ──
    async def _get_messages(self, conversation_idx: int, limit: int = 20) -> list[dict]:
        """获取指定会话的消息历史"""
        msgs = []
        if not self._conversations or conversation_idx >= len(self._conversations):
            return msgs
        try:
            conv = self._conversations[conversation_idx]
            el = conv.get('element')
            if el:
                await el.click()
                await asyncio.sleep(1.5)

            # 查找消息元素
            msg_selectors = [
                '[data-e2e="message-item"]',
                '[class*="MessageItem"]',
                '[class*="message-item"]',
            ]
            items = []
            for sel in msg_selectors:
                items = await self._page.query_selector_all(sel)
                if items:
                    break

            for item in items[-limit:]:
                try:
                    text = await item.inner_text()
                    is_mine = 'self' in (await item.get_attribute('class') or '') or \
                              'outgoing' in (await item.get_attribute('class') or '')
                    msgs.append({
                        'text': text.strip(),
                        'is_mine': is_mine,
                    })
                except:
                    continue

            msgs.reverse()
        except:
            pass
        return msgs

    def fetch_messages(self, conversation_idx: int, limit: int = 20) -> list[dict]:
        """同步获取消息历史"""
        try:
            loop = asyncio.new_event_loop()
            asyncio.set_event_loop(loop)
            result = loop.run_until_complete(self._get_messages(conversation_idx, limit))
            loop.close()
            return result
        except:
            return []


if __name__ == "__main__":
    # 测试
    bot = TikTokBot()
    bot.set_on_log(lambda m: print(m))
    print("=" * 50)
    print("TikTok 私信机器人 - 测试模式")
    print("=" * 50)
    print("\n1. 登录")
    print("2. 检查状态")
    print("3. 获取会话")
    print("4. 退出")

    while True:
        cmd = input("\n选择: ").strip()
        if cmd == "1":
            print("正在打开浏览器...")
            ok = bot.login()
            print("结果:", "✅ 成功" if ok else "❌ 失败/超时")
        elif cmd == "2":
            ok = bot.is_logged_in()
            print("登录状态:", "✅ 已登录" if ok else "❌ 未登录")
        elif cmd == "3":
            convos = bot.fetch_conversations()
            for i, c in enumerate(convos):
                print(f"  [{i}] {c.get('name','?')} - {c.get('preview','')[:40]}")
        elif cmd == "4":
            bot.close()
            break
