#!/usr/bin/env python3
"""
═══════════════════════════════════════════════════════════════
🔬 CODE SCANNER - ICHIGRIDEA PIPELINE
═══════════════════════════════════════════════════════════════
Scanne le code MQL5 et extrait les symboles, fonctions, classes.

Usage:
    python code_scanner.py --scan
    python code_scanner.py --report
    python code_scanner.py --check-orphans
"""

import json
import os
import re
from datetime import datetime
from pathlib import Path
from dataclasses import dataclass, asdict
from typing import List, Dict, Optional


@dataclass
class Symbol:
    """Représente un symbole MQL5 (fonction, classe, enum, struct)"""
    name: str
    type: str  # function, class, enum, struct, input, global
    file: str
    line: int
    signature: str
    dependencies: List[str]


class CodeScanner:
    def __init__(self, base_dir: str = "."):
        self.base_dir = Path(base_dir)
        self.include_dir = self.base_dir / "Include"
        self.symbols: List[Symbol] = []
        self.errors: List[str] = []
        self.warnings: List[str] = []
        
    def scan_file(self, filepath: Path) -> List[Symbol]:
        """Scanne un fichier MQL5 et extrait les symboles"""
        symbols = []
        
        try:
            with open(filepath, "r", encoding="utf-8", errors="ignore") as f:
                content = f.read()
                lines = content.split("\n")
        except Exception as e:
            self.errors.append(f"Erreur lecture {filepath}: {e}")
            return symbols
        
        # Patterns MQL5
        patterns = {
            "class": r"class\s+(C?\w+)(?:\s*:\s*public\s+\w+)?",
            "struct": r"struct\s+(S?\w+)",
            "enum": r"enum\s+(ENUM_\w+|\w+)",
            "function": r"^(?:bool|int|double|void|string|datetime|color|long|ulong)\s+(\w+)\s*\(",
            "input": r"^input\s+\w+\s+(\w+)\s*=",
            "global": r"^(?:extern\s+)?(?:static\s+)?(?:\w+)\s+(g_\w+)\s*[=;]",
        }
        
        for i, line in enumerate(lines, 1):
            line = line.strip()
            
            # Ignorer les commentaires
            if line.startswith("//") or line.startswith("/*"):
                continue
            
            for sym_type, pattern in patterns.items():
                match = re.search(pattern, line, re.MULTILINE)
                if match:
                    name = match.group(1)
                    
                    # Extraire les dépendances
                    deps = self._extract_dependencies(content, name)
                    
                    symbols.append(Symbol(
                        name=name,
                        type=sym_type,
                        file=str(filepath.relative_to(self.base_dir)),
                        line=i,
                        signature=line[:100],
                        dependencies=deps
                    ))
        
        return symbols
    
    def _extract_dependencies(self, content: str, symbol_name: str) -> List[str]:
        """Extrait les dépendances d'un symbole"""
        deps = set()
        
        # Chercher les #include
        includes = re.findall(r'#include\s+"([^"]+)"', content)
        for inc in includes:
            deps.add(Path(inc).stem)
        
        # Chercher les appels de fonction g_*
        globals_used = re.findall(r'\b(g_\w+)\b', content)
        deps.update(globals_used[:10])  # Limiter à 10
        
        return list(deps)[:20]  # Limiter à 20 dépendances
    
    def scan_all(self) -> Dict:
        """Scanne tous les fichiers MQH"""
        print("🔬 Scan du code MQL5...")
        
        if not self.include_dir.exists():
            self.errors.append(f"Répertoire Include/ non trouvé")
            return {}
        
        for filepath in self.include_dir.glob("*.mqh"):
            print(f"   📄 {filepath.name}")
            file_symbols = self.scan_file(filepath)
            self.symbols.extend(file_symbols)
        
        # Scanner aussi le main EA
        mq5_file = self.base_dir / "IchiGridEA.mq5"
        if mq5_file.exists():
            print(f"   📄 IchiGridEA.mq5")
            file_symbols = self.scan_file(mq5_file)
            self.symbols.extend(file_symbols)
        
        return self._build_index()
    
    def _build_index(self) -> Dict:
        """Construit l'index des symboles"""
        index = {
            "timestamp": datetime.utcnow().isoformat() + "Z",
            "total_symbols": len(self.symbols),
            "by_type": {},
            "by_file": {},
            "symbols": []
        }
        
        # Grouper par type
        for sym in self.symbols:
            if sym.type not in index["by_type"]:
                index["by_type"][sym.type] = []
            index["by_type"][sym.type].append(sym.name)
            
            # Grouper par fichier
            if sym.file not in index["by_file"]:
                index["by_file"][sym.file] = []
            index["by_file"][sym.file].append({
                "name": sym.name,
                "type": sym.type,
                "line": sym.line
            })
            
            # Liste complète
            index["symbols"].append(asdict(sym))
        
        return index
    
    def find_orphans(self) -> List[str]:
        """Trouve les symboles orphelins (déclarés mais non utilisés)"""
        orphans = []
        
        # Collecter toutes les références
        all_refs = set()
        for sym in self.symbols:
            all_refs.update(sym.dependencies)
        
        # Trouver les symboles non référencés
        for sym in self.symbols:
            if sym.type in ["function", "class"] and sym.name not in all_refs:
                # Vérifier que ce n'est pas une fonction publique
                if not sym.name.startswith(("OnInit", "OnTick", "OnDeinit", "Init", "Deinit")):
                    orphans.append(f"{sym.name} ({sym.file}:{sym.line})")
        
        return orphans
    
    def check_naming_conventions(self) -> List[str]:
        """Vérifie les conventions de nommage"""
        violations = []
        
        for sym in self.symbols:
            if sym.type == "class" and not sym.name.startswith("C"):
                violations.append(f"Classe sans préfixe C: {sym.name}")
            
            if sym.type == "struct" and not sym.name.startswith("S"):
                violations.append(f"Struct sans préfixe S: {sym.name}")
            
            if sym.type == "enum" and not sym.name.startswith("ENUM_"):
                if not sym.name.isupper():
                    violations.append(f"Enum sans préfixe ENUM_: {sym.name}")
            
            if sym.type == "global" and not sym.name.startswith("g_"):
                violations.append(f"Global sans préfixe g_: {sym.name}")
            
            if sym.type == "input" and not sym.name.startswith("Inp"):
                violations.append(f"Input sans préfixe Inp: {sym.name}")
        
        return violations
    
    def generate_report(self) -> str:
        """Génère un rapport markdown"""
        index = self.scan_all()
        orphans = self.find_orphans()
        violations = self.check_naming_conventions()
        
        report = f"""# 🔬 Rapport de Scan - IchiGridEA

**Date**: {index.get('timestamp', 'N/A')}
**Symboles totaux**: {index.get('total_symbols', 0)}

## 📊 Par type

| Type | Nombre |
|------|--------|
"""
        for sym_type, symbols in index.get("by_type", {}).items():
            report += f"| {sym_type} | {len(symbols)} |\n"
        
        report += f"""
## 📁 Par fichier

| Fichier | Symboles |
|---------|----------|
"""
        for file, symbols in sorted(index.get("by_file", {}).items()):
            report += f"| {file} | {len(symbols)} |\n"
        
        report += f"""
## ⚠️ Violations de nommage ({len(violations)})

"""
        for v in violations[:20]:
            report += f"- {v}\n"
        
        report += f"""
## 👻 Symboles potentiellement orphelins ({len(orphans)})

"""
        for o in orphans[:20]:
            report += f"- {o}\n"
        
        if self.errors:
            report += f"""
## ❌ Erreurs ({len(self.errors)})

"""
            for e in self.errors:
                report += f"- {e}\n"
        
        return report
    
    def save_index(self, output_file: str = "CODE_INDEX.json"):
        """Sauvegarde l'index en JSON"""
        index = self.scan_all()
        
        with open(output_file, "w", encoding="utf-8") as f:
            json.dump(index, f, indent=2, ensure_ascii=False)
        
        print(f"✅ Index sauvegardé: {output_file}")
        return index


if __name__ == "__main__":
    import sys
    
    scanner = CodeScanner()
    
    if "--scan" in sys.argv:
        scanner.save_index()
        
    elif "--report" in sys.argv:
        report = scanner.generate_report()
        print(report)
        
        # Sauvegarder
        with open("pipeline/reports/code_scan_report.md", "w") as f:
            f.write(report)
        print("\n📄 Rapport sauvegardé: pipeline/reports/code_scan_report.md")
        
    elif "--check-orphans" in sys.argv:
        scanner.scan_all()
        orphans = scanner.find_orphans()
        print(f"👻 Symboles orphelins ({len(orphans)}):")
        for o in orphans:
            print(f"   - {o}")
            
    else:
        print(__doc__)
