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

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')

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_db():
    conn = sqlite3.connect(DB_FILE)
    c = conn.cursor()
    c.execute('''CREATE TABLE IF NOT EXISTS ocpp_messages (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        instance TEXT NOT NULL DEFAULT 'Steve1',
        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(message_id, chargebox_id):
    try:
        conn = sqlite3.connect(DB_FILE)
        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_FILE)
    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():
        conn.close()
        return

    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 
        (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))

    conn.commit()
    conn.close()

    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}')

def retroactive_link():
    print(f'\n[{INSTANCE_NAME}] 🔍 Retraitement retroactif...')
    conn = sqlite3.connect(DB_FILE)
    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()
    print(f'[{INSTANCE_NAME}] ✅ {updated} CALLRESULT mis a jour sur {len(orphans)} orphelins\n')

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

    # === VIDER LA BASE AVANT RÉIMPORT ===
    conn = sqlite3.connect(DB_FILE)
    c = conn.cursor()
    c.execute("DELETE FROM ocpp_messages")
    conn.commit()
    conn.execute("VACUUM")
    conn.close()
    print(f'[{INSTANCE_NAME}] 🧹 Base videe')

    count = 0

    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 match:
                insert_message(
                    match.group('timestamp'),
                    match.group('chargebox'),
                    match.group('direction'),
                    match.group('payload')
                )
                count += 1
                if count % 1000 == 0:
                    print(f"  [{INSTANCE_NAME}] {count} messages importes...")

    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:
        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:
                    insert_message(
                        match.group('timestamp'), 
                        match.group('chargebox'), 
                        match.group('direction'), 
                        match.group('payload')
                    )

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

        except KeyboardInterrupt:
            print(f'\n[{INSTANCE_NAME}] Arret du parser...')
            process.terminate()
            break
        except Exception as e:
            print(f'\n[{INSTANCE_NAME}] ⚠️ Erreur: {e}, reconnexion dans 2s...')
            process.terminate()
            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()
