"""
Weixin Channels uploader using Playwright.
Best-effort selectors for upload and publish flows.
"""
from datetime import datetime
from pathlib import Path
from typing import Optional, List, Dict, Any
from uuid import uuid4
import os
import re
import shutil
import asyncio
import time
import subprocess

from playwright.async_api import Playwright, async_playwright
from loguru import logger

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


class WeixinUploader(BaseUploader):
    """Weixin Channels video uploader using Playwright"""

    UPLOAD_TIMEOUT = 420
    PUBLISH_TIMEOUT = 90
    PAGE_READY_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://channels.weixin.qq.com/platform"
        self._temp_upload_paths: List[Path] = []
        self._post_create_submitted = False
        self._publish_api_error: Optional[str] = None

    def _resolve_headless_mode(self) -> str:
        mode = (settings.WEIXIN_HEADLESS_MODE or "").strip().lower()
        return mode or "headful"

    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.WEIXIN_FORCE_SWIFTSHADER or headless:
            args.extend([
                "--enable-unsafe-swiftshader",
                "--use-gl=swiftshader",
            ])
        options: Dict[str, Any] = {"headless": headless, "args": args}
        chrome_path = (settings.WEIXIN_CHROME_PATH or "").strip()
        if chrome_path:
            if Path(chrome_path).exists():
                options["executable_path"] = chrome_path
            else:
                logger.warning(f"[weixin] WEIXIN_CHROME_PATH not found: {chrome_path}")
        else:
            channel = (settings.WEIXIN_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 / "weixin_network.log"

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

    def _record_video_enabled(self) -> bool:
        return bool(self._debug_artifacts_enabled() and settings.WEIXIN_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 _is_upload_related_url(self, url: str) -> bool:
        lowered = (url or "").lower()
        keywords = (
            "upload",
            "media",
            "video",
            "vod",
            "cos",
            "qcloud",
            "finder",
            "weixin",
            "channels",
        )
        return any(keyword in lowered for keyword in keywords)

    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:
        # post_create 响应监听始终注册（不依赖 debug 开关）
        def log_post_create(response):
            try:
                url = response.url or ""
                if "/post/post_create" in url:
                    if response.status < 400:
                        self._post_create_submitted = True
                        logger.info("[weixin][publish] post_create API ok")
                    else:
                        self._publish_api_error = f"发布请求失败（HTTP {response.status}）"
                        logger.warning(f"[weixin][publish] post_create_failed status={response.status}")
            except Exception:
                pass

        page.on("response", log_post_create)

        if not self._debug_artifacts_enabled():
            return

        def log_console(msg):
            try:
                if msg.type in ("error", "warning"):
                    text = f"[weixin][console] {msg.type}: {msg.text}"
                    logger.warning(text)
                    self._append_debug_log(text)
            except Exception:
                pass

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

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

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

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

                url = response.url or ""
                if "/post/post_create" in url:
                    if response.status < 400:
                        self._post_create_submitted = True
                        self._append_debug_log("[weixin][publish] post_create_ok")
                    else:
                        self._publish_api_error = f"发布请求失败（HTTP {response.status}）"
                        self._append_debug_log(f"[weixin][publish] post_create_failed status={response.status}")
            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 _is_login_page(self, page) -> bool:
        url = (page.url or "").lower()
        if "login" in url or "qrcode" in url or "qr" in url:
            return True

        login_texts = [
            "\u626b\u7801\u767b\u5f55",
            "\u5fae\u4fe1\u626b\u7801",
            "\u8bf7\u4f7f\u7528\u5fae\u4fe1\u626b\u7801",
        ]
        for text in login_texts:
            if await self._is_text_visible(page, text, exact=False):
                return True
        return False

    def _iter_scopes(self, page):
        scopes = [page]
        try:
            for frame in page.frames:
                if frame != page.main_frame:
                    scopes.append(frame)
        except Exception:
            pass
        return scopes

    async def _click_first_match(self, page, selectors: List[str], timeout: int = 1000) -> bool:
        scopes = self._iter_scopes(page)
        for scope in scopes:
            for selector in selectors:
                try:
                    locator = await self._first_visible_locator(scope.locator(selector), timeout=timeout)
                    if locator:
                        await locator.click()
                        await asyncio.sleep(0.5)
                        return True
                except Exception:
                    continue
        return False

    async def _click_by_text(self, page, texts: List[str], allow_fuzzy: bool = True) -> bool:
        scopes = self._iter_scopes(page)
        for scope in scopes:
            for text in texts:
                for exact in (True, False):
                    if not exact and (not allow_fuzzy or len(text) < 3):
                        continue
                    try:
                        locator = scope.get_by_text(text, exact=exact).first
                        if await locator.is_visible():
                            await locator.click()
                            await asyncio.sleep(0.5)
                            return True
                    except Exception:
                        continue
        return False

    async def _scroll_down(self, page, steps: int = 4) -> None:
        for _ in range(steps):
            try:
                await page.mouse.wheel(0, 900)
            except Exception:
                pass
            try:
                await page.keyboard.press("PageDown")
            except Exception:
                pass
            await asyncio.sleep(0.3)

    async def _scroll_to_bottom(self, scope) -> None:
        try:
            await scope.evaluate(
                """
                () => {
                  const scrollingElement = document.scrollingElement || document.documentElement;
                  if (scrollingElement) {
                    scrollingElement.scrollTop = scrollingElement.scrollHeight;
                  }
                  window.scrollTo(0, document.body.scrollHeight);
                }
                """
            )
        except Exception:
            pass
        try:
            await scope.evaluate(
                """
                () => {
                  const candidates = Array.from(document.querySelectorAll('*'));
                  for (const el of candidates) {
                    const style = window.getComputedStyle(el);
                    const overflowY = style.overflowY || '';
                    const canScroll = el.scrollHeight > el.clientHeight && overflowY !== 'visible' && overflowY !== 'hidden';
                    if (canScroll) {
                      el.scrollTop = el.scrollHeight;
                    }
                  }
                }
                """
            )
        except Exception:
            pass

    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)
            safe_name = name.replace("/", "_").replace(" ", "_")
            debug_path = debug_dir / f"weixin_{safe_name}.png"
            await page.screenshot(path=str(debug_path), full_page=True)
            logger.info(f"[weixin] saved debug screenshot: {debug_path}")
        except Exception as e:
            logger.warning(f"[weixin] failed to save debug screenshot: {e}")

    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"weixin_{timestamp}_{status}.webm"
            await video.save_as(str(file_path))
            self._append_debug_log(f"[weixin][record] saved={file_path}")

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

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

    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 _save_publish_success_screenshot(self, page) -> Optional[str]:
        try:
            timestamp = time.strftime("%Y%m%d_%H%M%S", time.localtime())
            filename = f"weixin_success_{timestamp}_{int(time.time() * 1000) % 1000:03d}.png"
            file_path = self._publish_screenshot_dir() / filename
            await self._apply_page_zoom(page, zoom=1.0)
            await page.screenshot(path=str(file_path), full_page=False)
            return f"/api/publish/screenshot/{filename}"
        except Exception as e:
            logger.warning(f"[weixin] 保存发布成功截图失败: {e}")
            return None

    async def _apply_page_zoom(self, page, zoom: float = 0.8) -> None:
        try:
            await page.evaluate(
                """
                (z) => {
                  const value = String(z);
                  const current = document.documentElement.style.zoom || (document.body && document.body.style.zoom) || '';
                  if (window.__vigentZoomApplied === value && current === value) {
                    return;
                  }
                  document.documentElement.style.zoom = value;
                  if (document.body) {
                    document.body.style.zoom = value;
                  }
                  window.__vigentZoomApplied = value;
                }
                """,
                zoom,
            )
        except Exception:
            pass

    async def _click_publish_via_dom(self, scope) -> bool:
        try:
            return await scope.evaluate(
                """
                () => {
                  const selectors = ['button', '[role="button"]', '[class*="btn"]', '[class*="button"]'];
                  const candidates = [];
                  for (const sel of selectors) {
                    candidates.push(...document.querySelectorAll(sel));
                  }
                  const texts = ['发表', '发布'];
                  for (const text of texts) {
                    for (const el of candidates) {
                      const content = (el.textContent || '').trim();
                      if (!content || !content.includes(text)) continue;
                      const disabled = !!(el.disabled || el.getAttribute('aria-disabled') === 'true' || el.classList.contains('disabled'));
                      if (disabled) continue;
                      if (typeof el.scrollIntoView === 'function') {
                        el.scrollIntoView({block: 'center', inline: 'center'});
                      }
                      el.click();
                      return true;
                    }
                  }
                  return false;
                }
                """
            )
        except Exception:
            return False

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

    def _new_temp_mp4_path(self) -> Path:
        temp_dir = Path("/tmp/vigent_uploads")
        temp_dir.mkdir(parents=True, exist_ok=True)
        return temp_dir / f"{uuid4().hex}.mp4"

    def _run_ffmpeg(self, cmd: List[str], output_path: Path, label: str) -> Optional[Path]:
        try:
            result = subprocess.run(cmd, capture_output=True, text=True)
            if result.returncode != 0:
                stderr = (result.stderr or "").strip()
                logger.warning(f"[weixin] ffmpeg {label} failed: {stderr}")
                self._append_debug_log(f"[weixin][ffmpeg] {label} failed: {stderr}")
                try:
                    if output_path.exists():
                        output_path.unlink()
                except Exception:
                    pass
                return None

            self._track_temp_upload_path(output_path)
            logger.info(f"[weixin] ffmpeg {label} ok: {output_path}")
            self._append_debug_log(f"[weixin][ffmpeg] {label} ok: {output_path}")
            return output_path
        except Exception as e:
            logger.warning(f"[weixin] ffmpeg {label} error: {e}")
            self._append_debug_log(f"[weixin][ffmpeg] {label} error: {e}")
            try:
                if output_path.exists():
                    output_path.unlink()
            except Exception:
                pass
            return None

    def _maybe_transcode_upload_file(self, src: Path) -> Path:
        mode = (settings.WEIXIN_TRANSCODE_MODE or "").strip().lower()
        if mode in ("", "off", "false", "0", "none"):
            return src

        if mode == "faststart":
            output_path = self._new_temp_mp4_path()
            cmd = [
                "ffmpeg",
                "-y",
                "-i",
                str(src),
                "-c",
                "copy",
                "-movflags",
                "+faststart",
                str(output_path),
            ]
            result = self._run_ffmpeg(cmd, output_path, "faststart")
            return result or src

        if mode == "reencode":
            output_path = self._new_temp_mp4_path()
            cmd = [
                "ffmpeg",
                "-y",
                "-i",
                str(src),
                "-c:v",
                "libx264",
                "-preset",
                "veryfast",
                "-profile:v",
                "main",
                "-pix_fmt",
                "yuv420p",
                "-r",
                "30",
                "-g",
                "60",
                "-keyint_min",
                "60",
                "-sc_threshold",
                "0",
                "-vf",
                "scale=trunc(iw/2)*2:trunc(ih/2)*2",
                "-c:a",
                "aac",
                "-b:a",
                "128k",
                "-ar",
                "44100",
                "-movflags",
                "+faststart",
                str(output_path),
            ]
            result = self._run_ffmpeg(cmd, output_path, "reencode")
            return result or src

        logger.warning(f"[weixin] unknown WEIXIN_TRANSCODE_MODE: {mode}")
        return src

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

        parent_suffix = Path(src.parent.name).suffix
        if not parent_suffix:
            return self._maybe_transcode_upload_file(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:
            try:
                os.symlink(src, target)
            except Exception:
                shutil.copy2(src, target)

        self._track_temp_upload_path(target)
        logger.info(f"[weixin] using temp upload file: {target}")
        return self._maybe_transcode_upload_file(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"[weixin] failed to cleanup temp upload file: {e}")

    async def _try_click_publish_button(self, page, timeout: float = 6.0) -> bool:
        selectors = [
            "button:has-text('发表视频')",
            "button:has-text('发布视频')",
            "button:has-text('发表动态')",
            "a:has-text('发表视频')",
            "a:has-text('发表动态')",
            "a[href*='post/create']",
            "a[href*='post/create'] span",
            "div[role='button']:has-text('发表视频')",
            "div[role='button']:has-text('发表动态')",
        ]
        start_time = time.monotonic()
        while time.monotonic() - start_time < timeout:
            if await self._click_first_match(page, selectors, timeout=500):
                return True
            await asyncio.sleep(0.5)
        return False

    async def _wait_for_publish_panel(self, page, timeout: int = 10) -> bool:
        start_time = time.monotonic()
        while time.monotonic() - start_time < timeout:
            if await self._is_text_visible(page, "发表动态", exact=False):
                return True
            if await self._is_text_visible(page, "视频描述", exact=False):
                return True
            if await self._find_file_input(page):
                return True
            await asyncio.sleep(0.5)
        return False

    async def _goto_publish_urls(self, page) -> bool:
        candidates = [
            "https://channels.weixin.qq.com/platform/post/create",
            "https://channels.weixin.qq.com/platform/post/create?type=video",
            "https://channels.weixin.qq.com/platform/post/create?tab=video",
            "https://channels.weixin.qq.com/platform/post/create?from=post_list",
            "https://channels.weixin.qq.com/platform/post/create?scene=video",
        ]
        for url in candidates:
            try:
                await page.goto(url, wait_until="domcontentloaded")
                if await self._wait_for_publish_panel(page, timeout=6):
                    logger.info(f"[weixin] opened publish page: {url}")
                    return True
            except Exception:
                continue
        return False

    async def _click_and_maybe_switch_page(self, page, click_fn, timeout: float = 3.0):
        wait_task = asyncio.create_task(page.context.wait_for_event("page"))
        clicked = await click_fn()
        if not clicked:
            wait_task.cancel()
            return False, page

        new_page = None
        try:
            new_page = await asyncio.wait_for(wait_task, timeout=timeout)
        except Exception:
            wait_task.cancel()

        if new_page:
            try:
                await new_page.wait_for_load_state("domcontentloaded")
            except Exception:
                pass
            return True, new_page

        return True, page

    async def _go_to_publish_page(self, page):
        if await self._wait_for_publish_panel(page, timeout=2):
            await self._apply_page_zoom(page)
            return page

        logger.info("[weixin] 尝试进入发表视频页面...")

        async def click_home_publish():
            return await self._click_by_text(page, ["发表视频", "发布视频", "发表动态"], allow_fuzzy=True)

        clicked, next_page = await self._click_and_maybe_switch_page(page, click_home_publish)
        if clicked:
            await self._apply_page_zoom(next_page)
            if await self._wait_for_publish_panel(next_page, timeout=6):
                return next_page

        await self._click_by_text(page, ["视频号助手"], allow_fuzzy=True)

        await self._click_by_text(page, ["内容管理"], allow_fuzzy=True)
        await asyncio.sleep(0.5)
        await self._click_by_text(page, ["视频"], allow_fuzzy=False)
        await asyncio.sleep(1)

        try:
            await page.goto("https://channels.weixin.qq.com/platform/post/list", wait_until="domcontentloaded")
        except Exception:
            pass

        await asyncio.sleep(0.5)

        async def click_list_publish():
            if await self._click_by_text(page, ["发表视频", "发布视频", "发表动态"], allow_fuzzy=True):
                return True
            return await self._try_click_publish_button(page, timeout=6)

        clicked, next_page = await self._click_and_maybe_switch_page(page, click_list_publish)
        if clicked:
            await self._apply_page_zoom(next_page)
            if await self._wait_for_publish_panel(next_page, timeout=6):
                return next_page

        if await self._goto_publish_urls(page):
            await self._apply_page_zoom(page)
            return page

        logger.warning(f"[weixin] 进入发表页面失败，当前URL: {page.url}")
        await self._apply_page_zoom(page)
        return page

    async def _open_upload_entry(self, page) -> bool:
        selectors = [
            "button:has-text('\\u53d1\\u5e03')",
            "button:has-text('\\u53d1\\u8868')",
            "button:has-text('\\u4e0a\\u4f20')",
            "button:has-text('\\u53d1\\u89c6\\u9891')",
            "button:has-text('\\u53d1\\u8868\\u89c6\\u9891')",
            "button:has-text('\\u89c6\\u9891')",
            "a:has-text('\\u53d1\\u5e03')",
            "a:has-text('\\u53d1\\u8868')",
            "a:has-text('\\u53d1\\u8868\\u89c6\\u9891')",
            "div[role='button']:has-text('\\u53d1\\u8868\\u89c6\\u9891')",
        ]
        return await self._click_first_match(page, selectors)

    async def _find_file_input(self, page):
        selectors = [
            "input[type='file'][accept*='video']",
            "input[type='file'][accept*='mp4']",
            "input[type='file']",
        ]
        scopes = self._iter_scopes(page)
        for scope in scopes:
            for selector in selectors:
                try:
                    locator = scope.locator(selector)
                    if await locator.count() > 0:
                        return locator.first
                except Exception:
                    continue
        return None

    async def _click_upload_card(self, page) -> bool:
        selectors = [
            "div:has-text('上传视频')",
            "div:has-text('上传时长')",
            "div:has-text('MP4')",
            "div:has-text('H.264')",
            "div:has-text('上传')",
        ]
        return await self._click_first_match(page, selectors)

    async def _try_file_chooser_upload(self, page, upload_path: Path) -> bool:
        try:
            async with page.expect_file_chooser(timeout=2500) as chooser_info:
                clicked = await self._click_upload_card(page)
                if not clicked:
                    await self._open_upload_entry(page)
                    await self._click_upload_card(page)
            file_chooser = await chooser_info.value
            await file_chooser.set_files(str(upload_path))
            info = f"[weixin][file_chooser] used path={upload_path}"
            logger.info(info)
            self._append_debug_log(info)
            return True
        except Exception:
            return False

    async def _is_upload_placeholder_visible(self, page) -> bool:
        selectors = [
            "div:has-text('上传时长')",
            "div:has-text('码率')",
            "div:has-text('MP4')",
            "div:has-text('H.264')",
        ]
        scopes = self._iter_scopes(page)
        for scope in scopes:
            for selector in selectors:
                try:
                    locator = scope.locator(selector).first
                    if await locator.count() > 0 and await locator.is_visible():
                        return True
                except Exception:
                    continue
        return False

    async def _has_enabled_publish_button(self, page) -> bool:
        selectors = [
            "button:has-text('发表')",
            "button:has-text('发布')",
            "div[role='button']:has-text('发表')",
            "a:has-text('发表')",
        ]
        scopes = self._iter_scopes(page)
        for scope in scopes:
            await self._scroll_to_bottom(scope)
            for selector in selectors:
                try:
                    locator = scope.locator(selector)
                    count = await locator.count()
                    for i in range(count):
                        candidate = locator.nth(i)
                        try:
                            await candidate.scroll_into_view_if_needed()
                        except Exception:
                            pass
                        if await candidate.is_visible() and await candidate.is_enabled():
                            return True
                except Exception:
                    continue
        return False

    async def _upload_video(self, page) -> bool:
        page = await self._go_to_publish_page(page)
        await self._save_debug_screenshot(page, "publish_page")
        for attempt in range(self.MAX_CLICK_RETRIES):
            input_locator = await self._find_file_input(page)
            if not input_locator:
                await self._open_upload_entry(page)
                await self._click_upload_card(page)
                await asyncio.sleep(1)
                input_locator = await self._find_file_input(page)

            try:
                upload_path = self._prepare_upload_file()
                try:
                    size = upload_path.stat().st_size
                    info = f"[weixin][upload_file] path={upload_path} size={size} suffix={upload_path.suffix}"
                    logger.info(info)
                    self._append_debug_log(info)
                except Exception as e:
                    logger.warning(f"[weixin] failed to stat upload file: {e}")

                if await self._try_file_chooser_upload(page, upload_path):
                    return True

                if input_locator:
                    await input_locator.set_input_files(str(upload_path))
                    try:
                        file_info = await input_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"[weixin][file_input] name={file_info.get('name')} size={file_info.get('size')} type={file_info.get('type')}"
                            logger.info(text)
                            self._append_debug_log(text)
                            return True
                        upload_started = False
                        for _ in range(3):
                            await asyncio.sleep(0.4)
                            if await self._is_upload_in_progress(page):
                                upload_started = True
                                break
                        if upload_started:
                            logger.info("[weixin] upload started after file input set")
                            return True

                        text = "[weixin][file_input] empty after set_input_files and no upload signal"
                        if attempt + 1 >= self.MAX_CLICK_RETRIES:
                            logger.warning(text)
                        else:
                            logger.info(text)
                        self._append_debug_log(text)
                    except Exception as e:
                        logger.warning(f"[weixin] failed to read file input info: {e}")
            except Exception as e:
                logger.warning(f"[weixin] set_input_files failed: {e}")

            await asyncio.sleep(1)
            logger.info(f"[weixin] retrying file input ({attempt + 1}/{self.MAX_CLICK_RETRIES})")

        logger.warning(f"[weixin] file input not found, url: {page.url}")
        await self._save_debug_screenshot(page, "upload_input_missing")

        return False

    async def _is_upload_in_progress(self, page) -> bool:
        progress_texts = [
            "取消上传",
            "上传中",
            "正在上传",
            "生成中",
            "文件上传中",
            "请稍后点击发布",
        ]
        for text in progress_texts:
            if await self._is_text_visible(page, text, exact=False):
                return True
        return False

    async def _wait_for_upload_complete(self, page):
        success_texts = [
            "\u4e0a\u4f20\u6210\u529f",
            "\u4e0a\u4f20\u5b8c\u6210",
            "\u5df2\u4e0a\u4f20",
            "\u5904\u7406\u5b8c\u6210",
            "\u8f6c\u7801\u5b8c\u6210",
        ]
        failure_texts = [
            "\u4e0a\u4f20\u5931\u8d25",
            "\u4e0a\u4f20\u5f02\u5e38",
            "\u4e0a\u4f20\u51fa\u9519",
            "\u4e0a\u4f20\u8d85\u65f6",
            "\u7f51\u7edc\u9519\u8bef",
            "\u7f51\u7edc\u5f02\u5e38",
            "\u91cd\u65b0\u4e0a\u4f20",
            "\u4e0a\u4f20\u4e2d\u65ad",
        ]
        in_progress_texts = [
            "\u53d6\u6d88\u4e0a\u4f20",
            "\u4e0a\u4f20\u4e2d",
            "\u6b63\u5728\u4e0a\u4f20",
            "\u751f\u6210\u4e2d",
            "\u6587\u4ef6\u4e0a\u4f20\u4e2d",
            "\u8bf7\u7a0d\u540e\u70b9\u51fb\u53d1\u5e03",
        ]

        start_time = time.time()
        while time.time() - start_time < self.UPLOAD_TIMEOUT:
            for text in failure_texts:
                if await self._is_text_visible(page, text, exact=False):
                    await self._save_debug_screenshot(page, "upload_failed")
                    return False, f"上传失败：{text}"

            for text in success_texts:
                if await self._is_text_visible(page, text, exact=False):
                    return True, f"上传完成：{text}"

            delete_visible = await self._is_text_visible(page, "删除", exact=False)
            cancel_visible = await self._is_text_visible(page, "取消上传", exact=False)
            upload_in_progress = cancel_visible
            if not upload_in_progress:
                for text in in_progress_texts:
                    if await self._is_text_visible(page, text, exact=False):
                        upload_in_progress = True
                        break

            if upload_in_progress:
                logger.info("[weixin] upload still in progress...")
                if int(time.time() - start_time) % 20 == 0:
                    await self._save_debug_screenshot(page, "upload_waiting")
                await asyncio.sleep(self.POLL_INTERVAL)
                continue

            if delete_visible and not cancel_visible:
                return True, "上传完成：可删除视频"

            logger.info("[weixin] waiting for upload...")
            if int(time.time() - start_time) % 20 == 0:
                await self._save_debug_screenshot(page, "upload_waiting")
            await asyncio.sleep(self.POLL_INTERVAL)

        return False, "上传超时"

    def _normalize_tags(self, tags: List[str]) -> List[str]:
        normalized: List[str] = []
        for tag in tags:
            value = (tag or "").strip().lstrip("#")
            if not value:
                continue
            if value not in normalized:
                normalized.append(value)
        return normalized

    async def _fill_text_field(self, page, locator, text: str) -> bool:
        if not text:
            return False

        try:
            await locator.click(timeout=2000)
        except Exception:
            return False

        try:
            tag_name = await locator.evaluate("el => (el.tagName || '').toLowerCase()")
        except Exception:
            tag_name = ""

        try:
            is_contenteditable = (await locator.get_attribute("contenteditable") or "").lower() == "true"
        except Exception:
            is_contenteditable = False

        try:
            if is_contenteditable or tag_name == "div":
                await page.keyboard.press("Control+KeyA")
                await page.keyboard.press("Backspace")
                await page.keyboard.type(text)
                current = await locator.evaluate("el => (el.innerText || el.textContent || '').trim()")
            else:
                await locator.fill(text)
                try:
                    current = await locator.input_value()
                except Exception:
                    current = await locator.evaluate("el => (el.value || '').trim()")

            expected = text[: min(8, len(text))]
            return expected in (current or "")
        except Exception:
            return False

    async def _fill_title(self, page, title: str) -> bool:
        if not title:
            return False

        title_text = title[:30]
        selectors = [
            "input[placeholder*='视频标题']",
            "textarea[placeholder*='视频标题']",
            "input[placeholder*='标题（必填）']",
            "textarea[placeholder*='标题（必填）']",
            "input[placeholder*='\\u6807\\u9898']",
            "textarea[placeholder*='\\u6807\\u9898']",
            "input[placeholder*='\\u4f5c\\u54c1']",
            "textarea[placeholder*='\\u4f5c\\u54c1']",
            "input[placeholder*='\\u6982\\u62ec']",
            "textarea[placeholder*='\\u6982\\u62ec']",
            "input[placeholder*='\\u77ed\\u6807\\u9898']",
            "textarea[placeholder*='\\u77ed\\u6807\\u9898']",
        ]

        for selector in selectors:
            try:
                locator = page.locator(selector).first
                if await locator.count() > 0:
                    if await self._fill_text_field(page, locator, title_text):
                        logger.info("[weixin] 标题填写成功")
                        return True
            except Exception:
                continue

        return False

    async def _fill_description(self, page, description: str) -> bool:
        if not description:
            return False

        label_texts = ["视频描述", "描述（必填）", "描述"]
        for label_text in label_texts:
            try:
                label = page.get_by_text(label_text, exact=False).first
                if await label.count() == 0:
                    continue
                relation_selectors = [
                    "xpath=following::textarea[1]",
                    "xpath=following::*[@contenteditable='true'][1]",
                    "xpath=following::div[contains(@class,'editor')][1]",
                ]
                for relation in relation_selectors:
                    try:
                        target = label.locator(relation).first
                        if await target.count() > 0 and await self._fill_text_field(page, target, description):
                            logger.info("[weixin] 视频描述填写成功（标签定位）")
                            return True
                    except Exception:
                        continue
            except Exception:
                continue

        selectors = [
            "textarea[placeholder*='视频描述']",
            "textarea[placeholder*='描述（必填）']",
            "div[contenteditable='true'][data-placeholder*='视频描述']",
            "div[contenteditable='true'][data-placeholder*='描述']",
            "div[contenteditable='true'][placeholder*='视频描述']",
            "div[contenteditable='true'][placeholder*='描述']",
            "textarea[placeholder*='\\u6dfb\\u52a0\\u63cf\\u8ff0']",
            "textarea[placeholder*='\\u63cf\\u8ff0']",
            "textarea[placeholder*='\\u7b80\\u4ecb']",
            "textarea[placeholder*='\\u6587\\u6848']",
            "textarea[placeholder*='\\u5185\\u5bb9']",
        ]

        for selector in selectors:
            try:
                locator = page.locator(selector).first
                if await locator.count() > 0:
                    if await self._fill_text_field(page, locator, description):
                        logger.info("[weixin] 视频描述填写成功")
                        return True
            except Exception:
                continue

        # 最后兜底，避免页面改版导致无法输入
        fallback_editables = page.locator("div[contenteditable='true']")
        try:
            count = await fallback_editables.count()
        except Exception:
            count = 0
        for idx in range(min(count, 5)):
            try:
                target = fallback_editables.nth(idx)
                if await self._fill_text_field(page, target, description):
                    logger.info("[weixin] 视频描述填写成功（兜底contenteditable）")
                    return True
            except Exception:
                continue

        return False

    async def _fill_tags(self, page, tags: List[str]) -> bool:
        normalized_tags = self._normalize_tags(tags)
        if not normalized_tags:
            return False

        selectors = [
            "input[placeholder*='\\u8bdd\\u9898']",
            "input[placeholder*='\\u6807\\u7b7e']",
            "input[placeholder*='\\u6dfb\\u52a0']",
            "textarea[placeholder*='\\u8bdd\\u9898']",
        ]

        for selector in selectors:
            try:
                locator = page.locator(selector).first
                if await locator.count() == 0:
                    continue
                for tag in normalized_tags:
                    await locator.type(f"#{tag} ")
                    await page.keyboard.press("Enter")
                return True
            except Exception:
                continue

        return False

    async def set_schedule_time(self, page, publish_date: datetime) -> bool:
        selectors = [
            "label:has-text('\\u5b9a\\u65f6\\u53d1\\u5e03')",
            "button:has-text('\\u5b9a\\u65f6\\u53d1\\u5e03')",
            "label:has-text('\\u9884\\u7ea6\\u53d1\\u5e03')",
            "button:has-text('\\u9884\\u7ea6\\u53d1\\u5e03')",
        ]

        for selector in selectors:
            try:
                button = await self._first_visible_locator(page.locator(selector))
                if button:
                    await button.click()
                    await asyncio.sleep(0.5)
                    break
            except Exception:
                continue

        time_value = publish_date.strftime("%Y-%m-%d %H:%M")
        input_selectors = [
            "input[placeholder*='\\u65e5\\u671f']",
            "input[placeholder*='\\u65f6\\u95f4']",
            "input[placeholder*='\\u9009\\u62e9']",
        ]

        for selector in input_selectors:
            try:
                locator = page.locator(selector).first
                if await locator.count() > 0:
                    await locator.click()
                    await page.keyboard.press("Control+KeyA")
                    await page.keyboard.type(time_value)
                    await page.keyboard.press("Enter")
                    logger.info(f"[weixin] scheduled publish set: {time_value}")
                    return True
            except Exception:
                continue

        logger.warning("[weixin] schedule publish input not found")
        return False

    async def _click_publish(self, page, scheduled: bool) -> bool:
        selectors = [
            "button:has-text('\\u5b9a\\u65f6\\u53d1\\u8868')",
            "button:has-text('\\u5b9a\\u65f6\\u53d1\\u5e03')",
            "button:has-text('\\u53d1\\u8868')",
            "button:has-text('\\u53d1\\u5e03')",
            "button:has-text('\\u786e\\u8ba4\\u53d1\\u8868')",
            "button:has-text('\\u786e\\u8ba4')",
            "div[role='button']:has-text('\\u53d1\\u8868')",
            "a:has-text('\\u53d1\\u8868')",
        ]

        if not scheduled:
            selectors = [
                "button:has-text('\\u53d1\\u8868')",
                "button:has-text('\\u53d1\\u5e03')",
                "button:has-text('\\u786e\\u8ba4\\u53d1\\u8868')",
                "button:has-text('\\u786e\\u8ba4')",
                "div[role='button']:has-text('\\u53d1\\u8868')",
                "a:has-text('\\u53d1\\u8868')",
            ]

        start_time = time.monotonic()
        found_disabled = False
        while time.monotonic() - start_time < 90:
            scopes = self._iter_scopes(page)
            for scope in scopes:
                await self._scroll_to_bottom(scope)
                for selector in selectors:
                    try:
                        locator = scope.locator(selector)
                        count = await locator.count()
                        for i in range(count):
                            candidate = locator.nth(i)
                            try:
                                await candidate.scroll_into_view_if_needed()
                            except Exception:
                                pass
                            if await candidate.is_visible():
                                if await candidate.is_enabled():
                                    await candidate.click()
                                    await asyncio.sleep(1)
                                    return True
                                found_disabled = True
                    except Exception:
                        continue
                if await self._click_publish_via_dom(scope):
                    await asyncio.sleep(1)
                    return True
                for text in ("发表", "发布"):
                    try:
                        locator = scope.locator("button, [role='button'], a", has_text=text)
                        if await locator.count() > 0:
                            await locator.first.click(force=True)
                            await asyncio.sleep(1)
                            return True
                    except Exception:
                        continue
            await self._scroll_down(page, steps=1)
            await asyncio.sleep(1)

        if found_disabled:
            logger.warning("[weixin] publish button disabled, likely missing required fields or processing")
        await self._save_debug_screenshot(page, "publish_button_not_found")
        
        return False

    async def _wait_for_publish_result(self, page):
        """点击发表后等待结果：页面离开创建页即视为成功"""
        failure_texts = [
            "\u53d1\u5e03\u5931\u8d25",
            "\u53d1\u5e03\u5f02\u5e38",
            "\u53d1\u5e03\u51fa\u9519",
            "\u8bf7\u5b8c\u5584",
            "\u8bf7\u8865\u5145",
            "\u64cd\u4f5c\u5931\u8d25",
            "\u7f51\u7edc\u5f02\u5e38",
        ]

        # 记录点击发表时的 URL，用于判断是否跳转
        create_url = page.url
        start_time = time.time()

        while time.time() - start_time < self.PUBLISH_TIMEOUT:
            current_url = page.url

            # API 层面报错 → 直接失败
            if self._publish_api_error:
                return False, self._publish_api_error, False

            # 核心判定：URL 离开了创建页（跳转到列表页或其他页面）→ 发布成功
            if current_url != create_url and "/post/create" not in current_url:
                logger.info(f"[weixin] page navigated away from create page: {current_url}")
                return True, "发布成功：页面已跳转", False

            # post_create API 已确认成功 → 也视为成功
            if self._post_create_submitted:
                logger.info("[weixin] post_create API confirmed success")
                return True, "发布成功：API 已确认", False

            # 检查页面上的失败文案
            for text in failure_texts:
                if await self._is_text_visible(page, text, exact=False):
                    return False, f"发布失败：{text}", False

            logger.info("[weixin] waiting for publish result...")
            await asyncio.sleep(self.POLL_INTERVAL)

        return False, "发布超时", True

    async def upload(self, playwright: Playwright) -> Dict[str, Any]:
        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: Dict[str, Any] = {
                "storage_state": self.account_file,
                "viewport": {"width": 1440, "height": 1000},
                "device_scale_factor": 1,
                "user_agent": settings.WEIXIN_USER_AGENT,
                "locale": settings.WEIXIN_LOCALE,
                "timezone_id": settings.WEIXIN_TIMEZONE_ID,
            }
            if self._record_video_enabled():
                context_kwargs["record_video_dir"] = str(self._video_record_dir())
                context_kwargs["record_video_size"] = {
                    "width": settings.WEIXIN_RECORD_VIDEO_WIDTH,
                    "height": settings.WEIXIN_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 page.goto(self.upload_url, wait_until="domcontentloaded")
            await asyncio.sleep(2)
            await self._apply_page_zoom(page)

            if await self._is_login_page(page):
                return {
                    "success": False,
                    "message": "登录失效，请重新扫码登录微信视频号",
                    "url": None,
                }

            if not await self._upload_video(page):
                return {
                    "success": False,
                    "message": "未找到上传入口，请确认已进入发表视频页面",
                    "url": None,
                }

            upload_success, upload_reason = await self._wait_for_upload_complete(page)
            if not upload_success:
                return {
                    "success": False,
                    "message": upload_reason,
                    "url": None,
                }

            await asyncio.sleep(1)

            # 按新规则：标题和标签统一写入“视频描述”
            normalized_tags = self._normalize_tags(self.tags)
            description_parts: List[str] = []
            if self.title:
                description_parts.append(self.title.strip())
            if self.description:
                description_parts.append(self.description.strip())
            if normalized_tags:
                description_parts.append(" ".join([f"#{tag}" for tag in normalized_tags]))
            description_text = "\n".join([part for part in description_parts if part]).strip()

            if description_text:
                description_filled = await self._fill_description(page, description_text)
                if not description_filled:
                    logger.error("[weixin] 未找到视频描述输入框，无法写入标题和标签")
                    return {
                        "success": False,
                        "message": "未找到视频描述输入框，无法填写标题和标签",
                        "url": None,
                    }

            publish_date = self.publish_date
            if publish_date != 0 and isinstance(publish_date, datetime):
                await self.set_schedule_time(page, publish_date)

            self._post_create_submitted = False
            self._publish_api_error = None
            if not await self._click_publish(page, scheduled=self.publish_date != 0):
                return {
                    "success": False,
                    "message": "未定位到可点击的发表按钮，可能在页面底部或被遮挡",
                    "url": None,
                }

            publish_success, publish_reason, is_timeout = await self._wait_for_publish_result(page)
            await context.storage_state(path=self.account_file)

            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:
                timeout_message = "发布超时，请到视频号助手确认发布结果"
                if self._post_create_submitted:
                    timeout_message = "发布请求已提交，但未收到成功确认，请到视频号助手核验"
                return {
                    "success": False,
                    "message": timeout_message,
                    "url": None,
                }

            return {
                "success": False,
                "message": publish_reason,
                "url": None,
            }

        except Exception as e:
            logger.exception(f"[weixin] upload failed: {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"[weixin] 调试录屏已保存: {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]:
        async with async_playwright() as playwright:
            return await self.upload(playwright)
