"""
QUAL012: Background Task Tracing Checker

Ensures that background/async task code includes proper traceability in logging:
1. Functions that spawn threads or run background tasks should generate unique IDs
2. Logging calls in async/threaded code should include trace IDs
3. This enables debugging stuck tasks via log correlation (grep by trace ID)

This is critical for debugging stuck syncs, background jobs, and understanding
failures in concurrent/multi-threaded code.
"""

from __future__ import annotations

import re
from pathlib import Path
from typing import List

from stablestack.checkers.base import BaseChecker, Finding


class BackgroundTaskTracingChecker(BaseChecker):
    """Check that background/async task code has proper trace logging."""

    rule_id = "QUAL012"
    rule_name = "background-task-tracing"
    description = (
        "Ensures background task code includes unique trace IDs for debugging."
    )
    why = (
        "Every background thread/task should have a trace_id that appears in all its log messages. "
        "This enables grep-based log correlation when debugging stuck or failing jobs."
    )

    # File extensions to check
    file_extensions = {".py", ".ts", ".tsx", ".js", ".jsx"}

    # Patterns that indicate background/async task code
    BACKGROUND_PATTERNS_PY = [
        r"Thread\s*\(",  # threading.Thread
        r"ThreadPoolExecutor",
        r"ProcessPoolExecutor",
        r"asyncio\.create_task",
        r"asyncio\.ensure_future",
        r"\.submit\s*\(",  # executor.submit
        r"@celery\.",  # Celery tasks
        r"@task\b",  # Generic task decorators
        r"@background",
        r"background_task",
        r"run_in_executor",
        r"run_sync_in_background",
    ]

    BACKGROUND_PATTERNS_JS = [
        r"new\s+Worker\s*\(",  # Web Workers
        r"Promise\.all\s*\(",  # Parallel promises
        r"setTimeout\s*\(",  # Deferred execution
        r"setInterval\s*\(",
        r"process\.nextTick",
        r"setImmediate",
        r"\.fork\s*\(",  # child_process.fork
        r"cluster\.fork",
        r"bull\.process",  # Bull queue
        r"agenda\.define",  # Agenda jobs
        r"@Queue\s*\(",  # NestJS queues
        r"@Process\s*\(",
    ]

    # Patterns that indicate proper trace logging
    TRACE_ID_PATTERNS = [
        r"trace_id\s*[=:]",
        r"traceId\s*[=:]",
        r"sync_id\s*[=:]",
        r"syncId\s*[=:]",
        r"job_id\s*[=:]",
        r"jobId\s*[=:]",
        r"task_id\s*[=:]",
        r"taskId\s*[=:]",
        r"request_id\s*[=:]",
        r"requestId\s*[=:]",
        r"correlation_id\s*[=:]",
        r"correlationId\s*[=:]",
        r"log_prefix\s*[=:]",
        r"logPrefix\s*[=:]",
        r"self\.trace_id",
        r"self\.log_prefix",
        r"this\.traceId",
        r"this\.logPrefix",
        r"\[(?:Sync|Task|Job|Trace|Worker)\s+\w+\]",  # [Task abc123] in log
        # Pino child logger pattern (creates traced logger via logger.child({ traceId }))
        r"logger\.child\s*\(",
        r"\.child\s*\(\s*\{",
        # Injectable logger parameter (function accepts traced logger via DI)
        r"log\s*[=:]\s*logger\b",
        r"log\s*:\s*(?:import\s*\(|pino\.)?Logger",
        # tRPC context traced logger
        r"ctx\.log\.\w+\s*\(",
        # Traced logger factory functions
        r"createTracedLogger",
        r"createTraceId",
    ]

    # Logging patterns
    LOGGING_PATTERNS_PY = [
        r"logger\.\w+\s*\(",
        r"logging\.\w+\s*\(",
        r"log\.\w+\s*\(",
    ]

    LOGGING_PATTERNS_JS = [
        r"console\.\w+\s*\(",
        r"logger\.\w+\s*\(",
        r"log\.\w+\s*\(",
        r"winston\.\w+\s*\(",
        r"pino\.\w+\s*\(",
    ]

    def should_skip_file(self, path: Path) -> bool:
        """Skip test and spec files — they don't need trace IDs."""
        stem = path.stem.lower()
        if any(kw in stem for kw in ("test", "spec", "mock", "fixture")):
            return True
        parts = [p.lower() for p in path.parts]
        if any(d in parts for d in ("e2e", "tests", "__tests__", "test")):
            return True
        return super().should_skip_file(path)

    def check_file(self, path: Path, content: str) -> List[Finding]:
        """Check a single file for background task tracing issues."""
        findings = []

        if path.suffix == ".py":
            findings.extend(self._check_python(path, content))
        elif path.suffix in {".ts", ".tsx", ".js", ".jsx"}:
            findings.extend(self._check_javascript(path, content))

        return findings

    def _check_python(self, path: Path, content: str) -> List[Finding]:
        """Check Python file for background task tracing issues."""
        return self._check_content(
            path,
            content,
            self.BACKGROUND_PATTERNS_PY,
            self.LOGGING_PATTERNS_PY,
        )

    def _check_javascript(self, path: Path, content: str) -> List[Finding]:
        """Check JavaScript/TypeScript file for background task tracing issues."""
        return self._check_content(
            path,
            content,
            self.BACKGROUND_PATTERNS_JS,
            self.LOGGING_PATTERNS_JS,
        )

    def _check_content(
        self,
        path: Path,
        content: str,
        background_patterns: list[str],
        logging_patterns: list[str],
    ) -> List[Finding]:
        """Check content for background task tracing issues."""
        findings = []
        lines = content.splitlines()

        # Check if file has any background task patterns
        has_background_tasks = any(
            re.search(pattern, content) for pattern in background_patterns
        )

        if not has_background_tasks:
            return findings

        # Check if file has trace ID setup
        has_trace_id = any(
            re.search(pattern, content) for pattern in self.TRACE_ID_PATTERNS
        )

        if has_trace_id:
            return findings

        # File has background tasks but no trace IDs - find logging calls
        for i, line in enumerate(lines, 1):
            for pattern in logging_patterns:
                if re.search(pattern, line):
                    # Check if this specific log has inline trace info
                    line_has_trace = any(
                        re.search(p, line) for p in self.TRACE_ID_PATTERNS
                    )
                    if not line_has_trace:
                        findings.append(
                            self.create_finding(
                                path=path,
                                line=i,
                                column=1,
                                code=line.strip()[:80],
                                message=(
                                    "Log call in background task code lacks trace ID. "
                                    "Add trace_id/job_id to enable log correlation for debugging."
                                ),
                                fix_hint=(
                                    "Generate a unique trace_id at task start and include it in all logs:\n"
                                    "  trace_id = str(uuid.uuid4())[:8]\n"
                                    "  logger.info(f'[Task {trace_id}] Starting task...')"
                                ),
                            )
                        )
                        # Only report first logging issue per file to avoid noise
                        return findings

        return findings
