"""AI-powered scanner — sends source files directly to Grok for vulnerability analysis."""

from __future__ import annotations

import logging
from pathlib import Path
from typing import Any

from secagent.llm.grok_client import GrokClient
from secagent.prompts import load as _load_prompt
from secagent.scanners.base_scanner import BaseScanner

logger = logging.getLogger(__name__)

_SYSTEM_PROMPT = _load_prompt("ai_scanner.txt")

_SKIP_DIRS = {".git", "__pycache__", "node_modules", ".venv", "venv", ".secagent"}
_SKIP_EXTENSIONS = {".pyc", ".pyo", ".so", ".dll", ".exe", ".bin", ".png", ".jpg", ".gif", ".ico", ".pdf"}

# Grok context window is 2,000,000 tokens (~4 chars/token = ~8M chars).
# We reserve ~250K tokens for system prompt + output, leaving 1.75M tokens (~7M chars) for input.
# Files exceeding this are split into overlapping chunks so no code is missed.
_MAX_CONTENT_CHARS = 7_000_000
_CHUNK_SIZE = 6_800_000  # slightly under limit to account for file path header overhead
_CHUNK_OVERLAP = 200     # lines of overlap between chunks to avoid missing vulns at boundaries


class AIScanner(BaseScanner):
    """LLM-powered scanner — no external tools required, just a Grok API key."""

    def __init__(self, client: GrokClient | None = None) -> None:
        self._client = client

    @property
    def name(self) -> str:
        return "ai-scanner"

    def is_available(self) -> bool:
        """Available as long as a Grok API key is configured."""
        import os
        return bool(os.getenv("GROK_API_KEY", ""))

    def run(self, target: Path) -> list[dict[str, Any]]:
        client = self._client or GrokClient()
        findings: list[dict[str, Any]] = []

        for file_path in self._collect_files(target):
            try:
                content = file_path.read_text(encoding="utf-8", errors="replace")
            except OSError:
                continue

            if not content.strip():
                continue

            logger.info("AI scanner analysing %s", file_path)

            chunks = self._split_into_chunks(content)
            if len(chunks) > 1:
                logger.info("File too large — splitting into %d chunks", len(chunks))

            for i, chunk in enumerate(chunks):
                header = f"File: {file_path}"
                if len(chunks) > 1:
                    header += f" (chunk {i + 1}/{len(chunks)})"
                user_prompt = f"{header}\n\n```\n{chunk}\n```"

                response = client.chat(
                    system_prompt=_SYSTEM_PROMPT,
                    user_prompt=user_prompt,
                    json_mode=True,
                    max_tokens=4096,
                )

                raw_vulns = response.get("vulnerabilities", []) if isinstance(response, dict) else []
                for v in raw_vulns:
                    v["_file"] = str(file_path)
                    findings.append(v)

            logger.info("AI scanner: %d finding(s) in %s", len(findings), file_path)

        return findings

    @staticmethod
    def _split_into_chunks(content: str) -> list[str]:
        """Split large file content into overlapping line-boundary chunks.

        Files under ``_MAX_CONTENT_CHARS`` are returned as-is (single chunk).
        Larger files are split at line boundaries with a small overlap so
        vulnerabilities near chunk edges are not missed.
        """
        if len(content) <= _MAX_CONTENT_CHARS:
            return [content]

        lines = content.splitlines(keepends=True)
        chunks: list[str] = []
        current_lines: list[str] = []
        current_len = 0

        for line in lines:
            if current_len + len(line) > _CHUNK_SIZE and current_lines:
                chunks.append("".join(current_lines))
                # Keep last N lines as overlap for the next chunk
                current_lines = current_lines[-_CHUNK_OVERLAP:]
                current_len = sum(len(l) for l in current_lines)

            current_lines.append(line)
            current_len += len(line)

        if current_lines:
            chunks.append("".join(current_lines))

        return chunks

    @staticmethod
    def _collect_files(target: Path) -> list[Path]:
        if target.is_file():
            if target.suffix not in _SKIP_EXTENSIONS:
                return [target]
            return []

        results: list[Path] = []
        for p in sorted(target.rglob("*")):
            if any(skip in p.parts for skip in _SKIP_DIRS):
                continue
            if p.is_file() and p.suffix not in _SKIP_EXTENSIONS:
                results.append(p)
        return results
