# retoor <retoor@molodetz.nl>

import asyncio
import logging
import random
import re

from devplacepy.services.bot.config import (
    NOTIFICATION_CHECK_PROB,
    SESSION_COMMENT_CAP_MAX,
    SESSION_COMMENT_CAP_MIN,
)

logger = logging.getLogger(__name__)


class BotLoopMixin:
    async def _cycle(
        self,
        mood: str = "normal",
        *,
        session_posted: bool = False,
        session_projects: int = 0,
        session_follows: int = 0,
        session_gists: int = 0,
        can_project: bool = False,
        can_post: bool = False,
        can_message: bool = False,
    ) -> tuple[int, bool, int, int, int]:
        b = self.b
        curl = await b.url()
        page_text = (await b.html()).lower()

        on_post = "/posts/" in curl
        on_profile = "/profile/" in curl
        on_project_detail = "/projects/" in curl
        on_projects_list = bool(re.search(r"/projects(\?|$)", curl))
        on_gist_detail = "/gists/" in curl
        on_gists_list = bool(re.search(r"/gists(\?|$)", curl))
        on_news_detail = "/news/" in curl
        on_news_list = bool(re.search(r"/news(\?|$)", curl))
        on_notifications = "/notifications" in curl

        actions = 0
        did_post = session_posted
        projs = session_projects
        follows = session_follows
        gists = session_gists
        msgs = 0
        mood_factor = {"lurking": 0.3, "casual": 0.6, "normal": 1.0, "deep": 1.5}

        burst = max(1, int(random.gauss(3, 1) * mood_factor[mood]))
        for _ in range(burst):
            if mood == "lurking" and not on_post and random.random() < 0.4:
                await self._scroll_page()
                await b._idle(0.3, 1.0)
                actions += 1
                curl = await b.url()
                page_text = (await b.html()).lower()
                on_post = "/posts/" in curl
                continue

            if on_post:
                page_text = (await b.html()).lower()
                own_post = await self._is_own_post()
                mentioned = own_post and f"@{self.state.username.lower()}" in page_text

                await self._read_like_human(4, 90)

                if own_post and not mentioned:
                    self._log("Own post, no mention - skipping interactions")
                    await self._random_nav_click()
                    await b._idle(1.0, 2.5)
                else:
                    comment_p = {
                        "lurking": 0.5,
                        "casual": 0.75,
                        "normal": 0.85,
                        "deep": 0.9,
                    }[mood]
                    if random.random() < comment_p:
                        if await self._comment_on_post():
                            actions += 1
                        await b._idle(0.5, 1.5)
                    if random.random() < 0.5:
                        if await self._reply_to_random_comment():
                            actions += 1
                        await b._idle(0.5, 1.5)
                    if random.random() < 0.7:
                        if await self._vote_on_feed():
                            actions += 1
                        await b._idle(0.5, 1.5)
                    if random.random() < 0.3:
                        if await self._vote_on_comments():
                            actions += 1
                        await b._idle(0.5, 1.5)
                    if random.random() < 0.6:
                        if await self._vote_on_poll():
                            actions += 1
                        await b._idle(0.5, 1.5)
                    if random.random() < 0.6:
                        if await self._react_to_object():
                            actions += 1
                        await b._idle(0.5, 1.5)
                    if random.random() < 0.75:
                        await b.goto(f"{self.base_url}/feed")
                        await b._idle(0.8, 2.0)
                    elif random.random() < 0.3:
                        await self._random_nav_click()
                        await b._idle(1.0, 2.5)

            elif on_profile:
                if not follows and random.random() < 0.4:
                    if await self._follow_user():
                        follows += 1
                        actions += 1
                    await b._idle(0.5, 1.5)
                if can_message and not msgs and random.random() < 0.3:
                    if await self._send_message():
                        msgs += 1
                        actions += 1
                    await b._idle(0.5, 1.5)
                await self._scroll_page()
                await b._idle(0.5, 1.0)
                if random.random() < 0.3:
                    if await self._click_post_link():
                        actions += 1
                await self._random_nav_click()
                await b._idle(1.0, 2.0)
                actions += 1

            elif on_news_detail:
                await self._read_like_human(4, 60)
                comment_p = {"lurking": 0.4, "casual": 0.6, "normal": 0.7, "deep": 0.8}[
                    mood
                ]
                if random.random() < comment_p:
                    if await self._comment_on_post():
                        actions += 1
                    await b._idle(0.5, 1.5)
                if random.random() < 0.7:
                    await b.goto(f"{self.base_url}/feed")
                    await b._idle(0.8, 1.8)
                else:
                    await self._random_nav_click()
                    await b._idle(1.0, 2.5)

            elif on_news_list:
                await self._scroll_page()
                await b._idle(0.4, 1.0)
                if random.random() < 0.7:
                    if await self._open_content("news"):
                        actions += 1
                else:
                    await self._random_nav_click()
                await b._idle(0.8, 1.8)
                actions += 1

            elif on_project_detail:
                await self._read_like_human(4, 60)
                if random.random() < 0.6:
                    if await self._vote_on_project():
                        actions += 1
                    await b._idle(0.5, 1.5)
                if random.random() < 0.5:
                    if await self._react_to_object():
                        actions += 1
                    await b._idle(0.5, 1.5)
                if random.random() < 0.4:
                    if await self._comment_on_post():
                        actions += 1
                    await b._idle(0.5, 1.5)
                if random.random() < 0.7:
                    await b.goto(f"{self.base_url}/projects")
                    await b._idle(0.8, 1.8)
                else:
                    await self._random_nav_click()
                    await b._idle(1.0, 2.5)

            elif on_projects_list:
                if random.random() < 0.55:
                    if await self._vote_on_project():
                        actions += 1
                    await b._idle(0.5, 1.5)
                if can_project and projs < 1 and random.random() < 0.3:
                    if await self._click_create_project():
                        projs += 1
                        actions += 1
                    await b._idle(0.5, 1.0)
                elif random.random() < 0.6:
                    if await self._open_content("project"):
                        actions += 1
                else:
                    await self._scroll_page()
                await b._idle(0.8, 1.8)
                actions += 1

            elif on_gist_detail:
                await self._read_like_human(4, 60)
                if random.random() < 0.45:
                    if await self._comment_on_post():
                        actions += 1
                    await b._idle(0.5, 1.5)
                if random.random() < 0.6:
                    if await self._vote_on_feed():
                        actions += 1
                    await b._idle(0.5, 1.5)
                if random.random() < 0.5:
                    if await self._react_to_object():
                        actions += 1
                    await b._idle(0.5, 1.5)
                if random.random() < 0.4:
                    await self._random_nav_click()
                    await b._idle(1.0, 2.5)

            elif on_gists_list:
                if random.random() < 0.6:
                    if await self._vote_on_gist():
                        actions += 1
                    await b._idle(0.5, 1.5)
                if gists < 1 and random.random() < 0.25:
                    if await self._create_gist():
                        gists += 1
                        actions += 1
                    await b._idle(0.5, 1.0)
                elif random.random() < 0.5:
                    if await self._click_gist_link():
                        actions += 1
                else:
                    await self._scroll_page()
                await b._idle(0.8, 1.8)
                actions += 1

            elif on_notifications:
                await self._check_notifications()
                await b._idle(1.0, 2.0)
                await self._random_nav_click()
                actions += 1

            else:
                clicked = False
                if random.random() < 0.3:
                    acted = await self._react_to_object()
                    if not acted:
                        acted = await self._vote_on_poll()
                    if acted:
                        actions += 1
                        clicked = True
                if not clicked and random.random() < 0.12:
                    if random.random() < 0.5:
                        await self._search(
                            "feed" if random.random() < 0.7 else "projects"
                        )
                    else:
                        await self._browse_section()
                    actions += 1
                    clicked = True
                if not clicked and random.random() < 0.85:
                    if await self._click_post_link():
                        clicked = True
                        actions += 1
                if not clicked and can_post and not did_post and random.random() < 0.5:
                    if await self._create_post():
                        did_post = True
                        actions += 1
                        clicked = True
                if not clicked and random.random() < 0.2:
                    if await self._click_profile_link():
                        clicked = True
                        actions += 1
                if not clicked and gists < 1 and random.random() < 0.1:
                    if await self._create_gist():
                        gists += 1
                        actions += 1
                        clicked = True
                if not clicked:
                    if await self._vote_on_feed():
                        actions += 1
                        clicked = True
                if not clicked:
                    if await self._random_nav_click():
                        clicked = True
                if not clicked:
                    await self._scroll_page()
                    actions += 1
                await b._idle(1.0, 2.5)

            curl = await b.url()
            page_text = (await b.html()).lower()
            on_post = "/posts/" in curl
            on_profile = "/profile/" in curl
            on_project_detail = "/projects/" in curl
            on_projects_list = bool(re.search(r"/projects(\?|$)", curl))
            on_gist_detail = "/gists/" in curl
            on_gists_list = bool(re.search(r"/gists(\?|$)", curl))
            on_news_detail = "/news/" in curl
            on_news_list = bool(re.search(r"/news(\?|$)", curl))
            on_notifications = "/notifications" in curl

        return actions, did_post, projs, follows, gists

    async def run_forever(self, action_limit: int = 0) -> None:
        b = self.b
        self._save()
        lifetime_actions = 0
        crash_streak = 0
        session_number = 0
        sessions_since_project = 99
        sessions_since_post = 99
        sessions_since_message = 99

        self._bind_monitor()

        if self._startup_jitter:
            delay = random.uniform(0, self._startup_jitter)
            self.b.note(status=f"warming up, first session in {delay / 60:.0f}m")
            self._log(f"Startup jitter: first session in {delay / 60:.1f}m")
            try:
                await asyncio.sleep(delay)
            except asyncio.CancelledError:
                self._save()
                raise

        while True:
            session_number += 1
            mood, session_len, break_range = await self._session_type()
            self._session_start_cost = self.llm.total_cost
            self._session_start_calls = self.llm.total_calls
            self._session_banner(mood, session_len)
            self._session_history = []
            self._session_energy = 0.5
            self._session_stop_after = 0
            self._session_comments = 0
            self._session_comment_cap = random.randint(
                SESSION_COMMENT_CAP_MIN, SESSION_COMMENT_CAP_MAX
            )
            self._session_mention_replies = 0
            use_ai = False

            try:
                await b.launch()
            except Exception as e:
                self._log(f"Launch failed: {e}")
                crash_streak += 1
                wait = min(30 * crash_streak, 300)
                await asyncio.sleep(wait)
                continue

            session_ok = False
            try:
                if not await self._ensure_auth():
                    self._log("Auth failed")
                    await b.close()
                    await asyncio.sleep(60)
                    continue

                if not await self._adopt_account_api_key():
                    self._log("No own API key yet; ending session to avoid shared-key billing")
                    await b.close()
                    await asyncio.sleep(60)
                    continue

                self._bind_monitor()

                if self._ai_decisions:
                    await self._ensure_identity()
                use_ai = self._ai_decisions and bool(self.state.identity)

                await self._warm_cache()
                self._log(f"Session active ({mood}) as {self._identity()}")

                if not self._profile_verified:
                    self._profile_verified = await self._ensure_profile_signal()

                unread = await self._unread_notification_count()
                if unread:
                    self._log(f"Unread notifications badge: {unread}")
                check_prob = NOTIFICATION_CHECK_PROB.get(mood, 1.0)
                want_notifications = mood in ("normal", "deep") or (
                    unread > 0 and random.random() < check_prob
                )

                if mood in ("normal", "deep"):
                    await self._check_messages()
                    await b._idle(0.5, 1.5)
                if want_notifications:
                    await self._check_notifications()
                    await b._idle(1.0, 3.0)
                if mood in ("normal", "deep"):
                    await self._engage_community()
                    await b._idle(0.8, 2.0)
                await b.goto(f"{self.base_url}/feed")

                session_actions = 0
                max_actions = session_len + random.randint(0, 5)
                session_posted = False
                session_projects = 0
                session_follows = 0
                session_gists = 0
                session_messages_start = self.state.messages_sent
                sessions_since_project += 1
                sessions_since_post += 1
                sessions_since_message += 1
                can_create_project = (
                    sessions_since_project >= 3 and random.random() < 0.3
                )
                can_create_post = sessions_since_post >= 2 and random.random() < 0.5
                can_create_message = (
                    sessions_since_message >= 2 and random.random() < 0.4
                )

                while session_actions < max_actions:
                    if action_limit and lifetime_actions >= action_limit:
                        self._log(f"Hit max actions ({action_limit}), stopping")
                        self._save()
                        self._cost_summary("final")
                        return
                    (
                        taken,
                        session_posted,
                        session_projects,
                        session_follows,
                        session_gists,
                    ) = await (self._cycle_ai if use_ai else self._cycle)(
                        mood=mood,
                        session_posted=session_posted,
                        session_projects=session_projects,
                        session_follows=session_follows,
                        session_gists=session_gists,
                        can_project=can_create_project,
                        can_post=can_create_post,
                        can_message=can_create_message,
                    )
                    if session_posted:
                        sessions_since_post = 0
                    if session_projects:
                        sessions_since_project = 0
                    if self.state.messages_sent > session_messages_start:
                        sessions_since_message = 0
                    session_actions += max(taken, 1)
                    lifetime_actions += max(taken, 1)
                    self._save()
                    crash_streak = 0

                    if use_ai:
                        if (
                            self._session_stop_after
                            and session_actions >= self._session_stop_after
                        ):
                            self._log(
                                f"Energy spent (stop_after={self._session_stop_after}): ending session"
                            )
                            break
                        intra_pause = self._scaled_pause()
                    else:
                        fatigue = await self._fatigue_check(session_actions)
                        if fatigue:
                            self._log(f"Fatigue ({fatigue}): ending session")
                            break
                        intra_pause = random.randint(self._pause_min, self._pause_max)

                    self._log(
                        f"Session pause {intra_pause}s [{session_actions}/{max_actions}] {self._cost_tag()}"
                    )
                    await asyncio.sleep(intra_pause)

                session_ok = session_actions > 0
                self._session_summary(mood, session_actions)

            except asyncio.CancelledError:
                self._save()
                try:
                    await b.close()
                except Exception as e:
                    logger.debug("close on cancel failed: %s", e)
                raise
            except Exception as e:
                crash_streak += 1
                self._log(f"Session crash ({crash_streak}): {e}")
                self._save()
            finally:
                try:
                    await b.close()
                except Exception as e:
                    logger.debug("close in finally failed: %s", e)

            if crash_streak > 3:
                self._log("Too many consecutive crashes, exiting")
                self._save()
                self._cost_summary("final")
                break

            if not session_ok and crash_streak > 0:
                break_s = random.randint(30, 120)
                self._log(f"Recovery wait {break_s}s")
            else:
                break_s = max(5, int(random.randint(*break_range) * self._break_scale))
                if use_ai:
                    factor = 1.5 - 1.0 * self._session_energy
                    break_s = max(60, int(break_s * factor))
                break_h = break_s / 3600
                self._log(f"Session done. Break {break_h:.1f}h")

            self._save()
            try:
                await asyncio.sleep(break_s)
            except asyncio.CancelledError:
                self._save()
                raise

        self._cost_summary("final")
        self._log("Bot process exiting")
