"""Issue-filing mixin — classifies recent thoughts and creates Gitea issues."""

import logging
import re
from pathlib import Path

from config import settings

logger = logging.getLogger(__name__)


class _IssueFilingMixin:
    """Mixin providing automatic issue-filing from thought analysis.

    Expects the host class to provide:
    - self.count_thoughts() -> int
    - self.get_recent_thoughts(limit) -> list[Thought]
    - self._call_agent(prompt) -> str  (async)
    """

    @staticmethod
    def _references_real_files(text: str) -> bool:
        """Check that all source-file paths mentioned in *text* actually exist.

        Extracts paths that look like Python/config source references
        (e.g. ``src/timmy/session.py``, ``config/foo.yaml``) and verifies
        each one on disk relative to the project root.  Returns ``True``
        only when **every** referenced path resolves to a real file — or
        when no paths are referenced at all (pure prose is fine).
        """
        # Match paths like  src/thing.py  swarm/init.py  config/x.yaml
        # Requires at least one slash and a file extension.
        path_pattern = re.compile(
            r"(?<![/\w])"  # not preceded by path chars (avoid partial matches)
            r"((?:src|tests|config|scripts|data|swarm|timmy)"
            r"(?:/[\w./-]+\.(?:py|yaml|yml|json|toml|md|txt|cfg|ini)))"
        )
        paths = path_pattern.findall(text)
        if not paths:
            return True  # No file refs → nothing to validate

        # Project root: three levels up from this file (src/timmy/thinking/_issue_filing.py)
        project_root = Path(__file__).resolve().parent.parent.parent.parent
        for p in paths:
            if not (project_root / p).is_file():
                logger.info("Phantom file reference blocked: %s (not in %s)", p, project_root)
                return False
        return True

    async def _maybe_file_issues(self) -> None:
        """Every N thoughts, classify recent thoughts and file Gitea issues.

        Asks the LLM to review recent thoughts for actionable items —
        bugs, broken features, stale state, or improvement opportunities.
        Creates Gitea issues via MCP for anything worth tracking.

        Only runs when:
        - Gitea is enabled and configured
        - Thought count is divisible by thinking_issue_every
        - LLM extracts at least one actionable item

        Safety: every generated issue is validated to ensure referenced
        file paths actually exist on disk, preventing phantom-bug reports.
        """
        try:
            recent = self._get_recent_thoughts_for_issues()
            if recent is None:
                return

            classify_prompt = self._build_issue_classify_prompt(recent)
            raw = await self._call_agent(classify_prompt)
            items = self._parse_issue_items(raw)
            if items is None:
                return

            from timmy.mcp_tools import create_gitea_issue_via_mcp

            for item in items[:2]:  # Safety cap
                await self._file_single_issue(item, create_gitea_issue_via_mcp)

        except Exception as exc:
            logger.debug("Thought issue filing skipped: %s", exc)

    def _get_recent_thoughts_for_issues(self):
        """Return recent thoughts if conditions for filing issues are met, else None."""
        interval = settings.thinking_issue_every
        if interval <= 0:
            return None

        count = self.count_thoughts()
        if count == 0 or count % interval != 0:
            return None

        if not settings.gitea_enabled or not settings.gitea_token:
            return None

        recent = self.get_recent_thoughts(limit=interval)
        if len(recent) < interval:
            return None

        return recent

    @staticmethod
    def _build_issue_classify_prompt(recent) -> str:
        """Build the LLM prompt that extracts actionable issues from recent thoughts."""
        thought_text = "\n".join(f"- [{t.seed_type}] {t.content}" for t in reversed(recent))
        return (
            "You are reviewing your own recent thoughts for actionable items.\n"
            "Extract 0-2 items that are CONCRETE bugs, broken features, stale "
            "state, or clear improvement opportunities in your own codebase.\n\n"
            "Rules:\n"
            "- Only include things that could become a real code fix or feature\n"
            "- Skip vague reflections, philosophical musings, or repeated themes\n"
            "- Category must be one of: bug, feature, suggestion, maintenance\n"
            "- ONLY reference files that you are CERTAIN exist in the project\n"
            "- Do NOT invent or guess file paths — if unsure, describe the "
            "area of concern without naming specific files\n\n"
            "For each item, write an ENGINEER-QUALITY issue:\n"
            '- "title": A clear, specific title (e.g. "[Memory] MEMORY.md timestamp not updating")\n'
            '- "body": A detailed body with these sections:\n'
            "  **What's happening:** Describe the current (broken) behavior.\n"
            "  **Expected behavior:** What should happen instead.\n"
            "  **Suggested fix:** Which file(s) to change and what the fix looks like.\n"
            "  **Acceptance criteria:** How to verify the fix works.\n"
            '- "category": One of bug, feature, suggestion, maintenance\n\n'
            "Return ONLY a JSON array of objects with keys: "
            '"title", "body", "category"\n'
            "Return [] if nothing is actionable.\n\n"
            f"Recent thoughts:\n{thought_text}\n\nJSON array:"
        )

    @staticmethod
    def _parse_issue_items(raw: str):
        """Strip markdown fences and parse JSON issue list; return None on failure."""
        import json

        if not raw or not raw.strip():
            return None

        cleaned = raw.strip()
        if cleaned.startswith("```"):
            cleaned = cleaned.split("\n", 1)[-1].rsplit("```", 1)[0].strip()

        items = json.loads(cleaned)
        if not isinstance(items, list) or not items:
            return None

        return items

    async def _file_single_issue(self, item: dict, create_fn) -> None:
        """Validate one issue dict and create it via *create_fn* if it passes checks."""
        if not isinstance(item, dict):
            return
        title = item.get("title", "").strip()
        body = item.get("body", "").strip()
        category = item.get("category", "suggestion").strip()
        if not title or len(title) < 10:
            return

        combined = f"{title}\n{body}"
        if not self._references_real_files(combined):
            logger.info(
                "Skipped phantom issue: %s (references non-existent files)",
                title[:60],
            )
            return

        label = category if category in ("bug", "feature") else ""
        result = await create_fn(title=title, body=body, labels=label)
        logger.info("Thought→Issue: %s → %s", title[:60], result[:80])
