"""
aicontract — Smart Extensions
Rıza, Murat, Sevda, Nilüfer, Tarık, Selin, Leyla, Hamit'in katkıları.
"""

import ast
import os
import re
import sys
import json
import time
import math
import inspect
import textwrap
import functools
from typing import Optional, Callable
from dataclasses import dataclass, field, asdict


ANSI = {
    "red": "\033[91m",
    "yellow": "\033[93m",
    "green": "\033[92m",
    "blue": "\033[94m",
    "cyan": "\033[96m",
    "bold": "\033[1m",
    "dim": "\033[2m",
    "reset": "\033[0m",
}


def _c(t, col):
    return f"{ANSI.get(col, '')}{t}{ANSI['reset']}" if sys.stdout.isatty() else t


@dataclass
class FunctionMetrics:
    name: str
    lines: int = 0
    cyclomatic: int = 1
    params: int = 0
    returns: int = 0
    nested_depth: int = 0
    has_docstring: bool = False
    has_type_hints: bool = False
    has_error_handling: bool = False
    filepath: str = ""

    def to_vector(self) -> list[float]:
        return [
            self.lines,
            self.cyclomatic,
            self.params,
            self.returns,
            self.nested_depth,
            1.0 if self.has_docstring else 0.0,
            1.0 if self.has_type_hints else 0.0,
            1.0 if self.has_error_handling else 0.0,
        ]


@dataclass
class ProjectBaseline:
    function_count: int = 0
    means: list[float] = field(default_factory=lambda: [0.0] * 8)
    stds: list[float] = field(default_factory=lambda: [1.0] * 8)
    common_patterns: list[str] = field(default_factory=list)
    violation_history: dict = field(default_factory=dict)
    created_at: float = field(default_factory=time.time)
    version: str = "1"


def _extract_metrics(source: str, filepath: str = "") -> list[FunctionMetrics]:
    try:
        tree = ast.parse(textwrap.dedent(source))
    except SyntaxError:
        return []

    results = []
    for node in ast.walk(tree):
        if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
            continue

        m = FunctionMetrics(name=node.name, filepath=filepath)
        m.lines = (node.end_lineno or node.lineno) - node.lineno + 1
        m.params = len(node.args.args)

        for child in ast.walk(node):
            if isinstance(
                child,
                (
                    ast.If,
                    ast.While,
                    ast.For,
                    ast.ExceptHandler,
                    ast.With,
                    ast.Assert,
                    ast.comprehension,
                ),
            ):
                m.cyclomatic += 1
            if isinstance(child, ast.BoolOp):
                m.cyclomatic += len(child.values) - 1
            if isinstance(child, ast.Return):
                m.returns += 1

        has_arg_hints = all(
            a.annotation is not None for a in node.args.args if a.arg != "self"
        )
        m.has_type_hints = has_arg_hints and node.returns is not None

        m.has_docstring = (
            node.body
            and isinstance(node.body[0], ast.Expr)
            and isinstance(node.body[0].value, ast.Constant)
        )

        m.has_error_handling = any(
            isinstance(child, ast.Try) for child in ast.walk(node)
        )
        m.nested_depth = _max_depth(node)

        results.append(m)

    return results


def _max_depth(node: ast.AST, current: int = 0) -> int:
    block_nodes = (ast.If, ast.For, ast.While, ast.With, ast.Try)
    max_d = current
    for child in ast.iter_child_nodes(node):
        if isinstance(child, block_nodes):
            max_d = max(max_d, _max_depth(child, current + 1))
    return max_d


def _build_baseline(metrics_list: list[FunctionMetrics]) -> ProjectBaseline:
    if not metrics_list:
        return ProjectBaseline()

    vectors = [m.to_vector() for m in metrics_list]
    n = len(vectors)
    dim = len(vectors[0])

    means = [sum(v[i] for v in vectors) / n for i in range(dim)]
    stds = []
    for i in range(dim):
        variance = sum((v[i] - means[i]) ** 2 for v in vectors) / max(n - 1, 1)
        stds.append(math.sqrt(variance) or 1.0)

    return ProjectBaseline(function_count=n, means=means, stds=stds)


def _z_score(metrics: FunctionMetrics, baseline: ProjectBaseline) -> float:
    if baseline.function_count < 5:
        return 0.0
    vec = metrics.to_vector()
    z_scores = [
        abs((vec[i] - baseline.means[i]) / baseline.stds[i]) for i in range(len(vec))
    ]
    return sum(z_scores) / len(z_scores)


INTENT_VOCABULARY = {
    "get": ("READ", "Veri okuma"),
    "fetch": ("READ", "Veri çekme"),
    "find": ("READ", "Arama"),
    "load": ("READ", "Yükleme"),
    "read": ("READ", "Okuma"),
    "list": ("READ", "Listeleme"),
    "search": ("READ", "Arama"),
    "create": ("WRITE", "Oluşturma"),
    "add": ("WRITE", "Ekleme"),
    "insert": ("WRITE", "Ekleme"),
    "save": ("WRITE", "Kaydetme"),
    "store": ("WRITE", "Depolama"),
    "write": ("WRITE", "Yazma"),
    "update": ("MUTATE", "Güncelleme"),
    "modify": ("MUTATE", "Değiştirme"),
    "edit": ("MUTATE", "Düzenleme"),
    "patch": ("MUTATE", "Yama"),
    "set": ("MUTATE", "Ayarlama"),
    "delete": ("DELETE", "Silme"),
    "remove": ("DELETE", "Kaldırma"),
    "drop": ("DELETE", "Düşürme"),
    "purge": ("DELETE", "Temizleme"),
    "validate": ("VALIDATE", "Doğrulama"),
    "check": ("VALIDATE", "Kontrol"),
    "verify": ("VALIDATE", "Doğrulama"),
    "parse": ("TRANSFORM", "Ayrıştırma"),
    "convert": ("TRANSFORM", "Dönüştürme"),
    "format": ("TRANSFORM", "Biçimlendirme"),
    "calculate": ("COMPUTE", "Hesaplama"),
    "compute": ("COMPUTE", "Hesaplama"),
    "process": ("COMPUTE", "İşleme"),
    "send": ("IO", "Gönderme"),
    "notify": ("IO", "Bildirim"),
    "emit": ("IO", "Yayma"),
    "login": ("AUTH", "Kimlik doğrulama"),
    "auth": ("AUTH", "Yetkilendirme"),
    "authorize": ("AUTH", "Yetkilendirme"),
    "encrypt": ("SECURITY", "Şifreleme"),
    "hash": ("SECURITY", "Hash"),
    "sign": ("SECURITY", "İmzalama"),
}


@dataclass
class Intent:
    category: str
    description: str
    confidence: float
    signals: list[str]
    expectations: list[str]


def _get_fn_info(fn_or_source):
    """Extract function name and docstring from callable or source."""
    if callable(fn_or_source):
        try:
            return (
                fn_or_source.__name__,
                fn_or_source.__doc__ or "",
                inspect.getsource(fn_or_source),
            )
        except Exception:
            return "", "", ""
    else:
        source = textwrap.dedent(fn_or_source)
        try:
            tree = ast.parse(source)
            fn_nodes = [
                n
                for n in ast.walk(tree)
                if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef))
            ]
            if fn_nodes:
                return fn_nodes[0].name, ast.get_docstring(fn_nodes[0]) or "", source
        except Exception:
            pass
        return "", "", source


def _scan_intent_signals(name: str, doc: str, source: str) -> tuple[dict, list[str]]:
    """Scan for intent signals in name, docstring, and parameters."""
    candidates = {}
    signals = []

    name_words = re.split(r"[_\s]+", name.lower())
    for word in name_words:
        if word in INTENT_VOCABULARY:
            cat, _ = INTENT_VOCABULARY[word]
            candidates[cat] = candidates.get(cat, 0) + 2
            signals.append(f"isim:{word}")

    if doc:
        for word in re.findall(r"\w+", doc.lower()):
            if word in INTENT_VOCABULARY:
                cat, _ = INTENT_VOCABULARY[word]
                candidates[cat] = candidates.get(cat, 0) + 1
                signals.append(f"doc:{word}")

    try:
        tree = ast.parse(source)
        for node in ast.walk(tree):
            if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
                for arg in node.args.args:
                    for word in re.split(r"[_\s]+", arg.arg.lower()):
                        if word in INTENT_VOCABULARY:
                            cat, _ = INTENT_VOCABULARY[word]
                            candidates[cat] = candidates.get(cat, 0) + 1
    except Exception:
        pass

    return candidates, signals


def _get_expectations(category: str) -> list[str]:
    """Get expectations for a given intent category."""
    return {
        "READ": ["None dönebilmeli", "yan etki olmamalı", "idempotent olmalı"],
        "WRITE": ["hata fırlatabilmeli", "rollback desteği"],
        "MUTATE": ["önceki değeri döndürmeli veya loglamalı"],
        "DELETE": ["soft delete tercih edilmeli", "yetki kontrolü"],
        "VALIDATE": ["bool veya exception dönmeli"],
        "AUTH": ["timing-safe karşılaştırma", "brute force koruması"],
        "SECURITY": ["sabit zamanlı çalışmalı", "hata mesajı bilgi sızdırmamalı"],
        "TRANSFORM": ["saf fonksiyon olmalı", "None input handle edilmeli"],
        "COMPUTE": ["taşma kontrolü", "saf fonksiyon tercih edilmeli"],
        "IO": ["timeout olmalı", "retry mantığı"],
    }.get(category, ["tip hint olmalı", "docstring olmalı"])


def extract_intent(fn_or_source) -> Intent:
    """
    Extract intent from function name, docstring, or source code.

    Args:
        fn_or_source: Callable function or source code string

    Returns:
        Intent with category, description, confidence, signals, and expectations
    """
    name, doc, source = _get_fn_info(fn_or_source)
    candidates, signals = _scan_intent_signals(name, doc, source)

    if not candidates:
        return Intent("UNKNOWN", "Sinyal bulunamadı", 0.0, signals, [])

    best_cat = max(candidates, key=candidates.get)
    total_signals = sum(candidates.values())
    confidence = min(candidates[best_cat] / max(total_signals, 1), 1.0)

    expectations = _get_expectations(best_cat)
    _, description = INTENT_VOCABULARY.get(
        name.split("_")[0] if name else "", ("", best_cat)
    )

    return Intent(
        category=best_cat,
        description=description or best_cat,
        confidence=round(confidence, 2),
        signals=list(set(signals)),
        expectations=expectations,
    )


@dataclass
class DiffResult:
    changed: list[str] = field(default_factory=list)
    broken: list[str] = field(default_factory=list)
    improved: list[str] = field(default_factory=list)
    risk: str = "LOW"

    def __str__(self):
        risk_color = {
            "LOW": "green",
            "MEDIUM": "yellow",
            "HIGH": "red",
            "CRITICAL": "red",
        }.get(self.risk, "dim")
        lines = [
            f"\n{_c('aicontract/diff', 'bold')} — risk: {_c(self.risk, risk_color)}"
        ]
        if self.broken:
            lines.append(_c("  Kırılan kontratlar:", "red"))
            for b in self.broken:
                lines.append(f"    ✗ {b}")
        if self.changed:
            lines.append(_c("  Değişiklikler:", "yellow"))
            for c in self.changed:
                lines.append(f"    ~ {c}")
        if self.improved:
            lines.append(_c("  İyileşmeler:", "green"))
            for i in self.improved:
                lines.append(f"    ✓ {i}")
        if not self.broken and not self.changed:
            lines.append(_c("  Anlamlı değişiklik yok.", "dim"))
        return "\n".join(lines)


def diff(old_source: str, new_source: str, *, silent: bool = False) -> DiffResult:
    result = DiffResult()

    try:
        old_tree = ast.parse(textwrap.dedent(old_source))
        new_tree = ast.parse(textwrap.dedent(new_source))
    except SyntaxError as e:
        result.broken.append(f"Sözdizimi hatası: {e}")
        result.risk = "CRITICAL"
        if not silent:
            print(result)
        return result

    old_fns = {
        n.name: n
        for n in ast.walk(old_tree)
        if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef))
    }
    new_fns = {
        n.name: n
        for n in ast.walk(new_tree)
        if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef))
    }

    for name in old_fns:
        if name not in new_fns:
            result.broken.append(f"'{name}' fonksiyonu silindi")

    for name, new_fn in new_fns.items():
        old_fn = old_fns.get(name)
        if old_fn is None:
            result.changed.append(f"'{name}' yeni eklendi")
            continue

        old_args = [a.arg for a in old_fn.args.args]
        new_args = [a.arg for a in new_fn.args.args]
        if old_args != new_args:
            result.broken.append(
                f"'{name}' parametre listesi değişti: {old_args} → {new_args}"
            )

        old_ret = ast.unparse(old_fn.returns) if old_fn.returns else None
        new_ret = ast.unparse(new_fn.returns) if new_fn.returns else None
        if old_ret != new_ret:
            if old_ret and not new_ret:
                result.broken.append(f"'{name}' dönüş tipi kaldırıldı ({old_ret})")
            elif not old_ret and new_ret:
                result.improved.append(f"'{name}' dönüş tipi eklendi ({new_ret})")
            else:
                result.broken.append(
                    f"'{name}' dönüş tipi değişti: {old_ret} → {new_ret}"
                )

        old_async = isinstance(old_fn, ast.AsyncFunctionDef)
        new_async = isinstance(new_fn, ast.AsyncFunctionDef)
        if old_async != new_async:
            change = "sync→async" if new_async else "async→sync"
            result.broken.append(f"'{name}' çağrı tipi değişti ({change})")

        old_m = _extract_metrics(old_source)
        new_m = _extract_metrics(new_source)
        old_fn_m = next((m for m in old_m if m.name == name), None)
        new_fn_m = next((m for m in new_m if m.name == name), None)
        if old_fn_m and new_fn_m:
            if new_fn_m.cyclomatic > old_fn_m.cyclomatic + 3:
                result.broken.append(
                    f"'{name}' karmaşıklık arttı: {old_fn_m.cyclomatic}→{new_fn_m.cyclomatic}"
                )
            elif new_fn_m.cyclomatic < old_fn_m.cyclomatic:
                result.improved.append(
                    f"'{name}' karmaşıklık azaldı: {old_fn_m.cyclomatic}→{new_fn_m.cyclomatic}"
                )
            if new_fn_m.has_type_hints and not old_fn_m.has_type_hints:
                result.improved.append(f"'{name}' tip hint eklendi")
            if new_fn_m.has_docstring and not old_fn_m.has_docstring:
                result.improved.append(f"'{name}' docstring eklendi")
            if new_fn_m.has_error_handling and not old_fn_m.has_error_handling:
                result.improved.append(f"'{name}' hata yönetimi eklendi")

    if result.broken:
        result.risk = "CRITICAL" if len(result.broken) >= 3 else "HIGH"
    elif result.changed:
        result.risk = "MEDIUM"
    else:
        result.risk = "LOW"

    if not silent:
        print(result)

    return result


_baseline: Optional[ProjectBaseline] = None
_baseline_path: str = ".aicontract_baseline.json"


def learn(project_dir: str = ".", *, save: bool = True) -> ProjectBaseline:
    global _baseline

    all_metrics = []
    py_files = 0
    t0 = time.time()

    skip_dirs = {
        "__pycache__",
        ".git",
        ".venv",
        "venv",
        "env",
        "node_modules",
        "dist",
        "build",
        ".mypy_cache",
    }

    for root, dirs, files in os.walk(project_dir):
        dirs[:] = [d for d in dirs if d not in skip_dirs]
        for fname in files:
            if not fname.endswith(".py"):
                continue
            py_files += 1
            try:
                with open(os.path.join(root, fname)) as f:
                    src = f.read()
                metrics = _extract_metrics(src, fname)
                all_metrics.extend(metrics)
            except Exception:
                pass

    _baseline = _build_baseline(all_metrics)

    elapsed = (time.time() - t0) * 1000
    print(
        f"{_c('aicontract/learn', 'bold')} — "
        f"{py_files} dosya · {len(all_metrics)} fonksiyon · "
        f"{elapsed:.0f}ms\n"
        f"  Ortalama karmaşıklık: {_baseline.means[1]:.1f} · "
        f"Ortalama uzunluk: {_baseline.means[0]:.1f} satır"
    )

    if save:
        try:
            with open(os.path.join(project_dir, _baseline_path), "w") as f:
                json.dump(asdict(_baseline), f, indent=2)
        except Exception:
            pass

    return _baseline


def load_baseline(path: str = None):
    global _baseline
    p = path or _baseline_path
    if os.path.exists(p):
        with open(p) as f:
            data = json.load(f)
        _baseline = ProjectBaseline(**data)
        return _baseline
    return None


@dataclass
class AnomalyResult:
    z_score: float
    level: str
    signals: list[str]

    def __str__(self):
        color = {"NORMAL": "green", "SUSPICIOUS": "yellow", "ANOMALY": "red"}.get(
            self.level, "dim"
        )
        out = f"  anomaly: {_c(self.level, color)} (z={self.z_score:.2f})"
        for s in self.signals:
            out += f"\n    · {s}"
        return out


def anomaly(fn_or_source, *, silent: bool = False) -> AnomalyResult:
    global _baseline

    if _baseline is None:
        loaded = load_baseline()
        if loaded is None:
            return AnomalyResult(
                0.0, "NORMAL", ["Baseline yok — önce aicontract.learn('.') çalıştır"]
            )

    if callable(fn_or_source):
        try:
            source = inspect.getsource(fn_or_source)
        except Exception:
            return AnomalyResult(0.0, "NORMAL", ["Kaynak kod alınamadı"])
    else:
        source = fn_or_source

    metrics_list = _extract_metrics(textwrap.dedent(source))
    if not metrics_list:
        return AnomalyResult(0.0, "NORMAL", [])

    m = metrics_list[0]
    z = _z_score(m, _baseline)

    signals = []
    labels = [
        "satır sayısı",
        "karmaşıklık",
        "parametre sayısı",
        "return sayısı",
        "iç içe derinlik",
        "docstring",
        "tip hint",
        "hata yönetimi",
    ]
    vec = m.to_vector()
    for i, (val, mean, std, label) in enumerate(
        zip(vec, _baseline.means, _baseline.stds, labels)
    ):
        z_i = abs(val - mean) / max(std, 0.1)
        if z_i > 2.0:
            direction = "yüksek" if val > mean else "düşük"
            signals.append(f"{label} {direction}: {val:.0f} (ortalama: {mean:.1f})")

    level = "NORMAL" if z < 1.5 else "SUSPICIOUS" if z < 2.5 else "ANOMALY"
    result = AnomalyResult(z_score=round(z, 2), level=level, signals=signals)

    if not silent:
        print(f"\n{_c('aicontract/anomaly', 'bold')} → {_c(m.name, 'blue')}")
        print(result)

    return result


class ContractError(Exception):
    pass


def contract(
    fn: Callable = None,
    *,
    pure: bool = False,
    max_complexity: int = None,
    max_lines: int = None,
    returns_not_none: bool = False,
    requires_docstring: bool = False,
    requires_type_hints: bool = False,
    strict: bool = False,
):
    def decorator(func):
        violations = []

        try:
            source = inspect.getsource(func)
            tree = ast.parse(textwrap.dedent(source))
            metrics_list = _extract_metrics(source)
            m = metrics_list[0] if metrics_list else None
        except Exception as e:
            if strict:
                raise ContractError(f"Kontrat kontrolü başarısız: {e}")
            return func

        if m:
            if max_complexity and m.cyclomatic > max_complexity:
                violations.append(
                    f"Karmaşıklık limiti aşıldı: {m.cyclomatic} > {max_complexity}"
                )
            if max_lines and m.lines > max_lines:
                violations.append(f"Satır limiti aşıldı: {m.lines} > {max_lines}")
            if requires_docstring and not m.has_docstring:
                violations.append("Docstring zorunlu ama eksik")
            if requires_type_hints and not m.has_type_hints:
                violations.append("Tip hint zorunlu ama eksik")

        if pure:
            for node in ast.walk(tree):
                if isinstance(node, ast.Global):
                    violations.append(
                        f"pure=True ama global kullanılıyor: {node.names}"
                    )

        if violations:
            print(
                f"\n{_c('aicontract/contract', 'bold')} → {_c(func.__name__, 'blue')}"
            )
            for v in violations:
                print(f"  {_c('✗', 'red')} {v}")
            print()
            if strict:
                raise ContractError(
                    f"@contract: '{func.__name__}' {len(violations)} kural ihlali"
                )

        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            result = func(*args, **kwargs)
            if returns_not_none and result is None:
                raise ContractError(
                    f"'{func.__name__}' None döndü ama returns_not_none=True"
                )
            return result

        wrapper._contract_violations = violations
        wrapper._contract_rules = {
            "pure": pure,
            "max_complexity": max_complexity,
            "max_lines": max_lines,
            "returns_not_none": returns_not_none,
        }
        return wrapper

    if fn is not None:
        return decorator(fn)
    return decorator


class _WatchContext:
    def __init__(self, strict: bool = False, silent: bool = False):
        self.strict = strict
        self.silent = silent
        self._pre_fns: set = set()

    def __enter__(self):
        frame = sys._getframe(1)
        self._pre_fns = {k for k, v in frame.f_locals.items() if callable(v)}
        self._frame = frame
        return self

    def __exit__(self, *args):
        try:
            from .inline import check as inline_check
        except ImportError:
            from inline import check as inline_check

        frame = self._frame
        post_fns = {k: v for k, v in frame.f_locals.items() if callable(v)}
        new_fns = {k: v for k, v in post_fns.items() if k not in self._pre_fns}

        for name, fn in new_fns.items():
            try:
                source = inspect.getsource(fn)
                result = inline_check(source)
                violations = result.violations
                if violations and not self.silent:
                    print(
                        f"\n{_c('aicontract/watch', 'bold')} → {_c(name, 'blue')}: "
                        f"{len(violations)} ihlal"
                    )
                    for v in violations[:3]:
                        sev_color = "red" if v.severity == "error" else "yellow"
                        print(
                            f"  [{_c(v.severity, sev_color)}] {v.rule_id}: {v.message}"
                        )
                if self.strict and any(v.severity == "error" for v in violations):
                    raise ContractError(f"watch(strict=True): '{name}' hata içeriyor")
            except (OSError, TypeError):
                pass
        return False


def watch(*, strict: bool = False, silent: bool = False) -> _WatchContext:
    return _WatchContext(strict=strict, silent=silent)


# ════════════════════════════════════════════════════════════════
# 7. SIDE EFFECT DETECTION — Kemal'ın bulgusu
# ════════════════════════════════════════════════════════════════

_IMPURE_MODULES = {
    "requests",
    "httpx",
    "urllib",
    "urllib2",
    "urllib3",
    "socket",
    "subprocess",
    "os",
    "sys",
    "shutil",
    "pathlib",
    "open",
    "print",
    "logging",
    "random",
    "time",
    "datetime",
    "threading",
    "multiprocessing",
    "asyncio",
    "sqlalchemy",
    "pymongo",
    "redis",
    "psycopg2",
    "boto3",
    "paramiko",
    "smtplib",
    "ftplib",
}

_IMPURE_CALLS = {"print", "input", "exit", "quit", "breakpoint"}


def check_side_effects(source: str) -> list[str]:
    """
    Kodun yan etkilerini kontrol eder.

    Returns:
        List of side effect descriptions
    """
    try:
        tree = ast.parse(textwrap.dedent(source))
    except SyntaxError:
        return []

    violations = []

    for fn_node in ast.walk(tree):
        if not isinstance(fn_node, (ast.FunctionDef, ast.AsyncFunctionDef)):
            continue
        fn_name = fn_node.name

        for node in ast.walk(fn_node):
            # global / nonlocal
            if isinstance(node, ast.Global):
                violations.append(
                    f"'{fn_name}': global {node.names} — dışarıdan durum okuyor/yazıyor"
                )
            if isinstance(node, ast.Nonlocal):
                violations.append(
                    f"'{fn_name}': nonlocal {node.names} — kapalı değişkeni değiştiriyor"
                )

            # Fonksiyon içinde import
            if isinstance(node, ast.Import):
                mods = [a.name for a in node.names]
                bad = [m for m in mods if m.split(".")[0] in _IMPURE_MODULES]
                if bad:
                    violations.append(
                        f"'{fn_name}': import {bad} — yan etkili modül yükleniyor"
                    )

            if isinstance(node, ast.ImportFrom):
                mod = (node.module or "").split(".")[0]
                if mod in _IMPURE_MODULES:
                    violations.append(
                        f"'{fn_name}': from {node.module} import ... — yan etkili modül"
                    )

            # Yan etkili modül metot çağrıları
            if isinstance(node, ast.Call):
                func = node.func
                if isinstance(func, ast.Attribute):
                    if isinstance(func.value, ast.Name):
                        if func.value.id in _IMPURE_MODULES:
                            violations.append(
                                f"'{fn_name}': {func.value.id}.{func.attr}() — yan etkili modül çağrısı"
                            )
                elif isinstance(func, ast.Name):
                    if func.id in _IMPURE_CALLS:
                        violations.append(
                            f"'{fn_name}': {func.id}() — yan etkili built-in"
                        )

        # Mutable default argument
        for default in fn_node.args.defaults + fn_node.args.kw_defaults:
            if default is None:
                continue
            if isinstance(default, (ast.List, ast.Dict, ast.Set)):
                violations.append(
                    f"'{fn_node.name}': mutable default argument — her çağrıda aynı nesne paylaşılır"
                )

    return violations


__all__ = [
    "diff",
    "learn",
    "load_baseline",
    "anomaly",
    "extract_intent",
    "contract",
    "watch",
    "check_side_effects",
    "Intent",
    "DiffResult",
    "AnomalyResult",
    "FunctionMetrics",
    "ProjectBaseline",
    "ContractError",
]
