# retoor <retoor@molodetz.nl>

import asyncio
import hashlib
import logging
import random

from devplacepy.services.bot.config import (
    HOME_URL,
    MAX_THREAD_REPLIES,
    MENTION_REPLIES_PER_SESSION,
    PROJECT_STATUSES,
    bio_has_signal,
    ensure_bio_signal,
)

logger = logging.getLogger(__name__)


class BotSocialMixin:
    async def _click_profile_link(self) -> bool:
        b = self.b
        links = b.page.locator("a[href*='/profile/']")
        count = await links.count()
        if count == 0:
            return False

        for i in range(count):
            h = await links.nth(i).get_attribute("href")
            if h and self.state.username not in h:
                try:
                    await links.nth(i).click(timeout=5000)
                    await b._idle(1.0, 2.0)
                    uname = h.split("/profile/")[-1].split("?")[0].split("/")[0]
                    if uname not in self.state.known_users:
                        self.state.known_users.append(uname)
                    self.state.profiles_viewed += 1
                    self._action(
                        "PROFILE", f"viewed @{uname} [{self.state.profiles_viewed}]"
                    )
                    return True
                except Exception as e:
                    logger.debug("_click_profile_link failed: %s", e)
                    continue
        return False

    async def _click_create_project(self) -> bool:
        b = self.b
        await b.goto(f"{self.base_url}/projects")
        ok = await b.click("#create-project-btn")
        if not ok:
            return False
        await b._idle(0.8, 1.5)

        if not await b.text("#create-project-modal"):
            return False

        if self._project_cache:
            raw_title, desc = self._project_cache.pop(0)
            asyncio.create_task(self._refill_cache_async("project"))
        else:
            raw_title = await self._generate(
                self.llm.generate_project_title, self.state.persona
            )
            if not raw_title:
                self._log("Create project: title generation failed")
                return False
            raw_title = raw_title[:200]
            desc = await self._generate(
                self.llm.generate_project_desc, raw_title, self.state.persona
            )
            if not desc:
                self._log("Create project: description generation failed")
                return False
            desc = desc[:5000]

        title = self.llm.strip_md(raw_title)[:120] or raw_title[:120]
        types = ["game", "software", "mobile_app", "website", "game_asset"]
        ptype = random.choice(types)
        status = random.choice(PROJECT_STATUSES)

        await b.fill("#title", title)
        await b._idle(0.2, 0.5)
        await b.fill("#description", desc)
        await b._idle(0.2, 0.5)

        await b.click(f"input[name='project_type'][value='{ptype}']")
        await b._idle(0.2, 0.5)

        await b.click(f"input[name='status'][value='{status}']")
        await b._idle(0.2, 0.5)

        ok = await b.click("#create-project-modal .btn-primary")
        if ok:
            self.state.projects_created += 1
            self._action(
                "PROJECT", f"{title} ({ptype}) [{self.state.projects_created}]"
            )
            await asyncio.sleep(2)
            return True
        return False

    async def _report_issue(self) -> bool:
        b = self.b
        await b.goto(f"{self.base_url}/issues")
        ok = await b.click("[data-modal='create-issue-modal']")
        if not ok:
            return False
        await b._idle(0.5, 1.5)

        if not await b.text("#create-issue-modal"):
            return False

        topic = random.choice(
            [
                "UI glitch",
                "Broken link",
                "Performance issue",
                "Auth problem",
                "Formatting error",
            ]
        )
        if self._issue_cache:
            title, desc = self._issue_cache.pop(0)
            asyncio.create_task(self._refill_cache_async("issue"))
        else:
            generated = await self._generate(self.llm.generate_issue, topic)
            if not generated:
                self._log("Report issue: generation failed")
                return False
            title, desc = generated

        await b.fill("#issue-title", title[:200])
        await b._idle(0.2, 0.5)
        await b.fill("textarea[name='description']", desc[:2000])
        await b._idle(0.5, 1.0)

        ok = await b.click(
            "button:has-text('Submit Report'), #create-issue-modal .btn-primary"
        )
        if ok:
            self.state.issues_filed += 1
            self._action("ISSUE", f"{title[:50]} [{self.state.issues_filed}]")
            await asyncio.sleep(2)
            return True
        return False

    async def _profile_has_signal(self) -> bool:
        profile = await self._fetch_own_profile()
        user = profile.get("profile_user") or {}
        website = (user.get("website") or "").strip().rstrip("/")
        return bio_has_signal(user.get("bio") or "") and website == HOME_URL

    async def _ensure_profile_signal(self) -> bool:
        if self.state.profile_filled and await self._profile_has_signal():
            return True
        self._log("Profile signal missing, refreshing profile")
        ok = await self._update_profile()
        await self.b.goto(f"{self.base_url}/feed")
        await self.b._idle(0.8, 2.0)
        return ok

    async def _update_profile(self) -> bool:
        b = self.b
        await b.goto(f"{self.base_url}/profile/{self.state.username}")

        ok = await b.click("[data-edit-field='bio']")
        if not ok:
            return False
        await b._idle(0.3, 0.8)

        bio = await self._generate(self.llm.generate_bio)
        if not bio:
            self._log("Update profile: bio generation failed")
            return False
        bio = ensure_bio_signal(bio[:400])[:500]
        await b.fill("textarea[name='bio']", bio)
        await b._idle(0.3, 0.8)

        filled = ["bio"]
        fields = await self._generate(
            self.llm.generate_profile_fields, self.state.username
        )
        if fields:
            location, git_link, website = fields
            for field_name, value in (
                ("location", location),
                ("git_link", git_link),
                ("website", website),
            ):
                if not value:
                    continue
                await b.click(f"[data-edit-field='{field_name}']")
                await b._idle(0.2, 0.5)
                if await b.fill(
                    f"input[name='{field_name}'], textarea[name='{field_name}']", value
                ):
                    filled.append(field_name)
                await b._idle(0.2, 0.5)

        ok = await b.click(
            "form[action*='/profile/update'] button[type='submit'], button[type='submit']"
        )
        if ok:
            self.state.profile_filled = True
            self._save()
            self._action("PROFILE", f"updated own profile ({', '.join(filled)})")
            await asyncio.sleep(2)
            return True
        return False

    async def _follow_user(self) -> bool:
        b = self.b
        curl = await b.url()
        if "/profile/" not in curl:
            return False
        uname = curl.split("/profile/")[-1].split("?")[0]
        if uname == self.state.username:
            return False
        form = b.page.locator("form[action*='/follow/'] button[type='submit']")
        if await form.count() == 0:
            return False
        text = (await form.first.inner_text()).strip()
        if text.lower() not in ("follow", "unfollow"):
            return False
        try:
            await form.first.click(timeout=3000)
            self._action(
                "FOLLOW",
                f"{'followed' if text.lower() == 'follow' else 'unfollowed'} @{uname}",
            )
            await b._idle(1.0, 2.0)
            return True
        except Exception as e:
            logger.debug("_follow_user failed: %s", e)
            return False

    def _classify_notification(self, text: str) -> bool:
        tl = text.lower()
        return (
            f"@{self.state.username.lower()}" in tl
            or "mentioned you" in tl
            or "replied to your" in tl
            or "replied to you" in tl
            or "tagged you" in tl
        )

    async def _collect_notifications(self) -> list[dict]:
        b = self.b
        cards = b.page.locator(".notification-card")
        try:
            count = await cards.count()
        except Exception:
            count = 0
        if count == 0:
            return await self._collect_notifications_fallback()
        items: list[dict] = []
        seen_keys: set = set()
        for i in range(min(count, 30)):
            card = cards.nth(i)
            text = ""
            try:
                body = card.locator(".notification-text").first
                if await body.count() > 0:
                    text = (await body.inner_text() or "")[:600]
            except Exception:
                text = ""
            if not text:
                try:
                    text = (await card.inner_text() or "")[:600]
                except Exception:
                    text = ""
            href = ""
            try:
                link = card.locator("a.card-link").first
                if await link.count() > 0:
                    href = (await link.get_attribute("href")) or ""
            except Exception:
                href = ""
            unread = False
            try:
                cls = (await card.get_attribute("class")) or ""
                unread = "unread" in cls.split()
            except Exception:
                unread = False
            if not text and not href:
                continue
            key = hashlib.sha256(f"{href}|{text[:200]}".encode()).hexdigest()[:24]
            if key in seen_keys:
                continue
            seen_keys.add(key)
            items.append(
                {
                    "key": key,
                    "href": href,
                    "text": text,
                    "is_mention": self._classify_notification(text),
                    "unread": unread,
                }
            )
        return items

    async def _collect_notifications_fallback(self) -> list[dict]:
        b = self.b
        loc = b.page.locator(".notification-item, .notif-item, [data-notification-id]")
        try:
            count = await loc.count()
        except Exception:
            count = 0
        items: list[dict] = []
        seen_keys: set = set()
        for i in range(min(count, 30)):
            el = loc.nth(i)
            try:
                text = (await el.inner_text() or "")[:600]
            except Exception:
                text = ""
            href = ""
            try:
                inner = el.locator("a[href]").first
                if await inner.count() > 0:
                    href = (await inner.get_attribute("href")) or ""
            except Exception:
                href = ""
            if not text and not href:
                continue
            key = hashlib.sha256(f"{href}|{text[:200]}".encode()).hexdigest()[:24]
            if key in seen_keys:
                continue
            seen_keys.add(key)
            items.append(
                {
                    "key": key,
                    "href": href,
                    "text": text,
                    "is_mention": self._classify_notification(text),
                    "unread": True,
                }
            )
        return items

    async def _check_notifications(self) -> bool:
        b = self.b
        await b.goto(f"{self.base_url}/notifications")
        await b._idle(0.8, 1.8)
        notifications = await self._collect_notifications()
        if not notifications:
            self._log("Notifications: none")
            return True
        unseen_mentions = [
            n
            for n in notifications
            if n["is_mention"]
            and n["unread"]
            and n["key"] not in self.state.notifications_seen
        ]
        self._log(
            f"Notifications: {len(notifications)} ({len(unseen_mentions)} unseen mentions)"
        )
        self.state.mentions_received += len(unseen_mentions)
        replied = 0
        for n in unseen_mentions:
            if self._session_mention_replies >= MENTION_REPLIES_PER_SESSION:
                self._log(
                    f"Mention reply cap reached ({MENTION_REPLIES_PER_SESSION}/session), leaving the rest"
                )
                break
            ok = await self._respond_to_mention(n["href"], n["text"])
            if ok:
                self.state.mentions_replied += 1
                self._session_mention_replies += 1
                replied += 1
            self.state.notifications_seen.append(n["key"])
            if len(self.state.notifications_seen) > 500:
                self.state.notifications_seen = self.state.notifications_seen[-250:]
            await b._idle(2.0, 5.0)
        if replied == 0:
            remaining = [
                n
                for n in notifications
                if n["key"] not in self.state.notifications_seen
            ]
            if remaining:
                pick = random.choice(remaining)
                target = pick["href"]
                if target and not target.startswith("http"):
                    target = self.base_url + (
                        target if target.startswith("/") else "/" + target
                    )
                if target:
                    try:
                        await b.goto(target)
                        self._log(f"Notifications: opened {target[-60:]}")
                    except Exception as e:
                        logger.debug("notification goto failed: %s", e)
                self.state.notifications_seen.append(pick["key"])
        try:
            mark = b.page.locator(
                "button:has-text('Mark all read'), form[action*='/mark-all-read'] button"
            )
            if await mark.count() > 0:
                await mark.first.click(timeout=3000)
                await b._idle(0.5, 1.5)
        except Exception as e:
            logger.debug("mark-read failed: %s", e)
        return True

    async def _respond_to_mention(self, href: str, snippet: str) -> bool:
        b = self.b
        target = href
        if target and not target.startswith("http"):
            target = self.base_url + (href if href.startswith("/") else "/" + href)
        if not target:
            return False
        try:
            await b.goto(target)
        except Exception as e:
            logger.debug("mention goto failed: %s", e)
            return False
        await b._idle(2.0, 4.0)
        curl = await b.url()
        if not any(k in curl for k in ("/posts/", "/gists/", "/projects/", "/news/")):
            self._log("Mention target has no comment thread, skipping")
            return False
        thread_id = self._thread_id(curl)
        if (
            thread_id
            and self.state.thread_reply_counts.get(thread_id, 0) >= MAX_THREAD_REPLIES
        ):
            self._log(
                f"Conversation taper on {thread_id[:12]} ({MAX_THREAD_REPLIES} replies), letting it rest"
            )
            return False
        needle = f"@{self.state.username}"
        comment_loc = None
        comment_selectors = [
            f".comment:has-text('{needle}')",
            f"li.comment:has-text('{needle}')",
            f".comment-item:has-text('{needle}')",
            f"article.comment:has-text('{needle}')",
        ]
        for sel in comment_selectors:
            try:
                loc = b.page.locator(sel).first
                if await loc.count() > 0:
                    comment_loc = loc
                    break
            except Exception:
                continue
        parent_text = ""
        mentioner = ""
        if comment_loc is not None:
            try:
                parent_text = (await comment_loc.inner_text() or "")[:125000]
            except Exception:
                parent_text = ""
            try:
                author_link = comment_loc.locator("a[href*='/profile/']").first
                if await author_link.count() > 0:
                    ah = await author_link.get_attribute("href") or ""
                    if "/profile/" in ah:
                        mentioner = (
                            ah.split("/profile/")[-1].split("?")[0].split("/")[0]
                        )
            except Exception:
                mentioner = ""
            if mentioner and mentioner.lower() == self.state.username.lower():
                self._log("Mention target resolves to self, skipping")
                return False
            try:
                await comment_loc.scroll_into_view_if_needed(timeout=2000)
                await b._idle(0.3, 0.9)
            except Exception as e:
                logger.debug("scroll mention failed: %s", e)
            try:
                reply_btn = comment_loc.locator(
                    ".comment-action-btn, button:has-text('Reply'), a:has-text('Reply')"
                ).first
                if await reply_btn.count() > 0:
                    await reply_btn.click(timeout=3000)
                    await b._idle(0.5, 1.5)
            except Exception as e:
                logger.debug("reply-btn click failed: %s", e)
        try:
            post_body = (
                await b.text(".post-content")
                or await b.text(".gist-detail-desc")
                or await b.text(".news-detail-content")
                or await b.text(".news-detail-desc")
                or await b.text(".project-detail-desc")
                or await b.text(".rendered-content")
                or await b.text("article")
                or snippet
            )
        except Exception:
            post_body = snippet
        reply = await self._generate(
            self.llm.generate_comment,
            post_body[:1500],
            self.state.persona,
            mentioner,
            parent_text,
        )
        if not reply:
            self._log("Mention reply: generation failed/empty")
            return False
        if mentioner and f"@{mentioner.lower()}" not in reply.lower():
            reply = f"@{mentioner} {reply}"
        reply = self._sanitize_mentions(reply)[:2000]
        textarea_sels = [
            ".comment .reply-form textarea[name='content']",
            ".reply-form textarea[name='content']",
            "form.comment-form textarea[name='content']",
            "textarea#comment-content",
            "textarea.emoji-picker-target",
            "textarea[name='content']",
        ]
        typed = False
        for sel in textarea_sels:
            if await self._type_like_human(sel, reply):
                typed = True
                break
        if not typed:
            self._log("Mention reply: no textarea found")
            return False
        await b._idle(0.5, 1.5)
        submit_sels = [
            ".reply-form button[type='submit']",
            "form.comment-form button[type='submit']",
            "button.btn-primary:has-text('Reply')",
            "button.btn-primary:has-text('Post')",
            "form[action*='/comments/create'] button[type='submit']",
            ".comment-form button[type='submit']",
        ]
        for sel in submit_sels:
            if await b.click(sel):
                self.state.comments_posted += 1
                self.state.replies_to_own_posts += 1
                if thread_id:
                    self.state.thread_reply_counts[thread_id] = (
                        self.state.thread_reply_counts.get(thread_id, 0) + 1
                    )
                    if len(self.state.thread_reply_counts) > 500:
                        recent = list(self.state.thread_reply_counts.items())[-250:]
                        self.state.thread_reply_counts = dict(recent)
                self._action("MENTION", f"replied to @{mentioner or '?'}: {reply[:60]}")
                await asyncio.sleep(2)
                return True
        self._log("Mention reply: submit failed")
        return False

    async def _check_messages(self) -> bool:
        b = self.b
        await b.goto(f"{self.base_url}/messages")
        await b._idle()
        conv = b.page.locator(
            ".messages-conversations a, .conversation-item, .message-item, .messages-list a"
        )
        if await conv.count() == 0:
            self._log("Messages: no conversations")
            return True
        try:
            await conv.first.click(timeout=3000)
            await b._idle(1.0, 2.0)
        except Exception as e:
            logger.debug("_check_messages open failed: %s", e)
            return True
        if random.random() < 0.7:
            return await self._compose_message(reply=True)
        self._log("Messages: read conversation")
        return True

    async def _spoke_last_in_thread(self) -> bool:
        bubbles = self.b.page.locator(".messages-thread .message-bubble")
        count = await bubbles.count()
        if count == 0:
            return False
        try:
            classes = await bubbles.nth(count - 1).get_attribute("class") or ""
        except Exception as e:
            logger.debug("_spoke_last_in_thread read failed: %s", e)
            return False
        return "mine" in classes.split()

    async def _compose_message(self, reply: bool = False) -> bool:
        b = self.b
        box = b.page.locator(
            "form[action*='/messages/send'] input[name='content'], "
            "input[name='content'][placeholder*='message'], "
            "form[action*='/messages/send'] textarea[name='content']"
        )
        if await box.count() == 0:
            self._log("Message: compose box not found")
            return False
        if await self._spoke_last_in_thread():
            self._log("Message: I spoke last; waiting for a reply (dialog, not monologue)")
            return False
        context = ""
        try:
            context = (
                await b.text(".messages-thread") or await b.text(".message-list") or ""
            )[:400]
        except Exception:
            context = ""
        text = await self._generate(self.llm.generate_dm, self.state.persona, context)
        if not text:
            return False
        text = text[:500]
        if not await self._type_like_human(
            "form[action*='/messages/send'] input[name='content'], input[name='content'], "
            "form[action*='/messages/send'] textarea[name='content']",
            text,
        ):
            return False
        await b._idle(0.4, 1.0)
        if await b.click(
            "button.messages-send-btn, form[action*='/messages/send'] button[type='submit']"
        ):
            self.state.messages_sent += 1
            self._action(
                "MESSAGE",
                f"{'replied' if reply else 'sent'} [{self.state.messages_sent}]: {text[:50]}",
            )
            await asyncio.sleep(2)
            return True
        return False

    async def _send_message(self) -> bool:
        b = self.b
        curl = await b.url()
        if "/profile/" not in curl:
            return False
        btn = b.page.locator("a[href*='with_uid=']").first
        if await btn.count() == 0:
            return False
        href = await btn.get_attribute("href") or ""
        if "with_uid=" not in href:
            return False
        try:
            await b.goto((self.base_url + href) if href.startswith("/") else href)
            await b._idle(1.0, 2.0)
        except Exception as e:
            logger.debug("_send_message goto failed: %s", e)
            return False
        return await self._compose_message(reply=False)
