"""Fixer Agent — generates secure patches for verified vulnerabilities.

Uses the Grok LLM when available, otherwise falls back to built-in
regex-based fixes for common secret patterns.
"""

from __future__ import annotations

import logging
import re
from pathlib import Path

from secagent.context.context_builder import build_context, extract_snippet
from secagent.llm.grok_client import GrokClient
from secagent.models import FixResult, Vulnerability
from secagent.prompts import load as _load_prompt
from secagent.utils.file_loader import read_file_lines

logger = logging.getLogger(__name__)

_SYSTEM_PROMPT = _load_prompt("fixer.txt")

# ── Built-in secret fix patterns ────────────────────────────────────
# Each tuple: (regex matching the assignment, replacement template, reason)
_SECRET_PATTERNS: list[tuple[re.Pattern[str], str, str]] = [
    (
        re.compile(
            r"""^(\s*)([\w]+)\s*=\s*['"]([^'"]{6,})['"](.*)$"""
        ),
        r'\1\2 = os.getenv("\2", "")\4',
        "Moved hardcoded secret to environment variable",
    ),
]


def generate_fix(
    vuln: Vulnerability,
    client: GrokClient | None = None,
) -> FixResult | None:
    """Generate a patch for *vuln*.

    Tries the LLM first; falls back to built-in regex fixes for secrets.
    Returns ``None`` when no fix could be generated.
    """
    if vuln.line <= 0:
        logger.info(
            "Skipping fix for %s — no line number available (dependency vuln).",
            vuln.id,
        )
        return None

    context = build_context(vuln.file, vuln.line)
    snippet = extract_snippet(vuln.file, vuln.line)

    # ── Try LLM fix first ────────────────────────────────────────────
    if client and client.api_key:
        user_prompt = (
            f"Vulnerability: {vuln.description}\n"
            f"Type: {vuln.vuln_type}\n"
            f"File: {vuln.file}\n"
            f"Line: {vuln.line}\n\n"
            f"Code snippet:\n```\n{snippet}\n```\n\n"
            f"Full context:\n```\n{context}\n```"
        )

        result = client.chat(_SYSTEM_PROMPT, user_prompt, max_tokens=4096)
        if isinstance(result, dict) and "patched_snippet" in result:
            # LLM often returns a bare filename; always use the absolute path
            # from the vulnerability so apply_patch can locate the file.
            raw_file = result.get("file", vuln.file)
            fix_file = vuln.file if Path(raw_file).name == Path(vuln.file).name else raw_file
            fix = FixResult(
                file=fix_file,
                original_snippet=result.get("original_snippet", ""),
                patched_snippet=result.get("patched_snippet", ""),
                reason=result.get("reason", ""),
                vulnerability=vuln,
            )
            logger.info("Fixer (LLM) generated patch for %s: %s", vuln.id, fix.reason)
            return fix

    # ── Fallback: built-in regex fix for secrets ─────────────────────
    if vuln.vuln_type == "secret":
        fix = _builtin_secret_fix(vuln)
        if fix:
            return fix

    logger.info("Fixer: no fix generated for %s.", vuln.id)
    return None


def _builtin_secret_fix(vuln: Vulnerability) -> FixResult | None:
    """Attempt a simple regex-based fix for hardcoded secrets.

    Replaces ``VAR = "secret_value"`` with ``VAR = os.getenv("VAR", "")``.
    """
    lines = read_file_lines(vuln.file)
    if not lines or vuln.line <= 0 or vuln.line > len(lines):
        return None

    target_line = lines[vuln.line - 1]

    for pattern, replacement, reason in _SECRET_PATTERNS:
        match = pattern.match(target_line)
        if match:
            patched_line = pattern.sub(replacement, target_line)
            # Make the env var name uppercase
            var_name = match.group(2)
            patched_line = patched_line.replace(
                f'os.getenv("{var_name}"',
                f'os.getenv("{var_name.upper()}"',
            )

            return FixResult(
                file=vuln.file,
                original_snippet=target_line.rstrip("\n\r"),
                patched_snippet=patched_line.rstrip("\n\r"),
                reason=reason,
                vulnerability=vuln,
            )

    return None
