"""Quality standards for GreyMatter.

Codified rules that every piece of code must meet.
Replaces prose quality docs with structured, queryable standards.
"""
import json
from typing import Any

from .db import Database

SEVERITY_ORDER = {"critical": 0, "important": 1, "minor": 2}

# Standards mined from real build history: 3 code review cycles, 144 tests,
# 32 commits on feat/greymatter-solo-core. Each standard traces to a specific
# commit where the anti-pattern was caught and fixed.

DEFAULT_STANDARDS = [
    # --- CRITICAL: Security (from c1178f5, afe9176) ---
    {
        "id": "qs-no-shell-true",
        "category": "security",
        "rule": "Never use shell=True in subprocess calls — use shlex.split + argument list",
        "severity": "critical",
        "example_bad": "subprocess.run(command, shell=True)",
        "example_good": "subprocess.run(shlex.split(command))",
        "rationale": "shell=True enables command injection, especially dangerous with LLM-generated inputs",
        "source_commit": "c1178f5",
    },
    {
        "id": "qs-command-allowlist",
        "category": "security",
        "rule": "LLM-exposed shell tools must use a command allowlist, not a blocklist",
        "severity": "critical",
        "example_good": "ALLOWED = {'ls','cat','grep'}; if parts[0] not in ALLOWED: reject",
        "rationale": "Local 7B models are easier to jailbreak than frontier models — allowlists fail safe",
        "source_commit": "c1178f5",
    },
    {
        "id": "qs-write-confinement",
        "category": "security",
        "rule": "LLM-exposed file write tools must confine writes to a specific directory",
        "severity": "critical",
        "example_good": "p = Path(path).resolve(); assert str(p).startswith(str(WRITE_ROOT.resolve()))",
        "example_bad": "Path(path).write_text(content)  # unrestricted",
        "rationale": "Path traversal lets jailbroken models overwrite config, certs, or code",
        "source_commit": "c1178f5",
    },
    {
        "id": "qs-sql-no-interpolation",
        "category": "security",
        "rule": "Never interpolate column names into SQL — regex validate against ^[a-z_]+$",
        "severity": "critical",
        "example_good": "if not re.match(r'^[a-z_]+$', col): raise ValueError",
        "rationale": "Prevents SQL injection via dynamic column names",
        "source_commit": "afe9176",
    },
    {
        "id": "qs-like-wildcard-escape",
        "category": "security",
        "rule": "Escape %, _, and \\ in user-supplied LIKE parameters",
        "severity": "critical",
        "example_good": "escaped = tags.replace('%','\\\\%'); query += \" LIKE ? ESCAPE '\\\\'\"",
        "example_bad": "query += f\" LIKE '%{tags}%'\"",
        "rationale": "Unescaped LIKE wildcards bypass intended search filters",
        "source_commit": "d1b7f83",
    },

    # --- CRITICAL: Architecture (from afe9176) ---
    {
        "id": "qs-typing-protocol",
        "category": "typing",
        "rule": "Use Protocol types in function signatures, not concrete implementations",
        "severity": "critical",
        "example_good": "def __init__(self, db: Database)",
        "example_bad": "def __init__(self, db: SQLiteDatabase)",
        "rationale": "Enables swapping implementations without changing callers — caught in 5 managers",
        "source_commit": "afe9176",
    },
    {
        "id": "qs-async-client",
        "category": "async",
        "rule": "Use AsyncClient in async methods, never sync clients",
        "severity": "critical",
        "example_bad": "async def complete(self): client = anthropic.Anthropic()",
        "example_good": "async def complete(self): client = anthropic.AsyncAnthropic()",
        "rationale": "Sync clients block the event loop — silent performance killer",
        "source_commit": "afe9176",
    },
    {
        "id": "qs-id-length",
        "category": "ids",
        "rule": "Minimum 8-char random suffix for collision safety (36^8 = 2.8T combinations)",
        "severity": "critical",
        "example_bad": "f'pat-{_short_id(4)}'  # 36^4 = 1.7M, collisions at ~1.3K",
        "example_good": "f'pat-{_short_id(8)}'  # 36^8 = 2.8T, collisions at ~1.9M",
        "rationale": "Short IDs hit birthday paradox collisions at modest scale",
        "source_commit": "afe9176",
    },

    # --- CRITICAL: Data integrity (from c1178f5, d1b7f83) ---
    {
        "id": "qs-batch-transaction",
        "category": "data",
        "rule": "Wrap batch inserts/updates in a single transaction",
        "severity": "critical",
        "example_good": "with self.db.transaction(): for m in meetings: self.db.execute(...)",
        "example_bad": "for m in meetings: self.db.execute(...)  # N auto-commits",
        "rationale": "Partial failures leave database in inconsistent state; single transaction is atomic",
        "source_commit": "c1178f5",
    },
    {
        "id": "qs-connection-context-manager",
        "category": "data",
        "rule": "Use context managers for database connections — never manual open/close",
        "severity": "critical",
        "example_good": "with psycopg2.connect(...) as conn:",
        "example_bad": "conn = psycopg2.connect(...); try: ... finally: conn.close()",
        "rationale": "Leaked connections on connect failure exhaust connection pool",
        "source_commit": "c1178f5",
    },

    # --- IMPORTANT: Input validation (from afe9176, d1b7f83, c1178f5) ---
    {
        "id": "qs-validate-enums",
        "category": "validation",
        "rule": "Validate enum-like parameters against a defined set before using them",
        "severity": "important",
        "example_good": "if entry_type not in ENTRY_TYPES: raise ValueError",
        "rationale": "Invalid enum values produce confusing downstream errors instead of clear rejections",
        "source_commit": "afe9176",
    },
    {
        "id": "qs-validate-required-params",
        "category": "validation",
        "rule": "Validate required parameters before executing action — don't pass empty strings to queries",
        "severity": "important",
        "example_good": "if not account: return error('summary requires account')",
        "example_bad": "result = salesos.account_summary('')  # empty string query",
        "rationale": "Empty string params silently return wrong results instead of clear errors",
        "source_commit": "c1178f5",
    },
    {
        "id": "qs-defensive-get",
        "category": "validation",
        "rule": "Use dict.get() for external/untrusted data — never assume keys exist",
        "severity": "important",
        "example_good": "meeting_id = m.get('id'); if not meeting_id: continue",
        "example_bad": "meeting_id = m['id']  # KeyError on malformed input",
        "rationale": "Imported data (JSON files, API responses) may have missing or null fields",
        "source_commit": "c1178f5",
    },

    # --- IMPORTANT: Correctness (from d1b7f83) ---
    {
        "id": "qs-loop-continue-not-break",
        "category": "logic",
        "rule": "In collection loops with size filters, use continue (skip item) not break (stop iteration)",
        "severity": "important",
        "example_good": "if too_large: continue  # skip this one, try smaller ones",
        "example_bad": "if too_large: break  # stops checking remaining items",
        "rationale": "break drops all remaining items — later items may fit the budget",
        "source_commit": "d1b7f83",
    },
    {
        "id": "qs-update-all-related-columns",
        "category": "sql",
        "rule": "When updating a derived column, update all related columns in the same statement",
        "severity": "important",
        "example_good": "UPDATE patterns SET confidence=?, confirmations=? WHERE id=?",
        "example_bad": "UPDATE patterns SET confidence=? WHERE id=?  # confirmations stale",
        "rationale": "Stale related columns cause inconsistent reads — confidence said high but confirmations said 2",
        "source_commit": "afe9176",
    },
    {
        "id": "qs-null-guard-insert-rowid",
        "category": "data",
        "rule": "Guard against None from last_insert_rowid() and similar DB introspection calls",
        "severity": "important",
        "rationale": "DB introspection functions return None on edge cases — unguarded access crashes",
        "source_commit": "d1b7f83",
    },
    {
        "id": "qs-token-budget-floor",
        "category": "logic",
        "rule": "Budget floor should be 0 (nothing), not a minimum allocation that ignores caller intent",
        "severity": "important",
        "example_good": "max(remaining - 500, 0)  # zero means nothing left",
        "example_bad": "max(remaining - 500, 500)  # always allocates 500 even when exhausted",
        "rationale": "Hard minimums override the caller's max_tokens constraint",
        "source_commit": "d1b7f83",
    },

    # --- IMPORTANT: Testing (from d1b7f83, test suite patterns) ---
    {
        "id": "qs-db-close-tests",
        "category": "testing",
        "rule": "Tests using database connections must use yield fixtures and close in teardown",
        "severity": "important",
        "example_good": "db = SQLiteDatabase(':memory:'); yield db; db.close()",
        "example_bad": "return SQLiteDatabase(':memory:')  # connection leaked",
        "rationale": "Leaked connections cause resource exhaustion — caught in 4 test fixtures at once",
        "source_commit": "d1b7f83",
    },
    {
        "id": "qs-testing-coverage",
        "category": "testing",
        "rule": "Every module has a matching test file with at least one test per public method",
        "severity": "important",
        "rationale": "Untested code is unverified code — 14 modules, 16 test files, 99 tests",
    },
    {
        "id": "qs-test-invalid-input",
        "category": "testing",
        "rule": "Test invalid/rejected inputs, not just happy paths",
        "severity": "important",
        "example_good": "test_invalid_entry_type_rejected, test_update_column_regex_validation",
        "rationale": "Happy-path-only tests miss the validation bugs that code review catches",
    },

    # --- MINOR: Style and conventions ---
    {
        "id": "qs-typing-returns",
        "category": "typing",
        "rule": "All public functions have return type annotations, including -> None on __init__",
        "severity": "minor",
        "rationale": "Return types document API contracts — missing -> None on __init__ caught in review",
        "source_commit": "d1b7f83",
    },
    {
        "id": "qs-naming-convention",
        "category": "naming",
        "rule": "snake_case for functions/variables, PascalCase for classes, UPPER_CASE for constants",
        "severity": "minor",
        "rationale": "Consistent naming reduces cognitive load",
    },
    {
        "id": "qs-init-exports",
        "category": "architecture",
        "rule": "__init__.py exports the public API for each package",
        "severity": "minor",
        "rationale": "Explicit exports prevent internal implementation leakage",
    },
]


class StandardsManager:
    """Manages quality standards for GreyMatter code."""

    def __init__(self, db: Database) -> None:
        self.db = db

    def create(
        self,
        standard_id: str,
        category: str,
        rule: str,
        severity: str,
        example_good: str | None = None,
        example_bad: str | None = None,
        rationale: str | None = None,
    ) -> dict[str, Any]:
        """Create a new quality standard."""
        self.db.execute(
            """INSERT INTO quality_standards
               (id, category, rule, severity, example_good, example_bad, rationale)
               VALUES (?, ?, ?, ?, ?, ?, ?)""",
            (standard_id, category, rule, severity, example_good, example_bad, rationale),
        )
        return self.get(standard_id)

    def get(self, standard_id: str) -> dict[str, Any] | None:
        """Get a single standard by ID."""
        return self.db.execute_one(
            "SELECT * FROM quality_standards WHERE id = ?", (standard_id,)
        )

    def get_active(self, severity_filter: list[str] | None = None) -> list[dict[str, Any]]:
        """Get all active standards, optionally filtered by severity."""
        query = "SELECT * FROM quality_standards WHERE active = 1"
        params: list = []
        if severity_filter:
            placeholders = ", ".join("?" for _ in severity_filter)
            query += f" AND severity IN ({placeholders})"
            params.extend(severity_filter)
        query += " ORDER BY severity, category"
        return self.db.execute(query, tuple(params))

    def get_context_payload(self) -> str:
        """Return compact JSON for agent context injection."""
        standards = self.get_active(severity_filter=["critical", "important"])
        payload = [
            {"category": s["category"], "rule": s["rule"], "severity": s["severity"]}
            for s in standards
        ]
        return json.dumps(payload, separators=(",", ":"))

    def deactivate(self, standard_id: str) -> None:
        """Deactivate a standard so it no longer appears in active queries."""
        self.db.execute(
            "UPDATE quality_standards SET active = 0 WHERE id = ?", (standard_id,)
        )

    def seed_defaults(self) -> int:
        """Seed default standards (including anti-laziness). Returns count of new standards created."""
        count = 0
        all_standards = DEFAULT_STANDARDS + ANTI_LAZINESS_STANDARDS
        for std in all_standards:
            existing = self.get(std["id"])
            if existing is None:
                self.create(
                    standard_id=std["id"],
                    category=std["category"],
                    rule=std["rule"],
                    severity=std["severity"],
                    example_good=std.get("example_good"),
                    example_bad=std.get("example_bad"),
                    rationale=std.get("rationale"),
                )
                count += 1
        return count

# --- ANTI-LAZINESS STANDARDS ---
# These standards enforce production-quality code and prevent common
# shortcuts that lead to technical debt. Mined from real correction
# patterns observed in agent output.

ANTI_LAZINESS_STANDARDS = [
    # --- CRITICAL: Anti-laziness enforcement ---
    {
        "id": "qs-no-bare-todo",
        "category": "laziness",
        "rule": "No bare TODO/FIXME without ticket references — every TODO must link to a tracked issue",
        "severity": "critical",
        "example_bad": "# TODO: fix this later",
        "example_good": "# TODO(GH-142): handle retry backoff per RFC in issue",
        "rationale": "Bare TODOs are promises without accountability — they rot and never get done",
    },
    {
        "id": "qs-no-empty-except",
        "category": "laziness",
        "rule": "No empty except/catch blocks — every exception must be logged or re-raised",
        "severity": "critical",
        "example_bad": "except Exception:\\n    pass",
        "example_good": "except ValueError as e:\\n    logger.warning(\"Invalid input: %s\", e)\\n    raise",
        "rationale": "Empty except blocks silently swallow errors — the #1 cause of impossible-to-debug production failures",
    },
    {
        "id": "qs-no-stub-functions",
        "category": "laziness",
        "rule": "No stub functions with bare pass/NotImplementedError without explanation of what is missing and why",
        "severity": "critical",
        "example_bad": "def process_data(data):\\n    pass  # implement later",
        "example_good": "def process_data(data):\\n    raise NotImplementedError(\\n        \"Blocked on upstream API v2 release (GH-98). \"\\n        \"Needs: parse response.metadata.enrichments field\"\\n    )",
        "rationale": "Stub functions are lies in the API contract — callers assume the function works until production explodes",
    },
    {
        "id": "qs-no-print-in-production",
        "category": "laziness",
        "rule": "No print() in production code — use the logging module with appropriate levels",
        "severity": "important",
        "example_bad": "print(f\"Processing {item}\")",
        "example_good": "logger.info(\"Processing %s\", item)",
        "rationale": "print() cannot be filtered, leveled, or routed — it pollutes stdout and breaks structured logging pipelines",
    },
    {
        "id": "qs-no-commented-out-code",
        "category": "laziness",
        "rule": "No commented-out code blocks (3+ consecutive lines) — delete it, git remembers",
        "severity": "important",
        "example_bad": "# def old_handler(req):\\n#     result = db.query(req)\\n#     return result",
        "example_good": "# Removed old_handler — replaced by async_handler in commit abc123",
        "rationale": "Commented-out code is noise that decays — it confuses readers and never gets uncommented correctly",
    },
]
