import asyncio
import random
import time
from contextlib import asynccontextmanager
from pathlib import Path
from typing import Callable, Optional, Set

from loguru import logger

import flow_xhs.runtime.config as runtime_config
from flow_xhs.runtime.enums import Platform
from flow_xhs.runtime.browser.manager import browser_manager, stealth_async
from flow_xhs.runtime.database.manager import DatabaseManager
from flow_xhs.platform.factory import PlatformFactory
from flow_xhs.platform.issues import build_unknown_issue, detect_issue_from_page


class AccountLeaseManager:
    """Manages account locking and leasing for multi-tenant isolation."""
    _locked_accounts: Set[str] = set()  # format: "platform:user_no"
    _lock = asyncio.Lock()

    @classmethod
    def build_account_key(cls, platform: Platform, user_no: str) -> str:
        return f"{platform.value}:{user_no}"

    @classmethod
    def _match_account_identifier(cls, account: dict, identifier: str) -> bool:
        target = str(identifier or "").strip()
        if not target:
            return False
        user_no = str(account.get("user_no") or "").strip()
        return target == user_no

    @classmethod
    async def try_acquire_account_lock(cls, platform: Platform, user_no: str) -> bool:
        if not user_no:
            return False
        accounts = DatabaseManager.get_all_accounts(only_active=True)
        valid_accounts = [acc for acc in accounts if acc['platform'] == platform.value]
        target = next((acc for acc in valid_accounts if cls._match_account_identifier(acc, user_no)), None)
        if not target:
            return False
        key = cls.build_account_key(platform, target["user_no"])
        async with cls._lock:
            if key in cls._locked_accounts:
                return False
            cls._locked_accounts.add(key)
            logger.info(f"Leased account: {key}")
            return True

    @classmethod
    async def release_account_lock(cls, platform: Platform, user_no: str) -> None:
        if not user_no:
            return
        accounts = DatabaseManager.get_all_accounts(only_active=False)
        valid_accounts = [acc for acc in accounts if acc['platform'] == platform.value]
        target = next((acc for acc in valid_accounts if cls._match_account_identifier(acc, user_no)), None)
        if not target:
            return
        key = cls.build_account_key(platform, target["user_no"])
        async with cls._lock:
            if key in cls._locked_accounts:
                cls._locked_accounts.remove(key)
                logger.info(f"Released account: {key}")

    @classmethod
    @asynccontextmanager
    async def lease_account(cls, platform: Platform, user_no: Optional[str] = None):
        """Lease an account for a request. If user_no is None, pick a random active one."""
        account = None
        account_key = None

        async with cls._lock:
            accounts = DatabaseManager.get_all_accounts(only_active=True)
            valid_accounts = [acc for acc in accounts if acc['platform'] == platform.value]

            if user_no:
                target = next((acc for acc in valid_accounts if cls._match_account_identifier(acc, user_no)), None)
                if target:
                    key = cls.build_account_key(platform, target['user_no'])
                    if key not in cls._locked_accounts:
                        account = target
                        account_key = key
            else:
                # Pick a random one that is not locked
                available = [
                    acc for acc in valid_accounts
                    if cls.build_account_key(platform, acc['user_no']) not in cls._locked_accounts
                ]
                if available:
                    account = random.choice(available)
                    account_key = cls.build_account_key(platform, account['user_no'])

            if not account:
                raise Exception(f"No available {platform.value} accounts to lease.")

            cls._locked_accounts.add(account_key)
            logger.info(f"Leased account: {account_key}")

        try:
            yield account
        finally:
            async with cls._lock:
                if account_key in cls._locked_accounts:
                    cls._locked_accounts.remove(account_key)
                    logger.info(f"Released account: {account_key}")


class AccountManager:
    """Business logic for managing accounts across platforms."""
    DEFAULT_REMOTE_SCREENSHOT_NAME = "login_viewport.png"

    @staticmethod
    def list_accounts(only_active=False):
        accounts = DatabaseManager.get_all_accounts(only_active=only_active)
        for acc in accounts:
            acc["uid"] = acc["user_no"]
            acc["status"] = "Active" if acc["is_logged_in"] else "Inactive"
            if acc.get("check_time"):
                acc["last_active"] = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(acc["check_time"]))
            else:
                acc["last_active"] = "N/A"
        return accounts

    @staticmethod
    def add_or_update_account(account_data: dict):
        DatabaseManager.upsert_account(account_data)
        logger.info(f"Account {account_data.get('nickname')} ({account_data.get('platform')}) updated.")

    @staticmethod
    def delete_account(platform, user_no):
        DatabaseManager.delete_account(platform, user_no)
        logger.info(f"Account {user_no} deleted from {platform}.")

    @staticmethod
    def _resolve_screenshot_path(raw_path: str) -> Path:
        default_name = AccountManager.DEFAULT_REMOTE_SCREENSHOT_NAME
        if not raw_path:
            return Path(runtime_config.APP_DATA_DIR) / "outputs" / default_name

        path = Path(raw_path).expanduser()
        if (path.exists() and path.is_dir()) or raw_path.endswith(("/", "\\")):
            return path / default_name
        if path.suffix.lower() in {".png", ".jpg", ".jpeg"}:
            return path
        return path.with_suffix(".png")

    @staticmethod
    def _find_account(platform_enum: Platform, account_uid: str) -> Optional[dict]:
        account_uid = str(account_uid or "").strip()
        if not account_uid:
            return None
        accounts = DatabaseManager.get_all_accounts(only_active=False)
        for item in accounts:
            if item.get("platform") != platform_enum.value:
                continue
            if str(item.get("user_no") or "").strip() == account_uid:
                return item
        return None

    @staticmethod
    def get_login_profile(platform_enum: Platform):
        return PlatformFactory.get_login_profile(platform_enum)

    @staticmethod
    async def _prepare_remote_login_screenshot(
        page,
        *,
        login_url: str,
        qr_selector: str,
        screenshot_file: Path,
        mode: str,
        timeout_seconds: int,
        account_uid: str | None,
        emit_progress: Callable[[str], None],
    ) -> dict:
        from playwright.async_api import TimeoutError as PlaywrightTimeoutError

        max_attempts = 2
        last_issue = None

        for attempt in range(1, max_attempts + 1):
            qr = page.locator(qr_selector).first
            try:
                await qr.wait_for(state="visible", timeout=30_000)
                await page.screenshot(path=str(screenshot_file), full_page=False)
                screenshot_path = str(screenshot_file.resolve())
                logger.info(f"Remote login screenshot saved: {screenshot_path}")
                emit_progress(
                    "login.qr_ready",
                    mode=mode,
                    login_url=login_url,
                    screenshot_path=screenshot_path,
                    timeout_seconds=timeout_seconds,
                    account_uid=account_uid,
                )
                return {
                    "ready": True,
                    "screenshot_path": screenshot_path,
                    "issue": None,
                }
            except PlaywrightTimeoutError:
                pass

            issue = await detect_issue_from_page(page, include_body=True)
            if issue is None:
                issue = build_unknown_issue(
                    source="page",
                    failure_reason="登录二维码未就绪",
                    next_action="请刷新页面后重试登录",
                    agent_action="RETRY_LATER",
                    retry_recommended=True,
                )
            last_issue = issue
            logger.warning(
                "Remote login issue detected: "
                f"{issue.failure_type} retry={issue.retry_recommended}"
            )
            if issue.retry_recommended and attempt < max_attempts:
                await page.reload(wait_until="domcontentloaded")
                await page.wait_for_timeout(1500)
                continue
            break

        await page.screenshot(path=str(screenshot_file), full_page=False)
        screenshot_path = str(screenshot_file.resolve())
        issue_details = last_issue.to_details() if last_issue is not None else build_unknown_issue().to_details()
        logger.info(f"Remote login screenshot saved: {screenshot_path}")
        emit_progress(
            "login.issue_detected",
            mode=mode,
            login_url=login_url,
            screenshot_path=screenshot_path,
            timeout_seconds=timeout_seconds,
            account_uid=account_uid,
            issue=issue_details,
        )
        return {
            "ready": False,
            "screenshot_path": screenshot_path,
            "issue": last_issue,
        }

    @staticmethod
    async def login_and_add_account(
        platform_enum: Platform,
        *,
        mode: str = "local",
        timeout_seconds: int = 240,
        screenshot_path: str = "",
        account_uid: str = "",
        progress_callback: Callable[[dict], None] | None = None,
    ) -> dict:
        """Interactive login process via browser."""
        logger.info(f"Starting browser for {platform_enum.value} login, mode={mode}...")

        def _emit_progress(event: str, **payload) -> None:
            if progress_callback is None:
                return
            try:
                progress_callback({"event": event, **payload})
            except Exception as emit_err:
                logger.debug(f"Ignored progress callback error: {emit_err}")

        from playwright.async_api import TimeoutError as PlaywrightTimeoutError

        if mode not in {"local", "remote"}:
            return {
                "success": False,
                "reason": "invalid_mode",
                "message": f"Unsupported login mode: {mode}",
                "mode": mode,
            }

        result = {
            "success": False,
            "mode": mode,
            "timeout_seconds": timeout_seconds,
            "login_url": "",
            "screenshot_path": None,
            "account_uid": None,
            "user_no": None,
            "nickname": None,
            "reused_session": False,
        }
        profile = AccountManager.get_login_profile(platform_enum)
        result["login_url"] = profile.login_url
        target_account_uid = str(account_uid or "").strip()
        target_account = None
        if target_account_uid:
            target_account = AccountManager._find_account(platform_enum, target_account_uid)
            if not target_account:
                result["reason"] = "account_not_found"
                result["message"] = f"账号不存在: {target_account_uid}"
                result["account_uid"] = target_account_uid
                return result
            result["account_uid"] = target_account_uid

        headless = runtime_config.resolve_login_mode_headless(mode)
        account_key = target_account_uid or account_uid or None
        storage_state_seed = target_account.get("storage_state") if target_account else None
        if target_account and storage_state_seed:
            logger.info(f"Loaded stored session for account: {target_account_uid}")

        async with browser_manager.get_context(
            storage_state=storage_state_seed,
            headless=headless,
            account_key=account_key,
        ) as context:
            page = await context.new_page()
            await stealth_async(page)

            extractor = PlatformFactory.get_extractor(platform_enum, page)

            try:
                logger.info(f"Opening login page: {profile.login_url}")
                await page.goto(profile.login_url, wait_until="domcontentloaded")
                await page.wait_for_timeout(500)

                is_logged_in = await page.locator(profile.login_selector).count() > 0
                result["reused_session"] = bool(target_account and is_logged_in)
                if not is_logged_in:
                    if target_account:
                        logger.info(f"Stored session expired for account {target_account_uid}, fallback to QR login.")
                    if mode == "remote":
                        screenshot_file = AccountManager._resolve_screenshot_path(screenshot_path)
                        screenshot_file.parent.mkdir(parents=True, exist_ok=True)
                        remote_state = await AccountManager._prepare_remote_login_screenshot(
                            page,
                            login_url=profile.login_url,
                            qr_selector=profile.qr_selector,
                            screenshot_file=screenshot_file,
                            mode=mode,
                            timeout_seconds=timeout_seconds,
                            account_uid=result["account_uid"],
                            emit_progress=_emit_progress,
                        )
                        result["screenshot_path"] = remote_state["screenshot_path"]
                        if not remote_state["ready"]:
                            issue = remote_state.get("issue") or build_unknown_issue()
                            result["reason"] = "login_issue"
                            result["message"] = (
                                f"登录失败: {issue.failure_reason}，{issue.next_action}"
                            )
                            result["details"] = issue.to_details()
                            return result

                    logger.info("Please complete login in the browser...")
                    try:
                        await page.wait_for_selector(profile.login_selector, timeout=timeout_seconds * 1000)
                    except PlaywrightTimeoutError:
                        logger.error(f"Login timed out after {timeout_seconds}s")
                        result["reason"] = "login_timeout"
                        result["message"] = f"扫码登录超时（{timeout_seconds} 秒）"
                        return result
                elif target_account:
                    logger.info(f"Session still valid for account {target_account_uid}, skip QR login.")

                logger.info("Login detected!")

                storage_state = await context.storage_state()
                info = await extractor.get_self_info()
                user_no = str((info or {}).get(profile.account_id_field) or "").strip()
                if info and user_no:
                    if target_account_uid and target_account:
                        expected = str(target_account.get("user_no") or "").strip()
                        if expected and user_no != expected:
                            result["reason"] = "account_mismatch"
                            result["message"] = (
                                f"指定账号 {target_account_uid} 与当前登录账号 {user_no} 不一致，"
                                "请确认扫码账号或去掉 --account 参数"
                            )
                            return result
                    elif target_account_uid and user_no != target_account_uid:
                        result["reason"] = "account_mismatch"
                        result["message"] = (
                            f"指定账号 {target_account_uid} 与当前登录账号 {user_no} 不一致，"
                            "请确认扫码账号或去掉 --account 参数"
                        )
                        return result

                    account_data = {
                        "platform": platform_enum.value,
                        "user_no": user_no,
                        "nickname": info.get(profile.nickname_field),
                        "is_logged_in": not bool(info.get(profile.guest_field)),
                        "check_time": int(time.time()),
                        "storage_state": storage_state
                    }
                    DatabaseManager.upsert_account(account_data)
                    logger.info(f"Account {info.get(profile.nickname_field)} added successfully!")
                    result["success"] = True
                    result["account_uid"] = user_no
                    result["user_no"] = user_no
                    result["nickname"] = info.get(profile.nickname_field)
                    return result

                logger.info("Failed to fetch user info after login.")
                result["reason"] = "self_info_unavailable"
                result["message"] = "登录成功但未获取到用户信息"
                return result
            except Exception as e:
                logger.error(f"Login failed or timed out: {e}")
                result["reason"] = "login_failed"
                result["message"] = str(e)
                return result
