#!/usr/bin/env python3
import json
import re
import shutil
import sqlite3
import subprocess
import sys
import time
from pathlib import Path

# ==== A ADAPTER PAR INSTANCE (Steve1 / Steve2 / Steve3) ====
INSTANCE_NAME = "Steve0"
LOG_FILE = Path('/var/www/html/logs/steve.log')
DB_FILE = Path('/var/www/html/logs/ocpp-monitor-steve0/ocpp_logs.db')
# =============================================================

MIN_FREE_BYTES = 500 * 1024 * 1024  # 500 Mo minimum requis avant de lancer un VACUUM

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 get_connection(timeout=30):
    """Ouvre une connexion SQLite avec les PRAGMA de fiabilite/performance appliques."""
    conn = sqlite3.connect(DB_FILE, timeout=timeout)
    conn.execute("PRAGMA journal_mode=WAL")
    conn.execute("PRAGMA synchronous=NORMAL")
    conn.execute("PRAGMA busy_timeout=30000")
    return conn


def check_disk_space():
    """Verifie qu'il reste assez d'espace disque avant une operation lourde (VACUUM)."""
    try:
        _, _, free = shutil.disk_usage(DB_FILE.parent)
        return free >= MIN_FREE_BYTES, free
    except Exception:
        # Si on ne peut pas verifier, on ne bloque pas mais on log
        return True, None


def init_db():
    conn = get_connection()
    c = conn.cursor()
    c.execute('''CREATE TABLE IF NOT EXISTS ocpp_messages (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        instance TEXT NOT NULL DEFAULT 'Steve0',
        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,
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP
    )''')
    c.execute('CREATE INDEX IF NOT EXISTS idx_instance ON ocpp_messages(instance)')
    c.execute('CREATE INDEX IF NOT EXISTS idx_chargebox ON ocpp_messages(chargebox_id)')
    c.execute('CREATE INDEX IF NOT EXISTS idx_timestamp ON ocpp_messages(timestamp)')
    c.execute('CREATE INDEX IF NOT EXISTS idx_action ON ocpp_messages(action)')
    c.execute('CREATE INDEX IF NOT EXISTS idx_msgid ON ocpp_messages(message_id)')
    conn.commit()
    conn.close()
    print(f'[{INSTANCE_NAME}] Base initialisee: {DB_FILE}')


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(conn, message_id, chargebox_id):
    try:
        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()
        return row[0] if row else None
    except Exception:
        return None


def insert_message(conn, timestamp, chargebox, direction, payload_str):
    """Insere un message en utilisant la connexion fournie (pas de commit par appel)."""
    msg_type, msg_id, action, payload = parse_ocpp_payload(payload_str)
    direction = 'IN' if 'Received' in direction else 'OUT'

    c = conn.cursor()

    c.execute('''
        SELECT 1 FROM ocpp_messages
        WHERE timestamp = ? AND chargebox_id = ? AND message_id = ? AND direction = ?
        LIMIT 1
    ''', (timestamp, chargebox, msg_id, direction))

    if c.fetchone():
        return False

    if msg_type in ('CALLRESULT', 'CALLERROR') and action is None and msg_id:
        linked_action = find_action_for_message_id(conn, 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
        (instance, timestamp, chargebox_id, direction, message_type, action, message_id, payload, raw_payload)
        VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
    ''', (INSTANCE_NAME, timestamp, chargebox, direction, msg_type, action, msg_id, payload, payload_str))

    color = '\033[1;32m' if direction == 'IN' else '\033[1;33m'
    reset = '\033[0m'
    action_display = action or 'N/A'
    print(f'\033[1;36m[{INSTANCE_NAME}]\033[0m {color}[{direction}]{reset} {timestamp} | {chargebox} | {action_display} | {msg_id}')
    return True


def retroactive_link():
    print(f'\n[{INSTANCE_NAME}] 🔍 Retraitement retroactif...')
    conn = get_connection()
    try:
        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(conn, 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()
        print(f'[{INSTANCE_NAME}] ✅ {updated} CALLRESULT mis a jour sur {len(orphans)} orphelins\n')
    except sqlite3.OperationalError as e:
        print(f'[{INSTANCE_NAME}] ⚠️ Erreur retraitement retroactif: {e}\n')
    finally:
        conn.close()


def wipe_database():
    """Vide la base et la compacte. Retourne True si succes, False sinon."""
    ok, free = check_disk_space()
    if not ok:
        free_mb = (free or 0) // (1024 * 1024)
        print(f'[{INSTANCE_NAME}] ❌ Espace disque insuffisant ({free_mb} Mo libres, {MIN_FREE_BYTES // (1024*1024)} Mo requis). Abandon du vidage.')
        return False

    try:
        conn = get_connection()
        conn.execute("DELETE FROM ocpp_messages")
        conn.commit()
        conn.execute("VACUUM")
        conn.close()
        print(f'[{INSTANCE_NAME}] 🧹 Base videe')
        return True
    except sqlite3.OperationalError as e:
        print(f'[{INSTANCE_NAME}] ❌ Erreur lors du vidage de la base: {e}')
        return False


def import_all_logs():
    print(f"[{INSTANCE_NAME}] Import historique de {LOG_FILE}...")

    if not wipe_database():
        print(f'[{INSTANCE_NAME}] Import annule suite a l\'echec du vidage.')
        sys.exit(1)

    count = 0
    conn = get_connection()

    try:
        with open(LOG_FILE, 'r', encoding='utf-8', errors='ignore') as f:
            for line in f:
                line = line.strip()
                if not line:
                    continue
                match = OCPP_PATTERN.search(line)
                if not match:
                    continue
                try:
                    inserted = insert_message(
                        conn,
                        match.group('timestamp'),
                        match.group('chargebox'),
                        match.group('direction'),
                        match.group('payload')
                    )
                    if inserted:
                        count += 1
                        # Commit par lot de 200 messages : bon compromis fiabilite/perf
                        if count % 200 == 0:
                            conn.commit()
                        if count % 1000 == 0:
                            print(f"  [{INSTANCE_NAME}] {count} messages importes...")
                except sqlite3.OperationalError as e:
                    print(f'[{INSTANCE_NAME}] ⚠️ Erreur insertion ignoree: {e}')
                    continue
        conn.commit()
    except sqlite3.OperationalError as e:
        print(f'[{INSTANCE_NAME}] ❌ Erreur disque pendant import: {e}')
        try:
            conn.commit()
        except Exception:
            pass
    finally:
        conn.close()

    print(f"\n[{INSTANCE_NAME}] ✅ {count} messages historiques importes")
    retroactive_link()


def tail_logs():
    print(f'[{INSTANCE_NAME}] Surveillance de {LOG_FILE}')
    print('Appuyez sur Ctrl+C pour arreter\n')

    while True:
        conn = get_connection()
        process = subprocess.Popen(
            ['tail', '-n', '0', '-F', str(LOG_FILE)],
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            text=True
        )

        try:
            for line in process.stdout:
                line = line.strip()
                if not line:
                    continue
                match = OCPP_PATTERN.search(line)
                if match:
                    try:
                        insert_message(
                            conn,
                            match.group('timestamp'),
                            match.group('chargebox'),
                            match.group('direction'),
                            match.group('payload')
                        )
                        conn.commit()
                    except sqlite3.OperationalError as e:
                        print(f'[{INSTANCE_NAME}] ⚠️ Erreur insertion temps reel: {e}')

                if not LOG_FILE.exists():
                    print(f'\n[{INSTANCE_NAME}] 🔄 Rotation detectee, reconnexion...')
                    process.terminate()
                    conn.close()
                    time.sleep(2)
                    break

        except KeyboardInterrupt:
            print(f'\n[{INSTANCE_NAME}] Arret du parser...')
            process.terminate()
            conn.close()
            break
        except Exception as e:
            print(f'\n[{INSTANCE_NAME}] ⚠️ Erreur: {e}, reconnexion dans 2s...')
            process.terminate()
            conn.close()
            time.sleep(2)


if __name__ == '__main__':
    LOG_FILE.parent.mkdir(parents=True, exist_ok=True)
    DB_FILE.parent.mkdir(parents=True, exist_ok=True)
    init_db()

    if len(sys.argv) > 1 and sys.argv[1] == '--retroactive':
        retroactive_link()
    else:
        import_all_logs()
        print(f"\n--- [{INSTANCE_NAME}] Passage en mode temps reel ---\n")
        tail_logs()
