#!/usr/bin/env python3
# mend.py - Analyseur de code intelligent pour terminal
# Version 1.3.0 — ajout de MendAI local (sans Ollama)

import os
import sys
import json
import re
import ast
import subprocess
import argparse
from pathlib import Path
from datetime import datetime
from typing import Optional, Dict, List, Any, Set, Tuple
from dataclasses import dataclass, field
from collections import defaultdict
import hashlib

# ============================================================
# CONFIGURATION
# ============================================================

CONFIG_DIR = Path.home() / ".mend"
CONFIG_FILE = CONFIG_DIR / "config.json"
CACHE_FILE = CONFIG_DIR / "cache.json"

DEFAULT_CONFIG = {
    "ignore": ["node_modules", "dist", "build", ".git", "**/*.test.ts", "**/*.spec.js"],
    "strictMode": True,
    "ci": {
        # NB : "contract-break" est listé ici mais pas encore détecté par analyze().
        # Gardé tel quel pour compat config existante — à implémenter séparément.
        "failOn": ["contract-break", "silent-catch", "unused-import"]
    },
    "max_depth": 10,
    "report_format": "pretty"
}

# ============================================================
# CORE ENGINE
# ============================================================

@dataclass
class Issue:
    file: str
    line: int
    column: int
    type: str
    message: str
    severity: str
    suggestion: Optional[str] = None
    context: Optional[str] = None

@dataclass
class Dependency:
    file: str
    imports: Set[str]
    exports: Set[str]
    references: Set[str]
    hash: str

class DependencyGraph:
    def __init__(self, root: Path, ignore_patterns: List[str], max_depth: int = 10):
        self.root = root
        self.ignore_patterns = ignore_patterns
        self.max_depth = max_depth
        self.files: Dict[str, Dependency] = {}
        self.issues: List[Issue] = []
        self._visited = set()
        self._cache = self._load_cache()

    def _load_cache(self) -> Dict:
        if CACHE_FILE.exists():
            try:
                with open(CACHE_FILE, "r") as f:
                    return json.load(f)
            except:
                pass
        return {}

    def _save_cache(self):
        CONFIG_DIR.mkdir(parents=True, exist_ok=True)
        with open(CACHE_FILE, "w") as f:
            json.dump(self._cache, f, indent=2)

    def _should_ignore(self, path: Path) -> bool:
        str_path = str(path)
        for pattern in self.ignore_patterns:
            if pattern.startswith("**/"):
                if pattern[3:] in str_path:
                    return True
            elif pattern in str_path:
                return True
        return False

    def _get_file_hash(self, path: Path) -> str:
        try:
            with open(path, "rb") as f:
                return hashlib.md5(f.read()).hexdigest()
        except:
            return ""

    def _parse_file(self, path: Path) -> Tuple[Set[str], Set[str]]:
        imports = set()
        exports = set()
        try:
            content = path.read_text(encoding="utf-8", errors="ignore")
            if path.suffix in [".js", ".ts", ".jsx", ".tsx"]:
                import_patterns = [
                    r"import\s+.*?\s+from\s+['\"](.+?)['\"]",
                    r"import\s+['\"](.+?)['\"]",
                    r"require\s*\(\s*['\"](.+?)['\"]\s*\)",
                    r"export\s+.*?\s+from\s+['\"](.+?)['\"]",
                ]
                for pat in import_patterns:
                    imports.update(re.findall(pat, content))

                export_patterns = [
                    r"export\s+(?:default\s+)?([a-zA-Z_$][\w$]*)",
                    r"export\s*\{\s*([^}]+)\s*\}",
                ]
                for pat in export_patterns:
                    exports.update(re.findall(pat, content))

                silent_pattern = r"catch\s*\([^)]*\)\s*\{\s*\}"
                for match in re.finditer(silent_pattern, content):
                    line_num = content[:match.start()].count("\n") + 1
                    self.issues.append(Issue(
                        file=str(path),
                        line=line_num,
                        column=0,
                        type="silent-catch",
                        message="catch vide détecté : le bug est masqué",
                        severity="error",
                        suggestion="Ajouter un log ou une gestion d'erreur minimale"
                    ))

            elif path.suffix == ".py":
                try:
                    tree = ast.parse(content)
                    for node in ast.walk(tree):
                        if isinstance(node, ast.Import):
                            for alias in node.names:
                                imports.add(alias.name.split(".")[0])
                        elif isinstance(node, ast.ImportFrom):
                            if node.module:
                                imports.add(node.module.split(".")[0])
                        elif isinstance(node, ast.FunctionDef):
                            exports.add(node.name)
                        if isinstance(node, ast.ExceptHandler) and node.body and isinstance(node.body[0], ast.Pass):
                            self.issues.append(Issue(
                                file=str(path),
                                line=node.lineno,
                                column=0,
                                type="silent-catch",
                                message="except pass détecté : l'erreur est ignorée",
                                severity="error",
                                suggestion="Logger l'erreur ou la remonter"
                            ))
                except SyntaxError:
                    pass
        except Exception:
            pass
        return imports, exports

    def _extract_references(self, path: Path) -> Set[str]:
        refs = set()
        try:
            content = path.read_text(encoding="utf-8", errors="ignore")
            patterns = [r"['\"](\.[/\\][^'\"]+)['\"]", r"from\s+['\"](\.[/\\][^'\"]+)['\"]"]
            for pat in patterns:
                refs.update(re.findall(pat, content))
        except:
            pass
        return refs

    def build(self, current_path: Optional[Path] = None, depth: int = 0):
        if current_path is None:
            current_path = self.root
        if depth > self.max_depth:
            return
        if str(current_path) in self._visited:
            return

        self._visited.add(str(current_path))

        if current_path.is_file():
            if self._should_ignore(current_path):
                return
            key = str(current_path)
            file_hash = self._get_file_hash(current_path)

            if key in self._cache and self._cache[key].get("hash") == file_hash:
                cached = self._cache[key]
                dep = Dependency(
                    file=key,
                    imports=set(cached.get("imports", [])),
                    exports=set(cached.get("exports", [])),
                    references=set(cached.get("references", [])),
                    hash=file_hash
                )
                self.files[key] = dep
                return

            imports, exports = self._parse_file(current_path)
            refs = self._extract_references(current_path)

            dep = Dependency(
                file=key,
                imports=imports,
                exports=exports,
                references=refs,
                hash=file_hash
            )
            self.files[key] = dep

            self._cache[key] = {
                "hash": file_hash,
                "imports": list(imports),
                "exports": list(exports),
                "references": list(refs)
            }

            for ref in refs:
                ref_path = (current_path.parent / ref).resolve()
                if ref_path.exists():
                    self.build(ref_path, depth + 1)

        elif current_path.is_dir():
            try:
                for entry in sorted(current_path.iterdir()):
                    if self._should_ignore(entry):
                        continue
                    self.build(entry, depth + 1)
            except PermissionError:
                pass

    def analyze(self) -> List[Issue]:
        all_imports = defaultdict(set)
        all_exports = defaultdict(set)

        for dep in self.files.values():
            for imp in dep.imports:
                all_imports[imp].add(dep.file)
            for exp in dep.exports:
                all_exports[exp].add(dep.file)

        for dep in self.files.values():
            used_exports = set()
            for ref in dep.references:
                for exp, sources in all_exports.items():
                    if exp in ref or ref in exp:
                        used_exports.add(exp)

            for imp in dep.imports:
                if imp not in used_exports and imp not in dep.references:
                    if not imp.startswith(".") and "/" not in imp:
                        continue
                    self.issues.append(Issue(
                        file=dep.file,
                        line=0,
                        column=0,
                        type="unused-import",
                        message=f"import '{imp}' non utilisé dans ce fichier",
                        severity="warning",
                        suggestion="Supprimer l'import ou l'utiliser"
                    ))

        return self.issues

# ============================================================
# AI LOCALE (MendAI) — sans Ollama, sans API externe
# ============================================================

def _read_snippet(file: str, line: int, context: int = 1) -> str:
    """Lit quelques lignes autour de `line` dans `file`, pour donner du contexte à MendAI."""
    if not line:
        return ""
    try:
        lines = Path(file).read_text(encoding="utf-8", errors="ignore").splitlines()
        start = max(0, line - 1 - context)
        end = min(len(lines), line + context)
        return "\n".join(lines[start:end])
    except Exception:
        return ""


def _lang_for(file: str) -> str:
    suffix = Path(file).suffix
    return {".py": "python", ".js": "javascript", ".jsx": "javascript",
            ".ts": "typescript", ".tsx": "typescript"}.get(suffix, "")


def explain_issues_with_ai(issues: List[Issue], max_calls: int = 5) -> None:
    """
    Demande à MendAI (modèle local fine-tuné, voir train_mendai.py) d'expliquer
    les `max_calls` premiers problèmes détectés. N'échoue jamais bruyamment :
    si le modèle n'est pas installé, affiche juste comment l'installer.
    """
    try:
        sys.path.insert(0, str(CONFIG_DIR))
        import mendai_infer
    except ImportError:
        print("\nℹ️  MendAI local non trouvé. Place mendai_infer.py dans ~/.mend/ "
              "et l'adaptateur entraîné dans ~/.mend/model/ (voir train_mendai.py).")
        return

    if not mendai_infer.is_model_available():
        print("\nℹ️  Aucun modèle MendAI entraîné dans ~/.mend/model/. "
              "Lance train_mendai.py sur Colab puis copie l'adaptateur ici.")
        return

    print("\n🧠 MendAI — explications générées localement :\n")
    for issue in issues[:max_calls]:
        snippet = _read_snippet(issue.file, issue.line)
        explanation = mendai_infer.explain_issue(
            file=issue.file,
            issue_type=issue.type,
            line=issue.line,
            code_snippet=snippet or "(contexte non disponible)",
            lang=_lang_for(issue.file) or "text",
        )
        print(f"  📍 {issue.file}:{issue.line}")
        for ln in explanation.splitlines():
            print(f"     {ln}")
        print()

    if len(issues) > max_calls:
        print(f"  … et {len(issues) - max_calls} autre(s) problème(s) non détaillé(s) "
              f"(augmente --ai-max pour en voir plus).")


def ask_ai(question: str) -> None:
    """Mode question libre : mend --ask "..." """
    try:
        sys.path.insert(0, str(CONFIG_DIR))
        import mendai_infer
    except ImportError:
        print("ℹ️  MendAI local non trouvé. Place mendai_infer.py dans ~/.mend/ "
              "et l'adaptateur entraîné dans ~/.mend/model/ (voir train_mendai.py).")
        return
    print(mendai_infer.explain_free_form(question))

# ============================================================
# CLI
# ============================================================

def main():
    parser = argparse.ArgumentParser(
        description="🧠 mend - Analyseur de code intelligent",
        epilog="Ex: mend . --strict --ai"
    )
    parser.add_argument("path", nargs="?", default=".", help="Chemin du dépôt à analyser")
    parser.add_argument("--strict", action="store_true", help="Mode strict (échoue sur warning)")
    parser.add_argument("--ignore", "-i", nargs="+", help="Patterns à ignorer")
    parser.add_argument("--output", "-o", help="Fichier de sortie (json, md, html)")
    parser.add_argument("--init", action="store_true", help="Crée un fichier .mendrc par défaut")
    parser.add_argument("--ai", action="store_true",
                         help="Fait expliquer les problèmes détectés par MendAI (modèle local, sans Ollama)")
    parser.add_argument("--ai-max", type=int, default=5,
                         help="Nombre max de problèmes à faire expliquer par l'IA (défaut: 5)")
    parser.add_argument("--ask", metavar="QUESTION",
                         help="Pose une question libre à MendAI sans lancer d'analyse statique")
    parser.add_argument("--version", action="version", version="mend v1.3.0")

    args = parser.parse_args()

    if args.ask:
        ask_ai(args.ask)
        return

    if args.init:
        config_path = Path(".mendrc.json")
        if config_path.exists():
            print("⚠️  .mendrc.json existe déjà.")
            return
        with open(config_path, "w") as f:
            json.dump(DEFAULT_CONFIG, f, indent=2)
        print("✅ .mendrc.json créé.")
        return

    root = Path(args.path).resolve()
    if not root.exists():
        print(f"❌ {root} n'existe pas.")
        return

    config = DEFAULT_CONFIG.copy()
    rc_file = root / ".mendrc.json"
    if rc_file.exists():
        try:
            with open(rc_file, "r") as f:
                user_config = json.load(f)
                config.update(user_config)
        except:
            pass

    if args.ignore:
        config["ignore"].extend(args.ignore)

    if args.strict:
        config["strictMode"] = True

    print(f"🔍 Analyse de {root}...")
    graph = DependencyGraph(root, config["ignore"], config.get("max_depth", 10))
    graph.build()

    print(f"📊 {len(graph.files)} fichiers indexés.")
    issues = graph.analyze()

    if not issues:
        print("✅ Aucun problème détecté. Code propre !")
        return

    print(f"\n⚠️  {len(issues)} problèmes détectés :")
    for issue in issues:
        prefix = "❌" if issue.severity == "error" else "⚠️" if issue.severity == "warning" else "ℹ️"
        print(f"  {prefix} {issue.file}:{issue.line} [{issue.type}]")
        print(f"      {issue.message}")
        if issue.suggestion:
            print(f"      → {issue.suggestion}")
        if issue.context:
            print(f"      contexte: {issue.context}")
        print()

    if args.ai:
        explain_issues_with_ai(issues, max_calls=args.ai_max)

    if config["strictMode"] and any(i.severity == "error" for i in issues):
        sys.exit(1)

if __name__ == "__main__":
    main()
