"""
Xiaohongshu (小红书) uploader using Playwright.
"""
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
from loguru import logger

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


class XiaohongshuUploader(BaseUploader):
    """Xiaohongshu video uploader using Playwright"""

    UPLOAD_TIMEOUT = 420
    UPLOAD_IDLE_TIMEOUT = 90
    UPLOAD_SIGNAL_TIMEOUT = 12
    PUBLISH_TIMEOUT = 120
    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://creator.xiaohongshu.com/publish/publish?from=homepage&target=video"
        self._publish_api_submitted = False
        self._publish_api_error: Optional[str] = None
        self._temp_upload_paths: List[Path] = []

    def _track_temp_upload_path(self, path: Path) -> None:
        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:
            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)
            logger.info(f"[小红书] using hardlink upload file: {target}")
        except Exception:
            try:
                shutil.copy2(src, target)
                logger.info(f"[小红书] using copied upload file: {target}")
            except Exception as e:
                logger.warning(f"[小红书] 构建带后缀上传文件失败，回退原文件: {e}")
                return src

        self._track_temp_upload_path(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 _resolve_headless_mode(self) -> str:
        mode = (settings.XIAOHONGSHU_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.XIAOHONGSHU_FORCE_SWIFTSHADER or headless:
            args.extend([
                "--enable-unsafe-swiftshader",
                "--use-gl=swiftshader",
            ])

        options: Dict[str, Any] = {"headless": headless, "args": args}
        chrome_path = (settings.XIAOHONGSHU_CHROME_PATH or "").strip()
        if chrome_path:
            if Path(chrome_path).exists():
                options["executable_path"] = chrome_path
            else:
                logger.warning(f"[小红书] XIAOHONGSHU_CHROME_PATH 不存在: {chrome_path}")
        else:
            channel = (settings.XIAOHONGSHU_BROWSER_CHANNEL or "").strip()
            if channel:
                options["channel"] = channel

        return options

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

    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(" ", "_")
            file_path = debug_dir / f"xiaohongshu_{safe_name}.png"
            await page.screenshot(path=str(file_path), full_page=True)
            logger.info(f"[小红书] saved debug screenshot: {file_path}")
        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 _save_publish_success_screenshot(self, page) -> Optional[str]:
        try:
            timestamp = time.strftime("%Y%m%d_%H%M%S", time.localtime())
            filename = f"xiaohongshu_success_{timestamp}_{int(time.time() * 1000) % 1000:03d}.png"
            file_path = self._publish_screenshot_dir() / filename
            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 _attach_publish_listener(self, page) -> None:
        ignore_tokens = ("report", "collect", "analytics", "monitor", "perf")

        def on_response(response):
            try:
                request = response.request
                if request.method not in ("POST", "PUT"):
                    return

                url = (response.url or "").lower()
                if "xiaohongshu.com" not in url or "api" not in url:
                    return
                if not any(token in url for token in ("publish", "note/create", "note/publish", "note/save")):
                    return
                if any(token in url for token in ignore_tokens):
                    return

                if response.status < 400:
                    self._publish_api_submitted = True
                    logger.info("[小红书][publish] publish API ok")
                else:
                    self._publish_api_error = f"发布请求失败（HTTP {response.status}）"
                    logger.warning(f"[小红书][publish] publish API failed status={response.status}")
            except Exception:
                pass

        page.on("response", on_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_existing_locator(self, page, selectors: List[str], require_visible: bool = True):
        for selector in selectors:
            locator = page.locator(selector)
            try:
                if await locator.count() == 0:
                    continue
                candidate = locator.first
                if require_visible and not await candidate.is_visible():
                    continue
                return candidate
            except Exception:
                continue
        return None

    async def _is_login_page(self, page) -> bool:
        url = page.url.lower()
        if "login" in url or "signin" in url:
            return True
        if await self._is_text_visible(page, "扫码登录", exact=False):
            return True
        if await self._is_text_visible(page, "立即登录", exact=False):
            return True
        return False

    async def _go_to_publish_page(self, page):
        await page.goto(self.upload_url, wait_until="domcontentloaded", timeout=self.PAGE_READY_TIMEOUT * 1000)
        await asyncio.sleep(2)
        return page

    async def _find_file_input(self, page):
        selectors = [
            "input[type='file'][accept*='video']",
            "div[class*='upload'] input[type='file']",
            "input.upload-input",
            "input[type='file']",
        ]
        return await self._first_existing_locator(page, selectors, require_visible=False)

    async def _open_upload_entry(self, page) -> None:
        selectors = [
            "button:has-text('上传视频')",
            "button:has-text('上传')",
            "div[role='button']:has-text('上传视频')",
            "div[role='button']:has-text('上传')",
            "span:has-text('上传视频')",
        ]
        target = await self._first_existing_locator(page, selectors)
        if not target:
            return
        try:
            await target.scroll_into_view_if_needed()
        except Exception:
            pass
        try:
            await target.click(timeout=2000)
        except Exception:
            try:
                await target.evaluate("el => el.click()")
            except Exception:
                pass

    async def _is_upload_in_progress(self, page) -> bool:
        in_progress_texts = [
            "上传中",
            "正在上传",
            "处理中",
            "视频处理中",
            "转码中",
            "请稍候",
            "上传进度",
            "校验中",
            "准备中",
        ]
        for text in in_progress_texts:
            if await self._is_text_visible(page, text, exact=False):
                return True
        return False

    async def _is_upload_success(self, page) -> bool:
        success_texts = [
            "上传成功",
            "上传完成",
            "处理完成",
            "转码完成",
            "可发布",
        ]
        for text in success_texts:
            if await self._is_text_visible(page, text, exact=False):
                return True
        return await self._is_publish_button_enabled(page)

    async def _upload_failed_reason(self, page) -> Optional[str]:
        failure_texts = [
            "上传失败",
            "上传异常",
            "上传出错",
            "上传超时",
            "网络异常",
        ]
        for text in failure_texts:
            if await self._is_text_visible(page, text, exact=False):
                return f"上传失败：{text}"
        return None

    async def _upload_video(self, page) -> bool:
        page = await self._go_to_publish_page(page)
        await self._save_debug_screenshot(page, "publish_page")

        upload_path = self._prepare_upload_file()
        try:
            upload_size = upload_path.stat().st_size
            logger.info(
                f"[小红书][upload_file] path={upload_path} "
                f"size={upload_size} suffix={upload_path.suffix}"
            )
        except Exception as e:
            logger.warning(f"[小红书] 读取上传文件信息失败: {e}")

        for attempt in range(self.MAX_CLICK_RETRIES):
            file_input = await self._find_file_input(page)
            if not file_input:
                await self._open_upload_entry(page)
                await asyncio.sleep(1)
                file_input = await self._find_file_input(page)

            if not file_input:
                logger.info(f"[小红书] 未找到上传文件 input，准备重试 ({attempt + 1}/{self.MAX_CLICK_RETRIES})")
                await asyncio.sleep(1)
                continue

            try:
                await file_input.set_input_files(str(upload_path))
                logger.info(f"[小红书] 已设置上传文件: {upload_path.name}")

                try:
                    file_info = await file_input.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:
                        selected_name = str(file_info.get("name") or "")
                        logger.info(
                            "[小红书][file_input] "
                            f"name={selected_name} "
                            f"size={file_info.get('size')} "
                            f"type={file_info.get('type')}"
                        )
                        if upload_path.suffix and selected_name and not selected_name.lower().endswith(upload_path.suffix.lower()):
                            logger.warning(
                                "[小红书] file input 文件名后缀与上传文件不一致，"
                                f"expect=*{upload_path.suffix} actual={selected_name}"
                            )
                            if attempt + 1 < self.MAX_CLICK_RETRIES:
                                await asyncio.sleep(1)
                                continue
                            await self._save_debug_screenshot(page, "upload_input_name_mismatch")
                            return False

                        if not str(file_info.get("type") or "").strip():
                            logger.warning("[小红书] file input MIME 为空，可能影响站点识别")
                except Exception:
                    pass

                signal_detected = False
                bootstrap_error: Optional[str] = None
                deadline = time.time() + self.UPLOAD_SIGNAL_TIMEOUT
                while time.time() < deadline:
                    bootstrap_error = await self._upload_failed_reason(page)
                    if bootstrap_error:
                        break
                    if await self._is_upload_in_progress(page) or await self._is_upload_success(page):
                        signal_detected = True
                        break
                    await asyncio.sleep(0.6)

                if bootstrap_error:
                    logger.warning(f"[小红书] 上传启动阶段失败: {bootstrap_error}")
                    if attempt + 1 < self.MAX_CLICK_RETRIES:
                        await asyncio.sleep(1)
                        continue
                    return False

                if signal_detected:
                    return True

                logger.info("[小红书] 未立即检测到上传状态，进入后续上传监控")
                return True
            except Exception as e:
                logger.warning(f"[小红书] set_input_files 失败: {e}")

            await asyncio.sleep(1)

        await self._save_debug_screenshot(page, "upload_input_missing")
        return False

    async def _wait_for_upload_complete(self, page) -> tuple[bool, str]:
        start = time.time()
        idle_start = start
        while time.time() - start < self.UPLOAD_TIMEOUT:
            reason = await self._upload_failed_reason(page)
            if reason:
                logger.warning(f"[小红书] 上传失败检测: {reason}")
                return False, reason

            if await self._is_upload_success(page):
                return True, "上传完成"

            if await self._is_upload_in_progress(page):
                idle_start = time.time()
                logger.info("[小红书] 视频上传进行中...")
            else:
                if time.time() - idle_start > self.UPLOAD_IDLE_TIMEOUT:
                    await self._save_debug_screenshot(page, "upload_idle_timeout")
                    return False, "未检测到有效上传进度（疑似上传控件未生效）"
                logger.info("[小红书] 等待上传状态...")

            await asyncio.sleep(self.POLL_INTERVAL)

        return False, "视频上传超时"

    def _normalize_tags(self, tags: List[str]) -> List[str]:
        normalized: List[str] = []
        seen = set()
        for raw in tags:
            item = (raw or "").strip().lstrip("#")
            if not item:
                continue
            lowered = item.lower()
            if lowered in seen:
                continue
            seen.add(lowered)
            normalized.append(item)
        return normalized

    async def _fill_title(self, page) -> bool:
        selectors = [
            "input[placeholder*='标题']",
            "div.plugin.title-container input",
            "input.d-text",
        ]
        target = await self._first_existing_locator(page, selectors)
        if not target:
            return False

        try:
            await target.click(timeout=1500)
            await target.fill((self.title or "")[:30])
            return True
        except Exception:
            return False

    async def _fill_description(self, page, text: str) -> bool:
        selectors = [
            ".tiptap[contenteditable='true']",
            "[contenteditable='true'][data-placeholder*='描述']",
            "[contenteditable='true'][role='textbox']",
            "textarea[placeholder*='描述']",
            "textarea[placeholder*='正文']",
        ]
        target = await self._first_existing_locator(page, selectors)
        if not target:
            return False

        try:
            await target.click(timeout=1500)
            await page.keyboard.press("Control+KeyA")
            await page.keyboard.type(text)
            return True
        except Exception:
            return False

    async def set_schedule_time(self, page, publish_date: datetime) -> bool:
        try:
            toggle = await self._first_existing_locator(
                page,
                [
                    "label:has-text('定时发布')",
                    "span:has-text('定时发布')",
                    "div:has-text('定时发布')",
                ],
            )
            if not toggle:
                return False

            try:
                await toggle.click(timeout=2000)
            except Exception:
                await toggle.evaluate("el => el.click()")

            await asyncio.sleep(0.5)
            date_input = await self._first_existing_locator(
                page,
                [
                    "input[placeholder*='日期和时间']",
                    "input[placeholder*='发布时间']",
                    "input[placeholder*='选择日期']",
                ],
            )
            if not date_input:
                return False

            value = publish_date.strftime("%Y-%m-%d %H:%M")
            await date_input.click(timeout=2000)
            await page.keyboard.press("Control+KeyA")
            await page.keyboard.type(value)
            await page.keyboard.press("Enter")
            logger.info(f"[小红书] 已设置定时发布: {value}")
            return True
        except Exception as e:
            logger.warning(f"[小红书] 设置定时发布时间失败: {e}")
            return False

    async def _find_publish_button(self, page, scheduled: bool):
        selectors = [
            "button:has-text('定时发布')",
            "div[role='button']:has-text('定时发布')",
        ] if scheduled else [
            "button:has-text('发布')",
            "button:has-text('立即发布')",
            "div[role='button']:has-text('发布')",
        ]

        for selector in selectors:
            locator = page.locator(selector)
            try:
                if await locator.count() == 0:
                    continue
                candidate = locator.first
                if not await candidate.is_visible():
                    continue
                return candidate
            except Exception:
                continue
        return None

    async def _is_publish_button_enabled(self, page) -> bool:
        buttons = [
            await self._find_publish_button(page, scheduled=False),
            await self._find_publish_button(page, scheduled=True),
        ]
        for button in buttons:
            if not button:
                continue
            try:
                if await button.is_enabled():
                    return True
            except Exception:
                continue
        return False

    async def _click_publish(self, page, scheduled: bool) -> tuple[bool, str]:
        for _ in range(self.MAX_CLICK_RETRIES):
            button = await self._find_publish_button(page, scheduled)
            if not button:
                await asyncio.sleep(0.8)
                continue

            try:
                if not await button.is_enabled():
                    await asyncio.sleep(0.8)
                    continue
            except Exception:
                pass

            try:
                await button.click(timeout=2000)
                return True, "发布按钮点击成功"
            except Exception:
                try:
                    await button.evaluate("el => el.click()")
                    return True, "发布按钮 JS 点击成功"
                except Exception:
                    await asyncio.sleep(0.8)

        return False, "未找到可点击的发布按钮"

    async def _wait_for_publish_result(self, page) -> tuple[bool, str, bool]:
        create_url = page.url
        success_url_tokens = [
            "/publish/success",
            "/publish/result",
            "/publish/published",
        ]
        success_texts = [
            "发布成功",
            "发布完成",
            "审核中",
            "查看笔记",
            "去查看",
        ]
        failure_texts = [
            "发布失败",
            "发布异常",
            "发布出错",
            "网络异常",
            "请完善",
            "请补充",
        ]

        start_time = time.time()
        while time.time() - start_time < self.PUBLISH_TIMEOUT:
            if self._publish_api_error:
                return False, self._publish_api_error, False

            current_url = page.url
            lowered_url = current_url.lower()
            if any(token in lowered_url for token in success_url_tokens):
                return True, f"发布成功：跳转到 {current_url}", False

            if current_url != create_url and "/publish/publish" not in lowered_url:
                return True, f"发布成功：页面已跳转 {current_url}", False

            if self._publish_api_submitted:
                return True, "发布成功：API 已确认", False

            for text in failure_texts:
                if await self._is_text_visible(page, text, exact=False):
                    return False, f"发布失败：{text}", False

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

            logger.info("[小红书] 等待发布结果...")
            await asyncio.sleep(self.POLL_INTERVAL)

        return False, "发布超时", True

    async def upload(self, playwright: Playwright) -> Dict[str, Any]:
        browser = None
        context = None
        page = None
        try:
            launch_options = self._build_launch_options()
            browser = await playwright.chromium.launch(**launch_options)
            context = await browser.new_context(
                storage_state=self.account_file,
                viewport={"width": 1600, "height": 900},
                device_scale_factor=1,
                user_agent=settings.XIAOHONGSHU_USER_AGENT,
                locale=settings.XIAOHONGSHU_LOCALE,
                timezone_id=settings.XIAOHONGSHU_TIMEZONE_ID,
            )
            context = await set_init_script(context)

            page = await context.new_page()
            self._attach_publish_listener(page)

            await self._go_to_publish_page(page)
            if await self._is_login_page(page):
                return {
                    "success": False,
                    "message": "登录失效，请重新扫码登录小红书",
                    "url": None,
                }

            logger.info(f"[小红书] 正在上传: {self.file_path.name}")
            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:
                await self._save_debug_screenshot(page, "upload_failed")
                return {
                    "success": False,
                    "message": upload_reason,
                    "url": None,
                }

            await asyncio.sleep(1)
            title_filled = await self._fill_title(page)
            if not title_filled:
                logger.warning("[小红书] 未找到标题输入框，尝试在正文中补充标题")

            normalized_tags = self._normalize_tags(self.tags)
            body_parts: List[str] = []
            if self.description:
                body_parts.append(self.description.strip())
            if not title_filled and self.title:
                body_parts.insert(0, self.title.strip())
            if normalized_tags:
                body_parts.append(" ".join([f"#{tag}" for tag in normalized_tags]))
            body_text = "\n".join([part for part in body_parts if part]).strip()

            if body_text:
                body_ok = await self._fill_description(page, body_text)
                if not body_ok:
                    logger.warning("[小红书] 未找到正文输入框，跳过正文/话题填充")

            if self.publish_date != 0 and isinstance(self.publish_date, datetime):
                if not await self.set_schedule_time(page, self.publish_date):
                    return {
                        "success": False,
                        "message": "未找到定时发布控件，请检查小红书发布页结构",
                        "url": None,
                    }

            clicked, click_reason = await self._click_publish(page, scheduled=self.publish_date != 0)
            if not clicked:
                await self._save_debug_screenshot(page, "publish_button_not_clickable")
                return {
                    "success": False,
                    "message": click_reason,
                    "url": None,
                }

            publish_success, publish_reason, is_timeout = await self._wait_for_publish_result(page)

            await context.storage_state(path=self.account_file)
            logger.success("[小红书] Cookie 更新完毕")

            if publish_success:
                await asyncio.sleep(2)
                screenshot_url = await self._save_publish_success_screenshot(page)
                return {
                    "success": True,
                    "message": "发布成功，待审核" if self.publish_date == 0 else "已设置定时发布",
                    "url": None,
                    "screenshot_url": screenshot_url,
                }

            if is_timeout:
                return {
                    "success": False,
                    "message": f"发布状态未知（检测超时），请到小红书创作中心确认: {publish_reason}",
                    "url": None,
                }

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

        except Exception as e:
            logger.exception(f"[小红书] 上传失败: {e}")
            return {
                "success": False,
                "message": f"上传失败: {str(e)}",
                "url": None,
            }
        finally:
            self._cleanup_upload_file()

            if page:
                try:
                    if not page.is_closed():
                        await page.close()
                except Exception:
                    pass

            if context:
                try:
                    await context.close()
                except Exception:
                    pass

            if browser:
                try:
                    await browser.close()
                except Exception:
                    pass

    async def main(self) -> Dict[str, Any]:
        async with async_playwright() as playwright:
            return await self.upload(playwright)
