from __future__ import annotations

import os
import random
import re
import time
from datetime import datetime
from typing import Optional

from loguru import logger
from playwright.async_api import Page, TimeoutError

from flow_xhs.runtime.errors import InvalidPublishParameterError, PublishExecutionError
from flow_xhs.platform.issues import build_unknown_issue, detect_issue_from_page

URL_OF_PUBLISH = "https://creator.xiaohongshu.com/publish/publish?source=official"
PUBLISH_BUTTON_SELECTOR = ".publish-page-publish-btn button.bg-red"
PUBLISH_SUCCESS_URL_KEYWORDS = ("/notes", "/new/home", "/creator/home")


class RednotePublisher:
    """
    图文发布流程实现。
    业务逻辑严格对齐 publish_note.py 中 PublishAction。
    """

    def __init__(self, page: Page):
        self.page = page

    async def _rand_sleep(self, low: float, high: float) -> None:
        await self.page.wait_for_timeout(int(random.uniform(low, high) * 1000))

    async def open_publish_page(self, tab_name: str = "上传图文") -> None:
        await self.page.goto(URL_OF_PUBLISH, wait_until="domcontentloaded", timeout=300_000)
        await self.page.wait_for_timeout(2000)
        await self.page.locator("div.upload-content").first.wait_for(state="visible", timeout=30_000)
        await self.must_click_publish_tab(tab_name)
        await self.page.wait_for_timeout(1000)

    async def remove_pop_cover(self) -> None:
        pop = self.page.locator("div.d-popover")
        if await pop.count() > 0:
            await self.page.evaluate(
                """
                () => {
                  const node = document.querySelector('div.d-popover');
                  if (node) node.remove();
                }
                """
            )
        x = 380 + random.randint(0, 100)
        y = 20 + random.randint(0, 60)
        await self.page.mouse.click(x, y)

    async def must_click_publish_tab(self, tab_name: str) -> None:
        deadline = time.time() + 15
        while time.time() < deadline:
            tabs = self.page.locator("div.creator-tab")
            count = await tabs.count()
            for i in range(count):
                tab = tabs.nth(i)
                if not await tab.is_visible():
                    continue

                text = (await tab.inner_text() or "").strip()
                if text != tab_name:
                    continue

                blocked = await tab.evaluate(
                    """
                    (el) => {
                      const rect = el.getBoundingClientRect();
                      if (rect.width === 0 || rect.height === 0) return true;
                      const x = rect.left + rect.width / 2;
                      const y = rect.top + rect.height / 2;
                      const target = document.elementFromPoint(x, y);
                      return !(target === el || el.contains(target));
                    }
                    """
                )
                if blocked:
                    await self.remove_pop_cover()
                    await self.page.wait_for_timeout(200)
                    continue

                await tab.click()
                return

            await self.page.wait_for_timeout(200)

        raise TimeoutError(f"没有找到发布 TAB: {tab_name}")

    async def upload_images(self, image_paths: list[str]) -> list[str]:
        if not image_paths:
            raise InvalidPublishParameterError("没有可上传的本地图片")

        valid_paths: list[str] = []
        for p in image_paths:
            if os.path.exists(p):
                valid_paths.append(p)
                logger.info(f"有效图片: {p}")
            else:
                logger.warning(f"图片文件不存在，跳过: {p}")

        if not valid_paths:
            raise InvalidPublishParameterError("没有可上传的本地图片")

        for i, path in enumerate(valid_paths):
            selector = ".upload-input" if i == 0 else "input[type='file']"
            file_input = self.page.locator(selector).first
            await file_input.set_input_files(path)
            logger.info(f"图片已提交上传: index={i + 1}, path={path}")
            await self.wait_for_upload_complete(i + 1)
            await self._rand_sleep(0.8, 1.3)

        return valid_paths

    async def wait_for_upload_complete(self, expected_count: int) -> None:
        start = time.time()
        last = expected_count - 1
        while time.time() - start < 60:
            current = await self.page.locator(".img-preview-area .pr").count()
            if current != last:
                logger.info(f"等待图片上传: current={current}, expected={expected_count}")
                last = current
            if current >= expected_count:
                logger.info(f"图片上传完成: count={current}")
                return
            await self.page.wait_for_timeout(500)
        raise TimeoutError(f"第 {expected_count} 张图片上传超时(60s)")

    async def wait_for_publish_button_clickable(self, max_wait_seconds: int = 600):
        start = time.time()
        logger.info("开始等待发布按钮可点击")
        while time.time() - start < max_wait_seconds:
            btn = self.page.locator(PUBLISH_BUTTON_SELECTOR).first
            if await btn.count() > 0 and await btn.is_visible():
                disabled = await btn.get_attribute("disabled")
                aria_disabled = await btn.get_attribute("aria-disabled")
                cls = await btn.get_attribute("class") or ""
                if disabled is None and aria_disabled not in {"true", "1"} and "disabled" not in cls:
                    return btn
            await self.page.wait_for_timeout(1000)
        raise TimeoutError("等待发布按钮可点击超时")

    def _raise_publish_failure(self, issue=None) -> None:
        effective_issue = issue or build_unknown_issue()
        details = effective_issue.to_details()
        raise PublishExecutionError(
            f"发布失败: {details['failure_reason']}，{details['next_action']}",
            details=details,
        )

    def _extract_publish_id(self, url: str) -> str:
        if not url:
            return ""
        match = re.search(r"/([0-9a-z]{24,})", url)
        if match:
            return match.group(1)
        match = re.search(r"[?&]noteId=([^&#]+)", url)
        if match:
            return match.group(1)
        return ""

    async def wait_for_publish_result(self, initial_url: str, timeout_ms: int = 20_000) -> dict:
        deadline = time.time() + (timeout_ms / 1000)
        while time.time() < deadline:
            current_url = self.page.url
            if current_url != initial_url and any(keyword in current_url for keyword in PUBLISH_SUCCESS_URL_KEYWORDS):
                return {
                    "status": "verified",
                    "publish_url": current_url,
                    "publish_id": self._extract_publish_id(current_url),
                }

            issue = await detect_issue_from_page(self.page, include_body=False)
            if issue is not None:
                self._raise_publish_failure(issue)

            submit_btn = self.page.locator(PUBLISH_BUTTON_SELECTOR).first
            if await submit_btn.count() > 0 and await submit_btn.is_visible():
                await self.page.wait_for_timeout(500)
                continue

            if current_url != initial_url and current_url != URL_OF_PUBLISH:
                return {
                    "status": "verified",
                    "publish_url": current_url,
                    "publish_id": self._extract_publish_id(current_url),
                }

            await self.page.wait_for_timeout(500)

        issue = await detect_issue_from_page(self.page, include_body=True)
        if issue is None:
            issue = build_unknown_issue(
                source="state_machine",
                failure_reason="点击发布后未检测到成功或失败信号",
                next_action="请联系开发者",
            )
        self._raise_publish_failure(issue)

    async def upload_video(self, video_path: str) -> str:
        if not os.path.exists(video_path):
            raise InvalidPublishParameterError(f"视频文件不存在: {video_path}")

        file_input = self.page.locator(".upload-input").first
        if await file_input.count() == 0:
            file_input = self.page.locator("input[type='file']").first
        if await file_input.count() == 0:
            raise InvalidPublishParameterError("未找到视频上传输入框")

        await file_input.set_input_files(video_path)
        await self.wait_for_publish_button_clickable(max_wait_seconds=600)
        logger.info(f"视频上传/处理完成，发布按钮可点击: {video_path}")
        return video_path

    async def get_content_element(self):
        editor = self.page.locator("div.ql-editor").first
        if await editor.count() > 0:
            return editor

        handle = await self.page.evaluate_handle(
            """
            () => {
              const ps = Array.from(document.querySelectorAll('p'));
              const placeholder = ps.find((p) => (p.getAttribute('data-placeholder') || '').includes('输入正文描述'));
              if (!placeholder) return null;
              let cur = placeholder;
              for (let i = 0; i < 5 && cur; i++) {
                cur = cur.parentElement;
                if (!cur) break;
                if ((cur.getAttribute('role') || '') === 'textbox') return cur;
              }
              return null;
            }
            """
        )
        elem = handle.as_element()
        if elem is None:
            raise InvalidPublishParameterError("没有找到正文输入框")
        return elem

    async def check_title_max_length(self) -> None:
        suffix = self.page.locator("div.title-container div.max_suffix").first
        if await suffix.count() == 0:
            return
        text = (await suffix.inner_text() or "").strip()
        if "/" not in text:
            raise InvalidPublishParameterError(f"标题长度超过限制: {text}")
        curr, max_len = text.split("/", 1)
        raise InvalidPublishParameterError(f"当前输入长度为{curr}，最大长度为{max_len}")

    async def check_content_max_length(self) -> None:
        err = self.page.locator("div.edit-container div.length-error").first
        if await err.count() == 0:
            return
        text = (await err.inner_text() or "").strip()
        if "/" not in text:
            raise InvalidPublishParameterError(f"正文长度超过限制: {text}")
        curr, max_len = text.split("/", 1)
        raise InvalidPublishParameterError(f"当前输入长度为{curr}，最大长度为{max_len}")

    async def input_tags(self, content_elem, tags: list[str]) -> None:
        if not tags:
            return

        await content_elem.click()
        await self.page.wait_for_timeout(1000)
        for _ in range(20):
            await self.page.keyboard.press("ArrowDown")
            await self.page.wait_for_timeout(10)
        await self.page.keyboard.press("Enter")
        await self.page.keyboard.press("Enter")
        await self.page.wait_for_timeout(1000)

        for tag in tags:
            clean_tag = tag.lstrip("#")
            await self.page.keyboard.type("#", delay=80)
            for ch in clean_tag:
                await self.page.keyboard.type(ch, delay=50)
            await self.page.wait_for_timeout(1000)
            first_item = self.page.locator("#creator-editor-topic-container .item").first
            if await first_item.count() > 0:
                await first_item.click()
                logger.info(f"成功点击标签联想: {clean_tag}")
            else:
                await self.page.keyboard.type(" ", delay=50)
                logger.info(f"未命中标签联想，直接空格收尾: {clean_tag}")
            await self.page.wait_for_timeout(500)

    async def set_schedule_publish(self, schedule_time: datetime) -> None:
        switch = self.page.locator(".post-time-wrapper .d-switch").first
        await switch.click()
        await self.page.wait_for_timeout(800)

        date_text = schedule_time.strftime("%Y-%m-%d %H:%M")
        date_input = self.page.locator(".date-picker-container input").first
        await date_input.click()
        if os.name == "nt":
            await self.page.keyboard.press("Control+A")
        else:
            await self.page.keyboard.press("Meta+A")
            await self.page.keyboard.press("Control+A")
        await self.page.keyboard.type(date_text, delay=40)
        await self.page.wait_for_timeout(500)
        logger.info(f"定时发布时间已设置: {date_text}")

    async def submit_publish(
            self,
            title: str,
            content: str,
            tags: list[str],
            schedule_time: Optional[datetime],
    ) -> dict:
        title_input = self.page.locator("div.d-input input").first
        await title_input.click()
        await title_input.fill(title)
        await self.page.wait_for_timeout(500)
        await self.check_title_max_length()
        logger.info("标题长度检查通过")

        await self.page.wait_for_timeout(1000)
        content_elem = await self.get_content_element()
        await content_elem.click()
        await self.page.keyboard.type(content, delay=25)
        await self.input_tags(content_elem, tags)

        await self.page.wait_for_timeout(1000)
        await self.check_content_max_length()
        logger.info("正文长度检查通过")

        if schedule_time is not None:
            await self.set_schedule_publish(schedule_time)

        submit_btn = await self.wait_for_publish_button_clickable(max_wait_seconds=60)
        initial_url = self.page.url
        await submit_btn.click()
        return await self.wait_for_publish_result(initial_url=initial_url)

    async def submit_publish_video(
            self,
            title: str,
            content: str,
            tags: list[str],
            schedule_time: Optional[datetime],
    ) -> dict:
        title_input = self.page.locator("div.d-input input").first
        await title_input.click()
        await title_input.fill(title)
        await self.page.wait_for_timeout(500)
        await self.check_title_max_length()
        logger.info("标题长度检查通过")

        await self.page.wait_for_timeout(1000)
        content_elem = await self.get_content_element()
        await content_elem.click()
        await self.page.keyboard.type(content, delay=25)
        await self.input_tags(content_elem, tags)

        await self.page.wait_for_timeout(1000)
        await self.check_content_max_length()
        logger.info("正文长度检查通过")

        if schedule_time is not None:
            await self.set_schedule_publish(schedule_time)

        btn = await self.wait_for_publish_button_clickable(max_wait_seconds=600)
        initial_url = self.page.url
        await btn.click()
        return await self.wait_for_publish_result(initial_url=initial_url)

    async def publish_image_note(
            self,
            image_list: list[str],
            title: str = "",
            content: str = "",
            tags: Optional[list[str]] = None,
            schedule_at: Optional[datetime] = None,
    ) -> dict:
        if not image_list:
            raise InvalidPublishParameterError("图片不能为空")

        await self.open_publish_page("上传图文")
        uploaded = await self.upload_images(image_list)

        tags = tags or []
        normalized_tags = tags[:10] if len(tags) > 10 else tags
        if len(tags) > 10:
            logger.warning("标签数量超过 10，仅保留前 10 个")

        publish_result = await self.submit_publish(
            title=title,
            content=content,
            tags=normalized_tags,
            schedule_time=schedule_at,
        )
        return {
            "target": "image",
            "uploaded_count": len(uploaded),
            "status": publish_result["status"],
            "publish_url": publish_result.get("publish_url", ""),
            "publish_id": publish_result.get("publish_id", ""),
        }

    async def publish_video_note(
            self,
            video_path: str,
            title: str = "",
            content: str = "",
            tags: Optional[list[str]] = None,
            schedule_at: Optional[datetime] = None,
    ) -> dict:
        if not video_path:
            raise InvalidPublishParameterError("视频不能为空")

        await self.open_publish_page("上传视频")
        uploaded = await self.upload_video(video_path)
        await self._rand_sleep(0.8, 1.3)

        tags = tags or []
        normalized_tags = tags[:10] if len(tags) > 10 else tags
        if len(tags) > 10:
            logger.warning("标签数量超过 10，仅保留前 10 个")

        publish_result = await self.submit_publish_video(
            title=title,
            content=content,
            tags=normalized_tags,
            schedule_time=schedule_at,
        )
        return {
            "target": "video",
            "uploaded_count": 1 if uploaded else 0,
            "status": publish_result["status"],
            "publish_url": publish_result.get("publish_url", ""),
            "publish_id": publish_result.get("publish_id", ""),
        }
