import os
from flask import Flask, render_template, request, redirect, url_for, session, flash
from pymongo import MongoClient
from werkzeug.security import check_password_hash, generate_password_hash
from flask_wtf.csrf import CSRFProtect
from datetime import timedelta, datetime
from functools import wraps
from bson.objectid import ObjectId
import paramiko
from flask import jsonify, send_file
import logging
from flask_socketio import SocketIO, emit, join_room, leave_room
import subprocess
import threading
from werkzeug.utils import secure_filename
import requests
import socket
import zipfile
from dotenv import load_dotenv
load_dotenv()
async_mode = "eventlet"
app = Flask(__name__,
            template_folder='Frontend/HTML',
            static_folder='Frontend')
socketio = SocketIO(app, async_mode=async_mode)
BOT_UPLOAD_FOLDER = '/home/astraos/Astra-OS-Panel/bots'
try:
    os.makedirs(BOT_UPLOAD_FOLDER, exist_ok=True)
    try:
        os.chmod(BOT_UPLOAD_FOLDER, 0o755)
    except Exception:
        pass
except Exception as e:
    BOT_UPLOAD_FOLDER = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'Bots')
    os.makedirs(BOT_UPLOAD_FOLDER, exist_ok=True)
    print(f"WARNUNG: Konnte '/opt/astraos/Discord_Bots' nicht erstellen ({e}), benutze lokalen Ordner: {BOT_UPLOAD_FOLDER}")
app.config['BOT_UPLOAD_FOLDER'] = BOT_UPLOAD_FOLDER
ETS2_DLC_ARCHIVE_FOLDER = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'ets2_dlc_archives')
os.makedirs(ETS2_DLC_ARCHIVE_FOLDER, exist_ok=True)
running_bots = {}
ets2_dlc_build_locks = {}
ets2_dlc_build_locks_guard = threading.Lock()
class SuppressServerLoadFilter(logging.Filter):
    def filter(self, record):
        msg = record.getMessage()
        return (
            "GET /api/server_load" not in msg
            and "GET /api/system_status" not in msg
        )
log = logging.getLogger('werkzeug')
log.addFilter(SuppressServerLoadFilter())
app.secret_key = os.environ.get('SECRET_KEY', os.urandom(32))
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(hours=8)
app.config['SESSION_COOKIE_SECURE'] = True
app.config['SESSION_COOKIE_HTTPONLY'] = True
app.config['SESSION_COOKIE_SAMESITE'] = 'Lax'
csrf = CSRFProtect(app)
@app.context_processor
def inject_utility_processor():
    maintenance_status = dashboard_collection.find_one({"key": "scheduled_maintenance"}) if dashboard_collection is not None else None
    is_maintenance_active = maintenance_status and maintenance_status.get('enabled', False)
    return dict(
        timedelta=timedelta,
        maintenance_active=is_maintenance_active
    )
try:
    client = MongoClient(os.environ.get("MONGO_URI"))
    db = client["astra_os"]
    accounts_collection = db["accounts"]
    logs_collection = db["logs"]
    roles_collection = db["roles"]
    dashboard_collection = db["dashboard_stats"]
    discord_bots_collection = db["discord_bots_data"]
    ets2_dlcs_collection = db["ets2_dlcs_packages"]
    ip_analyzer_results_collection = db["ip_analyzer_results"]
    age_verification_collection = db["age_verification"]
    print("INFO: MongoDB-Verbindung erfolgreich hergestellt.")
    AVAILABLE_PERMISSIONS = [
        "view_dashboard",
        "view_user_management",
        "manage_users",
        "view_role_management",
        "manage_roles",
        "view_logs",
        "maintenance_management",
        "bypass_maintenance",
        "manage_discord_bots",
        "view_ets2_dlcs",
        "manage_ets2_dlcs",
        "events",
        "instranet",
        "view_ip_analyzer",
        "view_age_verification",
        "manage_age_verification",
        "view_ayran_tracker"
    ]
    if roles_collection.count_documents({}) == 0:
        print("WARNUNG: Keine Rollen in der Datenbank gefunden. Erstelle Standard-Rollen...")
        roles_collection.insert_many([
            {
                "name": "Owner",
                "permissions": AVAILABLE_PERMISSIONS,
                "is_deletable": False
            },
            {
                "name": "Benutzer",
                "permissions": ["view_dashboard"],
                "is_deletable": True
            }
        ])
        print("INFO: Standard-Rollen ('Owner', 'Benutzer') wurden erstellt.")
    else:
        # Bestehende Owner-Rollen auf neue Permissions anheben.
        roles_collection.update_many(
            {"name": "Owner"},
            {"$addToSet": {"permissions": {"$each": AVAILABLE_PERMISSIONS}}}
        )
    if accounts_collection.count_documents({}) == 0:
        print("WARNUNG: Keine Benutzer in der Datenbank gefunden. Erstelle Standard-Admin-Benutzer...")
        owner_role = roles_collection.find_one({"name": "Owner"})
        hashed_password = generate_password_hash("1234")
        accounts_collection.insert_one({
            'username': 'admin',
            'password': hashed_password,
            'role_id': owner_role['_id'],
            'creation_date': datetime.utcnow(),
            'discord_id': None,
            'discord_username': None,
            'discord_avatar': None
        })
        print("INFO: Standard-Admin-Benutzer ('admin', Passwort: '1234') mit Rolle 'Owner' wurde erstellt.")
    if dashboard_collection.count_documents({}) == 0:
        print("INFO: Leere dashboard_stats Collection - lege Standardstatistiken an.")
        dashboard_collection.insert_many([
            {"key": "active_users", "value": 1, "percent": 100},
            {"key": "server_load", "percent": 0, "status": "Nicht Erreichbar"},
            {"key": "open_tickets", "count": 0},
            {"key": "system_status", "status": "Online"},
            {"key": "scheduled_maintenance", "enabled": False, "time": None, "status": "Keine", "duration_minutes": None},
            {"key": "last_user_created", "time": None, "by": None}
        ])
        print("INFO: Standard Dashboard-Statistiken wurden erstellt.")
except Exception as e:
    print(f"FATAL: Verbindung zur MongoDB fehlgeschlagen: {e}")
    db = None
    accounts_collection = None
    logs_collection = None
    roles_collection = None
    dashboard_collection = None
    discord_bots_collection = None
    ets2_dlcs_collection = None
    ip_analyzer_results_collection = None
    print(f"FATAL: Verbindung zur MongoDB fehlgeschlagen: {e}")
    db = None
    accounts_collection = None
    logs_collection = None
    ets2_dlcs_collection = None

def _venv_paths():
    venv_bin = "/home/astraos/Astra-OS-Panel/venv/bin"
    return {
        "python": os.path.join(venv_bin, "python3"),
        # pip NICHT direkt verwenden (Shebang kann auf falschen Interpreter zeigen)
        "pip": os.path.join(venv_bin, "pip"),
    }

def _build_bot_command(filepath: str):
    venv = _venv_paths()
    vpy = venv["python"]

    # Keine Auto-Installation bei jedem Start; nur Bot ausführen.
    return f"{vpy} -u {filepath}"

def _spawn_bot_process(bot_id: str, filepath: str, bot_name: str = "N/A", emit_to_socket: bool = False):
    command = _build_bot_command(filepath)

    if emit_to_socket:
        socketio.emit('console_output', {'type': 'system', 'data': f'[startup] Command: {command}\n'}, room=bot_id)

    process = subprocess.Popen(
        command,
        shell=True,
        executable='/bin/bash',
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        text=True,
        encoding='utf-8',
        errors='replace'
    )

    running_bots[bot_id] = {'process': process, 'history': []}
    thread = threading.Thread(target=stream_bot_output, args=(bot_id, process))
    thread.daemon = True
    thread.start()

    if discord_bots_collection is not None:
        discord_bots_collection.update_one({'_id': ObjectId(bot_id)}, {'$set': {'status': 'running'}})

    if emit_to_socket:
        socketio.emit('status_update', {'bot_id': bot_id, 'status': 'running'}, room=bot_id)
        socketio.emit('console_output', {'type': 'system', 'data': f'--- Bot-Prozess gestartet (PID: {process.pid}) ---'}, room=bot_id)

    print(f"INFO: Bot '{bot_name}' (PID: {process.pid}) wurde gestartet.")
    return process

def start_bots_on_startup():
    if discord_bots_collection is None:
        print("WARNUNG: Bot-Autostart übersprungen, da keine DB-Verbindung besteht.")
        return

    bots_to_start = list(discord_bots_collection.find({'status': 'running'}))
    if not bots_to_start:
        print("INFO: Keine Bots für den Autostart markiert.")
        return

    print(f"INFO: Starte {len(bots_to_start)} Bot(s) automatisch...")
    for bot_info in bots_to_start:
        bot_id = str(bot_info['_id'])
        filepath = bot_info.get('filepath')

        if not os.path.exists(filepath):
            print(f"FEHLER: Bot-Datei für '{bot_info['name']}' nicht gefunden: {filepath}")
            continue

        try:
            _spawn_bot_process(bot_id, filepath, bot_name=bot_info.get('name', 'N/A'), emit_to_socket=False)
        except Exception as e:
            print(f"FEHLER: Bot '{bot_info.get('name','N/A')}' konnte nicht gestartet werden: {e}")

def stream_bot_output(bot_id, process):
    def stream_to_socket(pipe, log_type):
        try:
            for line in iter(pipe.readline, ''):
                log_entry = {'type': log_type, 'data': line}
                if bot_id in running_bots:
                    running_bots[bot_id]['history'].append(log_entry)
                    max_history_lines = 1000
                    if len(running_bots[bot_id]['history']) > max_history_lines:
                        running_bots[bot_id]['history'].pop(0)
                socketio.emit('console_output', log_entry, room=bot_id)
        except Exception as e:
            print(f"Stream-Fehler für Bot {bot_id}: {e}")
        finally:
            if pipe:
                pipe.close()
    stdout_thread = threading.Thread(target=stream_to_socket, args=(process.stdout, 'stdout'))
    stderr_thread = threading.Thread(target=stream_to_socket, args=(process.stderr, 'stderr'))
    stdout_thread.start()
    stderr_thread.start()
    process.wait()
    if bot_id in running_bots and running_bots.get(bot_id) is not None:
        del running_bots[bot_id]
    if discord_bots_collection is not None:
        discord_bots_collection.update_one({'_id': ObjectId(bot_id)}, {'$set': {'status': 'stopped'}})
    socketio.emit('status_update', {'bot_id': bot_id, 'status': 'stopped'}, room=bot_id)
    socketio.emit('console_output', {'bot_id': bot_id, 'type': 'system', 'data': '--- Bot-Prozess beendet ---'}, room=bot_id)

if os.environ.get('WERKZEUG_RUN_MAIN') != 'true':
    with app.app_context():
        start_bots_on_startup()

try:
    tickets_client = MongoClient(os.environ.get("MONGO_URI"))
    tickets_db = tickets_client["chainsaw_dc_data"]
    tickets_collection = tickets_db["tickets"]
except Exception as e:
    tickets_collection = None
    print(f"FATAL: Verbindung zu Tickets-DB fehlgeschlagen: {e}")

def add_log(action, details=None):
    if 'username' in session and logs_collection is not None:
        log_entry = {
            'username': session['username'],
            'action': action,
            'details': details,
            'timestamp': datetime.utcnow()
        }
        logs_collection.insert_one(log_entry)

def _ets2_archive_filename(package_id: str, package_name: str):
    safe_name = secure_filename(package_name or 'ets2_dlc_package') or 'ets2_dlc_package'
    return f'{package_id}_{safe_name}.zip'


def _ets2_archive_path(package_id: str, package_name: str):
    return os.path.join(ETS2_DLC_ARCHIVE_FOLDER, _ets2_archive_filename(package_id, package_name))


def _get_ets2_build_lock(package_id: str):
    with ets2_dlc_build_locks_guard:
        lock = ets2_dlc_build_locks.get(package_id)
        if lock is None:
            lock = threading.Lock()
            ets2_dlc_build_locks[package_id] = lock
        return lock


def _set_ets2_archive_state(package_id: str, **fields):
    if ets2_dlcs_collection is None:
        return
    ets2_dlcs_collection.update_one({'_id': ObjectId(package_id)}, {'$set': fields})


def _build_ets2_archive(package_id: str):
    if ets2_dlcs_collection is None:
        return

    package = ets2_dlcs_collection.find_one({'_id': ObjectId(package_id)})
    if not package:
        return

    folder_path = package.get('folder_path')
    if not folder_path or not os.path.isdir(folder_path):
        _set_ets2_archive_state(
            package_id,
            archive_status='error',
            archive_error='Ordnerpfad ist nicht mehr verfügbar.',
            archive_generated_at=None,
            archive_path=None
        )
        return

    archive_path = _ets2_archive_path(package_id, package.get('name', 'ets2_dlc_package'))
    temp_archive_path = f'{archive_path}.tmp'

    try:
        if os.path.exists(temp_archive_path):
            try:
                os.remove(temp_archive_path)
            except Exception:
                pass

        _set_ets2_archive_state(
            package_id,
            archive_status='building',
            archive_error=None,
            archive_path=archive_path,
            archive_generated_at=None
        )

        with zipfile.ZipFile(temp_archive_path, 'w', compression=zipfile.ZIP_STORED, allowZip64=True) as zipf:
            has_content = False
            for root, _, files in os.walk(folder_path):
                for filename in files:
                    file_path = os.path.join(root, filename)
                    arcname = os.path.relpath(file_path, folder_path)
                    zipf.write(file_path, arcname, compress_type=zipfile.ZIP_STORED)
                    has_content = True

            if not has_content:
                zipf.writestr('EMPTY_PACKAGE.txt', 'Dieses DLC-Paket enthaelt aktuell keine Dateien.')

        os.replace(temp_archive_path, archive_path)
        _set_ets2_archive_state(
            package_id,
            archive_status='ready',
            archive_error=None,
            archive_path=archive_path,
            archive_generated_at=datetime.utcnow()
        )
    except Exception as e:
        try:
            if os.path.exists(temp_archive_path):
                os.remove(temp_archive_path)
        except Exception:
            pass
        _set_ets2_archive_state(
            package_id,
            archive_status='error',
            archive_error=str(e),
            archive_generated_at=None,
            archive_path=archive_path
        )


def _schedule_ets2_archive_build(package_id: str):
    lock = _get_ets2_build_lock(package_id)
    if not lock.acquire(blocking=False):
        return False

    def runner():
        try:
            _build_ets2_archive(package_id)
        finally:
            try:
                lock.release()
            except Exception:
                pass
            with ets2_dlc_build_locks_guard:
                ets2_dlc_build_locks.pop(package_id, None)

    thread = threading.Thread(target=runner, daemon=True)
    thread.start()
    return True


def _format_ets2_dlcs_for_view(packages):
    for package in packages:
        dlc_date = package.get('dlc_date')
        try:
            package['dlc_date_display'] = datetime.strptime(dlc_date, '%Y-%m-%d').strftime('%d.%m.%Y') if dlc_date else 'N/A'
        except ValueError:
            package['dlc_date_display'] = str(dlc_date) if dlc_date else 'N/A'

        package_id = str(package['_id'])
        archive_path = package.get('archive_path') or _ets2_archive_path(package_id, package.get('name', 'ets2_dlc_package'))
        archive_ready = package.get('archive_status') == 'ready' and os.path.isfile(archive_path)
        folder_path = package.get('folder_path')

        if not archive_ready and folder_path and os.path.isdir(folder_path):
            package.setdefault('archive_status', 'pending')
            package['download_ready'] = False
            package['archive_status_display'] = 'Wird erstellt'
            package['archive_path'] = archive_path
            _schedule_ets2_archive_build(package_id)
        elif archive_ready:
            package['download_ready'] = True
            package['archive_status_display'] = 'Bereit'
            package['archive_path'] = archive_path
        else:
            package['download_ready'] = False
            package['archive_status_display'] = 'Fehler'
            package['archive_path'] = archive_path

        if package.get('archive_status') == 'error' and package.get('archive_error'):
            package['archive_status_display'] = f"Fehler: {package.get('archive_error')}"

    return packages

def get_user_permissions(user_id):
    user = accounts_collection.find_one({'_id': ObjectId(user_id)})
    if not user or 'role_id' not in user:
        return []
    role = roles_collection.find_one({'_id': user['role_id']})
    if not role:
        return []
    return role.get('permissions', [])
def login_required(f):
    @wraps(f)
    def decorated_function(*args, **kwargs):
        if 'user_id' not in session:
            flash("Bitte melden Sie sich an, um diese Seite zu sehen.", "warning")
            return redirect(url_for('login'))

        user = accounts_collection.find_one({'_id': ObjectId(session['user_id'])})
        if not user:
            session.clear()
            flash("Benutzer nicht gefunden.", "danger")
            return redirect(url_for('login'))

        if not user.get('discord_id') and request.endpoint not in ['link_discord', 'login_discord', 'callback_discord', 'logout', 'static']:
            return redirect(url_for('link_discord'))

        session['permissions'] = get_user_permissions(session['user_id'])
        maintenance_status = dashboard_collection.find_one({"key": "scheduled_maintenance"})
        if maintenance_status and maintenance_status.get('enabled', False):
            if 'bypass_maintenance' not in session['permissions']:
                return redirect(url_for('maintenance'))
        return f(*args, **kwargs)
    return decorated_function
def permission_required(permission):
    def decorator(f):
        @wraps(f)
        def decorated_function(*args, **kwargs):
            if 'user_id' not in session:
                return redirect(url_for('login'))
            user_perms = get_user_permissions(session['user_id'])
            if permission not in user_perms:
                flash("Sie haben keine Berechtigung, diese Seite anzuzeigen.", "danger")
                return redirect(url_for('dashboard'))
            return f(*args, **kwargs)
        return decorated_function
    return decorator
@app.route('/')
def home():
    if 'user_id' in session:
        return redirect(url_for('dashboard'))
    return redirect(url_for('login'))
@app.route('/login', methods=['GET', 'POST'])
def login():
    if 'user_id' in session:
        return redirect(url_for('dashboard'))
    maintenance_status = dashboard_collection.find_one({"key": "scheduled_maintenance"}) if dashboard_collection is not None else None
    if maintenance_status and maintenance_status.get('enabled', False):
        return redirect(url_for('maintenance'))
    if request.method == 'POST':
        username = request.form.get('username')
        password = request.form.get('password')
        if not username or not password:
            flash("Benutzername und Passwort sind erforderlich.", "danger")
            return render_template('login.html')
        if accounts_collection is None:
            flash("Datenbankverbindung nicht verfügbar. Bitte versuchen Sie es später erneut.", "danger")
            return render_template('login.html'), 500
        user = accounts_collection.find_one({'username': username})
        if user and check_password_hash(user['password'], password):
            session.permanent = True
            session['user_id'] = str(user['_id'])
            session['username'] = user['username']
            session['permissions'] = get_user_permissions(str(user['_id']))
            add_log("Benutzer-Login")

            if not user.get('discord_id'):
                flash("Bitte verknüpfe deinen Discord-Account, um fortzufahren.", "info")
                return redirect(url_for('link_discord'))

            flash("Erfolgreich angemeldet!", "success")
            return redirect(url_for('dashboard'))
        else:
            flash("Ungültiger Benutzername oder Passwort.", "danger")
            return render_template('login.html')
    return render_template('login.html')
@app.route('/api/server_load')
@login_required
def api_server_load():
    try:
        ssh = paramiko.SSHClient()
        ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
        ssh.connect(os.environ.get('SSH_HOST'), username=os.environ.get('SSH_USER'), password=os.environ.get('SSH_PASS'), timeout=10)
        stdin, stdout, stderr = ssh.exec_command("top -bn1 | grep 'Cpu(s)'")
        output = stdout.read().decode()
        ssh.close()
        import re
        match = re.search(r"(\d+\.\d+)\s*id", output)
        if match:
            idle = float(match.group(1))
            usage = 100 - idle
            return jsonify({"percent": round(usage, 1), "status": "Online"})
        else:
            return jsonify({"percent": 0, "status": "Fehler"})
    except paramiko.AuthenticationException:
        print("SSH-Fehler: Authentifizierung fehlgeschlagen.")
        return jsonify({"percent": 0, "status": "Auth Fehler"})
    except paramiko.SSHException as e:
        print(f"SSH-Fehler: {e}")
        return jsonify({"percent": 0, "status": "SSH Fehler"})
    except Exception as e:
        print(f"Allgemeiner Fehler bei Server-Auslastung: {e}")
        return jsonify({"percent": 0, "status": "Nicht erreichbar"})
@app.route('/api/system_status')
@login_required
def api_system_status():
    maintenance_status = dashboard_collection.find_one({"key": "scheduled_maintenance"}) if dashboard_collection is not None else None
    maintenance_active = maintenance_status and maintenance_status.get('enabled', False)
    discord_bots_maintenance = maintenance_status.get('discord_bots_maintenance', False) if maintenance_status else False
    scpsl_status = "offline"
    try:
        udp_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        udp_sock.settimeout(2)
        udp_sock.sendto(b"\x00", ("89.144.42.175", 7101))
        scpsl_status = "online"
        udp_sock.close()
    except Exception as e:
        print(f"[DEBUG] SCP:SL UDP-Ping failed: {e}")
        scpsl_status = "offline"
    if maintenance_active:
        scpsl_status = "wartung"
    gamepanel_status = "offline"
    try:
        resp = requests.get("https://gamepanel.mrsniff.de", timeout=3)
        if resp.status_code == 200:
            gamepanel_status = "online"
    except Exception:
        gamepanel_status = "offline"
    if maintenance_active:
        gamepanel_status = "wartung"
    discord_bots_status = "wartung" if discord_bots_maintenance or maintenance_active else "online"
    db_status = "online" if db is not None else "offline"
    if maintenance_active:
        db_status = "wartung"
    return jsonify({
        "scpsl": scpsl_status,
        "gamepanel": gamepanel_status,
        "discord_bots": discord_bots_status,
        "database": db_status,
        "maintenance": maintenance_active
    })
@app.route('/status')
@login_required
def status_page():
    maintenance_status = dashboard_collection.find_one({"key": "scheduled_maintenance"}) if dashboard_collection is not None else None
    maintenance_active = maintenance_status and maintenance_status.get('enabled', False)
    return render_template('status_page.html', active_page='status_page', maintenance_active=maintenance_active)
@app.route('/dashboard')
@login_required
def dashboard():
    user_count = accounts_collection.count_documents({}) if accounts_collection is not None else 1
    last_user = accounts_collection.find_one(sort=[("creation_date", -1)]) if accounts_collection is not None else None
    last_user_time = last_user["creation_date"] if last_user and "creation_date" in last_user else None
    last_user_by = last_user["username"] if last_user and "username" in last_user else None
    open_tickets = 0
    if tickets_collection is not None:
        open_tickets = tickets_collection.count_documents({"status": "open"})
    server_load = {"percent": 0, "status": "Lädt..."}
    system_status = {"status": "Online"}
    try:
        resp = requests.get(request.url_root.rstrip('/') + '/api/system_status', cookies=request.cookies, timeout=2)
        if resp.status_code == 200:
            data = resp.json()
            if data.get("maintenance"):
                system_status = {"status": "Online (Wartungsarbeiten)"}
            elif any(data.get(k) == "wartung" for k in ["scpsl", "gamepanel", "discord_bots", "database"]):
                system_status = {"status": "Systeme im Wartungsmodus"}
            elif all(data.get(k) == "online" for k in ["scpsl", "gamepanel", "discord_bots", "database"]):
                system_status = {"status": "Online"}
            else:
                system_status = {"status": "Offline"}
        else:
            system_status = {"status": "Offline"}
    except Exception:
        system_status = {"status": "Offline"}
    scheduled_maintenance = dashboard_collection.find_one({"key": "scheduled_maintenance"}) if dashboard_collection is not None else None
    def _dt_to_str(v):
        if v is None:
            return None
        try:
            return v.strftime("%d. %b %Y, %H:%M")
        except Exception:
            return str(v)
     maint = scheduled_maintenance or {"time": None, "status": "Keine", "duration_minutes": None}
     maint["time_str"] = _dt_to_str(maint.get("time"))
     
      user_perms = session.get('permissions', [])
      all_tools = [
          {"label": "👤 User Mgmt", "url": "user_management", "perm": "view_user_management"},
          {"label": "🤖 Bots", "url": "discord_bots", "perm": "manage_discord_bots"},
          {"label": "🚚 ETS2 DLCs", "url": "ets2_dlcs", "perm": "view_ets2_dlcs"},
          {"label": "🌐 IP Analyzer", "url": "ip_analyzer", "perm": "view_ip_analyzer"},
          {"label": "📜 Logs", "url": "logs", "perm": "view_logs"},
          {"label": "🎂 Age Verify", "url": "age_verification", "perm": "view_age_verification"},
          {"label": "🥛 Ayran Tracker", "url": "ayran_tracker", "perm": "view_ayran_tracker"},
      ]
      available_tools = [t for t in all_tools if t["perm"] in user_perms]
     
     stats = {
         "active_users": {"value": user_count, "percent": 0},
         "server_load": server_load,
         "open_tickets": {"count": open_tickets},
         "system_status": system_status,
         "scheduled_maintenance": maint,
         "last_user_created": {
             "time": last_user_time,
             "by": last_user_by,
             "time_str": _dt_to_str(last_user_time)
         }
     }
     return render_template('dashboard.html', stats=stats, active_page='dashboard', available_tools=available_tools)
@app.route('/logout')
@login_required
def logout():
    add_log("Benutzer-Logout")
    session.clear()
    flash("Sie wurden erfolgreich abgemeldet.", "info")
    return redirect(url_for('login'))
@app.route('/user_management')
@login_required
@permission_required('view_user_management')
def user_management():
    if accounts_collection is None or roles_collection is None:
        flash("Datenbankverbindung nicht verfügbar.", "danger")
        return redirect(url_for('dashboard'))
    users_list = list(accounts_collection.find())
    roles_list = list(roles_collection.find())
    for role in roles_list:
        role['_id'] = str(role['_id'])
    role_map = {role['_id']: role['name'] for role in roles_list}
    users_for_modal = {}
    for user in users_list:
        user_id_str = str(user['_id'])
        users_for_modal[user_id_str] = {
            '_id': user_id_str,
            'username': user['username'],
            'role_id': str(user.get('role_id')) if user.get('role_id') else None
        }
    users_for_table = list(accounts_collection.find())
    for user in users_for_table:
        user['role_name'] = role_map.get(str(user.get('role_id')), 'Unbekannt')
        user['discord_linked'] = bool(user.get('discord_id'))
    return render_template('user_management.html',
                           users_for_modal=users_for_modal,
                           users_for_table=users_for_table,
                           roles=roles_list,
                           active_page='user_management')
@app.route('/add_user', methods=['POST'])
@login_required
@permission_required('manage_users')
def add_user():
    if accounts_collection is None:
        flash("Datenbankverbindung nicht verfügbar.", "danger")
        return redirect(url_for('user_management'))
    username = request.form.get('username')
    password = request.form.get('password')
    role_id = request.form.get('role')
    if not username or not password or not role_id:
        flash("Alle Felder sind erforderlich.", "danger")
        return redirect(url_for('user_management'))
    if accounts_collection.find_one({'username': username}):
        flash("Ein Benutzer mit diesem Namen existiert bereits.", "danger")
        return redirect(url_for('user_management'))
    hashed_password = generate_password_hash(password)
    accounts_collection.insert_one({
        'username': username,
        'password': hashed_password,
        'role_id': ObjectId(role_id),
        'creation_date': datetime.utcnow(),
        'discord_id': None,
        'discord_username': None,
        'discord_avatar': None
    })
    add_log("Benutzer erstellt", f"Benutzername: {username}")
    if dashboard_collection is not None:
        dashboard_collection.update_one(
            {"key": "last_user_created"},
            {"$set": {
                "time": datetime.utcnow(),
                "by": session.get('username')
            }},
            upsert=True
        )
        user_count = accounts_collection.count_documents({})
        dashboard_collection.update_one(
            {"key": "active_users"},
            {"$set": {"value": user_count}},
            upsert=True
        )
    flash(f"Benutzer '{username}' wurde erfolgreich erstellt.", "success")
    return redirect(url_for('user_management'))
@app.route('/edit_user/<user_id>', methods=['POST'])
@login_required
@permission_required('manage_users')
def edit_user(user_id):
    if accounts_collection is None:
        flash("Datenbankverbindung nicht verfügbar.", "danger")
        return redirect(url_for('user_management'))
    username = request.form.get('username')
    password = request.form.get('password')
    role_id = request.form.get('role')
    if not username or not role_id:
        flash("Benutzername und Rolle sind erforderlich.", "danger")
        return redirect(url_for('user_management'))
    existing_user = accounts_collection.find_one({'username': username})
    if existing_user and str(existing_user['_id']) != user_id:
        flash("Ein anderer Benutzer mit diesem Namen existiert bereits.", "danger")
        return redirect(url_for('user_management'))
    update_data = {
        'username': username,
        'role_id': ObjectId(role_id)
    }
    if password:
        update_data['password'] = generate_password_hash(password)
    accounts_collection.update_one(
        {'_id': ObjectId(user_id)},
        {'$set': update_data}
    )
    add_log("Benutzer bearbeitet", f"Benutzername: {username}")
    flash(f"Benutzer '{username}' wurde erfolgreich aktualisiert.", "success")
    return redirect(url_for('user_management'))
@app.route('/delete_user/<user_id>', methods=['POST'])
@login_required
@permission_required('manage_users')
def delete_user(user_id):
    if accounts_collection is None:
        flash("Datenbankverbindung nicht verfügbar.", "danger")
        return redirect(url_for('user_management'))
    if session.get('user_id') == user_id:
        flash("Sie können sich nicht selbst löschen.", "danger")
        return redirect(url_for('user_management'))
    user = accounts_collection.find_one({'_id': ObjectId(user_id)})
    accounts_collection.delete_one({'_id': ObjectId(user_id)})
    add_log("Benutzer gelöscht", f"Benutzername: {user.get('username', 'N/A')}")
    flash("Benutzer wurde erfolgreich gelöscht.", "success")
    return redirect(url_for('user_management'))

@app.route('/settings', methods=['GET'])
@login_required
def settings():
    user = accounts_collection.find_one({'_id': ObjectId(session['user_id'])})
    return render_template('settings.html', user=user, active_page='settings')

@app.route('/update_password', methods=['POST'])
@login_required
def update_password():
    user_id = session['user_id']
    current_password = request.form.get('current_password')
    new_password = request.form.get('new_password')
    confirm_password = request.form.get('confirm_password')

    if not all([current_password, new_password, confirm_password]):
        flash("Alle Passwortfelder sind erforderlich.", "danger")
        return redirect(url_for('settings'))

    if new_password != confirm_password:
        flash("Die neuen Passwörter stimmen nicht überein.", "danger")
        return redirect(url_for('settings'))

    user = accounts_collection.find_one({'_id': ObjectId(user_id)})
    if not user or not check_password_hash(user['password'], current_password):
        flash("Das aktuelle Passwort ist nicht korrekt.", "danger")
        return redirect(url_for('settings'))

    hashed_password = generate_password_hash(new_password)
    accounts_collection.update_one({'_id': ObjectId(user_id)}, {'$set': {'password': hashed_password}})
    add_log("Passwort geändert")
    flash("Dein Passwort wurde erfolgreich geändert.", "success")
    return redirect(url_for('settings'))

@app.route('/unlink_discord', methods=['POST'])
@login_required
def unlink_discord():
    user_id = session['user_id']
    accounts_collection.update_one(
        {'_id': ObjectId(user_id)},
        {'$set': {'discord_id': None, 'discord_username': None, 'discord_avatar': None}}
    )
    add_log("Discord-Verknüpfung aufgehoben")
    flash("Deine Discord-Verknüpfung wurde entfernt. Bitte verknüpfe dein Konto erneut.", "info")
    return redirect(url_for('link_discord'))

@app.route('/link_discord')
@login_required
def link_discord():
    return render_template('link_discord.html')

@app.route('/login/discord')
def login_discord():
    client_id = os.environ.get('DISCORD_CLIENT_ID')
    redirect_uri = os.environ.get('DISCORD_REDIRECT_URI')
    scope = 'identify'
    discord_auth_url = (
        f'https://discord.com/api/oauth2/authorize?client_id={client_id}&'
        f'redirect_uri={redirect_uri}&response_type=code&scope={scope}'
    )
    return redirect(discord_auth_url)

@app.route('/callback/discord')
def callback_discord():
    code = request.args.get('code')
    if not code:
        flash("Discord-Authentifizierung fehlgeschlagen.", "danger")
        return redirect(url_for('login'))

    client_id = os.environ.get('DISCORD_CLIENT_ID')
    client_secret = os.environ.get('DISCORD_CLIENT_SECRET')
    redirect_uri = os.environ.get('DISCORD_REDIRECT_URI')

    token_url = 'https://discord.com/api/oauth2/token'
    token_data = {
        'client_id': client_id,
        'client_secret': client_secret,
        'grant_type': 'authorization_code',
        'code': code,
        'redirect_uri': redirect_uri,
        'scope': 'identify'
    }
    headers = {'Content-Type': 'application/x-www-form-urlencoded'}
    token_r = requests.post(token_url, data=token_data, headers=headers)
    if token_r.status_code != 200:
        flash("Fehler beim Abrufen des Tokens von Discord.", "danger")
        return redirect(url_for('login'))

    access_token = token_r.json()['access_token']
    user_info_url = 'https://discord.com/api/users/@me'
    user_info_r = requests.get(user_info_url, headers={'Authorization': f'Bearer {access_token}'})
    if user_info_r.status_code != 200:
        flash("Fehler beim Abrufen der Benutzerinformationen von Discord.", "danger")
        return redirect(url_for('login'))

    discord_user = user_info_r.json()
    discord_id = discord_user['id']
    discord_username = f"{discord_user['username']}"
    discord_avatar = discord_user.get('avatar')

    if 'user_id' in session:
        user_id = session['user_id']
        existing_link = accounts_collection.find_one({'discord_id': discord_id})
        if existing_link and str(existing_link['_id']) != user_id:
            flash("Dieser Discord-Account ist bereits mit einem anderen Benutzer verknüpft.", "danger")
            return redirect(url_for('settings'))

        accounts_collection.update_one(
            {'_id': ObjectId(user_id)},
            {'$set': {
                'discord_id': discord_id,
                'discord_username': discord_username,
                'discord_avatar': discord_avatar
            }}
        )
        add_log("Discord verknüpft")
        flash("Discord-Account erfolgreich verknüpft!", "success")
        return redirect(url_for('dashboard'))

    else:
        user = accounts_collection.find_one({'discord_id': discord_id})
        if user:
            session.permanent = True
            session['user_id'] = str(user['_id'])
            session['username'] = user['username']
            session['permissions'] = get_user_permissions(str(user['_id']))
            add_log("Benutzer-Login via Discord")
            flash("Erfolgreich mit Discord angemeldet!", "success")
            return redirect(url_for('dashboard'))
        else:
            flash("Kein AstraOS-Benutzer mit diesem Discord-Konto gefunden. Bitte melde dich zuerst normal an und verknüpfe deinen Account.", "warning")
            return redirect(url_for('login'))

@app.route('/role_management')
@login_required
@permission_required('view_role_management')
def role_management():
    if roles_collection is None:
        flash("Datenbankverbindung nicht verfügbar.", "danger")
        return redirect(url_for('dashboard'))
    roles = list(roles_collection.find())
    for role in roles:
        role['_id'] = str(role['_id'])
    return render_template('role_management.html', roles=roles, available_permissions=AVAILABLE_PERMISSIONS, active_page='role_management')
@app.route('/add_role', methods=['POST'])
@login_required
@permission_required('manage_roles')
def add_role():
    role_name = request.form.get('role_name')
    permissions = request.form.getlist('permissions')
    if not role_name:
        flash("Rollenname ist erforderlich.", "danger")
        return redirect(url_for('role_management'))
    if roles_collection.find_one({'name': role_name}):
        flash("Eine Rolle mit diesem Namen existiert bereits.", "danger")
        return redirect(url_for('role_management'))
    roles_collection.insert_one({
        "name": role_name,
        "permissions": permissions,
        "is_deletable": True
    })
    add_log("Rolle erstellt", f"Rolle: {role_name}")
    flash(f"Rolle '{role_name}' wurde erstellt.", "success")
    return redirect(url_for('role_management'))
@app.route('/edit_role/<role_id>', methods=['POST'])
@login_required
@permission_required('manage_roles')
def edit_role(role_id):
    role_name = request.form.get('role_name')
    permissions = request.form.getlist('permissions')
    if not role_name:
        flash("Rollenname ist erforderlich.", "danger")
        return redirect(url_for('role_management'))
    roles_collection.update_one(
        {'_id': ObjectId(role_id)},
        {'$set': {'name': role_name, 'permissions': permissions}}
    )
    add_log("Rolle bearbeitet", f"Rolle: {role_name}")
    flash(f"Rolle '{role_name}' wurde aktualisiert.", "success")
    return redirect(url_for('role_management'))
@app.route('/delete_role/<role_id>', methods=['POST'])
@login_required
@permission_required('manage_roles')
def delete_role(role_id):
    role = roles_collection.find_one({'_id': ObjectId(role_id)})
    if role and not role.get('is_deletable', True):
        flash("Diese Rolle kann nicht gelöscht werden.", "danger")
        return redirect(url_for('role_management'))
    roles_collection.delete_one({'_id': ObjectId(role_id)})
    add_log("Rolle gelöscht", f"Rolle: {role.get('name', 'N/A')}")
    flash("Rolle wurde gelöscht.", "success")
    return redirect(url_for('role_management'))

def calculate_current_age(birthdate_str):
    try:
        birthdate_dt = datetime.strptime(birthdate_str, '%d.%m.%Y')
    except ValueError:
        birthdate_dt = datetime.strptime(birthdate_str, '%Y-%m-%d')
    today = datetime.today()
    return today.year - birthdate_dt.year - ((today.month, today.day) < (birthdate_dt.month, birthdate_dt.day))

@app.route('/age_verification', methods=['GET', 'POST'])
@login_required
@permission_required('view_age_verification')
def age_verification():
    result = None
    search_query = request.args.get('search', '')

    if request.method == 'POST':
        birthdate_str = request.form.get('birthdate')
        steam_id = request.form.get('steam_id')
        ingame_name = request.form.get('ingame_name')

        if birthdate_str and steam_id and ingame_name:
            try:
                birthdate_dt = datetime.strptime(birthdate_str, '%d.%m.%Y')
                today = datetime.today()
                age = today.year - birthdate_dt.year - ((today.month, today.day) < (birthdate_dt.month, birthdate_dt.day))

                user_data = {
                    'birthdate': birthdate_str,
                    'steam_id': steam_id,
                    'ingame_name': ingame_name,
                    'age': age,
                    'checked_by': session.get('username'),
                    'timestamp': datetime.utcnow()
                }
                age_verification_collection.insert_one(user_data)

                result = {'age': age, 'steam_id': steam_id}
                if age < 13:
                    thirteenth_birthday = birthdate_dt.replace(year=birthdate_dt.year + 13)
                    ban_duration_days = (thirteenth_birthday - today).days + 1
                    result['ban_duration_days'] = ban_duration_days
                    result['ban_command'] = f"oban {steam_id} {ban_duration_days}d"

                add_log("Altersüberprüfung hinzugefügt", f"Ingame Name: {ingame_name}, SteamID: {steam_id}")
                flash("Altersüberprüfung erfolgreich durchgeführt und gespeichert.", "success")
            except ValueError:
                flash("Ungültiges Datumsformat. Bitte DD.MM.YYYY verwenden.", "danger")

    if search_query:
        users = list(age_verification_collection.find({
            "$or": [
                {"ingame_name": {"$regex": search_query, "$options": "i"}},
                {"steam_id": {"$regex": search_query, "$options": "i"}}
            ]
        }).sort("timestamp", -1))
    else:
        users = list(age_verification_collection.find().sort("timestamp", -1).limit(10))

    for user in users:
        user['age'] = calculate_current_age(user['birthdate'])

    return render_template('age_verification.html',
                           active_page='age_verification',
                           result=result,
                           users=users,
                           search_query=search_query)
@app.route('/age_verification/<user_id>')
@login_required
@permission_required('view_age_verification')
def view_user_age_details(user_id):
    user = age_verification_collection.find_one({'_id': ObjectId(user_id)})
    if not user:
        flash("Benutzer nicht gefunden.", "danger")
        return redirect(url_for('age_verification'))

    user['age'] = calculate_current_age(user['birthdate'])

    result = None
    if user.get('age') < 13:
        today = datetime.today()
        try:
            birthdate_dt = datetime.strptime(user['birthdate'], '%d.%m.%Y')
        except ValueError:
            birthdate_dt = datetime.strptime(user['birthdate'], '%Y-%m-%d')
        thirteenth_birthday = birthdate_dt.replace(year=birthdate_dt.year + 13)
        ban_duration_days = (thirteenth_birthday - today).days + 1
        if ban_duration_days > 0:
            result = {
                'ban_duration_days': ban_duration_days,
                'ban_command': f"oban {user['steam_id']} {ban_duration_days}d"
            }

    return render_template('view_user_age_details.html',
                           active_page='age_verification',
                           user=user,
                           result=result)
@app.route('/age_verification/delete/<user_id>', methods=['POST'])
@login_required
@permission_required('view_age_verification')
def delete_age_verification(user_id):
    entry = age_verification_collection.find_one({'_id': ObjectId(user_id)})
    if not entry:
        flash("Eintrag nicht gefunden.", "danger")
        return redirect(url_for('age_verification'))

    can_delete = False
    if 'manage_age_verification' in session.get('permissions', []):
        can_delete = True
    elif entry.get('checked_by') == session.get('username'):
        can_delete = True

    if not can_delete:
        flash("Sie haben keine Berechtigung, diesen Eintrag zu löschen.", "danger")
        return redirect(url_for('view_user_age_details', user_id=user_id))

    age_verification_collection.delete_one({'_id': ObjectId(user_id)})
    add_log("Altersüberprüfung gelöscht", f"Ingame Name: {entry.get('ingame_name', 'N/A')}, SteamID: {entry.get('steam_id', 'N/A')}")
    flash("Eintrag zur Altersüberprüfung wurde gelöscht.", "success")
    return redirect(url_for('age_verification'))
@app.route('/ip_analyzer', methods=['GET', 'POST'])
@login_required
@permission_required('view_ip_analyzer')
def ip_analyzer():
    ip_data = None
    error = None
    similar_ips = []
    if request.method == 'POST':
        ip_address = request.form.get("ip_address")
        is_cheater = request.form.get("is_cheater") == "on"
        steam_id = request.form.get("steam_id") if is_cheater else None
        if not ip_address:
            error = "Bitte eine IP-Adresse eingeben."
        elif is_cheater and not steam_id:
            error = "SteamID ist erforderlich, wenn die IP als Cheater markiert wird."
        else:
            try:
                api_key = os.environ.get('VPNAPI_KEY')
                response = requests.get(f"https://vpnapi.io/api/{ip_address}?key={api_key}", timeout=5)
                if response.status_code == 200:
                    ip_data = response.json()
                    ip_data.setdefault('security', {})
                    ip_data.setdefault('location', {})
                    ip_data.setdefault('network', {})
                    similar_ips = list(ip_analyzer_results_collection.find({
                        "$or": [
                            {"ip": ip_address},
                            {"data.location.city": ip_data.get('location', {}).get('city')},
                            {"data.network.autonomous_system_number": ip_data.get('network', {}).get('autonomous_system_number')}
                        ]
                    }))
                    ip_analyzer_results_collection.insert_one({
                        "ip": ip_address,
                        "is_cheater": is_cheater,
                        "steam_id": steam_id,
                        "data": ip_data,
                        "added_by": session["username"],
                        "timestamp": datetime.utcnow()
                    })
                    add_log("IP analysiert", f"IP: {ip_address}")
                    flash("IP-Adresse erfolgreich analysiert und gespeichert.", "success")
                else:
                    error = f"Fehler bei der API-Anfrage: Status {response.status_code}"
            except requests.exceptions.RequestException as e:
                error = f"Fehler bei der Verbindung zur API: {e}"
            except Exception as e:
                error = f"Ein unerwarteter Fehler ist aufgetreten: {e}"
    return render_template('ip_analyzer.html',
                           active_page='ip_analyzer',
                           ip_data=ip_data,
                           error=error,
                           similar_ips=similar_ips)

@app.route('/logs')
@login_required
@permission_required('view_logs')
def logs():
    page = request.args.get('page', 1, type=int)
    per_page = 25
    skip = (page - 1) * per_page

    log_entries = list(logs_collection.find().sort('timestamp', -1).skip(skip).limit(per_page))
    total_logs = logs_collection.count_documents({})
    total_pages = (total_logs + per_page - 1) // per_page

    return render_template('logs.html',
                           logs=log_entries,
                           page=page,
                           total_pages=total_pages,
                           active_page='logs')


@app.route('/ets2_dlcs')
@login_required
@permission_required('view_ets2_dlcs')
def ets2_dlcs():
    if ets2_dlcs_collection is None:
        flash("Datenbankverbindung nicht verfügbar.", "danger")
        return redirect(url_for('dashboard'))

    packages = _format_ets2_dlcs_for_view(list(ets2_dlcs_collection.find().sort('created_at', -1)))
    return render_template(
        'ets2_dlcs.html',
        active_page='ets2_dlcs',
        dlc_packages=packages,
        can_manage_ets2_dlcs='manage_ets2_dlcs' in session.get('permissions', [])
    )


@app.route('/ets2_dlcs/create', methods=['POST'])
@login_required
@permission_required('manage_ets2_dlcs')
def create_ets2_dlc_package():
    if ets2_dlcs_collection is None:
        flash("Datenbankverbindung nicht verfügbar.", "danger")
        return redirect(url_for('ets2_dlcs'))

    package_name = (request.form.get('package_name') or '').strip()
    folder_path_input = (request.form.get('folder_path') or '').strip()
    dlc_date = (request.form.get('dlc_date') or '').strip()

    if not package_name or not folder_path_input or not dlc_date:
        flash("Name, Ordnerpfad und Datum sind erforderlich.", "danger")
        return redirect(url_for('ets2_dlcs'))

    try:
        datetime.strptime(dlc_date, '%Y-%m-%d')
    except ValueError:
        flash("Ungültiges Datumsformat. Bitte YYYY-MM-DD verwenden.", "danger")
        return redirect(url_for('ets2_dlcs'))

    folder_path = os.path.abspath(folder_path_input)
    if not os.path.isdir(folder_path):
        flash("Der angegebene Ordnerpfad existiert nicht oder ist kein Ordner.", "danger")
        return redirect(url_for('ets2_dlcs'))

    if ets2_dlcs_collection.find_one({'name': package_name}):
        flash("Ein DLC-Paket mit diesem Namen existiert bereits.", "danger")
        return redirect(url_for('ets2_dlcs'))

    package_id = ets2_dlcs_collection.insert_one({
        'name': package_name,
        'folder_path': folder_path,
        'dlc_date': dlc_date,
        'created_by': session.get('username'),
        'created_at': datetime.utcnow(),
        'archive_status': 'pending',
        'archive_path': None,
        'archive_generated_at': None,
        'archive_error': None
    }).inserted_id
    add_log("ETS2 DLC-Paket erstellt", f"Name: {package_name}, Pfad: {folder_path}")
    _schedule_ets2_archive_build(str(package_id))
    flash("DLC-Paket wurde erstellt. Das Archiv wird im Hintergrund vorbereitet.", "success")
    return redirect(url_for('ets2_dlcs'))


@app.route('/ets2_dlcs/delete/<package_id>', methods=['POST'])
@login_required
@permission_required('manage_ets2_dlcs')
def delete_ets2_dlc_package(package_id):
    if ets2_dlcs_collection is None:
        flash("Datenbankverbindung nicht verfügbar.", "danger")
        return redirect(url_for('ets2_dlcs'))

    try:
        package_object_id = ObjectId(package_id)
    except Exception:
        flash("Ungültige Paket-ID.", "danger")
        return redirect(url_for('ets2_dlcs'))

    package = ets2_dlcs_collection.find_one({'_id': package_object_id})
    if not package:
        flash("DLC-Paket nicht gefunden.", "danger")
        return redirect(url_for('ets2_dlcs'))

    archive_path = package.get('archive_path') or _ets2_archive_path(package_id, package.get('name', 'ets2_dlc_package'))
    if package.get('archive_status') == 'ready' and os.path.isfile(archive_path):
        try:
            os.remove(archive_path)
        except Exception:
            pass

    ets2_dlcs_collection.delete_one({'_id': package_object_id})
    add_log("ETS2 DLC-Paket gelöscht", f"Name: {package.get('name', 'N/A')}")
    flash("DLC-Paket wurde gelöscht.", "success")
    return redirect(url_for('ets2_dlcs'))


@app.route('/ets2_dlcs/download/<package_id>')
@login_required
@permission_required('view_ets2_dlcs')
def download_ets2_dlc_package(package_id):
    if ets2_dlcs_collection is None:
        flash("Datenbankverbindung nicht verfügbar.", "danger")
        return redirect(url_for('ets2_dlcs'))

    try:
        package_object_id = ObjectId(package_id)
    except Exception:
        flash("Ungültige Paket-ID.", "danger")
        return redirect(url_for('ets2_dlcs'))

    package = ets2_dlcs_collection.find_one({'_id': package_object_id})
    if not package:
        flash("DLC-Paket nicht gefunden.", "danger")
        return redirect(url_for('ets2_dlcs'))

    archive_path = package.get('archive_path') or _ets2_archive_path(package_id, package.get('name', 'ets2_dlc_package'))
    if package.get('archive_status') != 'ready' or not os.path.isfile(archive_path):
        if package.get('folder_path') and os.path.isdir(package.get('folder_path')):
            _schedule_ets2_archive_build(package_id)
            flash("Das DLC-Archiv wird noch erstellt. Bitte in ein paar Augenblicken erneut versuchen.", "info")
        else:
            flash("Das gespeicherte Archiv ist nicht verfügbar.", "danger")
        return redirect(url_for('ets2_dlcs'))

    safe_name = secure_filename(package.get('name', 'ets2_dlc_package')) or 'ets2_dlc_package'
    add_log("ETS2 DLC-Paket heruntergeladen", f"Name: {package.get('name', 'N/A')}")
    return send_file(archive_path, as_attachment=True, download_name=f"{safe_name}.zip", conditional=True)

def stream_bot_output(bot_id, process):
    def stream_to_socket(pipe, log_type):
        try:
            for line in iter(pipe.readline, ''):
                log_entry = {'type': log_type, 'data': line}
                if bot_id in running_bots:
                    running_bots[bot_id]['history'].append(log_entry)
                    max_history_lines = 1000
                    if len(running_bots[bot_id]['history']) > max_history_lines:
                        running_bots[bot_id]['history'].pop(0)
                socketio.emit('console_output', log_entry, room=bot_id)
        except Exception as e:
            print(f"Stream-Fehler für Bot {bot_id}: {e}")
        finally:
            if pipe:
                pipe.close()
    stdout_thread = threading.Thread(target=stream_to_socket, args=(process.stdout, 'stdout'))
    stderr_thread = threading.Thread(target=stream_to_socket, args=(process.stderr, 'stderr'))
    stdout_thread.start()
    stderr_thread.start()
    process.wait()
    if bot_id in running_bots and running_bots.get(bot_id) is not None:
        del running_bots[bot_id]
    if discord_bots_collection is not None:
        discord_bots_collection.update_one({'_id': ObjectId(bot_id)}, {'$set': {'status': 'stopped'}})
    socketio.emit('status_update', {'bot_id': bot_id, 'status': 'stopped'}, room=bot_id)
    socketio.emit('console_output', {'bot_id': bot_id, 'type': 'system', 'data': '--- Bot-Prozess beendet ---'}, room=bot_id)
@app.route('/discord_bots')
@login_required
@permission_required('manage_discord_bots')
def discord_bots():
    bots = list(discord_bots_collection.find())
    for bot in bots:
        bot['status'] = 'running' if str(bot['_id']) in running_bots else 'stopped'
    return render_template('discord_bots.html', active_page='discord_bots', bots=bots)
@app.route('/upload_bot', methods=['POST'])
@login_required
@permission_required('manage_discord_bots')
def upload_bot():
    bot_name = request.form.get('bot_name')
    bot_file = request.files.get('bot_file')
    if not bot_name or not bot_file or bot_file.filename == '':
        flash("Bot-Name und Datei sind erforderlich.", "danger")
        return redirect(url_for('discord_bots'))
    if discord_bots_collection.find_one({'name': bot_name}):
        flash("Ein Bot mit diesem Namen existiert bereits.", "danger")
        return redirect(url_for('discord_bots'))
    filename = secure_filename(bot_file.filename)
    filepath = os.path.join(app.config['BOT_UPLOAD_FOLDER'], filename)
    bot_file.save(filepath)
    discord_bots_collection.insert_one({
        'name': bot_name,
        'filename': filename,
        'filepath': filepath,
        'created_at': datetime.utcnow(),
        'status': 'stopped'
    })
    add_log("Bot hochgeladen", f"Bot Name: {bot_name}")
    flash(f"Bot '{bot_name}' wurde erfolgreich hochgeladen.", "success")
    return redirect(url_for('discord_bots'))
@app.route('/delete_bot/<bot_id>', methods=['POST'])
@login_required
@permission_required('manage_discord_bots')
def delete_bot(bot_id):
    bot_info = discord_bots_collection.find_one({'_id': ObjectId(bot_id)})
    if not bot_info:
        flash("Bot nicht gefunden.", "danger")
        return redirect(url_for('discord_bots'))
    if bot_id in running_bots and running_bots.get(bot_id) is not None:
        process = running_bots[bot_id]['process']
        try:
            process.terminate()
            process.wait(timeout=5)
        except subprocess.TimeoutExpired:
            process.kill()
        del running_bots[bot_id]
        socketio.emit('status_update', {'bot_id': bot_id, 'status': 'deleted'}, room=bot_id)
    try:
        filepath = bot_info.get('filepath')
        if filepath and os.path.exists(filepath):
            os.remove(filepath)
    except Exception as e:
        flash(f"Fehler beim Löschen der Bot-Datei: {e}", "danger")
    discord_bots_collection.delete_one({'_id': ObjectId(bot_id)})
    add_log("Bot gelöscht", f"Bot Name: {bot_info.get('name', 'N/A')}")
    flash(f"Bot '{bot_info['name']}' wurde erfolgreich gelöscht.", "success")
    return redirect(url_for('discord_bots'))
@app.route('/bot_console/<bot_id>')
@login_required
@permission_required('manage_discord_bots')
def bot_console(bot_id):
    bot = discord_bots_collection.find_one({'_id': ObjectId(bot_id)})
    if not bot:
        flash("Bot nicht gefunden.", "danger")
        return redirect(url_for('discord_bots'))
    bot['status'] = 'running' if bot_id in running_bots else 'stopped'
    return render_template('bot_console.html', bot=bot, active_page='discord_bots')
@socketio.on('connect')
def handle_connect():
    print('Client verbunden')
@socketio.on('join_console')
def handle_join_console(data):
    bot_id = data['bot_id']
    join_room(bot_id)
    emit('console_output', {'type': 'system', 'data': f'Verbunden mit Konsole für Bot {bot_id}.'})
    if bot_id in running_bots and running_bots.get(bot_id) is not None and 'history' in running_bots[bot_id]:
        for log in running_bots[bot_id]['history']:
            emit('console_output', log)
@socketio.on('leave_console')
def handle_leave_console(data):
    bot_id = data['bot_id']
    leave_room(bot_id)
    print(f'Client hat Konsole für Bot {bot_id} verlassen.')
@socketio.on('toggle_bot')
def handle_toggle_bot(data):
    bot_id = data['bot_id']
    action = data['action']
    bot_info = discord_bots_collection.find_one({'_id': ObjectId(bot_id)})
    if not bot_info:
        emit('console_output', {'type': 'error', 'data': 'Bot nicht in der Datenbank gefunden.'}, room=bot_id)
        return

    if action == 'start' and bot_id not in running_bots:
        filepath = bot_info.get('filepath')
        if not os.path.exists(filepath):
            emit('console_output', {'type': 'error', 'data': f'Bot-Datei nicht gefunden: {filepath}'}, room=bot_id)
            return

        try:
            _spawn_bot_process(bot_id, filepath, bot_name=bot_info.get('name', 'N/A'), emit_to_socket=True)
            add_log("Bot gestartet", f"Bot Name: {bot_info.get('name', 'N/A')}")
        except Exception as e:
            emit('console_output', {'type': 'error', 'data': f'Fehler beim Starten des Bots: {e}'}, room=bot_id)

    elif action == 'stop' and bot_id in running_bots and running_bots.get(bot_id) is not None:
        process = running_bots[bot_id]['process']
        try:
            process.terminate()
            process.wait(timeout=5)
        except subprocess.TimeoutExpired:
            process.kill()
        if bot_id in running_bots:
             del running_bots[bot_id]
        discord_bots_collection.update_one({'_id': ObjectId(bot_id)}, {'$set': {'status': 'stopped'}})
        add_log("Bot gestoppt", f"Bot Name: {bot_info.get('name', 'N/A')}")
        emit('status_update', {'bot_id': bot_id, 'status': 'stopped'}, room=bot_id)
        emit('console_output', {'type': 'system', 'data': '--- Bot-Prozess wird gestoppt... ---'}, room=bot_id)
@app.route('/maintenance')
def maintenance():
    maint_info = dashboard_collection.find_one({"key": "scheduled_maintenance"}) if dashboard_collection is not None else {}
    if not maint_info or not maint_info.get('enabled', False):
        return redirect(url_for('login'))
    return render_template('maintenance.html', maint_info=maint_info)
@app.route('/maintenance_login', methods=['GET', 'POST'])
def maintenance_login():
    if 'user_id' in session:
        return redirect(url_for('dashboard'))
    maintenance_status = dashboard_collection.find_one({"key": "scheduled_maintenance"}) if dashboard_collection is not None else None
    if not maintenance_status or not maintenance_status.get('enabled', False):
        return redirect(url_for('login'))
    if request.method == 'POST':
        username = request.form.get('username')
        password = request.form.get('password')
        if not username or not password:
            flash("Benutzername und Passwort sind erforderlich.", "danger")
            return render_template('maintenance_login.html')
        if accounts_collection is None:
            flash("Datenbankverbindung nicht verfügbar.", "danger")
            return render_template('maintenance_login.html'), 500
        user = accounts_collection.find_one({'username': username})
        if user and check_password_hash(user['password'], password):
            user_perms = get_user_permissions(str(user['_id']))
            if 'bypass_maintenance' in user_perms:
                session.permanent = True
                session['user_id'] = str(user['_id'])
                session['username'] = user['username']
                session['permissions'] = user_perms
                flash("Erfolgreich angemeldet!", "success")
                return redirect(url_for('dashboard'))
            else:
                flash("Dieser Account hat keine Berechtigung für den Wartungs-Login.", "danger")
                return render_template('maintenance_login.html')
        else:
            flash("Ungültiger Benutzername oder Passwort.", "danger")
            return render_template('maintenance_login.html')
    return render_template('maintenance_login.html')
@app.route('/maintenance_management')
@login_required
@permission_required('maintenance_management')
def maintenance_management():
    maint_info = dashboard_collection.find_one({"key": "scheduled_maintenance"}) if dashboard_collection is not None else {}
    if maint_info and maint_info.get('time'):
        maint_info['time_local_str'] = maint_info['time'].strftime('%Y-%m-%dT%H:%M')
    if 'discord_bots_maintenance' not in maint_info:
        maint_info['discord_bots_maintenance'] = False
    return render_template('maintenance_management.html', active_page='maintenance_management', maint_info=maint_info)


@app.route('/impressum')
def impressum():
    # Impressumsdaten werden aus Umgebungsvariablen geladen, damit keine persönlichen Daten im Code liegen.
    impressum_data = {
        'name': os.environ.get('IMPRESSUM_NAME'),
        'address': os.environ.get('IMPRESSUM_ADDRESS'),
        'email': os.environ.get('IMPRESSUM_EMAIL'),
        'phone': os.environ.get('IMPRESSUM_PHONE'),
        'company_registration': os.environ.get('IMPRESSUM_COMPANY_REG')
    }
    return render_template('impressum.html', active_page='impressum', impressum=impressum_data)

@app.route('/datenschutz')
def datenschutz():
    # Datenschutzerklärungsdaten werden aus Umgebungsvariablen geladen
    datenschutz_data = {
        'name': os.environ.get('IMPRESSUM_NAME', 'AstraOS'),
        'email': os.environ.get('IMPRESSUM_EMAIL', 'kontakt@astra-os.de'),
        'address': os.environ.get('IMPRESSUM_ADDRESS', '')
    }
    return render_template('datenschutz.html', active_page='datenschutz', datenschutz=datenschutz_data)

@app.route('/update_maintenance', methods=['POST'])
@login_required
@permission_required('maintenance_management')
def update_maintenance():
    if dashboard_collection is None:
        flash("Datenbankverbindung nicht verfügbar.", "danger")
        return redirect(url_for('maintenance_management'))
    is_enabled = request.form.get('maintenance_enabled') == 'on'
    status_message = request.form.get('status_message', 'Keine')
    duration = request.form.get('duration_minutes')
    time_str = request.form.get('maintenance_time')
    discord_bots_maintenance = request.form.get('discord_bots_maintenance') == 'on'
    update_data = {
        "enabled": is_enabled,
        "status": status_message,
        "duration_minutes": int(duration) if duration and duration.isdigit() else None,
        "time": datetime.fromisoformat(time_str) if time_str else None,
        "discord_bots_maintenance": discord_bots_maintenance
    }
    dashboard_collection.update_one(
        {"key": "scheduled_maintenance"},
        {"$set": update_data},
        upsert=True
    )
     add_log("Wartungseinstellungen aktualisiert", f"Aktiviert: {is_enabled}, Nachricht: {status_message}")
     flash("Wartungseinstellungen wurden aktualisiert.", "success")
     return redirect(url_for('maintenance_management'))

@app.route('/ayran_tracker')
@login_required
@permission_required('view_ayran_tracker')
def ayran_tracker():
     return render_template('ayran_tracker.html', active_page='ayran_tracker')

@app.route('/api/v1/profile/<name>/<tag>', methods=['GET'])
@login_required
@permission_required('view_ayran_tracker')
def get_valorant_stats(name, tag):
     VALORANT_API_KEY = os.environ.get('VALORANT_API_KEY')
     VALORANT_API_BASE = "https://api.henrikdev.xyz/valorant"
     mode = request.args.get('mode', 'competitive').lower()
     
     if not VALORANT_API_KEY:
         return jsonify({"status": "error", "message": "API-Schlüssel nicht konfiguriert"}), 500
     
     headers = {"Authorization": VALORANT_API_KEY}
     
     try:
         acc_res = requests.get(f"{VALORANT_API_BASE}/v1/account/{name}/{tag}?force=true", headers=headers, timeout=10)
         if acc_res.status_code != 200:
             return jsonify({"status": "error", "message": "Spieler nicht gefunden oder API-Limit erreicht."}), acc_res.status_code
         
         acc_data = acc_res.json().get('data', {}) or {}
         region = acc_data.get("region", "eu")

         mmr_res = requests.get(f"{VALORANT_API_BASE}/v1/mmr/{region}/{name}/{tag}", headers=headers, timeout=10)
         mmr_data = mmr_res.json().get('data', {}) if mmr_res.status_code == 200 else {}
         if not mmr_data: mmr_data = {}

         matches_res = requests.get(f"{VALORANT_API_BASE}/v3/matches/{region}/{name}/{tag}?size=15&mode={mode}", headers=headers, timeout=10)
         matches_list = matches_res.json().get('data', []) if matches_res.status_code == 200 else []
         if not matches_list: matches_list = []

         mmr_hist_res = requests.get(f"{VALORANT_API_BASE}/v1/mmr-history/{region}/{name}/{tag}", headers=headers, timeout=10)
         mmr_history = mmr_hist_res.json().get('data', []) if mmr_hist_res.status_code == 200 else []
         if not mmr_history: mmr_history = []

         mmr_history_dict = {}
         for entry in mmr_history:
             m_id = entry.get("match_id")
             if m_id:
                 mmr_history_dict[m_id] = entry

         total_kills = 0
         total_deaths = 0
         wins = 0
         total_damage = 0
         total_rounds = 0
         agent_counts = {}
         processed_matches = []

         for match in matches_list:
             if not match: continue
             metadata = match.get('metadata', {}) or {}
             match_id = metadata.get('matchid')
             players = match.get('players', {}).get('all_players', []) or []
             teams = match.get('teams', {}) or {}
             
             me = next((p for p in players if p.get('name', '').lower() == name.lower() and p.get('tag', '').lower() == tag.lower()), None)
             if not me:
                 continue

             stats = me.get('stats', {}) or {}
             kills = stats.get('kills', 0) or 0
             deaths = stats.get('deaths', 0) or 0
             assists = stats.get('assists', 0) or 0
             score = stats.get('score', 0) or 0
             damage = stats.get('damage', 0) or 0
             
             total_kills += kills
             total_deaths += deaths
             total_damage += damage

             agent_name = me.get('character', 'Unknown') or 'Unknown'
             agent_counts[agent_name] = agent_counts.get(agent_name, 0) + 1

             my_team = (me.get('team', '') or '').lower()
             rounds_won = teams.get(my_team, {}).get('rounds_won', 0) if teams.get(my_team) else 0
             rounds_won = rounds_won or 0
             
             enemy_team = 'red' if my_team == 'blue' else 'blue'
             rounds_lost = teams.get(enemy_team, {}).get('rounds_won', 0) if teams.get(enemy_team) else 0
             rounds_lost = rounds_lost or 0
             
             total_rounds += (rounds_won + rounds_lost)
             
             is_win = rounds_won > rounds_lost
             if is_win:
                 wins += 1

             start_time = metadata.get('game_start', 0) or 0
             date_str = datetime.fromtimestamp(start_time).strftime('%d.%m.%Y') if start_time else "Unbekannt"

             rr_change = 0
             if match_id and match_id in mmr_history_dict:
                 rr_change = mmr_history_dict[match_id].get('mmr_change_to_last_game', 0) or 0

             processed_matches.append({
                 "map": metadata.get('map', 'Unknown') or 'Unknown',
                 "mode": metadata.get('mode', 'Competitive') or 'Competitive',
                 "date": date_str,
                 "result": "WIN" if is_win else "LOSS",
                 "rounds_won": rounds_won,
                 "rounds_lost": rounds_lost,
                 "kills": kills,
                 "deaths": deaths,
                 "assists": assists,
                 "score": score,
                 "agent": agent_name,
                 "rr_change": rr_change
             })

         match_count = len(processed_matches)
         kd_calculated = round(total_kills / total_deaths, 2) if total_deaths > 0 else total_kills
         winrate_calculated = round((wins / match_count) * 100, 1) if match_count > 0 else 0
         adr_calculated = round(total_damage / total_rounds, 1) if total_rounds > 0 else 0

         sorted_agents = sorted(agent_counts.items(), key=lambda x: x[1], reverse=True)
         top_agents = [{"name": a[0], "count": a[1]} for a in sorted_agents[:2]]

         add_log("Ayran Tracker verwendet", f"Spieler: {name}#{tag}, Mode: {mode}")

         return jsonify({
             "status": "success",
             "account": {
                 "name": acc_data.get('name'),
                 "tag": acc_data.get('tag'),
                 "level": acc_data.get('account_level'),
                 "card_url": acc_data.get('card', {}).get('small') if acc_data.get('card') else None
             },
             "rank": {
                 "current_tier_name": mmr_data.get('currenttierpatched', 'Unranked') or 'Unranked',
                 "ranking_in_tier": mmr_data.get('ranking_in_tier', 0) or 0,
             },
             "calculated_stats": {
                 "win_rate": f"{winrate_calculated}%",
                 "kd_ratio": str(kd_calculated),
                 "avg_damage_round": str(adr_calculated),
                 "matches_analyzed": match_count
             },
             "top_agents": top_agents,
             "matches": processed_matches
         })

     except Exception as e:
         return jsonify({"status": "error", "message": f"Fehler bei der Berechnung: {str(e)}"}), 500

@app.errorhandler(404)
def page_not_found(e):
    return render_template('404.html'), 404

if __name__ == '__main__':
    app.config['SESSION_COOKIE_SECURE'] = False
    print("INFO: Starte den Server im Entwicklungsmodus ohne HTTPS.")
    socketio.run(app, host='0.0.0.0', port=5000, debug=True, allow_unsafe_werkzeug=True)
