#!/usr/bin/env python3
"""
╔══════════════════════════════════════════════════════════════════════════════╗
║                    ICHIGRIDEA MASTER PIPELINE v1.0                           ║
║                                                                              ║
║  Script UNIQUE pour tout le projet IchiGridEA                                ║
║                                                                              ║
║  COMMANDES:                                                                  ║
║    python ICHIGRID_MASTER.py status      → État du projet                    ║
║    python ICHIGRID_MASTER.py checkpoint  → Créer un checkpoint               ║
║    python ICHIGRID_MASTER.py fix         → Corriger tous les modules         ║
║    python ICHIGRID_MASTER.py validate    → Valider conformité                ║
║    python ICHIGRID_MASTER.py deploy      → Tout déployer (git + OVH)         ║
║    python ICHIGRID_MASTER.py generate <section> <nom> <desc>                 ║
║                                                                              ║
║  FICHIERS AUTO-GÉNÉRÉS:                                                      ║
║    .checkpoints/     → Snapshots automatiques                                ║
║    PROJECT.json      → Source de vérité                                      ║
║    HASHES.json       → SHA-256 de tous les fichiers                          ║
╚══════════════════════════════════════════════════════════════════════════════╝
"""

import os, sys, json, hashlib, shutil, re, subprocess
from datetime import datetime, timezone
from pathlib import Path

CONFIG = {
    "version": "4.20",
    "github": "https://github.com/KinSushi/IchiGrid-EA.git",
    "branch": "main",
    "ovh_ip": "92.222.226.50",
    "ovh_user": "ubuntu",
    "ovh_key": "~/.ssh/ovh_serveur",
    "ovh_path": "/opt/ichigrid-mirror",
    "model": "claude-opus-4-5-20250514",
    "max_checkpoints": 50,
}

# ════════════════════════════════════════════════════════════════════════════════
# TEMPLATE MODULE (exactement celui fourni)
# ════════════════════════════════════════════════════════════════════════════════
TEMPLATE = '''//+------------------------------------------------------------------+
//|                                              {MODULE_NAME}.mqh   |
//|                                    Copyright 2026, SOVRALYS LLC  |
//|                                         https://www.sovralys.com |
//+------------------------------------------------------------------+
// <gen:header>
//| @project     : IchiGridEA
//| @section     : {SECTION_ID}
//| @version     : {VERSION}
//| @generated   : {ISO_TIMESTAMP}
//| @session     : {SESSION_ID}
//| @model       : {MODEL}
//| @spec_hash   : {SPEC_SHA256}
//| @code_hash   : {CODE_SHA256}
//| @spec_source : {GOOGLE_DOC_URL}
//| @depends     : {DEPENDENCIES}
//| @description : {DESCRIPTION}
// </gen:header>

#property copyright "Copyright 2026, SOVRALYS LLC"
#property link      "https://www.sovralys.com"
#property version   "{VERSION}"
#property strict

#ifndef __{MODULE_NAME_UPPER}_MQH__
#define __{MODULE_NAME_UPPER}_MQH__

//+------------------------------------------------------------------+
//| INCLUDES                                                          |
//+------------------------------------------------------------------+
{INCLUDES}

//+------------------------------------------------------------------+
//| ÉNUMÉRATIONS                                                      |
//+------------------------------------------------------------------+
{ENUMS}

//+------------------------------------------------------------------+
//| STRUCTURES                                                        |
//+------------------------------------------------------------------+
{STRUCTS}

//+------------------------------------------------------------------+
//| CONSTANTES                                                        |
//+------------------------------------------------------------------+
#define {MODULE_NAME_UPPER}_VERSION    "{VERSION}"
{CONSTANTS}

//+------------------------------------------------------------------+
//| VARIABLES GLOBALES                                                |
//+------------------------------------------------------------------+
{GLOBALS}

//+------------------------------------------------------------------+
//| INPUT PARAMETERS                                                  |
//+------------------------------------------------------------------+
input group "=== {MODULE_NAME} ==="
input bool InpEnable{MODULE_NAME} = true;    // Activer {MODULE_NAME}
{INPUTS}

//+------------------------------------------------------------------+
//| CLASSE PRINCIPALE                                                 |
//+------------------------------------------------------------------+
class C{MODULE_NAME}
{
private:
    bool              m_isInitialized;
    string            m_symbol;
    ENUM_TIMEFRAMES   m_timeframe;
    {PRIVATE_MEMBERS}
    
public:
    C{MODULE_NAME}();
    ~C{MODULE_NAME}();
    bool     Init(string symbol = "", ENUM_TIMEFRAMES tf = PERIOD_CURRENT);
    void     Deinit();
    void     OnTick();
    bool     IsInitialized() const { return m_isInitialized; }
    {PUBLIC_METHODS}
};

C{MODULE_NAME}::C{MODULE_NAME}() { m_isInitialized = false; m_symbol = ""; m_timeframe = PERIOD_CURRENT; {CONSTRUCTOR_INIT} }
C{MODULE_NAME}::~C{MODULE_NAME}() { Deinit(); }

bool C{MODULE_NAME}::Init(string symbol = "", ENUM_TIMEFRAMES tf = PERIOD_CURRENT)
{
    if(m_isInitialized) return true;
    m_symbol = (symbol == "") ? _Symbol : symbol;
    m_timeframe = (tf == PERIOD_CURRENT) ? Period() : tf;
    {INIT_LOGIC}
    m_isInitialized = true;
    Print("[{MODULE_NAME}] Initialisé sur ", m_symbol);
    return true;
}

void C{MODULE_NAME}::Deinit()
{
    if(!m_isInitialized) return;
    {DEINIT_LOGIC}
    m_isInitialized = false;
    Print("[{MODULE_NAME}] Déinitialisé");
}

void C{MODULE_NAME}::OnTick()
{
    if(!m_isInitialized) return;
    {ONTICK_LOGIC}
}

// <dev:begin>
//+------------------------------------------------------------------+
//| BLOC DÉVELOPPEUR - LIBRE                                          |
//+------------------------------------------------------------------+
// Ce bloc ne sera JAMAIS écrasé par le générateur

// </dev:begin>

//+------------------------------------------------------------------+
//| MÉTHODES PUBLIQUES                                                |
//+------------------------------------------------------------------+
{PUBLIC_METHOD_IMPLEMENTATIONS}

//+------------------------------------------------------------------+
//| FONCTIONS GLOBALES (WRAPPER)                                      |
//+------------------------------------------------------------------+
C{MODULE_NAME} *g_p{MODULE_NAME} = NULL;

bool Init{MODULE_NAME}(string symbol = "", ENUM_TIMEFRAMES tf = PERIOD_CURRENT)
{
    if(g_p{MODULE_NAME} != NULL) return g_p{MODULE_NAME}.IsInitialized();
    g_p{MODULE_NAME} = new C{MODULE_NAME}();
    if(g_p{MODULE_NAME} == NULL) { Print("[{MODULE_NAME}] Allocation échouée"); return false; }
    return g_p{MODULE_NAME}.Init(symbol, tf);
}

void Deinit{MODULE_NAME}()
{
    if(g_p{MODULE_NAME} != NULL) { g_p{MODULE_NAME}.Deinit(); delete g_p{MODULE_NAME}; g_p{MODULE_NAME} = NULL; }
}

void OnTick{MODULE_NAME}()
{
    if(g_p{MODULE_NAME} != NULL) g_p{MODULE_NAME}.OnTick();
}

#endif // __{MODULE_NAME_UPPER}_MQH__
'''

# ════════════════════════════════════════════════════════════════════════════════
# UTILITAIRES
# ════════════════════════════════════════════════════════════════════════════════
def sha256(content): return hashlib.sha256(content.encode('utf-8')).hexdigest()
def sha256_short(content): return sha256(content)[:16]
def sha256_file(path):
    h = hashlib.sha256()
    with open(path, 'rb') as f:
        for chunk in iter(lambda: f.read(8192), b''): h.update(chunk)
    return h.hexdigest()
def now_iso(): return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
def session_id(): return datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S")
def run(cmd, cwd=None):
    try:
        r = subprocess.run(cmd, shell=True, cwd=cwd, capture_output=True, text=True, timeout=120)
        return r.returncode == 0, r.stdout + r.stderr
    except Exception as e: return False, str(e)

def find_base():
    for p in [Path("."), Path("/home/claude/IchiGrid-EA"), Path(r"C:\Users\Enzo\Downloads\IchiGridEA\IchiGridEA")]:
        if (p / "IchiGridEA.mq5").exists() or (p / "Include").exists(): return p.resolve()
    return Path(".").resolve()

# ════════════════════════════════════════════════════════════════════════════════
# CHECKPOINTS
# ════════════════════════════════════════════════════════════════════════════════
def create_checkpoint(base, desc=""):
    cp_dir = base / ".checkpoints"; cp_dir.mkdir(exist_ok=True)
    cp_id = session_id(); cp_path = cp_dir / cp_id; cp_path.mkdir(exist_ok=True)
    
    for f in ["PROJECT.json", "HASHES.json", "CHANGELOG.md", "IchiGridEA.mq5"]:
        if (base / f).exists(): shutil.copy2(base / f, cp_path / f)
    
    for subdir in ["Include", "Core", "Memory", "Evolution", "Humanoid", "Interface"]:
        if (base / subdir).exists():
            (cp_path / subdir).mkdir(exist_ok=True)
            for mqh in (base / subdir).glob("*.mqh"): shutil.copy2(mqh, cp_path / subdir / mqh.name)
    
    files = lines = 0; sections = set(); hashes = {}
    for mqh in base.rglob("*.mqh"):
        if ".git" in str(mqh) or ".checkpoints" in str(mqh): continue
        content = mqh.read_text(errors='ignore')
        files += 1; lines += len(content.splitlines())
        hashes[str(mqh.relative_to(base))] = sha256_short(content)
        m = re.search(r'@section\s*:\s*(S?\d+)', content)
        if m: sections.add(m.group(1))
    
    ok, out = run("git rev-parse --short HEAD", str(base))
    meta = {"id": cp_id, "timestamp": now_iso(), "files": files, "lines": lines,
            "sections": sorted(list(sections)), "commit": out.strip() if ok else "N/A",
            "description": desc or f"Checkpoint {cp_id}", "hashes": hashes}
    
    with open(cp_path / "checkpoint.json", "w") as f: json.dump(meta, f, indent=2)
    
    for old in sorted(cp_dir.glob("*"), reverse=True)[CONFIG["max_checkpoints"]:]:
        if old.is_dir(): shutil.rmtree(old)
    
    print(f"✓ Checkpoint: {cp_id} ({files} fichiers, {lines:,} lignes)")
    return meta

# ════════════════════════════════════════════════════════════════════════════════
# VALIDATION & FIX
# ════════════════════════════════════════════════════════════════════════════════
REQUIRED = ["@project", "@section", "@version", "@generated", "@session", "@model", "@spec_hash", "@code_hash", "@spec_source", "@description"]

def validate_module(path, base):
    content = path.read_text(errors='ignore')
    r = {"path": str(path.relative_to(base)), "valid": True, "errors": [],
         "has_gen": "<gen:header>" in content, "has_dev": "<dev:begin>" in content}
    for f in REQUIRED:
        if f not in content: r["errors"].append(f); r["valid"] = False
    if not r["has_dev"]: r["errors"].append("<dev:begin>"); r["valid"] = False
    return r

def fix_module(path):
    content = path.read_text(errors='ignore'); original = content; name = path.stem
    
    if "<gen:header>" not in content:
        section = "S00"
        m = re.search(r'\bS(\d+)\b', content[:500])
        if m: section = "S" + m.group(1)
        header = f'''//+------------------------------------------------------------------+
//|                                              {name}.mqh   |
//|                                    Copyright 2026, SOVRALYS LLC  |
//|                                         https://www.sovralys.com |
//+------------------------------------------------------------------+
// <gen:header>
//| @project     : IchiGridEA
//| @section     : {section}
//| @version     : 1.00
//| @generated   : {now_iso()}
//| @session     : {session_id()}
//| @model       : {CONFIG["model"]}
//| @spec_hash   : N/A
//| @code_hash   : PENDING
//| @spec_source : N/A
//| @depends     : none
//| @description : Module {name}
// </gen:header>

'''
        lines = content.splitlines(); start = 0
        for i, line in enumerate(lines):
            if line.startswith('#property') or line.startswith('#ifndef'): start = i; break
        content = header + '\n'.join(lines[start:])
    
    if "@spec_hash" not in content and "<gen:header>" in content:
        content = content.replace("//| @code_hash", "//| @spec_hash   : N/A\n//| @code_hash")
    if "@spec_source" not in content and "<gen:header>" in content:
        content = content.replace("//| @depends", "//| @spec_source : N/A\n//| @depends")
    if "@project" not in content and "<gen:header>" in content:
        content = content.replace("// <gen:header>", "// <gen:header>\n//| @project     : IchiGridEA")
    if "@version" not in content and "<gen:header>" in content:
        content = content.replace("//| @generated", "//| @version     : 1.00\n//| @generated")
    
    if "<dev:begin>" not in content:
        dev = '\n// <dev:begin>\n//+------------------------------------------------------------------+\n//| BLOC DÉVELOPPEUR - LIBRE                                          |\n//+------------------------------------------------------------------+\n// Ce bloc ne sera JAMAIS écrasé\n\n// </dev:begin>\n'
        if "#endif" in content:
            pos = content.rfind("#endif"); content = content[:pos] + dev + "\n" + content[pos:]
        else: content += dev
    
    if "@code_hash" in content:
        temp = re.sub(r'@code_hash\s*:\s*\w+', '@code_hash   : ', content)
        content = re.sub(r'@code_hash\s*:\s*(\w+|PENDING)', f'@code_hash   : {sha256_short(temp)}', content)
    
    if content != original: path.write_text(content); return True
    return False

# ════════════════════════════════════════════════════════════════════════════════
# GÉNÉRATION
# ════════════════════════════════════════════════════════════════════════════════
def generate_module(section, name, desc, spec_url="N/A"):
    defaults = {"INCLUDES": "// Aucun", "ENUMS": "// Aucune", "STRUCTS": "// Aucune",
                "CONSTANTS": "// Aucune", "GLOBALS": "// Aucune", "INPUTS": "// Aucun",
                "PRIVATE_MEMBERS": "// Membres", "PUBLIC_METHODS": "// Méthodes",
                "CONSTRUCTOR_INIT": "", "INIT_LOGIC": "    // TODO", "DEINIT_LOGIC": "    // TODO",
                "ONTICK_LOGIC": "    // TODO", "PUBLIC_METHOD_IMPLEMENTATIONS": "// Impl"}
    
    r = {"MODULE_NAME": name, "MODULE_NAME_UPPER": name.upper(), "SECTION_ID": section,
         "VERSION": "1.00", "ISO_TIMESTAMP": now_iso(), "SESSION_ID": session_id(),
         "MODEL": CONFIG["model"], "SPEC_SHA256": "N/A", "CODE_SHA256": "PENDING",
         "GOOGLE_DOC_URL": spec_url, "DEPENDENCIES": "none", "DESCRIPTION": desc[:80]}
    r.update(defaults)
    
    code = TEMPLATE
    for k, v in r.items(): code = code.replace("{" + k + "}", str(v))
    code = code.replace("CODE_SHA256: PENDING", f"CODE_SHA256: {sha256_short(code.replace('CODE_SHA256: PENDING', 'CODE_SHA256: '))}")
    return code

# ════════════════════════════════════════════════════════════════════════════════
# TRAÇABILITÉ
# ════════════════════════════════════════════════════════════════════════════════
def update_trace(base):
    data = {}; lines = 0; sections = set()
    for ext in ["*.mqh", "*.mq5"]:
        for f in base.rglob(ext):
            if ".git" in str(f) or ".checkpoints" in str(f): continue
            c = f.read_text(errors='ignore'); rel = str(f.relative_to(base)).replace("\\", "/")
            l = len(c.splitlines()); lines += l
            m = re.search(r'@section\s*:\s*(S?\d+)', c)
            if m: sections.add(m.group(1))
            data[rel] = {"sha256": sha256_file(f), "lines": l, "section": m.group(1) if m else ""}
    
    with open(base / "HASHES.json", "w") as f:
        json.dump({"_generated": now_iso(), "_files": len(data), "_lines": lines, "files": data}, f, indent=2)
    with open(base / "PROJECT.json", "w") as f:
        json.dump({"_updated": now_iso(), "version": CONFIG["version"],
                   "stats": {"files": len(data), "lines": lines, "sections": len(sections), "total": 13340},
                   "sections": sorted(list(sections))}, f, indent=2)
    return {"files": len(data), "lines": lines, "sections": len(sections)}

# ════════════════════════════════════════════════════════════════════════════════
# ACTIONS
# ════════════════════════════════════════════════════════════════════════════════
def action_status(base):
    print("=" * 70 + "\n                    ICHIGRIDEA - STATUS\n" + "=" * 70)
    valid = total = has_gen = has_dev = 0
    for d in ["Include", "Core", "Memory", "Evolution", "Humanoid", "Interface"]:
        if not (base / d).exists(): continue
        for mqh in (base / d).glob("*.mqh"):
            total += 1; r = validate_module(mqh, base)
            if r["valid"]: valid += 1
            if r["has_gen"]: has_gen += 1
            if r["has_dev"]: has_dev += 1
    
    print(f"\n📊 CONFORMITÉ: {valid}/{total} ({valid*100//total if total else 0}%)")
    print(f"   <gen:header>: {has_gen}/{total} | <dev:begin>: {has_dev}/{total}")
    
    cp_dir = base / ".checkpoints"
    if cp_dir.exists():
        cps = sorted(cp_dir.glob("*"), reverse=True)
        print(f"\n💾 CHECKPOINTS: {len(cps)}")
        for cp in cps[:3]:
            if (cp / "checkpoint.json").exists():
                m = json.loads((cp / "checkpoint.json").read_text())
                print(f"   {m['id']}: {m.get('files', m.get('files_count', 0))} fichiers")
    
    if (base / "PROJECT.json").exists():
        p = json.loads((base / "PROJECT.json").read_text())
        print(f"\n📋 v{p.get('version', '?')} | {p.get('stats', {}).get('sections', len(p.get('sections', [])))} sections")

def action_fix(base):
    print("Correction..."); fixed = 0
    for d in ["Include", "Core", "Memory", "Evolution", "Humanoid", "Interface"]:
        if not (base / d).exists(): continue
        for mqh in (base / d).glob("*.mqh"):
            if fix_module(mqh): fixed += 1; print(f"  ✓ {mqh.name}")
    print(f"\n✓ {fixed} corrigés")

def action_validate(base):
    print("Validation..."); errors = 0
    for d in ["Include", "Core", "Memory", "Evolution", "Humanoid", "Interface"]:
        if not (base / d).exists(): continue
        for mqh in (base / d).glob("*.mqh"):
            r = validate_module(mqh, base)
            print(f"  {'✓' if r['valid'] else '✗'} {r['path']}")
            if r["errors"]: errors += 1; [print(f"      {e}") for e in r["errors"]]
    print(f"\n{'✓ OK' if errors == 0 else f'✗ {errors} erreurs'}")

def action_deploy(base):
    print("=" * 70 + "\n                    DÉPLOIEMENT\n" + "=" * 70)
    print("\n[1/5] Checkpoint..."); create_checkpoint(base, "Pre-deploy")
    print("\n[2/5] Fix..."); action_fix(base)
    print("\n[3/5] Traçabilité..."); s = update_trace(base); print(f"   {s['files']} fichiers")
    print("\n[4/5] Git...")
    os.chdir(base); run("git add -A")
    ok, out = run(f'git commit -m "Deploy v{CONFIG["version"]}"')
    print("   Rien" if "nothing" in out else "   ✓ Commit"); run(f"git push origin {CONFIG['branch']}")
    print("\n[5/5] OVH...")
    ok, _ = run(f'ssh -i {os.path.expanduser(CONFIG["ovh_key"])} {CONFIG["ovh_user"]}@{CONFIG["ovh_ip"]} "cd {CONFIG["ovh_path"]} && git pull"')
    print("   ✓ OK" if ok else "   ⚠ Échec")
    print("\n" + "=" * 70 + "\n                    TERMINÉ\n" + "=" * 70)

def action_generate(base, section, name, desc):
    code = generate_module(section, name, desc)
    out = base / "Include" / f"{name}.mqh"; out.write_text(code)
    print(f"✓ Généré: {out}")

# ════════════════════════════════════════════════════════════════════════════════
# MAIN
# ════════════════════════════════════════════════════════════════════════════════
def main():
    if len(sys.argv) < 2: print(__doc__); return
    action = sys.argv[1].lower(); base = find_base()
    print(f"📁 {base}\n")
    
    if action == "status": action_status(base)
    elif action == "checkpoint": create_checkpoint(base, " ".join(sys.argv[2:]))
    elif action == "fix": action_fix(base)
    elif action == "validate": action_validate(base)
    elif action == "deploy": action_deploy(base)
    elif action == "generate":
        if len(sys.argv) < 5: print("Usage: generate <section> <nom> <desc>"); return
        action_generate(base, sys.argv[2], sys.argv[3], " ".join(sys.argv[4:]))
    else: print(f"Inconnu: {action}\nActions: status, checkpoint, fix, validate, deploy, generate")

if __name__ == "__main__": main()
