#!/usr/bin/env python3
"""
OCPP Archive Viewer
Parcours tout le systeme pour trouver les archives Steve (.log.gz)
"""

import json
import gzip
import re
import sqlite3
import os
from pathlib import Path

from flask import Flask, render_template, Response, jsonify, request

# === CONFIGURATION ===
SEARCH_ROOTS = ["/home", "/opt", "/var", "/tmp"]  # Racines de recherche
EXCLUDED_DIRS = {"/proc", "/sys", "/dev", "/run", "/boot", "/snap"}  # Exclus
TEMP_DIR = Path("/var/www/html/logs/temp")
DB_TEMP = TEMP_DIR / "archive_temp.db"
PORT = 5000

app = Flask(__name__)

OCPP_PATTERN = re.compile(
    r'(?P<timestamp>\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}(?:,\d{3})?)'
    r'.*?chargeBoxId\s*[=:]\s*(?P<chargebox>[^,\]\s]+)'
    r'.*?(?P<direction>Received:|Sending:)'
    r'\s*(?P<payload>\[.*\])'
)

def init_temp_db():
    TEMP_DIR.mkdir(parents=True, exist_ok=True)
    if DB_TEMP.exists():
        DB_TEMP.unlink()
    
    conn = sqlite3.connect(DB_TEMP)
    c = conn.cursor()
    c.execute('''CREATE TABLE ocpp_messages (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        timestamp TEXT NOT NULL,
        chargebox_id TEXT NOT NULL,
        direction TEXT NOT NULL,
        message_type TEXT,
        action TEXT,
        message_id TEXT,
        payload TEXT NOT NULL,
        raw_payload TEXT NOT NULL
    )''')
    c.execute('CREATE INDEX idx_chargebox ON ocpp_messages(chargebox_id)')
    c.execute('CREATE INDEX idx_timestamp ON ocpp_messages(timestamp)')
    c.execute('CREATE INDEX idx_action ON ocpp_messages(action)')
    c.execute('CREATE INDEX idx_msgid ON ocpp_messages(message_id)')
    conn.commit()
    conn.close()

def parse_ocpp_payload(payload_str):
    try:
        data = json.loads(payload_str)
        if not isinstance(data, list) or len(data) < 2:
            return None, None, None, payload_str
        msg_type_num = data[0]
        msg_id = data[1]
        if msg_type_num == 2:
            action = data[2] if len(data) > 2 else None
            payload = json.dumps(data[3]) if len(data) > 3 else '{}'
            return 'CALL', msg_id, action, payload
        elif msg_type_num == 3:
            payload = json.dumps(data[2]) if len(data) > 2 else '{}'
            return 'CALLRESULT', msg_id, None, payload
        elif msg_type_num == 4:
            payload = json.dumps(data[2:]) if len(data) > 2 else '{}'
            return 'CALLERROR', msg_id, None, payload
        else:
            return 'UNKNOWN', msg_id, None, payload_str
    except (json.JSONDecodeError, IndexError, TypeError):
        return None, None, None, payload_str

def find_action_for_message_id(message_id, chargebox_id):
    try:
        conn = sqlite3.connect(DB_TEMP)
        c = conn.cursor()
        c.execute('''
            SELECT action FROM ocpp_messages 
            WHERE message_id = ? AND chargebox_id = ? AND message_type = 'CALL' AND action IS NOT NULL
            ORDER BY id DESC LIMIT 1
        ''', (message_id, chargebox_id))
        row = c.fetchone()
        conn.close()
        return row[0] if row else None
    except Exception:
        return None

def insert_message(timestamp, chargebox, direction, payload_str):
    msg_type, msg_id, action, payload = parse_ocpp_payload(payload_str)
    direction = 'IN' if 'Received' in direction else 'OUT'
    
    conn = sqlite3.connect(DB_TEMP)
    c = conn.cursor()
    
    if msg_type in ('CALLRESULT', 'CALLERROR') and action is None and msg_id:
        linked_action = find_action_for_message_id(msg_id, chargebox)
        if linked_action:
            action = f'↳ {linked_action}'
    
    if msg_type == 'CALL' and action:
        c.execute('''
            SELECT id FROM ocpp_messages 
            WHERE message_id = ? AND chargebox_id = ? AND message_type = 'CALLRESULT' AND action IS NULL
        ''', (msg_id, chargebox))
        orphan = c.fetchone()
        if orphan:
            c.execute('UPDATE ocpp_messages SET action = ? WHERE id = ?', 
                     (f'↳ {action}', orphan[0]))
    
    c.execute('''
        INSERT INTO ocpp_messages 
        (timestamp, chargebox_id, direction, message_type, action, message_id, payload, raw_payload) 
        VALUES (?, ?, ?, ?, ?, ?, ?, ?)
    ''', (timestamp, chargebox, direction, msg_type, action, msg_id, payload, payload_str))
    
    conn.commit()
    conn.close()

def retroactive_link():
    conn = sqlite3.connect(DB_TEMP)
    c = conn.cursor()
    c.execute('''
        SELECT id, message_id, chargebox_id FROM ocpp_messages 
        WHERE message_type = 'CALLRESULT' AND (action IS NULL OR action NOT LIKE '↳ %')
    ''')
    orphans = c.fetchall()
    updated = 0
    for msg_id_db, msg_id, chargebox in orphans:
        if not msg_id:
            continue
        linked_action = find_action_for_message_id(msg_id, chargebox)
        if linked_action:
            c.execute('UPDATE ocpp_messages SET action = ? WHERE id = ?', 
                     (f'↳ {linked_action}', msg_id_db))
            updated += 1
    conn.commit()
    conn.close()

def parse_archive(archive_path):
    init_temp_db()
    
    print(f"[Archive Viewer] Parsing {archive_path}...")
    count = 0
    
    with gzip.open(archive_path, 'rt', encoding='utf-8', errors='ignore') as f:
        for line in f:
            line = line.strip()
            if not line:
                continue
            match = OCPP_PATTERN.search(line)
            if match:
                insert_message(
                    match.group('timestamp'),
                    match.group('chargebox'),
                    match.group('direction'),
                    match.group('payload')
                )
                count += 1
                if count % 1000 == 0:
                    print(f"  {count} messages...")
    
    retroactive_link()
    print(f"[Archive Viewer] ✅ {count} messages importes")
    return count

def get_db():
    conn = sqlite3.connect(DB_TEMP)
    conn.row_factory = sqlite3.Row
    return conn

def find_all_archives():
    """Parcours le systeme pour trouver tous les steve-*.log.gz"""
    archives = []
    seen = set()
    
    for root in SEARCH_ROOTS:
        if not os.path.exists(root):
            continue
            
        try:
            for dirpath, dirnames, filenames in os.walk(root):
                # Exclure les repertoires systeme
                dirnames[:] = [d for d in dirnames 
                              if os.path.join(dirpath, d) not in EXCLUDED_DIRS
                              and not d.startswith('.')]
                
                for filename in filenames:
                    if filename.startswith('steve-') and filename.endswith('.log.gz'):
                        full_path = os.path.join(dirpath, filename)
                        if full_path not in seen:
                            seen.add(full_path)
                            try:
                                stat = os.stat(full_path)
                                archives.append({
                                    "name": filename,
                                    "path": full_path,
                                    "date": filename.replace("steve-", "").replace(".log.gz", ""),
                                    "size": stat.st_size,
                                    "size_human": f"{stat.st_size / 1024:.1f} ko",
                                    "folder": dirpath
                                })
                            except (OSError, PermissionError):
                                pass
        except PermissionError:
            pass
    
    return sorted(archives, key=lambda x: x['date'], reverse=True)

# === ROUTES ===

@app.route("/")
def index():
    return render_template("index.html")

@app.route("/api/archives")
def list_archives():
    archives = find_all_archives()
    return jsonify(archives)

@app.route("/api/load", methods=["POST"])
def load_archive():
    data = request.get_json()
    archive_path = data.get("path", "")
    
    if not archive_path:
        return jsonify({"error": "Chemin d'archive manquant"}), 400
    
    if not os.path.exists(archive_path):
        return jsonify({"error": "Archive non trouvee"}), 404
    
    count = parse_archive(archive_path)
    
    return jsonify({
        "success": True,
        "archive": os.path.basename(archive_path),
        "messages": count
    })

@app.route("/api/messages")
def get_messages():
    chargebox = request.args.get("chargebox", "")
    action = request.args.get("action", "")
    direction = request.args.get("direction", "")

    if not DB_TEMP.exists():
        return jsonify([])

    conn = get_db()
    query = "SELECT * FROM ocpp_messages WHERE 1=1"
    params = []

    if chargebox:
        query += " AND chargebox_id = ?"
        params.append(chargebox)

    if action:
        query += " AND (action = ? OR action = ?)"
        params.extend([action, f"↳ {action}"])

    if direction:
        query += " AND direction = ?"
        params.append(direction)

    query += " ORDER BY id DESC"

    rows = conn.execute(query, params).fetchall()
    conn.close()

    return jsonify([dict(row) for row in rows])

@app.route("/api/chargeboxes")
def get_chargeboxes():
    if not DB_TEMP.exists():
        return jsonify([])
    
    conn = get_db()
    rows = conn.execute(
        "SELECT DISTINCT chargebox_id FROM ocpp_messages ORDER BY chargebox_id"
    ).fetchall()
    conn.close()
    return jsonify([r["chargebox_id"] for r in rows])

@app.route("/api/actions")
def get_actions():
    if not DB_TEMP.exists():
        return jsonify([])
    
    conn = get_db()
    rows = conn.execute(
        "SELECT DISTINCT action FROM ocpp_messages WHERE action IS NOT NULL ORDER BY action"
    ).fetchall()
    conn.close()

    unique_actions = set()
    for r in rows:
        action = r["action"]
        if action:
            if action.startswith("↳ "):
                unique_actions.add(action[2:])
            else:
                unique_actions.add(action)

    return jsonify(sorted(unique_actions))

@app.route("/api/stats")
def get_stats():
    if not DB_TEMP.exists():
        return jsonify({"total": 0, "chargeboxes": 0, "errors": 0})

    conn = get_db()
    stats = {}
    stats["total"] = conn.execute("SELECT COUNT(*) FROM ocpp_messages").fetchone()[0]
    stats["chargeboxes"] = conn.execute(
        "SELECT COUNT(DISTINCT chargebox_id) FROM ocpp_messages"
    ).fetchone()[0]
    stats["errors"] = conn.execute(
        "SELECT COUNT(*) FROM ocpp_messages WHERE message_type = 'CALLERROR'"
    ).fetchone()[0]
    conn.close()
    return jsonify(stats)

if __name__ == "__main__":
    print("=" * 50)
    print("OCPP Archive Viewer - Mode Global")
    print(f"Recherche dans: {', '.join(SEARCH_ROOTS)}")
    print(f"Acces: http://localhost:{PORT}")
    print("=" * 50)
    app.run(host="0.0.0.0", port=PORT, debug=False, threaded=True)
