"""Production Code Standards — enforcement rules for NIKAME codegen.

These rules ensure every generated file meets production-grade standards:
- Async SQLAlchemy (no sync queries)
- Retry logic with tenacity on external calls
- Explicit response models / status codes on endpoints
- Structured logging with structlog
- Pool sizing from OptimizationProfile
- Cache error handling (miss/error/invalidation)
- Kafka DLQ handling
"""

from __future__ import annotations

import re
from dataclasses import dataclass, field
from pathlib import Path

import yaml

from nikame.codegen.rules import BaseRule, RuleResult, RuleViolation
from nikame.utils.logger import get_logger

_log = get_logger("standards")


# ═══════════════════════════════════════════════════════════════════
# Rule: Async SQLAlchemy Enforcement
# ═══════════════════════════════════════════════════════════════════

class AsyncSQLAlchemyRule(BaseRule):
    """All database queries must use async sessions. No synchronous SQLAlchemy."""

    NAME = "async_sqlalchemy"
    DESCRIPTION = "Enforce async SQLAlchemy — no synchronous queries"

    # Sync patterns that should NOT appear
    _SYNC_PATTERNS = [
        (re.compile(r'\bfrom sqlalchemy\.orm\s+import\s+Session\b'), "Synchronous Session import"),
        (re.compile(r'\bfrom sqlalchemy\s+import\s+create_engine\b'), "Synchronous create_engine"),
        (re.compile(r'(?<!async_)\bcreate_engine\s*\('), "Synchronous create_engine() call"),
        (re.compile(r'\bSession\s*\(\s*bind\s*='), "Synchronous Session(bind=)"),
        (re.compile(r'\bsessionmaker\s*\(\s*bind\s*='), "Synchronous sessionmaker(bind=)"),
        (re.compile(r"^(?![^#]*\bawait\b)[^#]*\.execute\s*\(", re.MULTILINE), "Possibly synchronous .execute()"),
    ]

    # Async patterns that SHOULD appear instead
    _ASYNC_REQUIRED = [
        "async_sessionmaker",
        "AsyncSession",
        "create_async_engine",
    ]

    def check(self, file_buffer: dict[str, str]) -> RuleResult:
        violations = []

        for path, content in file_buffer.items():
            if not path.endswith(".py"):
                continue
            if "sqlalchemy" not in content.lower():
                continue

            for pattern, desc in self._SYNC_PATTERNS:
                if pattern.search(content):
                    # Exception: alembic env.py is allowed to use sync engine
                    if "alembic" in path or "env.py" in Path(path).name:
                        continue
                    violations.append(RuleViolation(
                        rule=self.NAME,
                        severity="P0",
                        file=path,
                        message=f"{desc} — use async equivalent instead",
                        auto_fixable=True,
                    ))

        return RuleResult(rule_name=self.NAME, passed=len(violations) == 0, violations=violations)

    def fix(self, file_buffer: dict[str, str], violations: list[RuleViolation]) -> dict[str, str]:
        for v in violations:
            if not v.auto_fixable or v.rule != self.NAME:
                continue
            path = v.file
            if path not in file_buffer:
                continue
            content = file_buffer[path]

            # Replace sync with async equivalents
            content = content.replace(
                "from sqlalchemy.orm import Session",
                "from sqlalchemy.ext.asyncio import AsyncSession"
            )
            content = content.replace(
                "from sqlalchemy import create_engine",
                "from sqlalchemy.ext.asyncio import create_async_engine"
            )
            content = re.sub(
                r'\bcreate_engine\s*\(',
                'create_async_engine(',
                content
            )
            content = content.replace(
                "sessionmaker(bind=",
                "async_sessionmaker(bind="
            )
            file_buffer[path] = content

        return file_buffer


# ═══════════════════════════════════════════════════════════════════
# Rule: Retry Logic Enforcement
# ═══════════════════════════════════════════════════════════════════

class RetryLogicRule(BaseRule):
    """All external service calls must have retry logic with tenacity."""

    NAME = "retry_logic"
    DESCRIPTION = "Enforce retry logic on external HTTP/service calls"

    _EXTERNAL_CALL_PATTERNS = [
        re.compile(r'httpx\.(get|post|put|delete|patch|request|Client|AsyncClient)\s*\('),
        re.compile(r'requests\.(get|post|put|delete|patch)\s*\('),
        re.compile(r'aiohttp\.ClientSession\s*\('),
    ]

    def check(self, file_buffer: dict[str, str]) -> RuleResult:
        violations = []

        for path, content in file_buffer.items():
            if not path.endswith(".py"):
                continue

            has_external_calls = any(p.search(content) for p in self._EXTERNAL_CALL_PATTERNS)
            if not has_external_calls:
                continue

            # Check if tenacity retry is present
            has_retry = "tenacity" in content or "@retry" in content or "retry(" in content
            if not has_retry:
                violations.append(RuleViolation(
                    rule=self.NAME,
                    severity="P1",
                    file=path,
                    message="External HTTP calls without tenacity retry logic",
                    auto_fixable=True,
                ))

        return RuleResult(rule_name=self.NAME, passed=len(violations) == 0, violations=violations)

    def fix(self, file_buffer: dict[str, str], violations: list[RuleViolation]) -> dict[str, str]:
        for v in violations:
            if not v.auto_fixable or v.rule != self.NAME:
                continue
            path = v.file
            if path not in file_buffer:
                continue
            content = file_buffer[path]

            # Add tenacity import and a retry decorator hint
            retry_import = (
                "import os\n"
                "from tenacity import retry, stop_after_attempt, wait_exponential, before_log\n"
                "import logging\n\n"
                "logger = logging.getLogger(__name__)\n"
                "MAX_RETRIES = int(os.getenv('MAX_RETRY_ATTEMPTS', '3'))\n"
            )

            if "from tenacity" not in content:
                # Add import at top, after existing imports
                lines = content.splitlines()
                last_import = 0
                for i, line in enumerate(lines):
                    if line.startswith("import ") or line.startswith("from "):
                        last_import = i
                lines.insert(last_import + 1, "")
                lines.insert(last_import + 2, retry_import.strip())
                content = "\n".join(lines)

            file_buffer[path] = content

        return file_buffer


# ═══════════════════════════════════════════════════════════════════
# Rule: Explicit Response Models
# ═══════════════════════════════════════════════════════════════════

class ResponseModelRule(BaseRule):
    """All FastAPI endpoints must have explicit response models and status codes."""

    NAME = "response_models"
    DESCRIPTION = "Enforce response_model and status_code on all endpoints"

    _ENDPOINT_PATTERN = re.compile(
        r'@(?:app|router)\.(get|post|put|delete|patch)\s*\(\s*["\'][^"\']+["\']\s*\)',
        re.MULTILINE,
    )

    def check(self, file_buffer: dict[str, str]) -> RuleResult:
        violations = []

        for path, content in file_buffer.items():
            if not path.endswith(".py"):
                continue

            for m in self._ENDPOINT_PATTERN.finditer(content):
                decorator_text = m.group(0)
                # Check for response_model or status_code
                if "response_model" not in decorator_text and "status_code" not in decorator_text:
                    # Get the function name from the next line
                    pos = m.end()
                    rest = content[pos:pos + 200]
                    fn_match = re.search(r'(?:async )?def (\w+)', rest)
                    fn_name = fn_match.group(1) if fn_match else "unknown"

                    # Skip health and root endpoints
                    if fn_name in ("root", "health", "health_check", "readiness", "liveness"):
                        continue

                    violations.append(RuleViolation(
                        rule=self.NAME,
                        severity="P1",
                        file=path,
                        message=f"Endpoint '{fn_name}' missing response_model/status_code",
                        auto_fixable=False,
                    ))

        return RuleResult(rule_name=self.NAME, passed=len(violations) == 0, violations=violations)


# ═══════════════════════════════════════════════════════════════════
# Rule: Structured Logging
# ═══════════════════════════════════════════════════════════════════

class StructuredLoggingRule(BaseRule):
    """All logging must use structlog, not stdlib logging."""

    NAME = "structured_logging"
    DESCRIPTION = "Enforce structlog for structured logging"

    def check(self, file_buffer: dict[str, str]) -> RuleResult:
        violations = []

        for path, content in file_buffer.items():
            if not path.endswith(".py"):
                continue

            # Check for stdlib logging usage (not just import)
            uses_stdlib = bool(re.search(r'logging\.(info|warning|error|debug|critical)\s*\(', content))
            uses_print_log = bool(re.search(r'\bprint\s*\(\s*f?["\'].*(?:error|warn|info|debug)', content, re.IGNORECASE))

            if uses_stdlib or uses_print_log:
                # Flag if it uses stdlib logging even if structlog is present
                # Skip test files and config files
                if "test" in path or "conftest" in path or "alembic" in path:
                    continue

                violations.append(RuleViolation(
                    rule=self.NAME,
                    severity="P1",
                    file=path,
                    message="Uses stdlib logging — should use structlog for structured logging",
                    auto_fixable=True,
                ))

        return RuleResult(rule_name=self.NAME, passed=len(violations) == 0, violations=violations)

    def fix(self, file_buffer: dict[str, str], violations: list[RuleViolation]) -> dict[str, str]:
        for v in violations:
            if not v.auto_fixable or v.rule != self.NAME:
                continue
            path = v.file
            if path not in file_buffer:
                continue
            content = file_buffer[path]

            # Add structlog import
            if "import structlog" not in content:
                lines = content.splitlines()
                last_import = 0
                for i, line in enumerate(lines):
                    if line.startswith("import ") or line.startswith("from "):
                        last_import = i
                lines.insert(last_import + 1, "import structlog")
                lines.insert(last_import + 2, 'logger = structlog.get_logger(__name__)')
                content = "\n".join(lines)

            # Replace basic logging calls with structlog
            content = re.sub(r'logging\.(info|warning|error|debug|critical)\(',
                             r'logger.\1(', content)

            file_buffer[path] = content

        return file_buffer


# ═══════════════════════════════════════════════════════════════════
# Rule: Pool Sizing from Scale Tier
# ═══════════════════════════════════════════════════════════════════

class PoolSizingRule(BaseRule):
    """DB connection pools must be sized via env var, not hardcoded."""

    NAME = "pool_sizing"
    DESCRIPTION = "Enforce configurable pool sizing — no hardcoded pool_size"

    _HARDCODED_POOL = re.compile(r'pool_size\s*=\s*(\d+)')

    def check(self, file_buffer: dict[str, str]) -> RuleResult:
        violations = []

        for path, content in file_buffer.items():
            if not path.endswith(".py"):
                continue

            for m in self._HARDCODED_POOL.finditer(content):
                violations.append(RuleViolation(
                    rule=self.NAME,
                    severity="P1",
                    file=path,
                    message=f"Hardcoded pool_size={m.group(1)} — should read from env/settings",
                    auto_fixable=True,
                ))

        return RuleResult(rule_name=self.NAME, passed=len(violations) == 0, violations=violations)

    def fix(self, file_buffer: dict[str, str], violations: list[RuleViolation]) -> dict[str, str]:
        for v in violations:
            if not v.auto_fixable or v.rule != self.NAME:
                continue
            path = v.file
            if path not in file_buffer:
                continue
            content = file_buffer[path]

            # Replace hardcoded pool_size with env var
            content = self._HARDCODED_POOL.sub(
                'pool_size=int(os.getenv("DB_POOL_SIZE", "25"))',
                content,
            )
            # Ensure os is imported
            if "import os" not in content:
                content = "import os\n" + content

            file_buffer[path] = content

        return file_buffer


# ═══════════════════════════════════════════════════════════════════
# Rule: Cache Error Handling
# ═══════════════════════════════════════════════════════════════════

class CacheErrorHandlingRule(BaseRule):
    """Cache operations must handle miss, error, and invalidation separately."""

    NAME = "cache_error_handling"
    DESCRIPTION = "Enforce graceful cache error handling — never propagate cache errors"

    _BARE_CACHE_PATTERNS = [
        re.compile(r'await\s+redis_?\w*\.(get|set|delete)\s*\('),
        re.compile(r'cache\.(get|set|delete)\s*\('),
    ]

    def check(self, file_buffer: dict[str, str]) -> RuleResult:
        violations = []

        for path, content in file_buffer.items():
            if not path.endswith(".py"):
                continue

            has_cache_call = any(p.search(content) for p in self._BARE_CACHE_PATTERNS)
            if not has_cache_call:
                continue

            # Check if there's try/except around cache calls
            has_error_handling = "except" in content and ("redis" in content.lower() or "cache" in content.lower())
            if not has_error_handling:
                violations.append(RuleViolation(
                    rule=self.NAME,
                    severity="P1",
                    file=path,
                    message="Cache operations without try/except — cache errors should not propagate",
                    auto_fixable=False,
                ))

        return RuleResult(rule_name=self.NAME, passed=len(violations) == 0, violations=violations)


# ═══════════════════════════════════════════════════════════════════
# Rule: Kafka DLQ Handling
# ═══════════════════════════════════════════════════════════════════

class KafkaDLQRule(BaseRule):
    """Kafka consumers must have dead letter queue handling."""

    NAME = "kafka_dlq"
    DESCRIPTION = "Enforce DLQ handling for Kafka consumers"

    def check(self, file_buffer: dict[str, str]) -> RuleResult:
        violations = []

        for path, content in file_buffer.items():
            if not path.endswith(".py"):
                continue

            # Check for consumer patterns
            has_consumer = (
                "Consumer(" in content
                or "consume(" in content
                or "subscribe(" in content
            ) and ("kafka" in content.lower() or "confluent" in content.lower())

            if not has_consumer:
                continue

            # Check for DLQ pattern
            has_dlq = "dlq" in content.lower() or "dead_letter" in content.lower() or "dead letter" in content.lower()
            if not has_dlq:
                violations.append(RuleViolation(
                    rule=self.NAME,
                    severity="P1",
                    file=path,
                    message="Kafka consumer without DLQ handling — failed messages should go to a DLQ topic",
                    auto_fixable=False,
                ))

        return RuleResult(rule_name=self.NAME, passed=len(violations) == 0, violations=violations)


# ═══════════════════════════════════════════════════════════════════
# Rule: Base Init Enforcement
# ═══════════════════════════════════════════════════════════════════

class BaseInitRule(BaseRule):
    """Every directory in the project must have an __init__.py (except infra/ and scripts/)."""

    NAME = "base_init"
    DESCRIPTION = "Enforce recursive __init__.py in all packages"

    def check(self, file_buffer: dict[str, str]) -> RuleResult:
        violations = []
        directories = set()
        
        for path in file_buffer:
            p = Path(path)
            # Skip non-code directories
            if any(part in ("infra", "scripts", "alembic", "tests") for part in p.parts):
                continue
            
            # Record all parent directories
            for parent in p.parents:
                if str(parent) != "." and str(parent) != "/":
                    directories.add(str(parent))

        for directory in directories:
            init_file = f"{directory}/__init__.py"
            if init_file not in file_buffer:
                violations.append(RuleViolation(
                    rule=self.NAME,
                    severity="P0",
                    file=directory,
                    message=f"Missing __init__.py in package directory '{directory}'",
                    auto_fixable=True,
                ))

        return RuleResult(rule_name=self.NAME, passed=len(violations) == 0, violations=violations)

    def fix(self, file_buffer: dict[str, str], violations: list[RuleViolation]) -> dict[str, str]:
        for v in violations:
            if v.auto_fixable and v.rule == self.NAME:
                init_path = f"{v.file}/__init__.py"
                file_buffer[init_path] = ""
        return file_buffer


# ═══════════════════════════════════════════════════════════════════
# Rule: Async Route Enforcement
# ═══════════════════════════════════════════════════════════════════

class AsyncDefRule(BaseRule):
    """All API routes must use async def to avoid thread pool exhaustion."""

    NAME = "async_def_routes"
    DESCRIPTION = "Force async def on all FastAPI route handlers"

    _ROUTE_DECO = re.compile(r'@(?:app|router)\.(?:get|post|put|delete|patch|options|head)\s*\(')

    def check(self, file_buffer: dict[str, str]) -> RuleResult:
        violations = []
        for path, content in file_buffer.items():
            if not path.endswith(".py") or "app/api" not in path:
                continue

            lines = content.splitlines()
            for i, line in enumerate(lines):
                if self._ROUTE_DECO.search(line):
                    # Check the next line(s) for the function definition
                    for j in range(i + 1, min(i + 5, len(lines))):
                        nextLine = lines[j].strip()
                        if nextLine.startswith("def "):
                            violations.append(RuleViolation(
                                rule=self.NAME,
                                severity="P0",
                                file=path,
                                message=f"Route handler '{nextLine.split('(')[0]}' must be async",
                                auto_fixable=True,
                            ))
                            break
                        if nextLine.startswith("async def "):
                            break

        return RuleResult(rule_name=self.NAME, passed=len(violations) == 0, violations=violations)

    def fix(self, file_buffer: dict[str, str], violations: list[RuleViolation]) -> dict[str, str]:
        for v in violations:
            if v.auto_fixable and v.rule == self.NAME:
                content = file_buffer[v.file]
                file_buffer[v.file] = content.replace("\ndef ", "\nasync def ")
        return file_buffer


# ═══════════════════════════════════════════════════════════════════
# Rule: Absolute Import Enforcement
# ═══════════════════════════════════════════════════════════════════

class AbsoluteImportRule(BaseRule):
    """Consolidate all relative imports into absolute local imports."""

    NAME = "absolute_imports"
    DESCRIPTION = "Resolve relative imports to absolute package paths"

    def check(self, file_buffer: dict[str, str]) -> RuleResult:
        violations = []
        rel_pattern = re.compile(r'^from\s+\.+(\w+)?\s+import', re.MULTILINE)

        for path, content in file_buffer.items():
            if not path.endswith(".py"):
                continue
            if rel_pattern.search(content):
                violations.append(RuleViolation(
                    rule=self.NAME,
                    severity="P1",
                    file=path,
                    message="Found relative imports — absolute imports preferred",
                    auto_fixable=True,
                ))
        return RuleResult(rule_name=self.NAME, passed=len(violations) == 0, violations=violations)

    def fix(self, file_buffer: dict[str, str], violations: list[RuleViolation]) -> dict[str, str]:
        for v in violations:
            if v.auto_fixable and v.rule == self.NAME:
                path = v.file
                content = file_buffer[path]
                
                parts = Path(path).parts
                if not parts: continue
                
                # Dynamically determine the base package. E.g. services/api/core -> base_pkg = 'core' if package root
                # Usually it's the directory containing main.py or the first directory after 'services/api/'
                # Let's use a heuristic: the first directory after 'services/<svc_name>/' or just parts[0]
                base_pkg = "app"
                if "services" in parts:
                    try:
                        svc_idx = parts.index("services")
                        # e.g. services / api / app / main.py -> base_pkg = app
                        if len(parts) > svc_idx + 2:
                            base_pkg = parts[svc_idx + 2]
                    except ValueError:
                        pass
                else:
                    # If not in services structure, use the first path part as the package name 
                    # (only if it has an __init__.py, else default to 'app')
                    if parts[0] != "." and f"{parts[0]}/__init__.py" in file_buffer:
                        base_pkg = parts[0]

                # Replace relative imports with dynamic base_pkg
                # Replace 'from . import' with 'from base_pkg import'
                content = re.sub(r'^from\s+\.\s+import', f'from {base_pkg} import', content, flags=re.MULTILINE)
                # Replace 'from .xyz import' with 'from base_pkg.xyz import'
                content = re.sub(r'^from\s+\.(\w+)\s+import', rf'from {base_pkg}.\1 import', content, flags=re.MULTILINE)
                # Note: Currently only resolving single '.' relative imports.
                
                file_buffer[path] = content
        return file_buffer


# ═══════════════════════════════════════════════════════════════════
# Rule: Model Registry Enforcement
# ═══════════════════════════════════════════════════════════════════

class ModelRegistryRule(BaseRule):
    """All SQLAlchemy models must be imported in app/db/base.py for Alembic detection."""

    NAME = "model_registry"
    DESCRIPTION = "Ensure all database models are registered in app/db/base.py"

    def check(self, file_buffer: dict[str, str]) -> RuleResult:
        violations = []
        # Find the base.py dynamically
        base_py_paths = [k for k in file_buffer if k.endswith("db/base.py") or k.endswith("database/base.py")]
        if not base_py_paths:
            return RuleResult(rule_name=self.NAME, passed=True)

        base_py = base_py_paths[0]
        base_content = file_buffer[base_py]
        
        # Base package logic
        parts = Path(base_py).parts
        base_pkg = parts[parts.index("db") - 1] if "db" in parts else (parts[parts.index("database") - 1] if "database" in parts else "app")

        models = [p for p in file_buffer if f"{base_pkg}/models/" in p and p.endswith(".py") and "__init__" not in p]
        
        for model_path in models:
            model_name = Path(model_path).stem
            # Heuristic: check if 'from app.models.XYZ import ...' exists or 'import XYZ'
            if f"{base_pkg}.models.{model_name}" not in base_content:
                violations.append(RuleViolation(
                    rule=self.NAME,
                    severity="P0",
                    file=base_py,
                    message=f"Model '{model_name}' ({model_path}) is not registered in {base_py}",
                    auto_fixable=True,
                ))
        
        return RuleResult(rule_name=self.NAME, passed=len(violations) == 0, violations=violations)

    def fix(self, file_buffer: dict[str, str], violations: list[RuleViolation]) -> dict[str, str]:
        for v in violations:
            if v.auto_fixable and v.rule == self.NAME:
                base_py = v.file
                content = file_buffer[base_py]
                
                parts = Path(base_py).parts
                base_pkg = parts[parts.index("db") - 1] if "db" in parts else (parts[parts.index("database") - 1] if "database" in parts else "app")

                match = re.search(r"Model '(\w+)'", v.message)
                if match:
                    model_name = match.group(1)
                    import_line = f"from {base_pkg}.models.{model_name} import {model_name.capitalize()}  # noqa: F401"
                    if import_line not in content:
                        content += f"\n{import_line}"
                
                file_buffer[base_py] = content
        return file_buffer


# ═══════════════════════════════════════════════════════════════════
# Rule: Router Prefix Enforcement
# ═══════════════════════════════════════════════════════════════════

class RouterPrefixRule(BaseRule):
    """All routers registered in main.py should have an /api/v1 prefix."""

    NAME = "router_prefix"
    DESCRIPTION = "Enforce /api/v1 prefixes on all API routers"

    _INCLUDE_ROUTER = re.compile(r'app\.include_router\(\s*(\w+)')

    def check(self, file_buffer: dict[str, str]) -> RuleResult:
        violations = []
        main_py = "app/main.py"
        if main_py not in file_buffer:
            return RuleResult(rule_name=self.NAME, passed=True)

        content = file_buffer[main_py]
        for m in self._INCLUDE_ROUTER.finditer(content):
            start_pos = m.end()
            # Check if prefix="/api/v1" is in the remaining call
            call_block = content[start_pos:content.find(")", start_pos) + 1]
            if 'prefix=' not in call_block or '/api/v1' not in call_block:
                violations.append(RuleViolation(
                    rule=self.NAME,
                    severity="P1",
                    file=main_py,
                    message=f"Router '{m.group(1)}' included without versioned /api/v1 prefix",
                    auto_fixable=False,
                ))

        return RuleResult(rule_name=self.NAME, passed=len(violations) == 0, violations=violations)


# ═══════════════════════════════════════════════════════════════════
# Rule: Security Header Enforcement
# ═══════════════════════════════════════════════════════════════════

class SecurityHeaderRule(BaseRule):
    """Ensure basic security middleware (HSTS, XSS protection) is enabled."""

    NAME = "security_headers"
    DESCRIPTION = "Enforce security headers middleware in main.py"

    def check(self, file_buffer: dict[str, str]) -> RuleResult:
        violations = []
        main_py = "app/main.py"
        if main_py not in file_buffer:
            return RuleResult(rule_name=self.NAME, passed=True)

        content = file_buffer[main_py]
        if "TrustedHostMiddleware" not in content and "add_middleware" not in content:
            violations.append(RuleViolation(
                rule=self.NAME,
                severity="P1",
                file=main_py,
                message="Security middleware (TrustedHost/CORS) missing in main.py",
                auto_fixable=False,
            ))
        return RuleResult(rule_name=self.NAME, passed=len(violations) == 0, violations=violations)


# ═══════════════════════════════════════════════════════════════════
# Rule: Pydantic Field Description Enforcement
# ═══════════════════════════════════════════════════════════════════

class FieldDescriptionRule(BaseRule):
    """All Pydantic schema fields should have descriptions for OpenAPI documentation."""

    NAME = "field_descriptions"
    DESCRIPTION = "Enforce descriptions on Pydantic fields"

    _FIELD_PATTERN = re.compile(r'(\w+):\s*[\w\[\], ]+\s*=\s*Field\(')

    def check(self, file_buffer: dict[str, str]) -> RuleResult:
        violations = []
        for path, content in file_buffer.items():
            if not path.endswith(".py") or "app/schemas" not in path:
                continue
            
            for m in self._FIELD_PATTERN.finditer(content):
                field_block = content[m.start():content.find(")", m.end()) + 1]
                if "description=" not in field_block:
                    violations.append(RuleViolation(
                        rule=self.NAME,
                        severity="P1",
                        file=path,
                        message=f"Pydantic field '{m.group(1)}' missing description",
                        auto_fixable=False,
                    ))
        return RuleResult(rule_name=self.NAME, passed=len(violations) == 0, violations=violations)


# ═══════════════════════════════════════════════════════════════════
# Rule: Health Sync Enforcement
# ═══════════════════════════════════════════════════════════════════

class HealthSyncRule(BaseRule):
    """Ensure Docker healthchecks match the actual API health endpoint."""

    NAME = "health_sync"
    DESCRIPTION = "Verify Docker healthchecks match /health endpoint"

    def check(self, file_buffer: dict[str, str]) -> RuleResult:
        violations = []
        compose_files = [k for k in file_buffer if "docker-compose" in k]
        
        for cp in compose_files:
            try:
                compose = yaml.safe_load(file_buffer[cp])
                api = compose.get("services", {}).get("api", {})
                hc = api.get("healthcheck", {}).get("test", [])
                hc_str = " ".join(hc) if isinstance(hc, list) else str(hc)
                
                if "curl" in hc_str and "/health" not in hc_str:
                    violations.append(RuleViolation(
                        rule=self.NAME,
                        severity="P1",
                        file=cp,
                        message="Docker healthcheck target does not match standard /health endpoint",
                        auto_fixable=True,
                    ))
            except Exception:
                continue
        return RuleResult(rule_name=self.NAME, passed=len(violations) == 0, violations=violations)

    def fix(self, file_buffer: dict[str, str], violations: list[RuleViolation]) -> dict[str, str]:
        for v in violations:
            if v.auto_fixable and v.rule == self.NAME:
                content = file_buffer[v.file]
                # Simple replacement heuristic
                content = re.sub(r'curl\s+-f\s+http://localhost:(\d+)/?(\w*)', r'curl -f http://localhost:\1/health', content)
                file_buffer[v.file] = content
        return file_buffer


# ═══════════════════════════════════════════════════════════════════
# Rule: Metric Wiring Enforcement
# ═══════════════════════════════════════════════════════════════════

class MetricWiringRule(BaseRule):
    """If Prometheus is enabled in requirements, it must be instrumented in main.py."""

    NAME = "metric_wiring"
    DESCRIPTION = "Ensure Prometheus instrumentation is active if prometheus-fastapi-instrumentator is present"

    def check(self, file_buffer: dict[str, str]) -> RuleResult:
        violations = []
        req_files = [k for k in file_buffer if k.endswith("requirements.txt")]
        main_py = "app/main.py"
        
        if not req_files or main_py not in file_buffer:
            return RuleResult(rule_name=self.NAME, passed=True)

        has_prom = "prometheus-fastapi-instrumentator" in file_buffer[req_files[0]]
        if has_prom and "Instrumentator" not in file_buffer[main_py]:
            violations.append(RuleViolation(
                rule=self.NAME,
                severity="P1",
                file=main_py,
                message="Prometheus metrics enabled in requirements but not instrumented in main.py",
                auto_fixable=True,
            ))
        return RuleResult(rule_name=self.NAME, passed=len(violations) == 0, violations=violations)

    def fix(self, file_buffer: dict[str, str], violations: list[RuleViolation]) -> dict[str, str]:
        main_py = "app/main.py"
        content = file_buffer[main_py]
        
        for v in violations:
            if v.auto_fixable and v.rule == self.NAME:
                import_line = "from prometheus_fastapi_instrumentator import Instrumentator"
                inst_line = "Instrumentator().instrument(app).expose(app)"
                
                if import_line not in content:
                    lines = content.splitlines()
                    lines.insert(1, import_line)
                    content = "\n".join(lines)
                
                if inst_line not in content:
                    content += f"\n\n{inst_line}"
        
        file_buffer[main_py] = content
        return file_buffer


# ═══════════════════════════════════════════════════════════════════
# Rule: Dependency Injection (DI) Management
# ═══════════════════════════════════════════════════════════════════

class DIManagementRule(BaseRule):
    """API routes should use Depends() for database session injection."""

    NAME = "di_management"
    DESCRIPTION = "Enforce Depends() for database sessions in routes"

    _SESSION_ARG = re.compile(r'session\s*:\s*AsyncSession')

    def check(self, file_buffer: dict[str, str]) -> RuleResult:
        violations = []
        for path, content in file_buffer.items():
            if not path.endswith(".py") or "app/api" not in path:
                continue
            
            lines = content.splitlines()
            for i, line in enumerate(lines):
                if "def " in line and self._SESSION_ARG.search(line):
                    if "Depends" not in line:
                        violations.append(RuleViolation(
                            rule=self.NAME,
                            severity="P0",
                            file=path,
                            message=f"Session injection in '{line.strip()}' missing Depends()",
                            auto_fixable=True,
                        ))
        return RuleResult(rule_name=self.NAME, passed=len(violations) == 0, violations=violations)

    def fix(self, file_buffer: dict[str, str], violations: list[RuleViolation]) -> dict[str, str]:
        for v in violations:
            if v.auto_fixable and v.rule == self.NAME:
                content = file_buffer[v.file]
                if "from fastapi import Depends" not in content:
                    content = "from fastapi import Depends\n" + content
                if "from app.db.session import get_db" not in content:
                    content = "from app.db.session import get_db\n" + content
                
                content = content.replace(
                    "session: AsyncSession",
                    "session: AsyncSession = Depends(get_db)"
                )
                file_buffer[v.file] = content
        return file_buffer


# ═══════════════════════════════════════════════════════════════════
# Rule: Lifespan Wiring Enforcement
# ═══════════════════════════════════════════════════════════════════

class LifespanWiringRule(BaseRule):
    """Ensure all component lifespan hooks are wired into main.py."""

    NAME = "lifespan_wiring"
    DESCRIPTION = "Auto-register component lifespan hooks in main.py"

    def check(self, file_buffer: dict[str, str]) -> RuleResult:
        violations = []
        main_py = "app/main.py"
        if main_py not in file_buffer:
            return RuleResult(rule_name=self.NAME, passed=True)

        content = file_buffer[main_py]
        if "lifespan" not in content and "@asynccontextmanager" not in content:
            violations.append(RuleViolation(
                rule=self.NAME,
                severity="P1",
                file=main_py,
                message="FastAPI lifespan handler missing in main.py",
                auto_fixable=False,
            ))
        return RuleResult(rule_name=self.NAME, passed=len(violations) == 0, violations=violations)


# ═══════════════════════════════════════════════════════════════════
# Rule: CORS Origin Enforcement
# ═══════════════════════════════════════════════════════════════════

class CORSOriginRule(BaseRule):
    """CORS origins should be environment-driven, not hardcoded."""

    NAME = "cors_origin_config"
    DESCRIPTION = "Enforce environment-driven CORS configuration"

    def check(self, file_buffer: dict[str, str]) -> RuleResult:
        violations = []
        main_py = "app/main.py"
        if main_py not in file_buffer:
            return RuleResult(rule_name=self.NAME, passed=True)

        content = file_buffer[main_py]
        if "CORSMiddleware" in content:
            if 'allow_origins=["*"]' in content or "allow_origins=['*']" in content:
                 violations.append(RuleViolation(
                    rule=self.NAME,
                    severity="P1",
                    file=main_py,
                    message="CORS allow_origins is hardcoded to '*'. Use environment variable instead.",
                    auto_fixable=True,
                ))
        return RuleResult(rule_name=self.NAME, passed=len(violations) == 0, violations=violations)

    def fix(self, file_buffer: dict[str, str], violations: list[RuleViolation]) -> dict[str, str]:
        main_py = "app/main.py"
        content = file_buffer[main_py]
        for v in violations:
            if v.auto_fixable and v.rule == self.NAME:
                content = content.replace(
                    'allow_origins=["*"]',
                    'allow_origins=os.getenv("CORS_ORIGINS", "*").split(",")'
                )
                content = content.replace(
                    "allow_origins=['*']",
                    'allow_origins=os.getenv("CORS_ORIGINS", "*").split(",")'
                )
                if "import os" not in content:
                    content = "import os\n" + content
        file_buffer[main_py] = content
        return file_buffer


# ═══════════════════════════════════════════════════════════════════
# Rule: Gzip Middleware Enforcement
# ═══════════════════════════════════════════════════════════════════

class GzipMiddlewareRule(BaseRule):
    """Enable GZip compression for API responses."""

    NAME = "gzip_middleware"
    DESCRIPTION = "Ensure GZipMiddleware is enabled for response compression"

    def check(self, file_buffer: dict[str, str]) -> RuleResult:
        violations = []
        main_py = "app/main.py"
        if main_py not in file_buffer:
            return RuleResult(rule_name=self.NAME, passed=True)

        content = file_buffer[main_py]
        if "GZipMiddleware" not in content:
            violations.append(RuleViolation(
                rule=self.NAME,
                severity="P1",
                file=main_py,
                message="GZipMiddleware missing in main.py",
                auto_fixable=True,
            ))
        return RuleResult(rule_name=self.NAME, passed=len(violations) == 0, violations=violations)

    def fix(self, file_buffer: dict[str, str], violations: list[RuleViolation]) -> dict[str, str]:
        main_py = "app/main.py"
        content = file_buffer[main_py]
        for v in violations:
            if v.auto_fixable and v.rule == self.NAME:
                import_line = "from fastapi.middleware.gzip import GZipMiddleware"
                add_line = "app.add_middleware(GZipMiddleware, minimum_size=1000)"
                
                if import_line not in content:
                    lines = content.splitlines()
                    lines.insert(1, import_line)
                    content = "\n".join(lines)
                
                if add_line not in content:
                    # Find where other middlewares are added or after app initialisation
                    if "app = FastAPI" in content:
                        content = content.replace("app = FastAPI(", f"app = FastAPI(\n{add_line}\n", 1)
                    else:
                        content += f"\n{add_line}"
        file_buffer[main_py] = content
        return file_buffer


# ═══════════════════════════════════════════════════════════════════
# Rule: DB Session Naming Consistency
# ═══════════════════════════════════════════════════════════════════

class DBSessionNamingRule(BaseRule):
    """Enforce consistent naming for database sessions (db or session)."""

    NAME = "db_session_naming"
    DESCRIPTION = "Enforce consistent naming for AsyncSession arguments"

    _BAD_SESSION_NAME = re.compile(r'(\wsession|\wdb)\s*:\s*AsyncSession')

    def check(self, file_buffer: dict[str, str]) -> RuleResult:
        violations = []
        # This is a soft rule, just identifying names that aren't exactly 'db' or 'session'
        for path, content in file_buffer.items():
            if not path.endswith(".py"):
                continue
            
            # Simplified: check for AsyncSession type hint with non-standard name
            matches = re.finditer(r'(\w+)\s*:\s*AsyncSession', content)
            for m in matches:
                name = m.group(1)
                if name not in ("db", "session"):
                    violations.append(RuleViolation(
                        rule=self.NAME,
                        severity="P1",
                        file=path,
                        message=f"Non-standard variable name '{name}' for AsyncSession. Use 'db' or 'session'.",
                        auto_fixable=False,
                    ))
        return RuleResult(rule_name=self.NAME, passed=len(violations) == 0, violations=violations)


# ═══════════════════════════════════════════════════════════════════
# Registry: all production standards
# ═══════════════════════════════════════════════════════════════════

PRODUCTION_RULES: list[BaseRule] = [
    AsyncSQLAlchemyRule(),
    RetryLogicRule(),
    ResponseModelRule(),
    StructuredLoggingRule(),
    PoolSizingRule(),
    CacheErrorHandlingRule(),
    KafkaDLQRule(),
    # New Architectural Rules
    BaseInitRule(),
    AsyncDefRule(),
    AbsoluteImportRule(),
    ModelRegistryRule(),
    RouterPrefixRule(),
    SecurityHeaderRule(),
    FieldDescriptionRule(),
    HealthSyncRule(),
    MetricWiringRule(),
    DIManagementRule(),
    LifespanWiringRule(),
    CORSOriginRule(),
    GzipMiddlewareRule(),
    DBSessionNamingRule(),
]
