"""Laziness auto-detector for GreyMatter.

Scans conversation text, tool output, and code for laziness patterns --
corrections like "actually implement this", stub code, empty handlers,
and other signals that an agent is cutting corners.

Two modes:
1. **Code scanning**: Static analysis of source code for laziness anti-patterns
   (empty except, bare TODO, stub functions, print() in production, commented-out code).
2. **Conversation scanning**: Detects when a human corrects an agent for being lazy
   ("actually implement", "don't just stub it", "write real code"), which signals
   the agent's own quality filters failed.

Usage:
    detector = LazinessDetector()
    code_findings = detector.scan_code(source_text, filename="module.py")
    conversation_findings = detector.scan_conversation(chat_text)
    all_findings = detector.scan_all(code=source, conversation=chat)
"""
from __future__ import annotations

import re
import logging
from dataclasses import dataclass, field
from typing import Any

logger = logging.getLogger(__name__)


@dataclass
class LazinessFinding:
    """A single laziness detection result."""
    rule_id: str
    category: str  # "code" or "conversation"
    severity: str  # "critical", "important", "minor"
    line: int | None  # Line number (code scans only)
    text: str  # The offending text
    description: str  # Human-readable explanation
    suggestion: str  # How to fix it

    def to_dict(self) -> dict[str, Any]:
        return {
            "rule_id": self.rule_id,
            "category": self.category,
            "severity": self.severity,
            "line": self.line,
            "text": self.text[:200],  # Truncate for payload size
            "description": self.description,
            "suggestion": self.suggestion,
        }


@dataclass
class LazinessReport:
    """Aggregated laziness scan results."""
    findings: list[LazinessFinding] = field(default_factory=list)
    scanned_lines: int = 0
    laziness_score: float = 0.0  # 0.0 = clean, 1.0 = maximally lazy

    @property
    def critical_count(self) -> int:
        return sum(1 for f in self.findings if f.severity == "critical")

    @property
    def is_lazy(self) -> bool:
        return self.critical_count > 0 or len(self.findings) >= 3

    def to_dict(self) -> dict[str, Any]:
        return {
            "findings": [f.to_dict() for f in self.findings],
            "scanned_lines": self.scanned_lines,
            "laziness_score": round(self.laziness_score, 3),
            "critical_count": self.critical_count,
            "is_lazy": self.is_lazy,
            "total_findings": len(self.findings),
        }


# -- Code Laziness Patterns --

# Bare TODO/FIXME without ticket reference
_RE_BARE_TODO = re.compile(
    r"#\s*(TODO|FIXME|HACK|XXX)\b(?![\s:]*\([A-Z]+-\d+\))(?![\s:]*\(GH-\d+\))",
    re.IGNORECASE,
)

# Empty except blocks: except followed by pass/... with nothing meaningful
_RE_EMPTY_EXCEPT = re.compile(
    r"^\s*except\b.*:\s*$",
)
_RE_BARE_PASS = re.compile(r"^\s*(pass|\.\.\.)\s*(#.*)?$")

# Stub functions: def followed by only pass or NotImplementedError without message
_RE_FUNC_DEF = re.compile(r"^\s*(?:async\s+)?def\s+\w+\s*\(")
_RE_STUB_BODY = re.compile(
    r"^\s*(pass|raise\s+NotImplementedError\s*(\(\s*\))?)\s*(#.*)?$"
)

# print() in production code (not in test files or __main__)
_RE_PRINT_CALL = re.compile(r"^\s*print\s*\(")

# Commented-out code blocks (3+ consecutive comment lines that look like code)
_RE_COMMENTED_CODE = re.compile(
    r"^(\s*#\s*)(def |class |if |for |while |return |import |from |try:|except|with )"
)


# -- Conversation Laziness Correction Patterns --

# Phrases that indicate a human is correcting agent laziness
LAZINESS_CORRECTION_PHRASES = [
    r"actually\s+implement",
    r"don't\s+just\s+stub",
    r"write\s+real\s+code",
    r"don't\s+just\s+pass",
    r"actually\s+write",
    r"implement\s+it\s+properly",
    r"not\s+a\s+placeholder",
    r"stop\s+being\s+lazy",
    r"that'?s?\s+just\s+a\s+stub",
    r"finish\s+the\s+implementation",
    r"don't\s+leave\s+(it\s+)?empty",
    r"fill\s+in\s+the\s+(actual|real)",
    r"needs?\s+actual\s+implementation",
    r"not\s+just\s+a\s+skeleton",
    r"too\s+lazy",
    r"half[- ]baked",
    r"actually\s+do\s+(the\s+)?work",
    r"don't\s+skip",
    r"you\s+skipped",
    r"that'?s?\s+incomplete",
    r"finish\s+this",
    r"you\s+forgot\s+to\s+implement",
    r"where'?s?\s+the\s+(actual|real)\s+(code|implementation|logic)",
    r"this\s+does\s+nothing",
    r"empty\s+function",
    r"placeholder\s+only",
]

_LAZINESS_CORRECTION_RE = re.compile(
    "|".join(f"({p})" for p in LAZINESS_CORRECTION_PHRASES),
    re.IGNORECASE,
)


class LazinessDetector:
    """Detects laziness patterns in code and conversation text."""

    def scan_code(
        self,
        source: str,
        filename: str = "<unknown>",
    ) -> LazinessReport:
        """Scan source code for laziness anti-patterns.

        Args:
            source: The source code text to scan.
            filename: Filename for context (used to skip test files for print checks).

        Returns:
            LazinessReport with all findings.
        """
        lines = source.splitlines()
        report = LazinessReport(scanned_lines=len(lines))
        findings = report.findings

        is_test_file = "test_" in filename or "_test." in filename
        is_main = filename == "__main__.py" or "if __name__" in source

        # Track state for multi-line pattern detection
        in_except_block = False
        except_line = 0
        except_indent = 0
        func_def_line = 0
        func_def_indent = 0
        in_func_def = False
        commented_code_run = 0
        commented_code_start = 0

        for i, line in enumerate(lines, 1):
            stripped = line.strip()

            # --- Bare TODO/FIXME ---
            if _RE_BARE_TODO.search(line):
                findings.append(LazinessFinding(
                    rule_id="qs-no-bare-todo",
                    category="code",
                    severity="critical",
                    line=i,
                    text=stripped,
                    description=f"Bare TODO/FIXME without ticket reference at line {i}",
                    suggestion="Add a ticket reference: # TODO(GH-NNN): description",
                ))

            # --- Empty except blocks ---
            if _RE_EMPTY_EXCEPT.match(line):
                in_except_block = True
                except_line = i
                except_indent = len(line) - len(line.lstrip())
            elif in_except_block:
                current_indent = len(line) - len(line.lstrip()) if stripped else except_indent + 4
                if stripped and current_indent > except_indent:
                    if _RE_BARE_PASS.match(stripped):
                        findings.append(LazinessFinding(
                            rule_id="qs-no-empty-except",
                            category="code",
                            severity="critical",
                            line=except_line,
                            text=f"except ... pass (lines {except_line}-{i})",
                            description=f"Empty except block at line {except_line} -- exception silently swallowed",
                            suggestion="Log the exception or re-raise: logger.warning('Error: %s', e); raise",
                        ))
                    in_except_block = False
                elif stripped and current_indent <= except_indent:
                    in_except_block = False

            # --- Stub functions ---
            if _RE_FUNC_DEF.match(line):
                in_func_def = True
                func_def_line = i
                func_def_indent = len(line) - len(line.lstrip())
            elif in_func_def:
                current_indent = len(line) - len(line.lstrip()) if stripped else func_def_indent + 4
                if stripped and current_indent > func_def_indent:
                    # Skip docstrings
                    if stripped.startswith(('"""', "'''")):
                        continue
                    if _RE_STUB_BODY.match(stripped):
                        findings.append(LazinessFinding(
                            rule_id="qs-no-stub-functions",
                            category="code",
                            severity="critical",
                            line=func_def_line,
                            text=lines[func_def_line - 1].strip(),
                            description=f"Stub function at line {func_def_line} -- body is just pass/NotImplementedError",
                            suggestion="Implement the function or add a detailed NotImplementedError message explaining what is blocked",
                        ))
                    in_func_def = False
                elif stripped and current_indent <= func_def_indent:
                    in_func_def = False

            # --- print() in production code ---
            if not is_test_file and not is_main and _RE_PRINT_CALL.match(line):
                findings.append(LazinessFinding(
                    rule_id="qs-no-print-in-production",
                    category="code",
                    severity="important",
                    line=i,
                    text=stripped,
                    description=f"print() call in production code at line {i}",
                    suggestion="Use logging: logger.info('message %s', var)",
                ))

            # --- Commented-out code blocks ---
            if _RE_COMMENTED_CODE.match(line):
                if commented_code_run == 0:
                    commented_code_start = i
                commented_code_run += 1
            else:
                if commented_code_run >= 3:
                    findings.append(LazinessFinding(
                        rule_id="qs-no-commented-out-code",
                        category="code",
                        severity="important",
                        line=commented_code_start,
                        text=f"{commented_code_run} consecutive commented-out code lines",
                        description=f"Commented-out code block at lines {commented_code_start}-{commented_code_start + commented_code_run - 1}",
                        suggestion="Delete the commented code -- git history preserves it if needed later",
                    ))
                commented_code_run = 0

        # Handle trailing commented-out code block
        if commented_code_run >= 3:
            findings.append(LazinessFinding(
                rule_id="qs-no-commented-out-code",
                category="code",
                severity="important",
                line=commented_code_start,
                text=f"{commented_code_run} consecutive commented-out code lines",
                description=f"Commented-out code block at lines {commented_code_start}-{commented_code_start + commented_code_run - 1}",
                suggestion="Delete the commented code -- git history preserves it if needed later",
            ))

        # Calculate laziness score
        if lines:
            critical_weight = 0.15
            important_weight = 0.05
            score = sum(
                critical_weight if f.severity == "critical" else important_weight
                for f in findings
            )
            report.laziness_score = min(score, 1.0)

        return report

    def scan_conversation(self, text: str) -> LazinessReport:
        """Scan conversation/tool output for laziness correction patterns.

        Detects when a human is correcting an agent for being lazy -- these
        are signals that the agent's own quality filters failed.

        Args:
            text: Conversation or tool output text to scan.

        Returns:
            LazinessReport with conversation-level findings.
        """
        lines = text.splitlines()
        report = LazinessReport(scanned_lines=len(lines))

        for i, line in enumerate(lines, 1):
            match = _LAZINESS_CORRECTION_RE.search(line)
            if match:
                report.findings.append(LazinessFinding(
                    rule_id="laziness-correction-detected",
                    category="conversation",
                    severity="critical",
                    line=i,
                    text=line.strip()[:200],
                    description=f"Laziness correction detected: '{match.group()}'",
                    suggestion="Agent should have caught this -- review quality standards enforcement",
                ))

        if report.findings:
            report.laziness_score = min(len(report.findings) * 0.2, 1.0)

        return report

    def scan_all(
        self,
        code: str | None = None,
        conversation: str | None = None,
        filename: str = "<unknown>",
    ) -> LazinessReport:
        """Scan both code and conversation text.

        Args:
            code: Source code to scan for laziness patterns.
            conversation: Conversation text to scan for correction patterns.
            filename: Filename for code context.

        Returns:
            Combined LazinessReport.
        """
        combined = LazinessReport()

        if code:
            code_report = self.scan_code(code, filename=filename)
            combined.findings.extend(code_report.findings)
            combined.scanned_lines += code_report.scanned_lines

        if conversation:
            conv_report = self.scan_conversation(conversation)
            combined.findings.extend(conv_report.findings)
            combined.scanned_lines += conv_report.scanned_lines

        # Recalculate combined score
        if combined.findings:
            critical_weight = 0.15
            important_weight = 0.05
            score = sum(
                critical_weight if f.severity == "critical" else important_weight
                for f in combined.findings
            )
            combined.laziness_score = min(score, 1.0)

        return combined
