"""Code Analysis Agent — static analysis via AST parsing."""
from __future__ import annotations

import ast
import logging
import re
from pathlib import Path
from typing import Any, Dict, List

from mesh.codex.base_agent import CodexAgent
from mesh.codex.models import CodexTask, CodexTaskResult, FindingSeverity

logger = logging.getLogger(__name__)

# Patterns that suggest hardcoded secrets
_SECRET_PATTERNS = [
    re.compile(r"""(?:password|secret|api_key|token|auth)\s*=\s*['"][^'"]{8,}['"]""", re.I),
    re.compile(r"""(?:AWS_SECRET|PRIVATE_KEY)\s*=\s*['"][^'"]+['"]""", re.I),
]

# SQL injection patterns (string formatting into SQL)
_SQL_INJECTION_PATTERNS = [
    re.compile(r"""execute\s*\(\s*f?['"].*%s"""),
    re.compile(r"""execute\s*\(\s*f['"].*\{"""),
]

# Command injection patterns
_CMD_INJECTION_PATTERNS = [
    re.compile(r"""os\.system\s*\("""),
    re.compile(r"""subprocess\.\w+\s*\(.*shell\s*=\s*True"""),
]


class CodeAnalysisAgent(CodexAgent):
    """Scans Python files for code quality issues using AST parsing.

    Checks for:
    - Unused imports
    - Bare except clauses
    - Mutable default arguments
    - Hardcoded secrets
    - SQL injection patterns
    - Command injection patterns
    - Missing error handling in I/O operations
    - Type annotation gaps
    """

    def __init__(self, mesh_db, config=None):
        super().__init__("analyzer", mesh_db, config)

    async def execute(self, task: CodexTask) -> CodexTaskResult:
        target = task.target
        files = self._collect_python_files(target)
        if not files:
            return CodexTaskResult(
                task_id=task.task_id,
                success=True,
                findings=[],
                recommendations=["No Python files found at target path"],
            )

        all_findings: List[Dict[str, Any]] = []
        for filepath in files:
            content = self._get_context(str(filepath))
            if content is None:
                continue
            findings = self._analyze_file(filepath, content)
            all_findings.extend(findings)

        # Sort by severity
        severity_order = {s.value: i for i, s in enumerate(FindingSeverity)}
        all_findings.sort(key=lambda f: severity_order.get(f["severity"], 99))

        # Generate recommendations
        recommendations = self._summarize(all_findings, len(files))

        return CodexTaskResult(
            task_id=task.task_id,
            success=True,
            findings=all_findings,
            recommendations=recommendations,
        )

    def _analyze_file(self, filepath: Path, content: str) -> List[Dict[str, Any]]:
        findings = []
        rel_path = str(filepath)

        # AST-based checks
        try:
            tree = ast.parse(content, filename=str(filepath))
        except SyntaxError as e:
            findings.append({
                "severity": FindingSeverity.HIGH.value,
                "category": "syntax",
                "location": f"{rel_path}:{e.lineno}",
                "message": f"Syntax error: {e.msg}",
                "suggestion": "Fix the syntax error before further analysis",
            })
            return findings

        findings.extend(self._check_unused_imports(tree, content, rel_path))
        findings.extend(self._check_bare_excepts(tree, rel_path))
        findings.extend(self._check_mutable_defaults(tree, rel_path))
        findings.extend(self._check_missing_return_types(tree, rel_path))

        # Regex-based checks on raw source
        findings.extend(self._check_secrets(content, rel_path))
        findings.extend(self._check_sql_injection(content, rel_path))
        findings.extend(self._check_cmd_injection(content, rel_path))

        return findings

    def _check_unused_imports(self, tree: ast.AST, content: str,
                              filepath: str) -> List[Dict[str, Any]]:
        findings = []
        imported_names: Dict[str, int] = {}  # name -> lineno

        for node in ast.walk(tree):
            if isinstance(node, ast.Import):
                for alias in node.names:
                    name = alias.asname or alias.name.split(".")[0]
                    imported_names[name] = node.lineno
            elif isinstance(node, ast.ImportFrom):
                if node.module and node.names:
                    for alias in node.names:
                        if alias.name == "*":
                            continue
                        name = alias.asname or alias.name
                        imported_names[name] = node.lineno

        # Walk all Name nodes to see which imports are actually used
        used_names = set()
        for node in ast.walk(tree):
            if isinstance(node, ast.Name):
                used_names.add(node.id)
            elif isinstance(node, ast.Attribute):
                # For chained attrs like `os.path`, the base Name is already caught
                pass

        for name, lineno in imported_names.items():
            if name.startswith("_"):
                continue  # Skip private/internal imports
            if name not in used_names:
                findings.append({
                    "severity": FindingSeverity.LOW.value,
                    "category": "unused_import",
                    "location": f"{filepath}:{lineno}",
                    "message": f"Unused import: '{name}'",
                    "suggestion": f"Remove unused import '{name}' or prefix with underscore",
                })

        return findings

    def _check_bare_excepts(self, tree: ast.AST,
                            filepath: str) -> List[Dict[str, Any]]:
        findings = []
        for node in ast.walk(tree):
            if isinstance(node, ast.ExceptHandler):
                if node.type is None:
                    findings.append({
                        "severity": FindingSeverity.MEDIUM.value,
                        "category": "bare_except",
                        "location": f"{filepath}:{node.lineno}",
                        "message": "Bare 'except:' clause catches all exceptions including SystemExit and KeyboardInterrupt",
                        "suggestion": "Use 'except Exception:' to catch only standard exceptions",
                    })
        return findings

    def _check_mutable_defaults(self, tree: ast.AST,
                                filepath: str) -> List[Dict[str, Any]]:
        findings = []
        for node in ast.walk(tree):
            if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
                for default in node.args.defaults + node.args.kw_defaults:
                    if default is None:
                        continue
                    if isinstance(default, (ast.List, ast.Dict, ast.Set)):
                        findings.append({
                            "severity": FindingSeverity.MEDIUM.value,
                            "category": "mutable_default",
                            "location": f"{filepath}:{node.lineno}",
                            "message": f"Mutable default argument in function '{node.name}'",
                            "suggestion": "Use None as default and create the mutable object inside the function body",
                        })
        return findings

    def _check_missing_return_types(self, tree: ast.AST,
                                    filepath: str) -> List[Dict[str, Any]]:
        findings = []
        for node in ast.walk(tree):
            if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
                # Skip private, dunder, and short functions
                if node.name.startswith("_"):
                    continue
                if node.returns is None:
                    findings.append({
                        "severity": FindingSeverity.INFO.value,
                        "category": "type_hint",
                        "location": f"{filepath}:{node.lineno}",
                        "message": f"Function '{node.name}' missing return type annotation",
                        "suggestion": f"Add return type annotation to '{node.name}'",
                    })
        return findings

    def _check_secrets(self, content: str, filepath: str) -> List[Dict[str, Any]]:
        findings = []
        for i, line in enumerate(content.splitlines(), 1):
            for pattern in _SECRET_PATTERNS:
                if pattern.search(line):
                    # Skip if it looks like a placeholder or env var lookup
                    if "os.environ" in line or "getenv" in line or "***" in line:
                        continue
                    findings.append({
                        "severity": FindingSeverity.CRITICAL.value,
                        "category": "hardcoded_secret",
                        "location": f"{filepath}:{i}",
                        "message": "Possible hardcoded secret or credential",
                        "suggestion": "Use environment variables or a secrets manager instead",
                    })
                    break  # One finding per line
        return findings

    def _check_sql_injection(self, content: str, filepath: str) -> List[Dict[str, Any]]:
        findings = []
        for i, line in enumerate(content.splitlines(), 1):
            for pattern in _SQL_INJECTION_PATTERNS:
                if pattern.search(line):
                    findings.append({
                        "severity": FindingSeverity.HIGH.value,
                        "category": "sql_injection",
                        "location": f"{filepath}:{i}",
                        "message": "Possible SQL injection — string formatting in SQL query",
                        "suggestion": "Use parameterized queries with ? placeholders",
                    })
                    break
        return findings

    def _check_cmd_injection(self, content: str, filepath: str) -> List[Dict[str, Any]]:
        findings = []
        for i, line in enumerate(content.splitlines(), 1):
            for pattern in _CMD_INJECTION_PATTERNS:
                if pattern.search(line):
                    findings.append({
                        "severity": FindingSeverity.HIGH.value,
                        "category": "command_injection",
                        "location": f"{filepath}:{i}",
                        "message": "Possible command injection via shell execution",
                        "suggestion": "Use subprocess.run() with a list of arguments and shell=False",
                    })
                    break
        return findings

    def _summarize(self, findings: List[Dict[str, Any]],
                   file_count: int) -> List[str]:
        recommendations = []
        if not findings:
            recommendations.append(f"Scanned {file_count} files — no issues found")
            return recommendations

        # Count by severity
        by_severity: Dict[str, int] = {}
        for f in findings:
            sev = f["severity"]
            by_severity[sev] = by_severity.get(sev, 0) + 1

        recommendations.append(
            f"Scanned {file_count} files, found {len(findings)} issues: "
            + ", ".join(f"{count} {sev}" for sev, count in sorted(by_severity.items()))
        )

        # Count by category
        by_category: Dict[str, int] = {}
        for f in findings:
            cat = f["category"]
            by_category[cat] = by_category.get(cat, 0) + 1

        if by_category.get("hardcoded_secret", 0) > 0:
            recommendations.append("PRIORITY: Remove hardcoded secrets and use environment variables")
        if by_category.get("sql_injection", 0) > 0:
            recommendations.append("PRIORITY: Fix SQL injection vulnerabilities with parameterized queries")
        if by_category.get("command_injection", 0) > 0:
            recommendations.append("PRIORITY: Fix command injection risks by avoiding shell=True")
        if by_category.get("bare_except", 0) > 0:
            recommendations.append(f"Replace {by_category['bare_except']} bare except clauses with specific exception types")
        if by_category.get("mutable_default", 0) > 0:
            recommendations.append(f"Fix {by_category['mutable_default']} mutable default arguments")
        if by_category.get("unused_import", 0) > 0:
            recommendations.append(f"Clean up {by_category['unused_import']} unused imports")

        return recommendations
