"""
Douyin (抖音) uploader using Playwright
Based on social-auto-upload implementation
"""
from datetime import datetime
from pathlib import Path
from typing import Optional, List, Dict, Any
import asyncio
import os
import re
import shutil
import time

from playwright.async_api import Playwright, async_playwright, TimeoutError as PlaywrightTimeoutError
from loguru import logger

from .base_uploader import BaseUploader
from .cookie_utils import set_init_script
from app.core.config import settings


class DouyinUploader(BaseUploader):
    """Douyin video uploader using Playwright"""
    
    # 超时配置 (秒)
    UPLOAD_TIMEOUT = 300  # 视频上传超时
    PUBLISH_TIMEOUT = 180  # 发布检测超时
    PUBLISH_BUTTON_TIMEOUT = 60  # 等待发布按钮可点超时
    POST_UPLOAD_STAGE_TIMEOUT = 60  # 上传完成后到发布结果的总超时
    PAGE_REDIRECT_TIMEOUT = 60  # 页面跳转超时
    POLL_INTERVAL = 2  # 轮询间隔
    MAX_CLICK_RETRIES = 3  # 按钮点击重试次数
    
    def __init__(
        self,
        title: str,
        file_path: str,
        tags: List[str],
        publish_date: Optional[datetime] = None,
        account_file: Optional[str] = None,
        description: str = "",
        user_id: Optional[str] = None,
    ):
        super().__init__(title, file_path, tags, publish_date, account_file, description)
        self.user_id = user_id
        self.upload_url = "https://creator.douyin.com/creator-micro/content/upload"
        self._temp_upload_paths: List[Path] = []
        self._video_upload_committed = False
        self._cover_generated = False
        self._cover_gen_count = 0
        self._publish_api_error: Optional[str] = None

    def _resolve_headless_mode(self) -> str:
        mode = (settings.DOUYIN_HEADLESS_MODE or "").strip().lower()
        return mode or "headless-new"

    def _build_launch_options(self) -> Dict[str, Any]:
        mode = self._resolve_headless_mode()
        args = [
            "--no-sandbox",
            "--disable-dev-shm-usage",
            "--disable-blink-features=AutomationControlled",
        ]

        headless = mode not in ("headful", "false", "0", "no")
        if headless and mode in ("new", "headless-new", "headless_new"):
            args.append("--headless=new")

        if settings.DOUYIN_FORCE_SWIFTSHADER or headless:
            args.extend([
                "--enable-unsafe-swiftshader",
                "--use-gl=swiftshader",
            ])

        options: Dict[str, Any] = {"headless": headless, "args": args}
        chrome_path = (settings.DOUYIN_CHROME_PATH or "").strip()
        if chrome_path:
            if Path(chrome_path).exists():
                options["executable_path"] = chrome_path
            else:
                logger.warning(f"[抖音] DOUYIN_CHROME_PATH 不存在: {chrome_path}")
        else:
            channel = (settings.DOUYIN_BROWSER_CHANNEL or "").strip()
            if channel:
                options["channel"] = channel

        return options

    def _debug_log_path(self) -> Path:
        debug_dir = Path(__file__).parent.parent.parent / "debug_screenshots"
        debug_dir.mkdir(exist_ok=True)
        return debug_dir / "douyin_network.log"

    def _debug_artifacts_enabled(self) -> bool:
        return bool(settings.DEBUG and settings.DOUYIN_DEBUG_ARTIFACTS)

    def _record_video_enabled(self) -> bool:
        return bool(self._debug_artifacts_enabled() and settings.DOUYIN_RECORD_VIDEO)

    def _append_debug_log(self, message: str) -> None:
        if not self._debug_artifacts_enabled():
            return
        try:
            timestamp = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
            log_path = self._debug_log_path()
            with log_path.open("a", encoding="utf-8") as handle:
                handle.write(f"[{timestamp}] {message}\n")
        except Exception:
            pass

    def _video_record_dir(self) -> Path:
        debug_dir = Path(__file__).parent.parent.parent / "debug_screenshots" / "videos"
        debug_dir.mkdir(parents=True, exist_ok=True)
        return debug_dir

    async def _save_recorded_video(self, video, success: bool) -> Optional[Path]:
        if not self._record_video_enabled():
            return None
        if not video:
            return None

        try:
            timestamp = time.strftime("%Y%m%d_%H%M%S", time.localtime())
            status = "success" if success else "failed"
            file_path = self._video_record_dir() / f"douyin_{timestamp}_{status}.webm"
            await video.save_as(str(file_path))
            self._append_debug_log(f"[douyin][record] saved={file_path}")

            if success and not settings.DOUYIN_KEEP_SUCCESS_VIDEO:
                try:
                    file_path.unlink(missing_ok=True)
                except TypeError:
                    if file_path.exists():
                        file_path.unlink()
                self._append_debug_log("[douyin][record] removed_success_video")
                return None

            return file_path
        except Exception as e:
            logger.warning(f"[抖音] 保存录屏失败: {e}")
            return None

    async def _save_debug_screenshot(self, page, name: str) -> None:
        if not self._debug_artifacts_enabled():
            return
        try:
            debug_dir = Path(__file__).parent.parent.parent / "debug_screenshots"
            debug_dir.mkdir(exist_ok=True)
            file_path = debug_dir / f"douyin_{name}.png"
            await page.screenshot(path=str(file_path), full_page=True)
        except Exception as e:
            logger.warning(f"[抖音] 保存截图失败: {e}")

    def _publish_screenshot_dir(self) -> Path:
        user_key = re.sub(r"[^A-Za-z0-9_-]", "_", self.user_id or "legacy")[:64] or "legacy"
        target = settings.PUBLISH_SCREENSHOT_DIR / user_key
        target.mkdir(parents=True, exist_ok=True)
        return target

    async def _open_manage_review_tab(self, page) -> tuple[bool, str]:
        manage_url = "https://creator.douyin.com/creator-micro/content/manage"
        try:
            if "content/manage" not in page.url:
                await page.goto(manage_url)
                await page.wait_for_load_state("domcontentloaded")
                await asyncio.sleep(1.5)

            review_selectors = [
                "button:has-text('审核中')",
                "div[role='tab']:has-text('审核中')",
                "span:has-text('审核中')",
                "button:has-text('待审核')",
                "div[role='tab']:has-text('待审核')",
                "span:has-text('待审核')",
            ]

            for selector in review_selectors:
                target = await self._first_visible_locator(page.locator(selector), timeout=1200)
                if not target:
                    continue
                try:
                    await target.scroll_into_view_if_needed(timeout=1000)
                except Exception:
                    pass
                try:
                    await target.click(timeout=2500)
                except Exception:
                    await target.evaluate("el => el.click()")
                await asyncio.sleep(1.0)
                self._append_debug_log(f"[douyin][manage] review_tab_clicked selector={selector}")
                return True, selector

            text_click_targets = [
                page.get_by_text("审核中", exact=False).first,
                page.get_by_text("待审核", exact=False).first,
            ]
            for target in text_click_targets:
                try:
                    if not await target.is_visible(timeout=800):
                        continue
                    try:
                        await target.scroll_into_view_if_needed(timeout=1000)
                    except Exception:
                        pass
                    try:
                        await target.click(timeout=2000)
                    except Exception:
                        await target.evaluate("el => el.click()")
                    await asyncio.sleep(1.0)
                    self._append_debug_log("[douyin][manage] review_tab_clicked selector=text")
                    return True, "text"
                except Exception:
                    continue

            return False, "review-tab-not-found"
        except Exception as e:
            return False, f"open-manage-error:{e}"

    async def _save_publish_success_screenshot(self, page) -> Optional[str]:
        try:
            timestamp = time.strftime("%Y%m%d_%H%M%S", time.localtime())
            filename = f"douyin_success_review_{timestamp}_{int(time.time() * 1000) % 1000:03d}.png"
            file_path = self._publish_screenshot_dir() / filename
            opened, reason = await self._open_manage_review_tab(page)
            if not opened:
                self._append_debug_log(f"[douyin][manage] review_tab_open_failed reason={reason}")
                return None

            title_text = self.title[:30]
            title_found = False
            for _ in range(2):
                deadline = time.time() + 8
                while time.time() < deadline:
                    try:
                        title_locator = page.get_by_text(title_text, exact=False).first
                        if await title_locator.is_visible(timeout=500):
                            title_found = True
                            break
                    except Exception:
                        pass
                    await asyncio.sleep(0.4)
                if title_found:
                    break
                # 审核中列表有延迟，刷新后重查一次
                await page.reload(wait_until="domcontentloaded")
                await asyncio.sleep(1.0)
                opened, _ = await self._open_manage_review_tab(page)
                if not opened:
                    break

            self._append_debug_log(
                f"[douyin][manage] review_screenshot title_found={title_found} title={title_text}"
            )
            await page.screenshot(path=str(file_path), full_page=False)
            return f"/api/publish/screenshot/{filename}"
        except Exception as e:
            logger.warning(f"[抖音] 保存发布成功截图失败: {e}")
            return None

    def _track_temp_upload_path(self, path: Path) -> None:
        if path not in self._temp_upload_paths:
            self._temp_upload_paths.append(path)

    def _prepare_upload_file(self) -> Path:
        src = self.file_path
        if src.suffix:
            return src

        parent_suffix = Path(src.parent.name).suffix
        if not parent_suffix:
            logger.warning(f"[抖音] 上传文件缺少扩展名，且无法从父目录推断后缀: {src}")
            self._append_debug_log(f"[douyin][upload_file_prepare] no suffix source={src}")
            return src

        temp_dir = Path("/tmp/vigent_uploads")
        temp_dir.mkdir(parents=True, exist_ok=True)
        target = temp_dir / src.parent.name

        try:
            if target.exists():
                target.unlink()
        except Exception:
            pass

        try:
            os.link(src, target)
        except Exception:
            shutil.copy2(src, target)

        self._track_temp_upload_path(target)
        logger.info(f"[抖音] 使用临时上传文件: {target}")
        self._append_debug_log(f"[douyin][upload_file_prepare] source={src} prepared={target}")
        return target

    def _cleanup_upload_file(self) -> None:
        if not self._temp_upload_paths:
            return
        paths = list(self._temp_upload_paths)
        self._temp_upload_paths = []
        for path in paths:
            try:
                if path.exists():
                    path.unlink()
            except Exception as e:
                logger.warning(f"[抖音] 清理临时上传文件失败: {e}")

    def _should_log_request(self, request) -> bool:
        try:
            if request.resource_type not in ("xhr", "fetch"):
                return False
            if request.method not in ("POST", "PUT"):
                return False
            return True
        except Exception:
            return False

    def _attach_debug_listeners(self, page) -> None:
        def log_console(msg):
            try:
                if not self._debug_artifacts_enabled():
                    return
                if msg.type in ("error", "warning"):
                    text = f"[douyin][console] {msg.type}: {msg.text}"
                    logger.warning(text)
                    self._append_debug_log(text)
            except Exception:
                pass

        def log_page_error(err):
            try:
                if not self._debug_artifacts_enabled():
                    return
                text = f"[douyin][pageerror] {err}"
                logger.warning(text)
                self._append_debug_log(text)
            except Exception:
                pass

        def log_request_failed(request):
            try:
                if not self._debug_artifacts_enabled():
                    return
                failure = request.failure
                error_text = failure.error_text if failure else "unknown"
                if self._should_log_request(request):
                    text = f"[douyin][requestfailed] {request.method} {request.url} -> {error_text}"
                    logger.warning(text)
                    self._append_debug_log(text)
            except Exception:
                pass

        def log_request(request):
            try:
                if not self._debug_artifacts_enabled():
                    return
                if self._should_log_request(request):
                    text = f"[douyin][request] {request.method} {request.url}"
                    self._append_debug_log(text)
            except Exception:
                pass

        def log_response(response):
            try:
                request = response.request
                if self._debug_artifacts_enabled() and (self._should_log_request(request) or response.status >= 400):
                    text = f"[douyin][response] {response.status} {request.method} {response.url}"
                    logger.warning(text)
                    self._append_debug_log(text)

                if response.status < 400:
                    url = response.url
                    if "vod.bytedanceapi.com/" in url and "Action=CommitUploadInner" in url:
                        self._video_upload_committed = True
                        self._append_debug_log("[douyin][upload_signal] commit_upload_inner_ok")
                    if "/aweme/v1/cover/gen/post" in url:
                        self._cover_generated = True
                        self._cover_gen_count += 1
                        self._append_debug_log("[douyin][upload_signal] cover_gen_ok")
                else:
                    url = response.url
                    if "/web/api/media/aweme/create_v2/" in url or "/aweme/create_v2/" in url:
                        self._publish_api_error = f"发布请求失败（HTTP {response.status}）"
                        self._append_debug_log(
                            f"[douyin][publish_api_error] status={response.status} endpoint=create_v2"
                        )
            except Exception:
                pass

        page.on("console", log_console)
        page.on("pageerror", log_page_error)
        page.on("requestfailed", log_request_failed)
        page.on("request", log_request)
        page.on("response", log_response)

    async def _is_text_visible(self, page, text: str, exact: bool = False) -> bool:
        try:
            return await page.get_by_text(text, exact=exact).first.is_visible()
        except Exception:
            return False

    async def _first_visible_locator(self, locator, timeout: int = 1000):
        try:
            if await locator.count() == 0:
                return None
            candidate = locator.first
            if await candidate.is_visible(timeout=timeout):
                return candidate
        except Exception:
            return None
        return None

    async def _open_upload_page(self, page) -> None:
        home_url = "https://creator.douyin.com/creator-micro/home"
        clicked_high_quality = False
        try:
            await page.goto(home_url)
            await page.wait_for_load_state("domcontentloaded")
            await asyncio.sleep(1)
            for selector in [
                "button:has-text('高清发布')",
                "a:has-text('高清发布')",
                "div[role='button']:has-text('高清发布')",
            ]:
                try:
                    button = await self._first_visible_locator(page.locator(selector), timeout=1200)
                    if not button:
                        continue
                    await button.click()
                    logger.info(f"[抖音] 已点击上传入口: {selector}")
                    clicked_high_quality = True
                    await asyncio.sleep(1.5)
                    break
                except Exception:
                    continue
        except Exception as e:
            logger.warning(f"[抖音] 打开首页失败，将直接进入上传页: {e}")

        if "content/upload" not in page.url:
            await page.goto(self.upload_url)
            await page.wait_for_load_state("domcontentloaded")
            await asyncio.sleep(1)
        elif clicked_high_quality:
            await asyncio.sleep(1)

    async def _is_login_page(self, page) -> bool:
        try:
            current_url = page.url or ""
            lower_url = current_url.lower()
            if any(token in lower_url for token in ("passport", "login", "check_qrconnect", "sso")):
                return True

            text_hints = [
                "扫码登录",
                "验证码登录",
                "密码登录",
                "立即登录",
                "登录后",
                "抖音APP扫码登录",
                "创作者登录",
                "我想MCN机构",
            ]
            for text in text_hints:
                if await self._is_text_visible(page, text, exact=False):
                    return True

            selector_hints = [
                "input[placeholder*='手机号']",
                "input[placeholder*='验证码']",
                "button:has-text('立即登录')",
                "button:has-text('登录')",
            ]
            for selector in selector_hints:
                try:
                    target = await self._first_visible_locator(page.locator(selector), timeout=600)
                    if target:
                        return True
                except Exception:
                    continue
        except Exception:
            return False

        return False

    async def _handle_unfinished_draft(self, page) -> None:
        if not await self._is_text_visible(page, "你还有上次未发布的视频", exact=False):
            return

        logger.info("[抖音] 检测到未发布草稿，尝试放弃后重新上传")
        clicked = False
        for selector in [
            "button:has-text('放弃')",
            "a:has-text('放弃')",
            "span:has-text('放弃')",
        ]:
            try:
                target = await self._first_visible_locator(page.locator(selector), timeout=1200)
                if not target:
                    continue
                await target.click()
                clicked = True
                logger.info(f"[抖音] 已点击草稿放弃按钮: {selector}")
                await asyncio.sleep(0.8)
                break
            except Exception:
                continue

        if not clicked:
            return

        for selector in [
            "button:has-text('确认放弃')",
            "button:has-text('确认')",
            "button:has-text('确定')",
        ]:
            try:
                confirm_button = await self._first_visible_locator(page.locator(selector), timeout=800)
                if confirm_button:
                    await confirm_button.click()
                    logger.info(f"[抖音] 已确认放弃草稿: {selector}")
                    await asyncio.sleep(0.6)
                    break
            except Exception:
                continue

    async def _is_publish_form_ready(self, page) -> bool:
        current_url = page.url
        if "content/publish" in current_url or "content/post/video" in current_url:
            return True

        for text in ["基础信息", "作品描述", "发布设置", "重新上传"]:
            if await self._is_text_visible(page, text, exact=False):
                return True

        for selector in [
            "button:has-text('发布')",
            "button:has-text('定时发布')",
        ]:
            try:
                button = await self._first_visible_locator(page.locator(selector), timeout=600)
                if button:
                    return True
            except Exception:
                continue

        return False

    async def _wait_for_publish_form_ready(self, page, timeout: int = 60) -> bool:
        start_time = time.time()
        while time.time() - start_time < timeout:
            if await self._is_publish_form_ready(page):
                return True
            await asyncio.sleep(0.5)
        return False

    async def _is_upload_completed(self, page) -> bool:
        in_progress = await self._is_upload_in_progress(page)

        reupload_visible = await self._is_text_visible(page, "重新上传", exact=False)
        preview_video_visible = await self._is_text_visible(page, "预览视频", exact=False)
        preview_cover_visible = await self._is_text_visible(page, "预览封面/标题", exact=False)

        # 页面已出现“重新上传 + 预览”并且视频提交信号已到，优先认为可进入封面步骤
        # 避免“处理中”文案残留导致长时间不开始封面设置
        if reupload_visible and (preview_video_visible or preview_cover_visible) and self._video_upload_committed:
            self._append_debug_log("[douyin][upload_ready] visual_done_reupload_preview_commit")
            return True

        if reupload_visible and (preview_video_visible or preview_cover_visible) and not in_progress:
            self._append_debug_log("[douyin][upload_ready] visual_done_reupload_preview")
            return True

        if reupload_visible and not in_progress:
            self._append_debug_log("[douyin][upload_ready] visual_done_reupload")
            return True

        if (preview_video_visible or preview_cover_visible) and self._video_upload_committed and not in_progress:
            if self._cover_generated:
                self._append_debug_log("[douyin][upload_ready] visual_done_commit_and_cover")
            else:
                self._append_debug_log("[douyin][upload_ready] visual_done_commit")
            return True

        return False

    async def _is_upload_in_progress(self, page) -> bool:
        if await self._is_text_visible(page, "上传过程中请不要刷新", exact=False):
            return True

        if await self._is_text_visible(page, "取消上传", exact=False):
            return True

        progress_hints = ["已上传", "当前速度", "剩余时间", "上传中", "处理中", "转码中", "封面生成"]
        hit = 0
        for text in progress_hints:
            if await self._is_text_visible(page, text, exact=False):
                hit += 1
        return hit >= 2

    async def _wait_for_upload_completion(self, page, timeout: int) -> bool:
        start_time = time.time()
        while time.time() - start_time < timeout:
            try:
                if await self._is_upload_completed(page):
                    return True

                elapsed = int(time.time() - start_time)
                if await self._is_upload_in_progress(page):
                    logger.info(f"[抖音] 视频上传进行中...({elapsed}s)")
                    self._append_debug_log(f"[douyin][upload_wait] in_progress elapsed={elapsed}")
                else:
                    self._append_debug_log(f"[douyin][upload_wait] waiting_signals elapsed={elapsed}")
            except Exception:
                pass

            await asyncio.sleep(1)

        return False

    async def _find_publish_button(self, page, publish_label: str):
        selectors = [
            page.get_by_role("button", name=publish_label, exact=True).first,
            page.get_by_role("button", name=publish_label, exact=False).first,
            page.locator(f"button:has-text('{publish_label}')").first,
        ]

        if publish_label == "发布":
            selectors.extend([
                page.locator("button:has-text('发布')").first,
                page.locator("button:has-text('立即发布')").first,
            ])
        else:
            selectors.append(page.locator("button:has-text('定时发布')").first)

        for button in selectors:
            try:
                if await button.is_visible(timeout=600):
                    return button
            except Exception:
                continue
        return None

    async def _click_publish_button(self, page, publish_label: str, timeout: Optional[float] = None) -> tuple[bool, str]:
        start_time = time.time()
        last_error = ""
        wait_timeout = timeout if timeout is not None else self.PUBLISH_BUTTON_TIMEOUT

        while time.time() - start_time < wait_timeout:
            await self._dismiss_blocking_modal(page)
            button = await self._find_publish_button(page, publish_label)

            if button:
                try:
                    if await button.is_enabled():
                        self._append_debug_log("[douyin][publish_click] try=primary")
                        await button.click(timeout=5000)
                        self._append_debug_log(f"[douyin][publish_click] clicked label={publish_label}")
                        logger.info(f"[抖音] 点击了{publish_label}按钮")
                        return True, ""
                    self._append_debug_log(f"[douyin][publish_wait] disabled label={publish_label}")
                except Exception as e:
                    last_error = str(e)

            fallback_selectors = ["button:has-text('发布')", "button:has-text('定时发布')"]
            for selector in fallback_selectors:
                try:
                    target = await self._first_visible_locator(page.locator(selector), timeout=500)
                    if not target:
                        continue
                    if not await target.is_enabled():
                        continue
                    self._append_debug_log(f"[douyin][publish_click] try=fallback selector={selector}")
                    await target.click(timeout=3000)
                    self._append_debug_log(f"[douyin][publish_click] clicked selector={selector}")
                    logger.info(f"[抖音] 使用备用选择器点击了按钮: {selector}")
                    return True, ""
                except Exception as e:
                    last_error = str(e)
                    continue

            elapsed = int(time.time() - start_time)
            logger.info(f"[抖音] 等待{publish_label}按钮可点击...({elapsed}s)")
            self._append_debug_log(f"[douyin][publish_wait] label={publish_label} elapsed={elapsed}")
            await asyncio.sleep(1)

        return False, last_error or "发布按钮未进入可点击状态"

    async def _set_input_file_and_log(self, locator, upload_path: Path) -> None:
        await locator.set_input_files(str(upload_path))
        size = upload_path.stat().st_size
        info = f"[douyin][upload_file] path={upload_path} size={size} suffix={upload_path.suffix}"
        logger.info(info)
        self._append_debug_log(info)
        file_info = await locator.evaluate(
            """
            (input) => {
              const file = input && input.files ? input.files[0] : null;
              if (!file) return null;
              return { name: file.name, size: file.size, type: file.type };
            }
            """
        )
        if file_info:
            text = f"[douyin][file_input] name={file_info.get('name')} size={file_info.get('size')} type={file_info.get('type')}"
        else:
            text = "[douyin][file_input] empty"
        logger.info(text)
        self._append_debug_log(text)

    async def _try_upload_via_file_chooser(self, page, upload_path: Path) -> bool:
        for selector in [
            "button:has-text('上传视频')",
            "span:has-text('上传视频')",
            "div:has-text('上传视频')",
        ]:
            try:
                trigger = await self._first_visible_locator(page.locator(selector), timeout=1200)
                if not trigger:
                    continue
                self._append_debug_log(f"[douyin][upload_trigger] filechooser selector={selector}")
                async with page.expect_file_chooser(timeout=5000) as chooser_info:
                    await trigger.click()
                chooser = await chooser_info.value
                await chooser.set_files(str(upload_path))
                size = upload_path.stat().st_size
                self._append_debug_log(
                    f"[douyin][upload_file] path={upload_path} size={size} suffix={upload_path.suffix} mode=filechooser"
                )
                if await self._wait_for_publish_form_ready(page, timeout=20):
                    return True
            except PlaywrightTimeoutError:
                continue
            except Exception:
                continue
        return False

    async def _wait_for_publish_result(self, page, max_wait_time: int = 180):
        success_texts = ["发布成功", "作品已发布", "再发一条", "查看作品", "审核中", "待审核"]
        weak_texts = ["发布完成"]
        failure_texts = [
            "发布失败",
            "发布异常",
            "发布出错",
            "请完善",
            "请补充",
            "请先上传",
            "网络不佳",
            "网络异常",
            "网络开小差",
            "请稍后重试",
        ]
        start_time = time.time()
        poll_interval = 2
        weak_reason = None

        while time.time() - start_time < max_wait_time:
            if page.is_closed():
                return False, "页面已关闭", False

            if self._publish_api_error:
                return False, self._publish_api_error, False

            current_url = page.url
            if "content/manage" in current_url:
                return True, f"已跳转到管理页面 (URL: {current_url})", False

            for text in success_texts:
                if await self._is_text_visible(page, text, exact=False):
                    return True, f"检测到成功提示: {text}", False

            for text in failure_texts:
                if await self._is_text_visible(page, text, exact=False):
                    return False, f"检测到失败提示: {text}", False

            for text in weak_texts:
                if await self._is_text_visible(page, text, exact=False):
                    weak_reason = text

            logger.info("[抖音] 视频正在发布中...")
            await asyncio.sleep(poll_interval)

        if weak_reason:
            return False, f"检测到提示: {weak_reason}", True

        return False, "发布检测超时", True

    async def _is_cover_required(self, page) -> bool:
        try:
            if await page.get_by_text("设置封面", exact=False).count() == 0:
                return False

            # 老版页面通常会明确展示 "必填"
            if await page.get_by_text("必填", exact=False).count() > 0:
                self._append_debug_log("[douyin][cover] required_by_text")
                return True

            return False
        except Exception:
            return False

    async def _confirm_cover_selection(self, page, wait_enabled_timeout: float = 6.0) -> bool:
        confirm_selectors = [
            "button:has-text('完成')",
            "button:has-text('确定')",
            "button:has-text('保存')",
            "button:has-text('确认')",
        ]

        for selector in confirm_selectors:
            try:
                button = await self._first_visible_locator(page.locator(selector), timeout=600)
                if not button:
                    continue

                deadline = time.time() + wait_enabled_timeout
                while time.time() < deadline:
                    try:
                        if await button.is_enabled():
                            break
                    except Exception:
                        break
                    await asyncio.sleep(0.2)

                try:
                    if not await button.is_enabled():
                        continue
                except Exception:
                    continue

                await button.click()
                logger.info(f"[抖音] 封面已确认: {selector}")
                self._append_debug_log(f"[douyin][cover] confirmed selector={selector}")
                await asyncio.sleep(0.3)
                return True
            except Exception:
                continue

        return False

    async def _switch_to_horizontal_cover(self, scopes, timeout: float = 5.0) -> tuple[bool, bool]:
        selectors = [
            "button:has-text('设置横封面')",
            "div:has-text('设置横封面')",
            "span:has-text('设置横封面')",
            "button:has-text('横封面')",
            "div[role='tab']:has-text('横封面')",
            "span:has-text('横封面')",
            "button:has-text('横版封面')",
            "span:has-text('横版封面')",
        ]

        available = False
        deadline = time.time() + timeout
        while time.time() < deadline:
            for scope in scopes:
                for selector in selectors:
                    try:
                        target = await self._first_visible_locator(scope.locator(selector), timeout=700)
                        if not target:
                            continue
                        available = True
                        try:
                            await target.scroll_into_view_if_needed(timeout=1000)
                        except Exception:
                            pass
                        try:
                            await target.click(timeout=2000)
                        except Exception:
                            await target.evaluate("el => el.click()")

                        logger.info(f"[抖音] 已切换到横封面设置: {selector}")
                        self._append_debug_log(f"[douyin][cover] switched_horizontal selector={selector}")
                        await asyncio.sleep(0.2)
                        return True, True
                    except Exception:
                        continue
            await asyncio.sleep(0.25)

        return False, available

    async def _wait_for_cover_effect_pass(self, page, baseline_cover_count: int, timeout: float = 12.0) -> bool:
        pass_texts = [
            "封面效果检测通过",
            "封面效果检测已通过",
            "封面检测通过",
            "检测通过",
        ]

        deadline = time.time() + timeout
        while time.time() < deadline:
            if self._cover_gen_count > baseline_cover_count:
                self._append_debug_log("[douyin][cover] effect_passed_by_cover_gen")
                return True

            for text in pass_texts:
                if await self._is_text_visible(page, text, exact=False):
                    self._append_debug_log(f"[douyin][cover] effect_passed_by_text={text}")
                    return True

            await asyncio.sleep(0.25)

        self._append_debug_log("[douyin][cover] effect_check_timeout")
        return False

    async def _is_cover_configured_on_page(self, page) -> bool:
        try:
            if await page.get_by_text("设置封面", exact=False).count() == 0:
                return False

            has_horizontal_label = await self._is_text_visible(page, "横封面", exact=False)
            has_vertical_label = await self._is_text_visible(page, "竖封面", exact=False)
            if not has_horizontal_label and not has_vertical_label:
                return False

            selectors = [
                "div:has-text('竖封面') img",
                "div:has-text('横封面') img",
                "div[class*='cover'] img",
            ]
            for selector in selectors:
                try:
                    locator = page.locator(selector)
                    count = await locator.count()
                    for idx in range(min(count, 6)):
                        try:
                            if await locator.nth(idx).is_visible(timeout=400):
                                self._append_debug_log(f"[douyin][cover] configured_visual selector={selector}")
                                return True
                        except Exception:
                            continue
                except Exception:
                    continue

            # 某些页面不会给图片节点明显 class，兜底以标签+无“选择封面”判断
            has_choose_cover = await page.get_by_text("选择封面", exact=False).count() > 0
            if not has_choose_cover and has_horizontal_label and has_vertical_label:
                self._append_debug_log("[douyin][cover] configured_by_labels_no_choose_button")
                return True
        except Exception:
            return False

        return False

    async def _fill_title(self, page, title: str) -> bool:
        title_text = title[:30]
        locator_candidates = [
            page.locator("input[placeholder*='填写作品标题']"),
            page.locator("input[placeholder*='作品标题']"),
            page.locator("textarea[placeholder*='填写作品标题']"),
            page.locator("textarea[placeholder*='作品描述']"),
            page.locator("input[placeholder*='作品描述']"),
            page.locator("textarea[placeholder*='描述']"),
            page.locator("input[placeholder*='描述']"),
        ]

        for locator in locator_candidates:
            try:
                if await locator.count() > 0:
                    target = locator.first
                    await target.fill(title_text)
                    return True
            except Exception:
                continue

        try:
            editable = page.locator("div[contenteditable='true']").first
            if await editable.count() > 0:
                await editable.click()
                await page.keyboard.press("Control+KeyA")
                await page.keyboard.press("Delete")
                await page.keyboard.type(title_text)
                return True
        except Exception:
            pass

        return False

    async def _fill_description_with_tags(self, page) -> bool:
        tags = [tag.strip().lstrip("#") for tag in self.tags if tag and tag.strip()]
        if not tags:
            return True

        tag_text = " ".join(f"#{tag}" for tag in tags)
        locator_candidates = [
            page.locator("textarea[placeholder*='添加作品简介']"),
            page.locator("textarea[placeholder*='作品简介']"),
            page.locator("textarea[placeholder*='简介']"),
            page.locator("div[contenteditable='true'][data-placeholder*='添加作品简介']"),
            page.locator("div[contenteditable='true'][placeholder*='添加作品简介']"),
            page.locator(".zone-container"),
        ]

        for locator in locator_candidates:
            try:
                if await locator.count() == 0:
                    continue
                target = locator.first
                await target.click()
                try:
                    current_value = await target.input_value()
                    value = (current_value or "").strip()
                    merged = f"{value} {tag_text}".strip()
                    await target.fill(merged)
                except Exception:
                    await page.keyboard.type(tag_text + " ")
                logger.info(f"[抖音] 已添加话题: {tag_text}")
                return True
            except Exception:
                continue

        return False

    async def _select_cover_if_needed(self, page, require_horizontal: bool = False) -> bool:
        try:
            started_at = time.time()
            baseline_cover_count = self._cover_gen_count
            cover_button = page.get_by_text("选择封面", exact=False).first
            if not await cover_button.is_visible():
                if await self._is_cover_configured_on_page(page):
                    self._append_debug_log("[douyin][cover] already_configured_no_choose_button")
                    if require_horizontal:
                        await self._wait_for_cover_effect_pass(page, baseline_cover_count, timeout=3.0)
                    return True
                self._append_debug_log("[douyin][cover] no_cover_button")
                return False

            await cover_button.click()
            logger.info("[抖音] 尝试选择封面")
            await asyncio.sleep(0.3)

            dialog = page.locator(
                "div.dy-creator-content-modal-wrap, div[role='dialog'], "
                "div[class*='modal'], div[class*='dialog']"
            ).last
            scopes = [dialog] if await dialog.count() > 0 else [page]

            await self._dismiss_blocking_modal(page)

            switched, horizontal_available = await self._switch_to_horizontal_cover(scopes)
            if horizontal_available and not switched:
                # 弹窗可能遮挡了横封面入口，再尝试一次
                await self._dismiss_blocking_modal(page)
                switched, _ = await self._switch_to_horizontal_cover(scopes)

            if horizontal_available and not switched:
                if await self._is_cover_configured_on_page(page):
                    self._append_debug_log("[douyin][cover] horizontal_missed_but_already_configured")
                    await self._wait_for_cover_effect_pass(page, baseline_cover_count, timeout=3.0)
                    return True
                self._append_debug_log("[douyin][cover] horizontal_switch_missed")
                return False

            if require_horizontal and not switched:
                self._append_debug_log("[douyin][cover] required_horizontal_not_switched")
                return False

            # 某些旧版页面切到横封面后会默认选中，可直接完成
            if switched and await self._confirm_cover_selection(page, wait_enabled_timeout=1.5):
                if await dialog.count() > 0:
                    try:
                        await dialog.wait_for(state="hidden", timeout=4000)
                    except Exception:
                        pass
                if not await self._wait_for_cover_effect_pass(page, baseline_cover_count, timeout=12.0):
                    return False
                elapsed = int(time.time() - started_at)
                self._append_debug_log(f"[douyin][cover] fast_confirm_after_switch elapsed={elapsed}")
                return True

            # 没有横封面入口时，尝试直接确认（兼容其它页面形态）
            if not require_horizontal and not horizontal_available and await self._confirm_cover_selection(page, wait_enabled_timeout=1.5):
                if await dialog.count() > 0:
                    try:
                        await dialog.wait_for(state="hidden", timeout=4000)
                    except Exception:
                        pass
                elapsed = int(time.time() - started_at)
                self._append_debug_log(f"[douyin][cover] fast_confirm elapsed={elapsed}")
                return True

            selected = False
            for scope in scopes:
                for selector in [
                    "div[class*='cover'] img",
                    "div[class*='cover']",
                    "div[class*='frame'] img",
                    "div[class*='frame']",
                    "div[class*='preset']",
                ]:
                    try:
                        candidate = await self._first_visible_locator(scope.locator(selector))
                        if candidate:
                            await candidate.click()
                            logger.info("[抖音] 已选择封面帧")
                            selected = True
                            break
                    except Exception:
                        continue
                if selected:
                    break

            if not selected:
                self._append_debug_log("[douyin][cover] no_cover_candidate")
                return False

            confirmed = await self._confirm_cover_selection(page, wait_enabled_timeout=8.0)
            if confirmed and await dialog.count() > 0:
                try:
                    await dialog.wait_for(state="hidden", timeout=5000)
                except Exception:
                    pass

            if confirmed and (switched or require_horizontal):
                if not await self._wait_for_cover_effect_pass(page, baseline_cover_count, timeout=12.0):
                    return False

            elapsed = int(time.time() - started_at)
            self._append_debug_log(
                f"[douyin][cover] selected={selected} confirmed={confirmed} elapsed={elapsed}"
            )
            return confirmed
        except Exception as e:
            logger.warning(f"[抖音] 选择封面失败: {e}")
        return False

    async def _click_publish_confirm_modal(self, page):
        confirm_selectors = [
            "button:has-text('确认发布')",
            "button:has-text('继续发布')",
            "button:has-text('确定发布')",
            "button:has-text('发布确认')",
        ]
        for selector in confirm_selectors:
            try:
                button = page.locator(selector).first
                if await button.is_visible():
                    await button.click()
                    logger.info(f"[抖音] 点击了发布确认按钮: {selector}")
                    await asyncio.sleep(1)
                    return True
            except Exception:
                continue
        return False

    async def _dismiss_blocking_modal(self, page) -> bool:
        modal_locator = page.locator(
            "div.dy-creator-content-modal-wrap, div[role='dialog'], "
            "div[class*='modal'], div[class*='dialog']"
        )
        try:
            count = await modal_locator.count()
        except Exception:
            return False

        if count == 0:
            return False

        button_texts = [
            "我知道了",
            "知道了",
            "确定",
            "继续",
            "继续发布",
            "确认",
            "同意并继续",
            "完成",
            "暂不设置",
            "好的",
            "明白了",
        ]
        close_selectors = [
            "button[class*='close']",
            "span[class*='close']",
            "i[class*='close']",
        ]

        for index in range(count):
            modal = modal_locator.nth(index)
            try:
                if not await modal.is_visible():
                    continue

                for text in button_texts:
                    try:
                        button = modal.get_by_role("button", name=text).first
                        if await button.is_visible():
                            await button.click()
                            logger.info(f"[抖音] 关闭弹窗: {text}")
                            await asyncio.sleep(0.5)
                            return True
                    except Exception:
                        continue

                for selector in close_selectors:
                    try:
                        close_button = modal.locator(selector).first
                        if await close_button.is_visible():
                            await close_button.click()
                            logger.info("[抖音] 关闭弹窗: close")
                            await asyncio.sleep(0.5)
                            return True
                    except Exception:
                        continue
            except Exception:
                continue

        return False

    async def _verify_publish_in_manage(self, page):
        try:
            opened, reason = await self._open_manage_review_tab(page)
            if not opened:
                self._append_debug_log(f"[douyin][manage] verify_open_review_failed reason={reason}")
            title_text = self.title[:30]
            title_locator = page.get_by_text(title_text, exact=False).first
            if await title_locator.is_visible():
                return True, "审核中列表检测到新作品"
        except Exception as e:
            return False, f"无法验证内容管理: {e}"
        return False, "审核中列表未找到视频"
    
    async def set_schedule_time(self, page, publish_date):
        """Set scheduled publish time"""
        try:
            # Click "定时发布" radio button
            label_element = page.locator("[class^='radio']:has-text('定时发布')")
            await label_element.click()
            await asyncio.sleep(1)
            
            # Format time
            publish_date_hour = publish_date.strftime("%Y-%m-%d %H:%M")
            
            # Fill datetime input
            await page.locator('.semi-input[placeholder="日期和时间"]').click()
            await page.keyboard.press("Control+KeyA")
            await page.keyboard.type(str(publish_date_hour))
            await page.keyboard.press("Enter")
            
            await asyncio.sleep(1)
            logger.info(f"[抖音] 已设置定时发布: {publish_date_hour}")
            
        except Exception as e:
            logger.error(f"[抖音] 设置定时发布失败: {e}")
    
    async def upload(self, playwright: Playwright) -> dict:
        """Main upload logic with guaranteed resource cleanup"""
        browser = None
        context = None
        page = None
        recorded_video = None
        final_success = False
        try:
            launch_options = self._build_launch_options()
            browser = await playwright.chromium.launch(**launch_options)
            context_kwargs = {
                "storage_state": self.account_file,
                "viewport": {"width": 1920, "height": 1080},
                "user_agent": settings.DOUYIN_USER_AGENT,
                "locale": settings.DOUYIN_LOCALE,
                "timezone_id": settings.DOUYIN_TIMEZONE_ID,
            }
            if self._record_video_enabled():
                context_kwargs["record_video_dir"] = str(self._video_record_dir())
                context_kwargs["record_video_size"] = {
                    "width": settings.DOUYIN_RECORD_VIDEO_WIDTH,
                    "height": settings.DOUYIN_RECORD_VIDEO_HEIGHT,
                }
            context = await browser.new_context(
                **context_kwargs,
            )
            context = await set_init_script(context)
            
            page = await context.new_page()
            recorded_video = page.video
            self._attach_debug_listeners(page)
            
            await self._open_upload_page(page)

            upload_path = self._prepare_upload_file()
            logger.info(f"[抖音] 正在上传: {upload_path.name}")

            current_url = page.url
            if "login" in current_url or "passport" in current_url or await self._is_login_page(page):
                logger.error("[抖音] Cookie 已失效，被重定向到登录页")
                self._append_debug_log(f"[douyin][auth] login_required url={current_url}")
                await self._save_debug_screenshot(page, "login_redirect")
                return {
                    "success": False,
                    "message": "Cookie 已失效，请重新登录",
                    "url": None
                }

            await self._handle_unfinished_draft(page)

            self._video_upload_committed = False
            self._cover_generated = False
            upload_ok = await self._try_upload_via_file_chooser(page, upload_path)
            if not upload_ok:
                self._append_debug_log("[douyin][upload_flow] filechooser_not_triggered_first_try")

            if not upload_ok:
                logger.error("[抖音] 文件已选择但页面未进入发布态，按首页流程重试一次")
                await self._save_debug_screenshot(page, "upload_not_accepted_before_retry")

                await self._open_upload_page(page)
                await self._handle_unfinished_draft(page)

                self._video_upload_committed = False
                self._cover_generated = False
                upload_ok = await self._try_upload_via_file_chooser(page, upload_path)
                if not upload_ok:
                    self._append_debug_log("[douyin][upload_flow] filechooser_not_triggered_second_try")

            if not upload_ok:
                if await self._is_login_page(page):
                    logger.error("[抖音] 上传阶段检测到登录页，Cookie 已失效")
                    self._append_debug_log(f"[douyin][auth] login_required_after_upload_retry url={page.url}")
                    await self._save_debug_screenshot(page, "login_redirect")
                    return {
                        "success": False,
                        "message": "Cookie 已失效，请重新登录",
                        "url": None
                    }

                logger.error("[抖音] 上传未生效，页面结构可能已变化")
                await self._save_debug_screenshot(page, "publish_page_timeout")
                return {
                    "success": False,
                    "message": "未触发上传视频文件选择弹窗，无法进入发布页面",
                    "url": None
                }

            if not await self._wait_for_publish_form_ready(page, timeout=self.PAGE_REDIRECT_TIMEOUT):
                logger.error("[抖音] 等待发布页面超时")
                await self._save_debug_screenshot(page, "publish_page_timeout")
                return {
                    "success": False,
                    "message": "等待发布页面超时",
                    "url": None
                }
            
            # Fill title
            await asyncio.sleep(1)
            logger.info("[抖音] 正在填充标题和话题...")

            if not await self._fill_title(page, self.title):
                logger.warning("[抖音] 未找到作品描述输入框")

            if not await self._fill_description_with_tags(page):
                logger.warning("[抖音] 未找到简介输入框，无法自动填充话题")

            # Set scheduled publish time if needed
            if self.publish_date != 0:
                await self.set_schedule_time(page, self.publish_date)

            if not await self._wait_for_upload_completion(page, timeout=self.UPLOAD_TIMEOUT):
                logger.error("[抖音] 视频上传完成检测超时")
                await self._save_debug_screenshot(page, "upload_timeout")
                return {
                    "success": False,
                    "message": "视频上传未完成（超时），未执行发布",
                    "url": None
                }

            post_upload_deadline = time.time() + self.POST_UPLOAD_STAGE_TIMEOUT
            self._append_debug_log(
                f"[douyin][post_upload] stage_timeout={self.POST_UPLOAD_STAGE_TIMEOUT}s"
            )

            cover_required = await self._is_cover_required(page)
            if cover_required:
                logger.info("[抖音] 当前页面封面为必填，开始设置封面")
                cover_selected = False
                for attempt in range(2):
                    cover_selected = await self._select_cover_if_needed(page, require_horizontal=True)
                    if cover_selected:
                        break
                    await asyncio.sleep(1)

                if not cover_selected and await self._is_cover_configured_on_page(page):
                    logger.warning("[抖音] 封面流程返回失败，但页面已显示封面配置，继续发布")
                    self._append_debug_log("[douyin][cover] configured_fallback_continue")
                    cover_selected = True

                if cover_selected:
                    logger.info("[抖音] 封面设置完成")
                    self._append_debug_log("[douyin][cover] selected")
                else:
                    logger.error("[抖音] 封面设置未完成，停止发布")
                    self._append_debug_log("[douyin][cover] not_selected")
                    await self._save_debug_screenshot(page, "cover_not_selected")
                    return {
                        "success": False,
                        "message": "封面为必填但未设置成功，已停止发布",
                        "url": None
                    }
            else:
                logger.info("[抖音] 当前页面封面非必填，跳过封面设置")
                self._append_debug_log("[douyin][cover] optional_skip")

            logger.info("[抖音] 检测到上传完成，等待2秒后再点击发布")
            self._append_debug_log("[douyin][upload_ready] wait_before_publish=2s")
            await asyncio.sleep(2)

            remaining = post_upload_deadline - time.time()
            if remaining <= 0:
                return {
                    "success": False,
                    "message": "上传完成后发布阶段超时（超过60秒）",
                    "url": None
                }
            
            # Click publish button
            publish_label = "定时发布" if self.publish_date != 0 else "发布"
            self._publish_api_error = None
            click_timeout = max(5, min(self.PUBLISH_BUTTON_TIMEOUT, remaining))
            clicked, reason = await self._click_publish_button(page, publish_label, timeout=click_timeout)
            if not clicked and not cover_required:
                logger.warning("[抖音] 首次点击发布失败，尝试先设置封面后重试")
                self._append_debug_log("[douyin][cover] retry_select_before_publish")
                for _ in range(2):
                    if await self._select_cover_if_needed(page, require_horizontal=False):
                        await asyncio.sleep(1)
                        remaining = post_upload_deadline - time.time()
                        if remaining <= 0:
                            break
                        click_timeout = max(5, min(self.PUBLISH_BUTTON_TIMEOUT, remaining))
                        clicked, reason = await self._click_publish_button(
                            page,
                            publish_label,
                            timeout=click_timeout,
                        )
                        if clicked:
                            self._append_debug_log("[douyin][cover] retry_click_success")
                            break
                    await asyncio.sleep(1)

            if not clicked:
                logger.error(f"[抖音] 点击发布按钮失败: {reason}")
                await self._save_debug_screenshot(page, "publish_button_disabled")
                return {
                    "success": False,
                    "message": "发布按钮长时间不可点击或点击失败，请检查声明/权限/页面弹窗",
                    "url": None
                }

            await self._click_publish_confirm_modal(page)

            # 4. 检测发布完成
            remaining = int(post_upload_deadline - time.time())
            if remaining <= 0:
                return {
                    "success": False,
                    "message": "上传完成后发布阶段超时（超过60秒）",
                    "url": None
                }

            publish_wait_timeout = max(5, min(self.PUBLISH_TIMEOUT, remaining))
            publish_success, publish_reason, is_timeout = await self._wait_for_publish_result(
                page,
                max_wait_time=publish_wait_timeout,
            )
            if not publish_success and is_timeout:
                verify_success, verify_reason = await self._verify_publish_in_manage(page)
                if verify_success:
                    publish_reason = f"{publish_reason}; 管理页检测到同名作品: {verify_reason}"
                else:
                    publish_reason = f"{publish_reason}; {verify_reason}"
            if publish_success:
                logger.success(f"[抖音] 发布成功: {publish_reason}")
            else:
                if is_timeout:
                    logger.warning("[抖音] 发布检测超时，但这不一定代表失败")
                else:
                    logger.warning(f"[抖音] 发布未成功: {publish_reason}")
            
            # Save updated cookies
            await context.storage_state(path=self.account_file)
            logger.success("[抖音] Cookie 更新完毕")
            
            if publish_success:
                final_success = True
                try:
                    await page.wait_for_load_state("domcontentloaded", timeout=5000)
                except Exception:
                    pass
                await asyncio.sleep(3)
                screenshot_url = await self._save_publish_success_screenshot(page)
                return {
                    "success": True,
                    "message": "发布成功，待审核",
                    "url": None,
                    "screenshot_url": screenshot_url,
                }
            if is_timeout:
                return {
                    "success": False,
                    "message": f"发布状态未知（检测超时），请到抖音后台确认: {publish_reason}",
                    "url": None
                }
            return {
                "success": False,
                "message": f"发布失败: {publish_reason}",
                "url": None
            }
            
        except Exception as e:
            logger.exception(f"[抖音] 上传失败: {e}")
            return {
                "success": False,
                "message": f"上传失败: {str(e)}",
                "url": None
            }
        finally:
            # 确保资源释放
            video_path = None
            if page:
                try:
                    if not page.is_closed():
                        await page.close()
                except Exception:
                    pass
                video_path = await self._save_recorded_video(recorded_video, final_success)
                if video_path:
                    logger.info(f"[抖音] 调试录屏已保存: {video_path}")

            if context:
                try:
                    await context.close()
                except Exception:
                    pass
            if browser:
                try:
                    await browser.close()
                except Exception:
                    pass
            self._cleanup_upload_file()
    
    async def main(self) -> Dict[str, Any]:
        """Execute upload"""
        async with async_playwright() as playwright:
            return await self.upload(playwright)
