# retoor <retoor@molodetz.nl>

import asyncio
import logging
import random
import re
from typing import Any

from devplacepy.services.bot.config import (
    COMMENT_COOLDOWN_MAX_SECONDS,
    COMMENT_COOLDOWN_MIN_SECONDS,
    MAX_SIBLING_COMMENTS,
    REACT_RATE_DEFAULT,
    REACT_RATES,
    SIBLING_SNIPPET_LEN,
)

logger = logging.getLogger(__name__)


class BotEngageMixin:
    async def _vote_on_feed(self) -> bool:
        b = self.b
        sel = "button.vote-up, button.post-action-btn.vote-up, form[action*='/votes/'] button[type='submit']"
        btns = b.page.locator(sel)
        count = await btns.count()
        if count == 0:
            return False
        voted_set = set(self.state.voted_post_ids)
        known_set = set(self.state.known_users)
        candidates: list[tuple[int, str, bool]] = []
        for i in range(min(count, 30)):
            btn = btns.nth(i)
            post_id = ""
            try:
                form = btn.locator("xpath=ancestor::form[1]").first
                if await form.count() > 0:
                    action = await form.get_attribute("action") or ""
                    m = re.search(r"/votes/(?:post|comment|gist)/([a-f0-9-]+)", action)
                    if m:
                        post_id = m.group(1)
            except Exception:
                pass
            if not post_id:
                try:
                    card = btn.locator(
                        "xpath=ancestor::*[contains(@class,'post-card') or self::article][1]"
                    ).first
                    if await card.count() > 0:
                        link = card.locator("a[href*='/posts/']").first
                        if await link.count() > 0:
                            href = await link.get_attribute("href") or ""
                            m = re.search(r"/posts/([^/?#]+)", href)
                            if m:
                                post_id = m.group(1)
                except Exception:
                    pass
            if post_id and post_id in voted_set:
                continue
            is_known_author = False
            try:
                card = btn.locator(
                    "xpath=ancestor::*[contains(@class,'post-card') or self::article][1]"
                ).first
                if await card.count() > 0:
                    author = card.locator("a[href*='/profile/']").first
                    if await author.count() > 0:
                        ah = await author.get_attribute("href") or ""
                        uname = ah.split("/profile/")[-1].split("?")[0].split("/")[0]
                        if uname and uname in known_set:
                            is_known_author = True
            except Exception:
                pass
            candidates.append((i, post_id, is_known_author))
        if not candidates:
            return False
        reciprocal = [c for c in candidates if c[2]]
        pool = reciprocal if reciprocal and random.random() < 0.65 else candidates
        idx, post_id, _ = random.choice(pool)
        try:
            await btns.nth(idx).click(timeout=3000)
            self.state.votes_cast += 1
            if post_id:
                self.state.voted_post_ids.append(post_id)
                if len(self.state.voted_post_ids) > 1000:
                    self.state.voted_post_ids = self.state.voted_post_ids[-500:]
            self._action(
                "VOTE", f"post {post_id[:12] or '?'} (total {self.state.votes_cast})"
            )
            await b._idle(1.0, 2.0)
            return True
        except Exception as e:
            logger.debug("_vote_on_feed failed: %s", e)
            return False

    async def _vote_on_comments(self) -> bool:
        b = self.b
        sel = "button.comment-vote-btn, form[action*='/vote/comment'] button[type='submit']"
        btns = b.page.locator(sel)
        count = await btns.count()
        if count < 2:
            return False
        idx = random.randrange(count)
        try:
            await btns.nth(idx).click(timeout=3000)
            self.state.votes_cast += 1
            self._action("VOTE", f"comment (total {self.state.votes_cast})")
            await b._idle(1.0, 2.0)
            return True
        except Exception as e:
            logger.debug("_vote_on_comments failed: %s", e)
            return False

    async def _is_own_post(self) -> bool:
        curl = await self.b.url()
        if "/posts/" not in curl:
            return False
        if curl in self.state.own_post_urls:
            return True
        author_selectors = [
            ".post-detail-header a[href*='/profile/']",
            ".post-detail .post-detail-header a[href*='/profile/']",
            ".post-author a[href*='/profile/']",
            ".post-header a[href*='/profile/']",
        ]
        for sel in author_selectors:
            loc = self.b.page.locator(sel).first
            if await loc.count() == 0:
                continue
            href = await loc.get_attribute("href") or ""
            if "/profile/" not in href:
                continue
            author = href.split("/profile/")[-1].split("?")[0].split("/")[0].lower()
            is_own = author == self.state.username.lower()
            if is_own and curl not in self.state.own_post_urls:
                self.state.own_post_urls.append(curl)
            return is_own
        try:
            author = await self.b.page.evaluate(
                """(uname) => {
                    const links = document.querySelectorAll('a[href*="/profile/"]');
                    for (const a of links) {
                        let p = a;
                        let in_nav = false;
                        while (p && p !== document.body) {
                            const cn = (p.className || '').toString();
                            if (p.tagName === 'HEADER' || p.tagName === 'NAV' || /topnav|navbar|sidebar/i.test(cn)) {
                                in_nav = true;
                                break;
                            }
                            p = p.parentElement;
                        }
                        if (in_nav) continue;
                        const m = a.getAttribute('href').match(/\\/profile\\/([^?/]+)/);
                        if (m) return m[1];
                    }
                    return '';
                }""",
                self.state.username,
            )
        except Exception as e:
            logger.debug("_is_own_post evaluate failed: %s", e)
            return False
        if not author:
            return False
        is_own = author.lower() == self.state.username.lower()
        if is_own and curl not in self.state.own_post_urls:
            self.state.own_post_urls.append(curl)
        return is_own

    async def _existing_comments(self) -> list[str]:
        bodies = self.b.page.locator(".comment-body")
        try:
            count = await bodies.count()
        except Exception:
            return []
        if count == 0:
            return []
        own = self.state.username.lower()
        collected: list[str] = []
        start = max(0, count - MAX_SIBLING_COMMENTS)
        for idx in range(start, count):
            node = bodies.nth(idx)
            try:
                text = (await node.locator(".comment-text").first.inner_text()).strip()
            except Exception:
                continue
            if not text:
                continue
            author = ""
            try:
                link = node.locator("a[href*='/profile/']").first
                if await link.count() > 0:
                    href = await link.get_attribute("href") or ""
                    author = href.split("/profile/")[-1].split("?")[0].split("/")[0]
            except Exception:
                author = ""
            if author and author.lower() == own:
                continue
            snippet = text[:SIBLING_SNIPPET_LEN]
            collected.append(f"{author}: {snippet}" if author else snippet)
        return collected

    async def _comment_on_post(self) -> bool:
        b = self.b
        curl = await b.url()
        if "/posts/" in curl:
            kind = "post"
        elif "/gists/" in curl:
            kind = "gist"
        elif "/news/" in curl:
            kind = "news"
        elif "/projects/" in curl:
            kind = "project"
        else:
            return False

        page_text = (await b.html()).lower()
        mentioned_here = f"@{self.state.username.lower()}" in page_text
        thread_id = self._thread_id(curl)

        if self._session_comments >= self._session_comment_cap and not mentioned_here:
            self._log(
                f"Comment cap reached ({self._session_comment_cap}/session), skipping"
            )
            return False

        if kind == "post":
            if await self._is_own_post() and not mentioned_here:
                self._log("Own post, no mention - not commenting")
                return False
        elif curl in self.state.own_post_urls:
            self._log(f"Own {kind} - not commenting")
            return False

        if (
            thread_id
            and thread_id in self.state.commented_post_ids
            and not mentioned_here
        ):
            self._log(f"Already commented on {thread_id[:12]}, not repeating")
            return False

        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 ""
        )
        if not post_body:
            self._log("Comment: no content found")
            return False

        known = list(
            {u for u in self.state.known_users if u and u != self.state.username}
        )
        mention_target = ""
        if known and random.random() < 0.3:
            mention_target = random.choice(known)

        style = self.llm.pick_comment_style()
        siblings = await self._existing_comments()
        siblings_block = "\n".join(f"- {c}" for c in siblings)
        siblings_text = " ".join(siblings)

        comment = ""
        for attempt in range(2):
            candidate = await self._generate(
                self.llm.generate_comment,
                post_body[:1500],
                self.state.persona,
                mention_target,
                "",
                style,
                siblings_block,
            )
            if not candidate:
                continue
            verdict = await self._generate(
                self.llm.quality_check,
                "comment",
                candidate,
                post_body[:1500],
                style,
                siblings_text,
            )
            if verdict is None or verdict[0]:
                comment = candidate
                break
            self.state.quality_rejections += 1
            action = "regenerating" if attempt == 0 else "skipping"
            self._log(f"Comment quality reject ({verdict[1]}), {action}")
        if not comment:
            self._log("Comment: no quality candidate")
            return False

        if mention_target and f"@{mention_target.lower()}" not in comment.lower():
            words = comment.split(" ", 1)
            if len(words) > 1:
                comment = f"{words[0]} @{mention_target} {words[1]}"
            else:
                comment = f"@{mention_target} {comment}"

        comment = self._sanitize_mentions(comment)[:2000]
        mention_log = f" @{mention_target}" if mention_target else ""

        textarea_sels = [
            "form.comment-form textarea[name='content']",
            "textarea#comment-content",
            "textarea.emoji-picker-target",
            "textarea[name='content']",
            "#comment-content",
        ]
        ok = False
        used_sel = ""
        for sel in textarea_sels:
            if await self._type_like_human(sel, comment):
                ok = True
                used_sel = sel
                break

        if not ok:
            self._log("Comment: no textarea found")
            return False

        self._log(f"Typed comment into {used_sel}")

        await b._idle(0.5, 1.5)

        submit_sels = [
            "form.comment-form button[type='submit']",
            "button.btn-primary:has-text('Post')",
            "form[action*='/comments/create'] button[type='submit']",
            ".comment-form button[type='submit']",
        ]
        ok = False
        for sel in submit_sels:
            if await b.click(sel):
                ok = True
                break

        if ok:
            self.state.comments_posted += 1
            self._session_comments += 1
            if thread_id and thread_id not in self.state.commented_post_ids:
                self.state.commented_post_ids.append(thread_id)
                if len(self.state.commented_post_ids) > 1000:
                    self.state.commented_post_ids = self.state.commented_post_ids[-500:]
            if kind == "news":
                self.state.news_comments += 1
            tag = "NEWS" if kind == "news" else "COMMENT"
            self._action(
                tag,
                f"on {kind}{mention_log} [{self.state.comments_posted}]: {comment[:60]}",
            )
            await asyncio.sleep(
                random.uniform(
                    COMMENT_COOLDOWN_MIN_SECONDS, COMMENT_COOLDOWN_MAX_SECONDS
                )
            )
            return True
        self._log("Comment: submit button not found/enabled")
        return False

    async def _reply_to_random_comment(self) -> bool:
        b = self.b
        curl = await b.url()
        if "/posts/" not in curl:
            return False
        if self._session_comments >= self._session_comment_cap:
            self._log(
                f"Comment cap reached ({self._session_comment_cap}/session), skipping reply"
            )
            return False
        comments = b.page.locator(
            ".comment, li.comment, article.comment, .comment-item"
        )
        count = await comments.count()
        if count == 0:
            return False
        limit = min(count, 10)
        for _ in range(limit):
            idx = random.randrange(limit)
            target = comments.nth(idx)
            try:
                text = (await target.inner_text() or "")[:125000]
            except Exception:
                continue
            if not text:
                continue
            if f"@{self.state.username.lower()}" in text.lower():
                continue
            mentioner = ""
            try:
                author_link = target.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:
                pass
            if not mentioner or mentioner.lower() == self.state.username.lower():
                continue
            try:
                await target.scroll_into_view_if_needed(timeout=2000)
                await b._idle(0.3, 0.8)
                reply_btn = target.locator(
                    ".comment-action-btn, button:has-text('Reply'), a:has-text('Reply')"
                ).first
                if await reply_btn.count() == 0:
                    continue
                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)
                continue
            post_body = await b.text(".post-content") or await b.text("article") or ""
            siblings = [c for c in await self._existing_comments() if text[:60] not in c]
            siblings_block = "\n".join(f"- {c}" for c in siblings)
            reply = await self._generate(
                self.llm.generate_comment,
                post_body[:1200],
                self.state.persona,
                mentioner,
                text,
                self.llm.pick_comment_style(),
                siblings_block,
            )
            if not reply:
                self._log("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 = [
                ".reply-form textarea[name='content']",
                ".comment-form textarea[name='content']",
                "textarea[name='content']",
            ]
            typed = False
            for sel in textarea_sels:
                if await self._type_like_human(sel, reply):
                    typed = True
                    break
            if not typed:
                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')",
            ]
            for sel in submit_sels:
                if await b.click(sel):
                    self.state.comments_posted += 1
                    self._session_comments += 1
                    if mentioner not in self.state.known_users:
                        self.state.known_users.append(mentioner)
                    self._action(
                        "REPLY",
                        f"to @{mentioner} [{self.state.comments_posted}]: {reply[:60]}",
                    )
                    await asyncio.sleep(
                        random.uniform(
                            COMMENT_COOLDOWN_MIN_SECONDS, COMMENT_COOLDOWN_MAX_SECONDS
                        )
                    )
                    return True
            return False
        return False

    async def _vote_on_gist(self) -> bool:
        b = self.b
        sel = (
            "form[action*='/votes/gist/'] button.gist-card-star, "
            "form[action*='/votes/gist/'] button[type='submit']"
        )
        btns = b.page.locator(sel)
        count = await btns.count()
        if count == 0:
            return False
        voted = set(self.state.voted_post_ids)
        candidates: list[tuple[int, str]] = []
        for i in range(min(count, 30)):
            gist_id = ""
            try:
                form = btns.nth(i).locator("xpath=ancestor::form[1]").first
                if await form.count() > 0:
                    action = await form.get_attribute("action") or ""
                    m = re.search(r"/votes/gist/([a-f0-9-]+)", action)
                    if m:
                        gist_id = m.group(1)
            except Exception:
                pass
            if gist_id and gist_id in voted:
                continue
            candidates.append((i, gist_id))
        if not candidates:
            return False
        idx, gist_id = random.choice(candidates)
        try:
            await btns.nth(idx).click(timeout=3000)
            self.state.votes_cast += 1
            if gist_id:
                self.state.voted_post_ids.append(gist_id)
                if len(self.state.voted_post_ids) > 1000:
                    self.state.voted_post_ids = self.state.voted_post_ids[-500:]
            self._action(
                "VOTE", f"gist {gist_id[:12] or '?'} (total {self.state.votes_cast})"
            )
            await b._idle(1.0, 2.0)
            return True
        except Exception as e:
            logger.debug("_vote_on_gist failed: %s", e)
            return False

    async def _vote_on_project(self) -> bool:
        b = self.b
        sel = (
            "form[action*='/votes/project/'] button.project-star-btn, "
            "form[action*='/votes/project/'] button[type='submit']"
        )
        btns = b.page.locator(sel)
        count = await btns.count()
        if count == 0:
            return False
        voted = set(self.state.voted_post_ids)
        candidates: list[tuple[int, str]] = []
        for i in range(min(count, 30)):
            project_id = ""
            try:
                form = btns.nth(i).locator("xpath=ancestor::form[1]").first
                if await form.count() > 0:
                    action = await form.get_attribute("action") or ""
                    m = re.search(r"/votes/project/([a-f0-9-]+)", action)
                    if m:
                        project_id = m.group(1)
            except Exception:
                pass
            if project_id and project_id in voted:
                continue
            candidates.append((i, project_id))
        if not candidates:
            return False
        idx, project_id = random.choice(candidates)
        try:
            await btns.nth(idx).click(timeout=3000)
            self.state.votes_cast += 1
            self.state.projects_starred += 1
            if project_id:
                self.state.voted_post_ids.append(project_id)
                if len(self.state.voted_post_ids) > 1000:
                    self.state.voted_post_ids = self.state.voted_post_ids[-500:]
            self._action(
                "STAR",
                f"project {project_id[:12] or '?'} (total {self.state.votes_cast})",
            )
            await b._idle(1.0, 2.0)
            return True
        except Exception as e:
            logger.debug("_vote_on_project failed: %s", e)
            return False

    async def _vote_on_poll(self) -> bool:
        b = self.b
        polls = b.page.locator(".poll[data-poll-uid]")
        count = await polls.count()
        if count == 0:
            return False
        voted = set(self.state.polls_voted)
        for i in range(min(count, 20)):
            poll = polls.nth(i)
            poll_uid = await poll.get_attribute("data-poll-uid") or ""
            if not poll_uid or poll_uid in voted:
                continue
            if await poll.locator(".poll-option.chosen").count() > 0:
                self.state.polls_voted.append(poll_uid)
                continue
            options = poll.locator(".poll-option:not([disabled])")
            opt_count = await options.count()
            if opt_count == 0:
                continue
            choice = options.nth(random.randrange(opt_count))
            option_uid = await choice.get_attribute("data-option-uid") or ""
            try:
                await choice.scroll_into_view_if_needed(timeout=2000)
                await b._idle(0.3, 0.8)
                await choice.click(timeout=3000)
            except Exception as e:
                logger.debug("_vote_on_poll click failed: %s", e)
                continue
            try:
                await poll.locator(".poll-option.chosen").first.wait_for(
                    state="visible", timeout=4000
                )
            except Exception as e:
                logger.debug("_vote_on_poll confirm failed: %s", e)
            self.state.polls_voted.append(poll_uid)
            if len(self.state.polls_voted) > 1000:
                self.state.polls_voted = self.state.polls_voted[-500:]
            self.state.poll_votes_cast += 1
            self._action(
                "POLL",
                f"voted {poll_uid[:12]} opt {option_uid[:8] or '?'} (total {self.state.poll_votes_cast})",
            )
            await b._idle(1.0, 2.0)
            return True
        return False

    async def _reaction_content(self, bar: Any) -> str:
        try:
            container = bar.locator(
                "xpath=ancestor::article[contains(concat(' ',normalize-space(@class),' '),' post-card ')][1]"
                " | ancestor::div[contains(concat(' ',normalize-space(@class),' '),' comment ')][1]"
            ).first
            if await container.count() > 0:
                text = (await container.inner_text() or "").strip()
                if text:
                    return text[:1200]
        except Exception as e:
            logger.debug("_reaction_content container failed: %s", e)
        body = (
            await self.b.text(".post-content")
            or await self.b.text(".gist-detail-desc")
            or await self.b.text(".news-detail-content")
            or await self.b.text(".news-detail-desc")
            or await self.b.text(".project-detail-desc")
            or await self.b.text(".rendered-content")
            or await self.b.text("article")
            or ""
        )
        return body[:1200]

    async def _react_to_object(self) -> bool:
        b = self.b
        bars = b.page.locator(".reaction-bar[data-reaction-uid]")
        count = await bars.count()
        if count == 0:
            return False
        decided = set(self.state.reacted_targets)
        react_rate = REACT_RATES.get(self.state.persona, REACT_RATE_DEFAULT)
        for i in range(min(count, 20)):
            bar = bars.nth(i)
            uid = await bar.get_attribute("data-reaction-uid") or ""
            rtype = await bar.get_attribute("data-reaction-type") or ""
            if not uid or uid in decided:
                continue

            self.state.reacted_targets.append(uid)
            if len(self.state.reacted_targets) > 2000:
                self.state.reacted_targets = self.state.reacted_targets[-1000:]

            if random.random() > react_rate:
                self._log(f"Reaction: skipping {rtype} {uid[:12]} (persona reticence)")
                return False

            snippet = await self._reaction_content(bar)
            if not snippet:
                self._log(f"Reaction: no content for {rtype} {uid[:12]}")
                return False
            emoji = await self._generate(
                self.llm.select_reaction, snippet, self.state.persona
            )
            if not emoji:
                self._log(f"Reaction: nothing fitting for {rtype} {uid[:12]}")
                return False

            add_btn = bar.locator(".reaction-add-btn").first
            if await add_btn.count() == 0:
                return False
            palette_btn = bar.locator(
                f".reaction-palette-btn[data-reaction-emoji='{emoji}']"
            ).first
            if await palette_btn.count() == 0:
                return False
            try:
                await add_btn.scroll_into_view_if_needed(timeout=2000)
                await b._idle(0.2, 0.6)
                await add_btn.click(timeout=3000)
                await b._idle(0.3, 0.7)
                await palette_btn.click(timeout=3000)
            except Exception as e:
                logger.debug("_react_to_object click failed: %s", e)
                return False
            try:
                await bar.locator(".reaction-chip.reacted").first.wait_for(
                    state="visible", timeout=4000
                )
            except Exception as e:
                logger.debug("_react_to_object confirm failed: %s", e)
            self.state.reactions_cast += 1
            self._action(
                "REACT",
                f"{emoji} on {rtype} {uid[:12]} (total {self.state.reactions_cast})",
            )
            await b._idle(1.0, 2.0)
            return True
        return False
