"""Base rules — Temel 22 kural."""

import ast
import re
from dataclasses import dataclass
from typing import Optional

try:
    from ..core.registry import FunctionContract  # noqa: F401 (backward compat)
except ImportError:
    pass


@dataclass
class Violation:
    rule_id: str
    severity: str
    confidence: float
    message: str
    location: str
    suggestion: str
    func_name: Optional[str] = None


def _parse(source):
    try:
        return ast.parse(source)
    except SyntaxError:
        return None


def _has_call(node, names):
    for child in ast.walk(node):
        if isinstance(child, ast.Call):
            fn = child.func
            name = (
                fn.attr
                if isinstance(fn, ast.Attribute)
                else (fn.id if isinstance(fn, ast.Name) else "")
            )
            if name.lower() in names:
                return True
    return False


def _count_branches(node):
    return sum(
        1
        for child in ast.walk(node)
        if isinstance(child, (ast.If, ast.For, ast.While, ast.ExceptHandler, ast.With))
    )


# ── DUP ───────────────────────────────────────


def rule_duplicate_name(contract, registry, source):
    existing = registry.get(contract.name)
    if existing and existing.file != contract.file:
        return Violation(
            "DUP001",
            "error",
            0.99,
            f"'{contract.name}' zaten {existing.file}:{existing.line} içinde kayıtlı.",
            f"{contract.file}:{contract.line}",
            f"Mevcut fonksiyonu kullan veya ismini değiştir: {existing.file}",
            contract.name,
        )


def rule_similar_name(contract, registry, source):
    def norm(n):
        n = re.sub(r"(?<!^)(?=[A-Z])", "_", n).lower()
        return re.sub(r"[_\-]", "", n)

    my = norm(contract.name)
    for name, fc in registry.functions.items():
        if name != contract.name and norm(name) == my:
            return Violation(
                "DUP002",
                "warning",
                0.75,
                f"'{contract.name}' ile '{name}' aynı şey olabilir.",
                f"{contract.file}:{contract.line}",
                f"Duplicate mi kontrol et: {fc.file}",
                contract.name,
            )


def rule_duplicate_logic(contract, registry, source):
    if not source or len(source.strip()) < 60:
        return None
    tree = _parse(source)
    if not tree:
        return None

    def semantic_signature(tree):
        tokens = []
        for node in ast.walk(tree):
            tokens.append(type(node).__name__)
        return tokens

    def jaccard_similarity(a, b):
        sa, sb = set(a), set(b)
        if not sa or not sb:
            return 0.0
        return len(sa & sb) / len(sa | sb)

    sig = semantic_signature(tree)
    if not hasattr(registry, "_body_sigs"):
        registry._body_sigs = []

    for other_name, other_sig in registry._body_sigs:
        if other_name == contract.name:
            continue
        sim = jaccard_similarity(sig, other_sig)
        if sim >= 0.75:
            return Violation(
                "DUP003",
                "warning",
                0.75,
                f"'{contract.name}' ile '{other_name}' %{int(sim * 100)} benzer logic.",
                f"{contract.file}:{contract.line}",
                "İkisini birleştir veya ortak helper yaz.",
                contract.name,
            )

    registry._body_sigs.append((contract.name, sig))


# ── DOM ───────────────────────────────────────


def rule_wrong_domain(contract, registry, source):
    if contract.domain not in registry.domains:
        return None
    d = registry.domains[contract.domain]
    if d.owner and d.owner != contract.file:
        return Violation(
            "DOM001",
            "warning",
            0.85,
            f"'{contract.domain}' sahibi {d.owner}, sen {contract.file} içindesin.",
            f"{contract.file}:{contract.line}",
            f"Fonksiyonu {d.owner} dosyasına taşı.",
            contract.name,
        )


def rule_no_domain(contract, registry, source):
    if not contract.domain or contract.domain in ("", "unknown"):
        domains = ", ".join(registry.domains.keys()) or "henüz yok"
        return Violation(
            "DOM002",
            "warning",
            0.95,
            f"'{contract.name}' için domain tanımlanmamış.",
            f"{contract.file}:{contract.line}",
            f"@ai_func'a domain='...' ekle. Mevcut: {domains}",
            contract.name,
        )


def rule_touches_foreign_domain(contract, registry, source):
    if contract.domain not in registry.domains:
        return None
    allowed = set(registry.domains[contract.domain].touches)
    if not allowed:
        return None
    for r in contract.touches:
        if r not in allowed:
            return Violation(
                "DOM003",
                "warning",
                0.70,
                f"'{contract.name}' sınır dışı '{r}' kaynağına dokunuyor.",
                f"{contract.file}:{contract.line}",
                f"'{r}' için ayrı servis yaz veya domain tanımını güncelle.",
                contract.name,
            )


# ── ARCH ──────────────────────────────────────


def rule_god_function(contract, registry, source):
    if not source:
        return None
    lines = [
        line
        for line in source.splitlines()
        if line.strip() and not line.strip().startswith("#")
    ]
    if len(lines) > 38:
        return Violation(
            "ARCH001",
            "warning",
            0.85,
            f"'{contract.name}' çok büyük ({len(lines)} satır). God function riski.",
            f"{contract.file}:{contract.line}",
            "Sorumluluklara göre küçük fonksiyonlara böl.",
            contract.name,
        )


def rule_too_many_branches(contract, registry, source):
    if not source:
        return None
    tree = _parse(source)
    if not tree:
        return None
    b = _count_branches(tree)
    if b > 4:
        return Violation(
            "ARCH002",
            "warning",
            0.80,
            f"'{contract.name}' çok fazla dal ({b} branch). Test edilmesi zor.",
            f"{contract.file}:{contract.line}",
            "Early return pattern kullan veya alt fonksiyonlara böl.",
            contract.name,
        )


def rule_too_many_args(contract, registry, source):
    if not source:
        return None
    tree = _parse(source)
    if not tree:
        return None
    for node in ast.walk(tree):
        if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
            n = len(node.args.args) + len(node.args.kwonlyargs)
            if n > 5:
                return Violation(
                    "ARCH003",
                    "info",
                    0.75,
                    f"'{contract.name}' {n} parametre alıyor.",
                    f"{contract.file}:{contract.line}",
                    "Parametreleri dataclass veya TypedDict'e topla.",
                    contract.name,
                )
            break


def rule_missing_return_type(contract, registry, source):
    if not source:
        return None
    tree = _parse(source)
    if not tree:
        return None
    for node in ast.walk(tree):
        if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
            if node.returns is None and not node.name.startswith("_"):
                return Violation(
                    "ARCH004",
                    "info",
                    0.80,
                    f"'{contract.name}' return type annotation eksik.",
                    f"{contract.file}:{contract.line}",
                    "Return type ekle: def func() -> ReturnType:",
                    contract.name,
                )
            break


# ── PERF ──────────────────────────────────────


def rule_db_in_loop(contract, registry, source):
    if not source:
        return None
    tree = _parse(source)
    if not tree:
        return None
    db_kw = {
        "connect",
        "execute",
        "query",
        "session",
        "cursor",
        "fetch",
        "fetchall",
        "fetchone",
        "commit",
        "save",
        "filter",
    }
    for node in ast.walk(tree):
        if isinstance(node, (ast.For, ast.While)):
            if _has_call(node, db_kw):
                return Violation(
                    "PERF001",
                    "warning",
                    0.80,
                    f"'{contract.name}' loop içinde db çağrısı var.",
                    f"{contract.file}:{contract.line}",
                    "Sorguyu loop dışına çıkar, bulk işlem kullan.",
                    contract.name,
                )


def rule_http_in_loop(contract, registry, source):
    if not source:
        return None
    tree = _parse(source)
    if not tree:
        return None
    http_kw = {"get", "post", "put", "patch", "delete", "request", "fetch", "urlopen"}
    for node in ast.walk(tree):
        if isinstance(node, (ast.For, ast.While)):
            if _has_call(node, http_kw):
                return Violation(
                    "PERF002",
                    "warning",
                    0.70,
                    f"'{contract.name}' loop içinde HTTP çağrısı olabilir.",
                    f"{contract.file}:{contract.line}",
                    "asyncio.gather() ile parallel çağrı yap veya batch endpoint kullan.",
                    contract.name,
                )


def rule_bare_exception(contract, registry, source):
    if not source:
        return None
    tree = _parse(source)
    if not tree:
        return None
    for node in ast.walk(tree):
        if isinstance(node, ast.ExceptHandler):
            if node.type is None or (
                isinstance(node.type, ast.Name) and node.type.id == "Exception"
            ):
                return Violation(
                    "PERF003",
                    "info",
                    0.75,
                    f"'{contract.name}' bare except veya except Exception kullanıyor.",
                    f"{contract.file}:{contract.line}",
                    "Spesifik exception yakala. Projenin custom exception sınıflarını kullan.",
                    contract.name,
                )


# ── ASY ───────────────────────────────────────


def rule_async_in_sync_chain(contract, registry, source):
    if contract.is_async:
        return None
    for caller_name in contract.called_by:
        caller = registry.get(caller_name)
        if caller and caller.is_async:
            return Violation(
                "ASY001",
                "error",
                0.90,
                f"'{contract.name}' sync ama async '{caller_name}' çağırıyor.",
                f"{contract.file}:{contract.line}",
                "Fonksiyonu 'async def' yap veya asyncio.to_thread() kullan.",
                contract.name,
            )


# ── RISK ──────────────────────────────────────


def rule_high_caller_count(contract, registry, source):
    n = len(contract.called_by)
    if n >= 5:
        callers = ", ".join(contract.called_by[:3])
        return Violation(
            "RISK001",
            "warning",
            0.75,
            f"'{contract.name}' {n} yerden çağrılıyor. Değiştirilirse yüksek etki.",
            f"{contract.file}:{contract.line}",
            f"Değiştirmeden önce kontrol et: {callers}",
            contract.name,
        )


def rule_no_test_for_high_risk(contract, registry, source):
    if contract.risk != "high":
        return None
    if not any("test" in c.lower() for c in contract.called_by):
        return Violation(
            "RISK002",
            "warning",
            0.80,
            f"Yüksek riskli '{contract.name}' için test bulunamadı.",
            f"{contract.file}:{contract.line}",
            "tests/ altında test yaz. risk='high' beyan edilmiş.",
            contract.name,
        )


# ── SEC ───────────────────────────────────────


def rule_hardcoded_secret(contract, registry, source):
    if not source:
        return None
    SAFE_VALUES = {
        "",
        "test",
        "example",
        "your_password_here",
        "changeme",
        "placeholder",
        "xxx",
        "todo",
        "password123",
        "admin123",
        "root",
        "user",
    }
    patterns = [
        r'password\s*=\s*["\'][^"\']{3,}["\']',
        r'secret\s*=\s*["\'][^"\']{3,}["\']',
        r'api_key\s*=\s*["\'][^"\']{3,}["\']',
        r'token\s*=\s*["\']{8,}["\']',
    ]
    for p in patterns:
        for match in re.finditer(p, source, re.IGNORECASE):
            value = match.group(0).split("=")[-1].strip().strip("\"'")
            if len(value) < 8:
                continue
            if value.lower() in SAFE_VALUES:
                continue
            if value.startswith(("${", "%(", "env(", "${", "{{")):
                continue
            return Violation(
                "SEC001",
                "error",
                0.85,
                f"'{contract.name}' içinde hardcoded secret olabilir.",
                f"{contract.file}:{contract.line}",
                "Secret'ı env variable'a taşı: os.environ['KEY']",
                contract.name,
            )


def rule_sql_injection(contract, registry, source):
    if not source:
        return None
    if re.search(
        r'(execute|query)\s*\(\s*f["\'].*?(SELECT|INSERT|UPDATE|DELETE)',
        source,
        re.IGNORECASE,
    ):
        return Violation(
            "SEC002",
            "error",
            0.80,
            f"'{contract.name}' içinde f-string SQL injection riski.",
            f"{contract.file}:{contract.line}",
            "Parameterized query kullan: execute('SELECT ... WHERE id = ?', (id,))",
            contract.name,
        )


# ── DEAD ──────────────────────────────────────


def rule_never_called(contract, registry, source):
    from datetime import datetime

    if contract.called_by:
        return None
    skip = {"main", "setup", "teardown", "run", "start", "init"}
    if contract.name.startswith(("test_", "_")) or contract.name in skip:
        return None
    try:
        registered = datetime.fromisoformat(contract.registered_at)
        now = datetime.now()
        age_days = (now - registered).days
    except Exception:
        age_days = 0
    if age_days < 2:
        return None
    if age_days >= 7:
        return Violation(
            "DEAD001",
            "info",
            0.80,
            f"'{contract.name}' 7+ gündür hiç çağrılmıyor. Dead code olabilir.",
            f"{contract.file}:{contract.line}",
            "Kullanılmıyorsa sil veya called_by listesini güncelle.",
            contract.name,
        )
    return Violation(
        "DEAD001",
        "info",
        0.50,
        f"'{contract.name}' 2-6 gündür çağrılmıyor. Izlenmeye devam edilecek.",
        f"{contract.file}:{contract.line}",
        "Kullanılmıyorsa sil.",
        contract.name,
    )


# ── REPL ──────────────────────────────────────


def rule_replaces_missing(contract, registry, source):
    if not contract.replaces:
        return None
    old = registry.get(contract.replaces)
    if old:
        return Violation(
            "REPL001",
            "info",
            0.99,
            f"'{contract.name}', '{contract.replaces}' yerine geçiyor ama eski hala var.",
            f"{contract.file}:{contract.line}",
            f"'{contract.replaces}' fonksiyonunu {old.file} içinden sil.",
            contract.name,
        )


# ── STYLE ─────────────────────────────────────


def rule_no_docstring(contract, registry, source):
    if not source:
        return None
    tree = _parse(source)
    if not tree:
        return None
    for node in ast.walk(tree):
        if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
            has_doc = (
                node.body
                and isinstance(node.body[0], ast.Expr)
                and isinstance(node.body[0].value, ast.Constant)
                and isinstance(node.body[0].value.value, str)
            )
            if not has_doc and not node.name.startswith("_"):
                return Violation(
                    "STYLE001",
                    "info",
                    0.90,
                    f"'{contract.name}' için docstring yok.",
                    f"{contract.file}:{contract.line}",
                    '"""Ne yaptığını açıkla.""" ekle.',
                    contract.name,
                )
            break


def rule_print_in_production(contract, registry, source):
    if (
        not source
        or contract.name.startswith("test_")
        or "debug" in contract.name.lower()
    ):
        return None
    tree = _parse(source)
    if not tree:
        return None
    for node in ast.walk(tree):
        if isinstance(node, ast.Call):
            fn = node.func
            if isinstance(fn, ast.Name) and fn.id == "print":
                return Violation(
                    "STYLE002",
                    "info",
                    0.85,
                    f"'{contract.name}' içinde print() var. Production'da logging kullan.",
                    f"{contract.file}:{contract.line}",
                    "import logging; logger.info('...') kullan.",
                    contract.name,
                )


def _find_dead_code_in_block(body):
    """Recursively find dead code in a block of statements."""
    for i, stmt in enumerate(body):
        if isinstance(stmt, (ast.Return, ast.Raise)):
            # Check remaining statements after return/raise
            if i < len(body) - 1:
                return i + 1, type(stmt).__name__
        if isinstance(stmt, (ast.Continue, ast.Break)):
            # Check if there's code after continue/break in same block
            if i < len(body) - 1:
                return i + 1, type(stmt).__name__
        # Check nested blocks (if, for, while, try)
        if isinstance(stmt, (ast.If, ast.For, ast.While, ast.Try)):
            nested_body = []
            if isinstance(stmt, ast.If):
                nested_body = stmt.body
                if stmt.orelse:
                    nested_body.extend(stmt.orelse)
            elif isinstance(stmt, (ast.For, ast.While)):
                nested_body = stmt.body
            elif isinstance(stmt, ast.Try):
                nested_body = stmt.body
                for handler in stmt.handlers:
                    nested_body.extend(handler.body)
                if stmt.finalbody:
                    nested_body.extend(stmt.finalbody)

            result = _find_dead_code_in_block(nested_body)
            if result:
                return result
    return None


def rule_dead_code(contract, registry, source):
    """DEAD001 - Unreachable code detection."""
    if not source:
        return None
    tree = _parse(source)
    if not tree:
        return None

    for node in ast.walk(tree):
        if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
            continue
        if node.name != contract.name:
            continue

        statements = node.body
        if len(statements) < 2:
            continue

        # Check function body
        for i, stmt in enumerate(statements[:-1]):
            if isinstance(stmt, (ast.Return, ast.Raise)):
                return Violation(
                    "DEAD001",
                    "error",
                    0.9,
                    f"'{contract.name}' icinde {type(stmt).__name__}'dan sonra ulasilamayan kod.",
                    f"{contract.file}:{node.lineno or contract.line}",
                    f"{type(stmt).__name__}'dan sonraki kodu kaldir.",
                    contract.name,
                )

        # Check nested blocks
        result = _find_dead_code_in_block(statements)
        if result:
            line_num, stmt_type = result
            return Violation(
                "DEAD001",
                "error",
                0.9,
                f"'{contract.name}' icinde {stmt_type}'dan sonra ulasilamayan kod.",
                f"{contract.file}:{node.lineno or contract.line}",
                f"{stmt_type}'dan sonraki kodu kaldir.",
                contract.name,
            )


def rule_duplicate_import(contract, registry, source):
    """IMP001 - Duplicate/unused imports detection."""
    if not source:
        return None
    tree = _parse(source)
    if not tree:
        return None

    imports = []
    for node in ast.iter_child_nodes(tree):
        if isinstance(node, ast.Import):
            for alias in node.names:
                imports.append(alias.name)
        elif isinstance(node, ast.ImportFrom):
            module = node.module or ""
            for alias in node.names:
                full_name = f"{module}.{alias.name}" if module else alias.name
                imports.append(full_name)

    if not imports:
        return None

    seen = set()
    duplicates = []
    for imp in imports:
        if imp in seen:
            duplicates.append(imp)
        seen.add(imp)

    if duplicates:
        return Violation(
            "IMP001",
            "warning",
            0.85,
            f"'{contract.name}' dosyasında duplicate import: {', '.join(set(duplicates))}",
            f"{contract.file}:{contract.line}",
            "Import'ları birleştir: import x; import x yerine import x",
            contract.name,
        )


def rule_god_module(contract, registry, source):
    """ARCH001v2 - Enhanced god module detection (module-level)."""
    if not source:
        return None
    if not hasattr(registry, "_module_stats"):
        registry._module_stats = {}

    filepath = contract.file
    if filepath in registry._module_stats:
        stats = registry._module_stats[filepath]
    else:
        tree = _parse(source)
        if not tree:
            return None

        lines = len([l for l in source.splitlines() if l.strip()])
        funcs = sum(
            1
            for node in ast.walk(tree)
            if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
        )
        complexity = sum(
            _count_branches(node)
            for node in ast.walk(tree)
            if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
        )

        stats = {"lines": lines, "funcs": funcs, "complexity": complexity}
        registry._module_stats[filepath] = stats

    is_god = False
    reasons = []

    if stats["lines"] > 300:
        is_god = True
        reasons.append(f"{stats['lines']} satır")
    if stats["funcs"] > 15:
        is_god = True
        reasons.append(f"{stats['funcs']} fonksiyon")
    if stats["complexity"] > 50:
        is_god = True
        reasons.append(f"karmaşıklık {stats['complexity']}")

    if is_god:
        return Violation(
            "ARCH001v2",
            "warning",
            0.8,
            f"'{contract.file}' god module: {', '.join(reasons)}",
            f"{contract.file}:1",
            "Modülü alt modüllere böl.",
            contract.name,
        )


def rule_happy_path_only(contract, registry, source):
    """AI002 - Happy path only (no error handling)."""
    if not source:
        return None
    tree = _parse(source)
    if not tree:
        return None

    has_try = any(isinstance(node, ast.Try) for node in ast.walk(tree))
    has_nested_access = "[" in source and "]" in source

    if not has_try and has_nested_access:
        return Violation(
            "AI002",
            "warning",
            0.75,
            f"'{contract.name}' sadece happy path. Try/except yok ama nested access var.",
            f"{contract.file}:{contract.line}",
            "Error handling ekle.",
            contract.name,
        )


def rule_n_plus_one_query(contract, registry, source):
    """AI005 - N+1 query pattern."""
    if not source:
        return None
    if "for " not in source and "while " not in source:
        return None

    db_patterns = ["execute(", "query(", "fetch(", ".get(", "cursor.execute"]
    has_db_in_loop = any(
        p in source and ("for " in source or "while " in source) for p in db_patterns
    )

    if has_db_in_loop:
        return Violation(
            "AI005",
            "error",
            0.85,
            f"'{contract.name}' N+1 query riski. Loop icinde DB cagriz.",
            f"{contract.file}:{contract.line}",
            "Batch query kullan.",
            contract.name,
        )


BASE_RULES = [
    rule_duplicate_name,
    rule_similar_name,
    rule_duplicate_logic,
    rule_wrong_domain,
    rule_no_domain,
    rule_touches_foreign_domain,
    rule_god_function,
    rule_too_many_branches,
    rule_too_many_args,
    rule_missing_return_type,
    rule_db_in_loop,
    rule_http_in_loop,
    rule_bare_exception,
    rule_async_in_sync_chain,
    rule_high_caller_count,
    rule_no_test_for_high_risk,
    rule_hardcoded_secret,
    rule_sql_injection,
    rule_never_called,
    rule_replaces_missing,
    rule_no_docstring,
    rule_print_in_production,
    rule_dead_code,
    rule_duplicate_import,
    rule_god_module,
    rule_happy_path_only,
    rule_n_plus_one_query,
]


def get_rules():
    """Return all rules."""
    return BASE_RULES
