"""GO-ERR-001: Unhandled Errors detection for Go.

Detects error values that are assigned but never checked,
which can hide security-critical failures.
"""

from __future__ import annotations

import re

from . import GoRule
from ..models import Finding, Severity

# err assigned but ignored with blank identifier
BLANK_ERR_PATTERN = re.compile(
    r'(?:_\s*,\s*err|err\s*,\s*_)\s*(?::=|=)'
)
# Explicit blank identifier for error
DISCARD_ERR_PATTERN = re.compile(
    r'_\s*=\s*\w+\.\w+\s*\('
)
# Function call result ignored (no assignment at all)
# Focus on security-critical functions
IGNORED_ERR_FUNCS = re.compile(
    r'^\s+(?:os\.(?:Remove|Chmod|Chown|Mkdir|MkdirAll|Rename)|'
    r'io\.(?:Copy|WriteString)|'
    r'fmt\.Fprint|'
    r'http\.(?:ListenAndServe|ListenAndServeTLS)|'
    r'json\.(?:NewEncoder|Marshal)|'
    r'conn\.Close|'
    r'resp\.Body\.Close|'
    r'file\.Close|'
    r'db\.Close|'
    r'rows\.Close)\s*\('
)
# err assigned then not checked (heuristic: err := ... without subsequent if err)
ERR_ASSIGN_PATTERN = re.compile(
    r'\berr\s*:?=\s*\S'
)
ERR_CHECK_PATTERN = re.compile(
    r'\bif\s+err\s*!=\s*nil\b'
)


class GoUnhandledErrorsRule(GoRule):
    """Detects unhandled error values in Go."""

    @property
    def rule_id(self) -> str:
        return "GO-ERR-001"

    @property
    def name(self) -> str:
        return "Unhandled Errors"

    @property
    def severity(self) -> Severity:
        return Severity.MEDIUM

    @property
    def cwe(self) -> str:
        return "CWE-391"

    @property
    def description(self) -> str:
        return (
            "Error value assigned but not checked. Unhandled errors can "
            "hide security-critical failures."
        )

    @property
    def detailed_description(self) -> str:
        return (
            "Error value assigned but not checked.\n\n"
            "Go's error handling relies on explicit checking of returned\n"
            "error values. Ignoring errors with _ or not checking err != nil\n"
            "can hide critical failures like failed auth, file permission\n"
            "errors, or database connection issues.\n\n"
            "Examples of vulnerable code:\n"
            "  _, _ = file.Write(sensitiveData)\n"
            "  _ = os.Chmod(path, 0777)\n"
            "  err := db.Close()  // err never checked\n\n"
            "How to fix:\n"
            "  - Always check returned errors: if err != nil { ... }\n"
            "  - Use errcheck or golangci-lint to catch missed errors\n"
            "  - Log errors at minimum, return or handle them properly"
        )

    def check(self, source: str, filename: str = "") -> list[Finding]:
        """Check for unhandled error patterns."""
        findings: list[Finding] = []
        lines = self._get_lines(source)

        # Explicit blank identifier for error
        for lineno, line, _match in self._search_lines(source, BLANK_ERR_PATTERN):
            findings.append(Finding(
                rule_id=self.rule_id,
                severity=self.severity,
                title="Discarded Error",
                description=(
                    "Error value explicitly discarded with blank identifier."
                ),
                file="",
                line=lineno,
                code_snippet=line.strip()[:80],
                recommendation=(
                    "Check the error value: if err != nil { return err }"
                ),
                cwe=self.cwe,
            ))

        # Security-critical function calls without error check
        for lineno, line, _match in self._search_lines(source, IGNORED_ERR_FUNCS):
            # Check if the return value is captured
            stripped = line.strip()
            if ":=" not in stripped and "=" not in stripped.split("(")[0]:
                findings.append(Finding(
                    rule_id=self.rule_id,
                    severity=Severity.HIGH,
                    title="Ignored Error (security function)",
                    description=(
                        "Return value of security-critical function ignored."
                    ),
                    file="",
                    line=lineno,
                    code_snippet=stripped[:80],
                    recommendation=(
                        "Capture and check the error return value."
                    ),
                    cwe=self.cwe,
                ))

        # err assigned but not checked within 3 lines
        for i, line in enumerate(lines, 1):
            stripped = line.strip()
            if stripped.startswith("//") or stripped.startswith("/*"):
                continue
            if ERR_ASSIGN_PATTERN.search(line) and "if " not in line:
                # Look ahead 3 lines for err check
                window = "\n".join(lines[i:i + 3])
                if not ERR_CHECK_PATTERN.search(window):
                    # Skip if it's already caught
                    if any(f.line == i for f in findings):
                        continue
                    # Skip if the line itself is an if statement
                    if stripped.startswith("if "):
                        continue
                    findings.append(Finding(
                        rule_id=self.rule_id,
                        severity=Severity.LOW,
                        title="Unchecked Error",
                        description=(
                            "Error assigned but not checked within 3 lines."
                        ),
                        file="",
                        line=i,
                        code_snippet=stripped[:80],
                        recommendation=(
                            "Add: if err != nil { return err } after this line."
                        ),
                        cwe=self.cwe,
                    ))

        return findings
