"""
aicontract — SQL Adapter
Schema-aware SQL analysis. YZ şemayı bilmez, biz enjekte ederiz.

Kullanım:
    from aicontract.sql_adapter import SQLAdapter
    adapter = SQLAdapter(connection_string="postgresql://...")
    violations = adapter.check(sql_source_code)
    context   = adapter.build_context()   # YZ'ye verilecek bağlam bloğu
"""

import re
import json
from dataclasses import dataclass, field
from typing import Optional

# İsteğe bağlı DB bağlantısı — yoksa sadece statik analiz çalışır
# ruff: noqa: F401 (optional dependency)
try:
    import sqlalchemy  # noqa: F401
    from sqlalchemy import create_engine, inspect, text  # noqa: F401

    HAS_SQLALCHEMY = True
except ImportError:
    HAS_SQLALCHEMY = False


@dataclass
class SQLViolation:
    rule_id: str
    severity: str  # error | warning | info
    message: str
    line: Optional[int] = None
    suggestion: str = ""


@dataclass
class SchemaInfo:
    tables: dict = field(default_factory=dict)
    # tables = {"users": {"columns": ["id","email"], "indexes": ["email_idx"]}}


class SQLAdapter:
    """
    YZ'nin yazdığı Python kodundaki SQL sorgularını yakalar,
    gerçek DB şemasına karşı doğrular.
    """

    IRREVERSIBLE_PATTERNS = [
        (r"\bDROP\s+TABLE\b", "SQL001", "DROP TABLE — geri dönüşsüz!"),
        (r"\bDROP\s+COLUMN\b", "SQL001", "DROP COLUMN — veri kaybı riski"),
        (r"\bTRUNCATE\b", "SQL001", "TRUNCATE — tüm satırlar silinir"),
        (
            r"\bDELETE\s+FROM\b(?!\s+WHERE)",
            "SQL002",
            "WHERE'siz DELETE — tüm tablo silinir",
        ),
    ]

    INJECTION_PATTERNS = [
        (r'f["\'].*SELECT.*\{', "SQL003", "f-string ile SQL — injection riski"),
        (r"%s.*SELECT", "SQL003", "String format ile SQL — injection riski"),
        (r'"\s*\+\s*\w+\s*\+\s*"', "SQL003", "String concat ile SQL — injection riski"),
    ]

    N_PLUS_ONE_PATTERN = re.compile(
        r"for\s+\w+\s+in\s+.+:\s*\n(?:\s+.*\n)*?\s+(?:db|session|conn|cursor)\.",
        re.MULTILINE,
    )

    def __init__(
        self, connection_string: Optional[str] = None, schema_file: Optional[str] = None
    ):
        self.connection_string = connection_string
        self.schema: SchemaInfo = SchemaInfo()
        self._engine = None

        if schema_file:
            self._load_schema_from_file(schema_file)
        elif connection_string and HAS_SQLALCHEMY:
            self._load_schema_from_db()

    # ── Şema yükleme ────────────────────────────────────────────────

    def _load_schema_from_file(self, path: str):
        with open(path) as f:
            data = json.load(f)
        self.schema.tables = data.get("tables", {})

    def _load_schema_from_db(self):
        """Gerçek DB'den şema çek."""
        try:
            engine = create_engine(self.connection_string)
            inspector = inspect(engine)
            for table_name in inspector.get_table_names():
                cols = [c["name"] for c in inspector.get_columns(table_name)]
                idxs = [i["name"] for i in inspector.get_indexes(table_name)]
                self.schema.tables[table_name] = {
                    "columns": cols,
                    "indexes": idxs,
                }
        except Exception as e:
            print(f"[aicontract/sql] DB bağlantısı kurulamadı: {e}")

    def export_schema(self, path: str):
        """Şemayı JSON'a yaz — bağlantı olmayan ortamlarda kullan."""
        with open(path, "w") as f:
            json.dump({"tables": self.schema.tables}, f, indent=2)

    # ── Analiz ──────────────────────────────────────────────────────

    def check(self, source: str) -> list[SQLViolation]:
        violations = []
        violations += self._check_irreversible(source)
        violations += self._check_injection(source)
        violations += self._check_n_plus_one(source)
        violations += self._check_unknown_tables(source)
        violations += self._check_unknown_columns(source)
        violations += self._check_missing_where_update(source)
        return violations

    def _check_irreversible(self, source: str) -> list[SQLViolation]:
        out = []
        for pattern, rule_id, msg in self.IRREVERSIBLE_PATTERNS:
            for m in re.finditer(pattern, source, re.IGNORECASE):
                line = source[: m.start()].count("\n") + 1
                out.append(
                    SQLViolation(
                        rule_id=rule_id,
                        severity="error",
                        message=msg,
                        line=line,
                        suggestion="Önce COUNT(*) ile kaç satır etkileneceğini doğrula, sonra çalıştır.",
                    )
                )
        return out

    def _check_injection(self, source: str) -> list[SQLViolation]:
        out = []
        for pattern, rule_id, msg in self.INJECTION_PATTERNS:
            for m in re.finditer(pattern, source, re.IGNORECASE | re.DOTALL):
                line = source[: m.start()].count("\n") + 1
                out.append(
                    SQLViolation(
                        rule_id=rule_id,
                        severity="error",
                        message=msg,
                        line=line,
                        suggestion="Parametre bağlama kullan: cursor.execute(sql, (value,))",
                    )
                )
        return out

    def _check_n_plus_one(self, source: str) -> list[SQLViolation]:
        out = []
        for m in self.N_PLUS_ONE_PATTERN.finditer(source):
            line = source[: m.start()].count("\n") + 1
            out.append(
                SQLViolation(
                    rule_id="SQL004",
                    severity="warning",
                    message="Loop içinde DB sorgusu — muhtemel N+1 problemi",
                    line=line,
                    suggestion="JOIN veya batch SELECT kullan.",
                )
            )
        return out

    def _check_unknown_tables(self, source: str) -> list[SQLViolation]:
        if not self.schema.tables:
            return []
        out = []
        pattern = re.compile(
            r'\b(?:FROM|JOIN|INTO|UPDATE)\s+([`"\']?)(\w+)\1', re.IGNORECASE
        )
        for m in pattern.finditer(source):
            table = m.group(2).lower()
            if table not in {t.lower() for t in self.schema.tables}:
                line = source[: m.start()].count("\n") + 1
                out.append(
                    SQLViolation(
                        rule_id="SQL005",
                        severity="error",
                        message=f"Tablo bulunamadı: '{table}'",
                        line=line,
                        suggestion=f"Mevcut tablolar: {', '.join(list(self.schema.tables)[:5])}",
                    )
                )
        return out

    def _check_unknown_columns(self, source: str) -> list[SQLViolation]:
        """SELECT col FROM table — col gerçekten var mı?"""
        if not self.schema.tables:
            return []
        out = []
        # Basit: SELECT col1, col2 FROM tablo kalıbını yakala
        pattern = re.compile(r"SELECT\s+([\w\s,.*]+?)\s+FROM\s+(\w+)", re.IGNORECASE)
        for m in pattern.finditer(source):
            cols_raw, table = m.group(1), m.group(2).lower()
            if cols_raw.strip() == "*":
                continue
            table_info = self.schema.tables.get(table)
            if not table_info:
                continue
            known_cols = {c.lower() for c in table_info["columns"]}
            for col in re.split(r",\s*", cols_raw):
                col = col.strip().lower().split(".")[-1]  # tablo.kolon → kolon
                if col and col != "*" and col not in known_cols:
                    line = source[: m.start()].count("\n") + 1
                    out.append(
                        SQLViolation(
                            rule_id="SQL006",
                            severity="error",
                            message=f"Kolon bulunamadı: '{col}' in '{table}'",
                            line=line,
                            suggestion=f"'{table}' kolonları: {', '.join(list(known_cols)[:6])}",
                        )
                    )
        return out

    def _check_missing_where_update(self, source: str) -> list[SQLViolation]:
        out = []
        for m in re.finditer(r"\bUPDATE\s+\w+\s+SET\b", source, re.IGNORECASE):
            # Aynı satırda veya birkaç satırda WHERE var mı?
            snippet = source[m.start() : m.start() + 300]
            if not re.search(r"\bWHERE\b", snippet, re.IGNORECASE):
                line = source[: m.start()].count("\n") + 1
                out.append(
                    SQLViolation(
                        rule_id="SQL007",
                        severity="error",
                        message="WHERE'siz UPDATE — tüm satırlar güncellenir",
                        line=line,
                        suggestion="WHERE koşulu ekle veya LIMIT 0 ile test et.",
                    )
                )
        return out

    # ── Context Generator ────────────────────────────────────────────

    def build_context(self) -> str:
        """
        YZ'ye verilecek YAML bağlam bloğu üretir.
        aicontract wrap komutu bunu prompt'un başına ekler.
        """
        if not self.schema.tables:
            return "# aicontract/sql: şema bilgisi yok — connection_string ver\n"

        lines = ["# aicontract/sql — DB şeması (YZ için otomatik enjekte)"]
        lines.append("schema:")
        for table, info in self.schema.tables.items():
            lines.append(f"  {table}:")
            lines.append(f"    columns: {info['columns']}")
            if info.get("indexes"):
                lines.append(f"    indexes: {info['indexes']}")

        lines.append("\nkurallar:")
        lines.append("  - WHERE olmadan DELETE/UPDATE YAZMA")
        lines.append("  - f-string ile SQL OLUŞTURMA — parametre bağlama kullan")
        lines.append("  - Loop içinde sorgu YAZMA — batch/join kullan")
        lines.append("  - Var olmayan tablo/kolon KULLANMA")
        return "\n".join(lines)
