"""
AI Traceability Detection Module — Ybe Check

Detects AI-generated code markers and leftover LLM artifacts in production code.

Static analysis only. No external API calls. No AST parsing.
Heuristic-based detection across three categories:
  1. AI Generation Markers (LOW)   — "Generated by", "@generated", tool names
  2. Prompt Artifacts (MEDIUM)     — LLM conversational leftovers in code
  3. Markdown Artifacts (HIGH)     — Triple backticks (raw LLM output committed)
"""

import os
import re

NAME = "ai_traceability"

# ── SKIP RULES ───────────────────────────────────────────────

SKIP_DIRS = {
    '.git', 'node_modules', '__pycache__', '.venv',
    'venv', 'dist', 'build', '.next', 'out'
}

SCAN_EXTENSIONS = {
    '.py', '.js', '.ts', '.jsx', '.tsx',
    '.java', '.go', '.rs', '.rb', '.php'
}

# ── 1. AI GENERATION MARKERS (LOW, -2 each) ─────────────────
# Only scanned in first 40 lines of each file.

AI_MARKER_PATTERNS = [
    re.compile(r'generated\s+by', re.IGNORECASE),
    re.compile(r'auto[- ]generated', re.IGNORECASE),
    re.compile(r'created\s+by\s+ai', re.IGNORECASE),
    re.compile(r'created\s+with\s+ai', re.IGNORECASE),
    re.compile(r'\bChatGPT\b', re.IGNORECASE),
    re.compile(r'\bCopilot\b', re.IGNORECASE),
    re.compile(r'\bClaude\b', re.IGNORECASE),
    re.compile(r'\bGemini\b', re.IGNORECASE),
    re.compile(r'\bOpenAI\b', re.IGNORECASE),
    re.compile(r'\bCodeWhisperer\b', re.IGNORECASE),
    re.compile(r'@generated\b', re.IGNORECASE),
    re.compile(r'vibe[- ]coded', re.IGNORECASE),
]

# ── 2. PROMPT ARTIFACTS (MEDIUM, -5 each) ────────────────────
# LLM conversational leftovers that shouldn't be in production code.

PROMPT_ARTIFACT_PATTERNS = [
    re.compile(r'Certainly!', re.IGNORECASE),
    re.compile(r'Here is the code', re.IGNORECASE),
    re.compile(r'As an AI language model', re.IGNORECASE),
    re.compile(r'Ignore previous instructions', re.IGNORECASE),
    re.compile(r"Sure!\s*Here'?s", re.IGNORECASE),
]

# ── 3. MARKDOWN ARTIFACTS (HIGH, -10 each) ───────────────────
# Triple backticks inside code files = raw LLM output committed.

MARKDOWN_BACKTICK_PATTERN = re.compile(r'^```', re.MULTILINE)


# ── HELPERS ──────────────────────────────────────────────────

def read_lines_safe(fpath):
    """Read file lines safely. Returns empty list on failure."""
    try:
        with open(fpath, 'r', encoding='utf-8', errors='ignore') as f:
            return f.readlines()
    except Exception:
        return []


def rel(fpath, repo_path):
    """Return path relative to repo root."""
    return os.path.relpath(fpath, repo_path)


def walk_files(repo_path):
    """Walk repo yielding code files, skipping ignored dirs."""
    for root, dirs, files in os.walk(repo_path):
        dirs[:] = [d for d in dirs if d not in SKIP_DIRS]
        for fname in files:
            ext = os.path.splitext(fname)[1].lower()
            if ext in SCAN_EXTENSIONS:
                yield os.path.join(root, fname)


# ── MAIN ENTRY POINT ─────────────────────────────────────────

def scan(repo_path: str) -> dict:
    try:
        details = []
        total_penalty = 0
        has_only_markers = True  # Track if we only have LOW-severity markers

        for fpath in walk_files(repo_path):
            lines = read_lines_safe(fpath)
            if not lines:
                continue

            relative = rel(fpath, repo_path)
            full_text = ''.join(lines)

            # ── 1. AI Generation Markers (first 40 lines only) ──
            scan_limit = min(len(lines), 40)
            for line_num in range(scan_limit):
                line = lines[line_num]
                for pattern in AI_MARKER_PATTERNS:
                    if pattern.search(line):
                        details.append({
                            "type": "AI Generation Marker",
                            "severity": "low",
                            "file": relative,
                            "line": line_num + 1,
                            "snippet": line.strip()[:100],
                            "reason": "AI-generated code marker detected — ensure code has been reviewed."
                        })
                        total_penalty += 2
                        break  # One match per line is enough

            # ── 2. Prompt Artifacts (all lines) ──────────────────
            for line_num, line in enumerate(lines):
                for pattern in PROMPT_ARTIFACT_PATTERNS:
                    if pattern.search(line):
                        has_only_markers = False
                        details.append({
                            "type": "Prompt Artifact",
                            "severity": "medium",
                            "file": relative,
                            "line": line_num + 1,
                            "snippet": line.strip()[:100],
                            "reason": "LLM conversational artifact found (e.g. 'Certainly!', 'Here is the code') — indicates unreviewed output."
                        })
                        total_penalty += 5
                        break  # One match per line

            # ── 3. Markdown Artifacts (triple backticks) ─────────
            for match in MARKDOWN_BACKTICK_PATTERN.finditer(full_text):
                has_only_markers = False
                line_num = full_text[:match.start()].count('\n') + 1
                details.append({
                    "type": "Markdown Artifact",
                    "severity": "high",
                    "file": relative,
                    "line": line_num,
                    "snippet": "```",
                    "reason": "Triple backticks found in code file — indicates raw LLM output was accidentally committed."
                })
                total_penalty += 10

        # ── SCORING ───────────────────────────────────────────
        if not details:
            score = 100
        else:
            # Cap total deduction at 40
            capped_penalty = min(total_penalty, 40)
            score = 100 - capped_penalty

            # If only AI markers (LOW), floor at 80
            if has_only_markers:
                score = max(score, 80)

            # Absolute floor
            score = max(score, 0)

        return {
            "name": "AI Traceability",
            "score": score,
            "issues": len(details),
            "details": details
        }

    except Exception as e:
        return {
            "name": "AI Traceability",
            "score": None,
            "issues": 0,
            "details": [],
            "warning": f"Could not run AI traceability scan: {e}"
        }
