import asyncio
import os
import re
import time
import logging
import threading
import traceback
import pyrogram
from pyrogram import Client, filters, handlers
from pyrogram.types import InlineKeyboardMarkup, InlineKeyboardButton, Message, CallbackQuery
from flask import Flask, render_template, request, redirect, url_for, flash, jsonify, session, Response
from werkzeug.security import generate_password_hash, check_password_hash
from functools import wraps

from config import Config
from database import init_db, get_fresh_db, db_session, Account, LiveConfig, ReactionRule, SystemLog, Setting, log_system_event
from bot import BotManager, GlobalAsyncLoop
from live_joiner import TGVoiceCallManager, sanitize_chat_target, parse_multiple_chat_targets

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("WebApp")

app = Flask(__name__)
app.config.from_object(Config)

# Ensure Database Tables & Default Config Exist
init_db()

# Synchronous Async Runner Helper for Flask Routes
def run_async(coro, timeout=25):
    """Run an async coroutine inside the GlobalAsyncLoop thread safely with explicit timeout"""
    loop = GlobalAsyncLoop.get_loop()
    future = asyncio.run_coroutine_threadsafe(coro, loop)
    return future.result(timeout=timeout)

# Ensure BotManager Userbot Sessions start automatically on WSGI boot safely with thread lock
_init_lock = threading.Lock()

@app.before_request
def ensure_bot_initialized():
    """Lazily trigger BotManager initialization once safely on WSGI boot without blocking HTTP requests"""
    if not getattr(app, '_bot_initialized', False):
        with _init_lock:
            if not getattr(app, '_bot_initialized', False):
                app._bot_initialized = True
                def _bg_init():
                    try:
                        bm = BotManager.get_instance()
                        run_async(bm.initialize(), timeout=30)
                    except Exception as e:
                        logger.warning(f"Background BotManager init notice: {e}")
                threading.Thread(target=_bg_init, name="BG_BotInit", daemon=True).start()

# Global 500 Internal Server Error Handler for 100% Debugger Visibility
@app.errorhandler(500)
@app.errorhandler(Exception)
def handle_unexpected_error(error):
    err_tb = traceback.format_exc()
    logger.error(f"500 Internal Server Error: {err_tb}")
    return f"""
    <!DOCTYPE html>
    <html>
    <head><title>500 Internal Error Debugger</title></head>
    <body style="background: #121212; color: #ffffff; font-family: monospace; padding: 30px;">
        <h2 style="color: #ff4d4d;">❌ 500 Internal Server Error Details</h2>
        <div style="background: #1e1e1e; padding: 20px; border-radius: 8px; border: 1px solid #444; line-height: 1.6;">
            <pre style="white-space: pre-wrap; font-size: 13px; margin: 0; color: #ff6666;">{err_tb}</pre>
        </div>
        <p style="margin-top: 20px;"><a href="/dashboard" style="color: #4da6ff; font-weight: bold; text-decoration: none;">⬅️ Return to Dashboard</a></p>
    </body>
    </html>
    """, 500

def login_required(f):
    @wraps(f)
    def decorated_function(*args, **kwargs):
        if not session.get('logged_in'):
            return redirect(url_for('login'))
        return f(*args, **kwargs)
    return decorated_function

@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
        username = request.form.get('username', '').strip()
        password = request.form.get('password', '').strip()
        
        admin_user = Config.ADMIN_USERNAME or Setting.get("ADMIN_USERNAME", "admin")
        admin_pass = Config.ADMIN_PASSWORD or Setting.get("ADMIN_PASSWORD", "admin123")

        if username == admin_user and password == admin_pass:
            session['logged_in'] = True
            log_system_event(f"Admin logged in from IP: {request.remote_addr}")
            return redirect(url_for('dashboard'))
        else:
            flash("Invalid Username or Password!", "danger")
    return render_template('login.html')

@app.route('/logout')
def logout():
    session.pop('logged_in', None)
    flash("You have been logged out.", "info")
    return redirect(url_for('login'))

@app.route('/')
@app.route('/dashboard')
@login_required
def dashboard():
    db = get_fresh_db()
    account_count = db.query(Account).count()
    active_account_count = db.query(Account).filter_by(is_active=True).count()
    reaction_rule_count = db.query(ReactionRule).count()
    active_rule_count = db.query(ReactionRule).filter_by(is_enabled=True).count()
    live_cfg = db.query(LiveConfig).first()
    recent_logs = db.query(SystemLog).order_by(SystemLog.id.desc()).limit(10).all()
    db.close()

    return render_template(
        'dashboard.html',
        account_count=account_count,
        active_account_count=active_account_count,
        reaction_rule_count=reaction_rule_count,
        active_rule_count=active_rule_count,
        live_cfg=live_cfg,
        recent_logs=recent_logs
    )

# --- Sessions Manager & Web Phone OTP Login ---
@app.route('/sessions', methods=['GET', 'POST'])
@app.route('/sessions/', methods=['GET', 'POST'])
@login_required
def sessions():
    if request.method == 'POST':
        session_string = request.form.get('session_string', '').strip()
        phone_number = request.form.get('phone_number', '').strip()
        clean_session = "".join(session_string.split())

        if not clean_session:
            flash("Session String is required!", "danger")
        else:
            try:
                db = get_fresh_db()
                existing = db.query(Account).filter_by(session_string=clean_session).first()
                if not existing:
                    acc = Account(
                        session_string=clean_session,
                        name="Telegram Account",
                        phone_number=phone_number or "Active",
                        is_active=True
                    )
                    db.add(acc)
                    db.commit()
                    log_system_event(f"Saved new string session to database: {clean_session[:15]}...")
                    flash("🎉 Telegram Session String saved successfully to database!", "success")
                else:
                    flash("ℹ️ Account session string already exists in database!", "info")
                db.close()

                bm = BotManager.get_instance()
                threading.Thread(target=lambda: run_async(bm.load_user_accounts(), timeout=30), daemon=True).start()

            except Exception as e:
                flash(f"Error adding session: {str(e)}", "danger")
        return redirect(url_for('sessions'))

    db = get_fresh_db()
    accounts = db.query(Account).all()
    db.close()
    return render_template('sessions.html', accounts=accounts)

@app.route('/sessions/send_otp', methods=['POST'])
@app.route('/sessions/send_otp/', methods=['POST'])
@login_required
def sessions_send_otp():
    phone_number = request.form.get('phone_number', '').strip()
    if not phone_number:
        return jsonify({"success": False, "message": "Phone number is required"})

    try:
        bm = BotManager.get_instance()
        hash_code, code_type, phone_clean = run_async(bm.send_login_otp(phone_number))
        return jsonify({
            "success": True,
            "message": f"OTP Code sent successfully via {code_type} to {phone_clean}!",
            "phone": phone_clean,
            "type": code_type
        })
    except Exception as e:
        logger.error(f"Send OTP error: {e}")
        return jsonify({"success": False, "message": str(e)})

@app.route('/sessions/verify_otp', methods=['POST'])
@app.route('/sessions/verify_otp/', methods=['POST'])
@login_required
def sessions_verify_otp():
    phone_number = request.form.get('phone_number', '').strip()
    otp_code = request.form.get('otp_code', '').strip()
    two_fa_password = request.form.get('two_fa_password', '').strip()

    if not phone_number or not otp_code:
        return jsonify({"success": False, "message": "Phone number and OTP Code are required"})

    try:
        bm = BotManager.get_instance()
        success, msg = run_async(bm.verify_login_otp(phone_number, otp_code, two_fa_password))
        return jsonify({"success": success, "message": msg})
    except Exception as e:
        logger.error(f"Verify OTP error: {e}")
        return jsonify({"success": False, "message": str(e)})

@app.route('/sessions/read_otp/<int:account_id>', methods=['GET'])
@app.route('/sessions/read_otp/<int:account_id>/', methods=['GET'])
@login_required
def sessions_read_otp(account_id):
    try:
        bm = BotManager.get_instance()
        success, msg, messages = run_async(bm.get_account_otp_messages(account_id))
        return jsonify({
            "success": success,
            "message": msg,
            "messages": messages
        })
    except Exception as e:
        return jsonify({
            "success": False,
            "message": str(e),
            "messages": []
        })

@app.route('/sessions/delete/<int:account_id>', methods=['POST'])
@login_required
def delete_session(account_id):
    db = get_fresh_db()
    acc = db.query(Account).get(account_id)
    if acc:
        db.delete(acc)
        db.commit()
        log_system_event(f"Deleted account session ID: {account_id}")
        flash("Session deleted successfully.", "success")
    db.close()
    return redirect(url_for('sessions'))

# --- Commercial Services Suite Page & Execution Routes ---
@app.route('/services', methods=['GET', 'POST'])
@app.route('/services/', methods=['GET', 'POST'])
@login_required
def services():
    db = get_fresh_db()
    account_count = db.query(Account).filter_by(is_active=True).count()
    db.close()
    return render_template('services.html', account_count=account_count)

@app.route('/services/poll_vote', methods=['POST'])
@app.route('/services/poll_vote/', methods=['POST'])
@login_required
def service_poll_vote():
    target_chat = request.form.get('target_chat', '').strip()
    message_id = int(request.form.get('message_id', 0))
    option_index = int(request.form.get('option_index', 0))

    bm = BotManager.get_instance()
    if bm.services_engine:
        success, msg = run_async(bm.services_engine.vote_on_poll(target_chat, message_id, option_index))
        flash(msg, "success" if success else "danger")
    else:
        flash("No active userbot accounts loaded to perform Poll Voting!", "warning")

    return redirect(url_for('services'))

@app.route('/services/boost_views', methods=['POST'])
@app.route('/services/boost_views/', methods=['POST'])
@login_required
def service_boost_views():
    target_chat = request.form.get('target_chat', '').strip()
    msg_ids_str = request.form.get('message_ids', '').strip()
    
    try:
        msg_ids = [int(i.strip()) for i in msg_ids_str.split(",") if i.strip()]
        bm = BotManager.get_instance()
        if bm.services_engine:
            success, msg = run_async(bm.services_engine.boost_post_views(target_chat, msg_ids))
            flash(msg, "success" if success else "danger")
        else:
            flash("No active userbot accounts loaded to boost post views!", "warning")
    except Exception as e:
        flash(f"Invalid message IDs format: {e}", "danger")

    return redirect(url_for('services'))

@app.route('/services/mass_join', methods=['POST'])
@app.route('/services/mass_join/', methods=['POST'])
@login_required
def service_mass_join():
    target_chat = request.form.get('target_chat', '').strip()

    bm = BotManager.get_instance()
    if bm.services_engine:
        success, msg = run_async(bm.services_engine.mass_join_chat(target_chat))
        flash(msg, "success" if success else "danger")
    else:
        flash("No active userbot accounts loaded for Mass Join!", "warning")

    return redirect(url_for('services'))

@app.route('/services/live_comment', methods=['POST'])
@app.route('/services/live_comment/', methods=['POST'])
@login_required
def service_live_comment():
    target_chat = request.form.get('target_chat', '').strip()
    comments_raw = request.form.get('comments', '').strip()

    comments_list = [c.strip() for c in comments_raw.split("\n") if c.strip()]
    bm = BotManager.get_instance()
    if bm.services_engine:
        success, msg = run_async(bm.services_engine.send_live_comments(target_chat, comments_list))
        flash(msg, "success" if success else "danger")
    else:
        flash("No active userbot accounts loaded for Live Comments!", "warning")

    return redirect(url_for('services'))

# --- Realtime Live Voice Chat Debugger Route ---
@app.route('/live/debug')
@app.route('/live/debug/')
@login_required
def live_debug():
    db = get_fresh_db()
    cfg = db.query(LiveConfig).first()
    target = cfg.target_chat if cfg and cfg.target_chat else "@ninetechnical"
    accounts = db.query(Account).all()
    db.close()

    try:
        bm = BotManager.get_instance()
        run_async(bm.load_user_accounts(), timeout=20)
        if bm.live_manager and bm.user_clients:
            debug_html = run_async(bm.live_manager.debug_live_join(target), timeout=20)
        else:
            debug_html = f"❌ Live Manager has 0 loaded active accounts out of {len(accounts)} DB entries.\nCheck System Logs tab for connection errors."
    except Exception as ex:
        debug_html = f"❌ Debug Exception: {ex}\n{traceback.format_exc()}"

    return f"""
    <!DOCTYPE html>
    <html>
    <head><title>TG Live Realtime Debugger</title></head>
    <body style="background: #121212; color: #ffffff; font-family: monospace; padding: 30px;">
        <h2 style="color: #4da6ff;">🛠️ TG Live Realtime Debugger Report</h2>
        <div style="background: #1e1e1e; padding: 20px; border-radius: 8px; border: 1px solid #333; line-height: 1.8;">
            <pre style="white-space: pre-wrap; font-size: 14px; margin: 0;">{debug_html}</pre>
        </div>
        <p style="margin-top: 25px;"><a href="/live/" style="color: #4da6ff; text-decoration: none; font-weight: bold;">⬅️ Back to Live Dashboard</a></p>
    </body>
    </html>
    """

# --- API Route for 24/7 Auto-Detect & Auto-Join Ping (Cron / UptimeRobot) ---
@app.route('/api/cron_autojoin', methods=['GET', 'POST'])
@app.route('/api/cron_autojoin/', methods=['GET', 'POST'])
def cron_autojoin():
    """cPanel Cron Job or External Ping Endpoint to keep Auto-Detect running 24/7"""
    db = get_fresh_db()
    cfg = db.query(LiveConfig).first()
    if not cfg or not cfg.target_chat:
        db.close()
        return jsonify({"success": False, "message": "Target chat not configured in settings"})

    target = cfg.target_chat
    is_muted = cfg.is_muted
    db.close()

    try:
        bm = BotManager.get_instance()
        success, msg = run_async(bm.load_user_accounts_and_join(target, is_muted), timeout=30)
        return jsonify({"success": success, "message": msg, "target": target})
    except Exception as ex:
        return jsonify({"success": False, "message": str(ex)})

# --- API Route to Clear Terminal Logs ---
@app.route('/api/logs/clear', methods=['POST'])
@login_required
def clear_terminal_logs():
    db = get_fresh_db()
    db.query(SystemLog).delete()
    db.commit()
    db.close()
    log_system_event("System Logs cleared cleanly.")
    return jsonify({"success": True, "message": "Terminal logs cleared!"})

# --- TG Live Voice Chat Manager ---
@app.route('/live', methods=['GET', 'POST'])
@app.route('/live/', methods=['GET', 'POST'])
@login_required
def live_control():
    db = get_fresh_db()
    cfg = db.query(LiveConfig).first()
    if not cfg:
        cfg = LiveConfig(target_chat="", is_auto_join=False, is_muted=True)
        db.add(cfg)
        db.commit()

    if request.method == 'POST':
        action = request.form.get('action')

        if action == 'save_config':
            cfg.target_chat = request.form.get('target_chat', '').strip()
            cfg.is_auto_join = 'is_auto_join' in request.form
            cfg.is_muted = 'is_muted' in request.form
            db.commit()
            flash("TG Live settings saved successfully!", "success")

        elif action == 'diagnose':
            target = request.form.get('target_chat', '').strip() or cfg.target_chat
            if not target:
                flash("Target Channel/Group username or ID is required for diagnostic test!", "danger")
            else:
                cfg.target_chat = target
                db.commit()

                try:
                    bm = BotManager.get_instance()
                    run_async(bm.load_user_accounts(), timeout=20)
                    if bm.live_manager:
                        success, diag_text = run_async(bm.live_manager.run_live_diagnostics(target), timeout=20)
                        cfg.last_status = diag_text
                        db.commit()
                        flash("🔍 Diagnostic Test completed!", "success" if success else "warning")
                    else:
                        flash("❌ Diagnostic Fail: 0 active accounts loaded", "danger")
                except Exception as ex:
                    flash(f"❌ Diagnostic Exception: {ex}", "danger")

        elif action == 'join_live':
            target = request.form.get('target_chat', '').strip() or cfg.target_chat
            if not target:
                flash("Target Channel/Group username or ID is required!", "danger")
            else:
                cfg.target_chat = target
                cfg.is_joined = True
                cfg.last_status = f"Connecting accounts to {target}..."
                db.commit()

                try:
                    bm = BotManager.get_instance()
                    log_system_event(f"🚀 Triggered TG Live Join task for {target}")
                    success, msg = run_async(bm.load_user_accounts_and_join(target, cfg.is_muted), timeout=30)
                    flash(f"🎙️ Live Join Result: {msg}", "success" if success else "warning")
                except Exception as ex:
                    log_system_event(f"Live Join Trigger Exception: {ex}", level="ERROR")
                    flash(f"❌ Trigger Error: {ex}", "danger")

        elif action == 'leave_live':
            try:
                bm = BotManager.get_instance()
                if bm.live_manager:
                    success, msg = run_async(bm.live_manager.leave_live(), timeout=10)
                    cfg.is_joined = False
                    cfg.last_status = "Disconnected"
                    db.commit()
                    flash(f"ℹ️ {msg}", "info")
                else:
                    cfg.is_joined = False
                    cfg.last_status = "Disconnected"
                    db.commit()
                    flash("ℹ️ Disconnected from Live.", "info")
            except Exception as ex:
                flash(f"Error leaving live: {ex}", "danger")

        elif action == 'toggle_mute':
            new_mute = not cfg.is_muted
            cfg.is_muted = new_mute
            db.commit()

            bm = BotManager.get_instance()
            if bm.live_manager:
                success, msg = run_async(bm.live_manager.toggle_mute(new_mute), timeout=10)
                flash(msg, "info")
            else:
                flash(f"Audio set to {'Muted' if new_mute else 'Unmuted'}", "info")

        db.close()
        return redirect(url_for('live_control'))

    db.close()
    return render_template('live.html', cfg=cfg)

# --- Auto Reactions Settings ---
@app.route('/reactions', methods=['GET', 'POST'])
@app.route('/reactions/', methods=['GET', 'POST'])
@login_required
def reactions():
    db = get_fresh_db()

    if request.method == 'POST':
        action = request.form.get('action')

        if action == 'add_rule':
            target_chat = request.form.get('target_chat', '').strip()
            emojis = request.form.get('emojis', '').strip()
            delay = int(request.form.get('delay', 0))

            if target_chat and emojis:
                rule = ReactionRule(
                    target_chat=sanitize_chat_target(target_chat),
                    emojis=emojis,
                    delay_seconds=delay,
                    is_enabled=True
                )
                db.add(rule)
                db.commit()
                log_system_event(f"Added Auto Reaction Rule for {target_chat}: [{emojis}]")
                flash("Reaction Rule added successfully!", "success")
            else:
                flash("Target chat and emojis are required!", "danger")

        elif action == 'toggle_rule':
            rule_id = int(request.form.get('rule_id', 0))
            rule = db.query(ReactionRule).get(rule_id)
            if rule:
                rule.is_enabled = not rule.is_enabled
                db.commit()
                flash(f"Rule for {rule.target_chat} is now {'Enabled' if rule.is_enabled else 'Disabled'}.", "info")

        elif action == 'delete_rule':
            rule_id = int(request.form.get('rule_id', 0))
            rule = db.query(ReactionRule).get(rule_id)
            if rule:
                db.delete(rule)
                db.commit()
                flash("Reaction Rule deleted successfully.", "success")

        db.close()
        return redirect(url_for('reactions'))

    rules = db.query(ReactionRule).all()
    db.close()
    return render_template('reactions.html', rules=rules)

# --- Settings & Admin API Credentials Route ---
@app.route('/settings', methods=['GET', 'POST'])
@app.route('/settings/', methods=['GET', 'POST'])
@login_required
def settings():
    if request.method == 'POST':
        bot_token = request.form.get('bot_token', '').strip()
        admin_id = request.form.get('admin_user_id', '').strip()
        api_id = request.form.get('api_id', '').strip()
        api_hash = request.form.get('api_hash', '').strip()

        if bot_token:
            Setting.set("BOT_TOKEN", bot_token)
        if admin_id:
            Setting.set("ADMIN_USER_ID", admin_id)
        if api_id:
            Setting.set("API_ID", api_id)
        if api_hash:
            Setting.set("API_HASH", api_hash)

        log_system_event("Updated Bot & System Settings")
        flash("Settings updated successfully! Please restart python app if API ID/Hash was changed.", "success")

        return redirect(url_for('settings'))

    bot_token = Config.BOT_TOKEN or Setting.get("BOT_TOKEN", "")
    admin_id = Config.ADMIN_USER_ID or Setting.get("ADMIN_USER_ID", "")
    api_id = Config.API_ID or Setting.get("API_ID", "35408643")
    api_hash = Config.API_HASH or Setting.get("API_HASH", "9c690318646543daa057cc714e91f540")

    return render_template(
        'settings.html',
        bot_token=bot_token,
        admin_user_id=admin_id,
        api_id=api_id,
        api_hash=api_hash
    )

# --- System Logs ---
@app.route('/logs')
@app.route('/logs/')
@login_required
def logs():
    db = get_fresh_db()
    system_logs = db.query(SystemLog).order_by(SystemLog.id.desc()).limit(100).all()
    db.close()
    return render_template('logs.html', logs=system_logs)

@app.route('/logs/clear', methods=['POST'])
@login_required
def clear_logs():
    db = get_fresh_db()
    db.query(SystemLog).delete()
    db.commit()
    db.close()
    flash("System logs cleared.", "info")
    return redirect(url_for('logs'))

# --- Realtime Status API for Live Stream Log Box ---
@app.route('/api/status')
def api_status():
    db = get_fresh_db()
    cfg = db.query(LiveConfig).first()

    status_data = {
        "is_joined": cfg.is_joined if cfg else False,
        "last_status": cfg.last_status if cfg else "Disconnected",
        "target_chat": cfg.target_chat if cfg else ""
    }

    # Fetch last 15 system logs for live streaming terminal output
    recent_logs = db.query(SystemLog).order_by(SystemLog.id.desc()).limit(15).all()
    db.close()

    formatted_logs = []
    for l in reversed(recent_logs):
        level_tag = f"[{l.level}]" if l.level else "[INFO]"
        log_time = l.timestamp.strftime('%H:%M:%S') if getattr(l, 'timestamp', None) else ""
        formatted_logs.append(f"[{log_time}] {level_tag} {l.message}")

    status_data["logs"] = formatted_logs
    return jsonify(status_data)

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000, debug=Config.DEBUG)
