# Copyright (C) 2023 Stefy Lanza <stefy@nexlab.net> and SexHack.me
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#   
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <https://www.gnu.org/licenses/>.

from flask import Flask, render_template, request, session, jsonify
from utils import check_port_available, run_command, create_daemon, run_action
import sys
import os
import json
import time
import threading
from datetime import datetime
from guiutils import get_buttons
import flask_restful as restful

from flask_socketio import SocketIO, emit

import logging

logging.getLogger(__name__)



# Flask App Setup
flask_app = Flask('SHMCamStudio', template_folder=TEMPLATE_DIR)
flask_api = restful.Api(flask_app)

# XXX BugFix in eventlet used in socketio
os.environ['EVENTLET_NO_GREENDNS'] = 'yes'

socketio = SocketIO(flask_app)


buttons, numrows = get_buttons()
outputs = {}
feedbacks = {}
inputs = {}
if numrows > 0:
    row = 1
    while row <= numrows:
        for b in buttons[row].keys():
            if 'output' in buttons[row][b].keys():
                outputs[buttons[row][b]['output']] = {'cfg': buttons[row][b]}
            elif 'input' in buttons[row][b].keys():
                inputs[buttons[row][b]['input']] = {'cfg': buttons[row][b]}
            elif 'feedback' in buttons[row][b].keys():
                if not buttons[row][b]['feedback'] in feedbacks.keys():
                    feedbacks[buttons[row][b]['feedback']] = [{'cfg': buttons[row][b]}]
                else:
                    feedbacks[buttons[row][b]['feedback']].append({'cfg': buttons[row][b]})
        row = row+1           



class PollAPI(restful.Resource):

    def _is_updated(self, request_time):
        """
        Returns if resource is updated or it's the first
        time it has been requested.
        args:
            request_time: last request timestamp
        """
        return os.stat('data.txt').st_mtime > request_time

    def get(self):
        """
        Returns 'data.txt' content when the resource has
        changed after the request time
        """
        request_time = time.time()
        while not self._is_updated(request_time):
            time.sleep(0.5)
        content = ''
        with open('data.txt') as data:
            content = data.read()
        return {'content': content,
                'date': datetime.now().strftime('%Y/%m/%d %H:%M:%S')}


class AppData(restful.Resource):

    def get(self):
        """
        Returns the current data content
        """
        content = ''
        with open('data.txt') as data:
            content = data.read()
        return {'content': content}


def prepare_panel_page():
    #buttons, numrows = get_buttons()
    row = 1
    style_rows=""
    htmlbuttons=""
    if numrows > 0:
        while row <= numrows:
            bspan = int(120/len(buttons[row].keys()))
            style_rows=style_rows+"\n.button_row"+str(row)+" {\n    grid-column: span "+str(bspan)+";\n}\n"

            col=0
            htmlbuttons=htmlbuttons+"<div class='button_row'>"
            for b in buttons[row].keys():
                command=buttons[row][b]['action']
                color=buttons[row][b]['color']
                pollclass=''
                if 'output' in buttons[row][b].keys():
                    pollclass='output_'+buttons[row][b]['output']
                if 'input' in buttons[row][b].keys():
                    pollclass='input_'+buttons[row][b]['input']
                elif 'feedback'in buttons[row][b].keys():
                    pollclass='feedback_'+buttons[row][b]['feedback']
                htmlbuttons=htmlbuttons+"""<button style="color:white;background-color:"""+color+""";" 
                class="button private button_row"""+str(row)+" "+pollclass+""" " onclick="executeCommand('"""+command+"""')">
                """+buttons[row][b]['title']+"""
                </button>"""
            
            htmlbuttons=htmlbuttons+"</div>"
            row=row+1
    return style_rows, htmlbuttons


@flask_app.route('/')
def index():
    style_rows, htmlbuttons = prepare_panel_page()
    return render_template('index.html',  style_rows=style_rows, htmlbuttons=htmlbuttons)

@flask_app.route('/panel')
def panel():
    style_rows, htmlbuttons = prepare_panel_page()
    return render_template('panel.html',  style_rows=style_rows, htmlbuttons=htmlbuttons)

@flask_app.route('/execute', methods=['POST'])
def execute():
    command_key = request.form.get('command')

    if config.has_section('ACTION:'+command_key):
        result = run_action(command_key)
        return result or 'OK'
    else:
        return "Invalid command", 400

@flask_app.route('/stream')
def stream():
    stream_url = config.get('Web', 'stream_url', fallback="https://192.168.42.1/HLS/record/Live.m3u8")
    return render_template('stream.html', stream_url=stream_url)


# Fake data for the chat interface
fake_messages = [
    {"sender": "me", "content": "Hello! 😊", "timestamp": "2025-06-21 20:00"},
    {"sender": "alice@C4.sexhackme", "content": "Hi! Check this: https://example.com", "timestamp": "2025-06-21 20:01"},
    {"sender": "bob@SC.spora", "content": "<img src='https://pbs.twimg.com/media/FsypEu2X0AEtwK3?format=jpg&name=small' alt='Placeholder Image'>", "timestamp": "2025-06-21 20:02"},
    {"sender": "system", "content": "system: *** Server maintenance scheduled at 1 AM", "type": "notify-system", "timestamp": "2025-06-21 20:03"},
    {"sender": "platform@SC.spora", "content": "platform: *** User joined the room", "type": "notify-platform", "timestamp": "2025-06-21 20:04"},
    {"sender": "bob@SC.spora", "content": "<span class='sender' data-sender='bob@SC.spora' style='color: #10b981'>bob@SC.spora</span> TIPPED <b>50 TOKENS</b> ($5.00)<div style='text-align: center; color: #6ee7b7'>PM REQUEST</div>", "type": "tip", "timestamp": "2025-06-21 20:05"}
]

fake_users = {
    "C4.sexhackme": [
        {"username": "alice", "status": "online", "tokens": 50},
        {"username": "charlie", "status": "offline", "tokens": 20}
    ],
    "SC.spora": [
        {"username": "bob", "status": "online", "tokens": 30}
    ]
}

fake_earnings = [
    {"platform": "C4.sexhackme", "lastSession": 100, "today": 250, "lastHour": 50, "sess": 100},
    {"platform": "SC.spora", "lastSession": 80, "today": 200, "lastHour": 30, "sess": 80}
]

fake_status = {
    "C4.sexhackme": "online",
    "SC.spora": "offline"
}

fake_rtsp_urls = [
    {"id": "rtsp1", "url": "rtsp://example.com/stream1"},
    {"id": "rtsp2", "url": "rtsp://example.com/stream2"}
]

# Test private chat data
test_private_chat = {
    "alice@C4.sexhackme": [
        {
            "sender": "me",
            "content": "Hello Alice!",
            "timestamp": datetime.now().isoformat()
        },
        {
            "sender": "alice@C4.sexhackme",
            "content": "Hi there! How can I help you?",
            "timestamp": datetime.now().isoformat()
        }
    ]
}

# Track unread messages in private chats
unread_private_messages = {
    "alice@C4.sexhackme": 1  # Number of unread messages
}

# Store the last update timestamp for each data type
last_updates = {
    "messages": time.time(),
    "users": time.time(),
    "earnings": time.time(),
    "status": time.time(),
    "rtsp_urls": time.time(),
    "private_chats": time.time()
}

# Store the last request timestamp for each client
client_last_request = {}

@flask_app.route("/chat")
def chat():
   return render_template('chat.html')

@flask_app.route("/api/chat")
def get_chat_data():
    """
    Consolidated API endpoint for chat data with non-blocking long polling.
    
    This implements a hybrid approach:
    1. SocketIO for real-time push notifications when data changes
    2. Long polling as a fallback mechanism with non-blocking behavior
    
    The client will receive immediate updates via SocketIO when available,
    and will fall back to polling if SocketIO is not working.
    """
    client_id = request.args.get('client_id', str(time.time()))
    
    # Initialize client's last request time if it's a new client
    if client_id not in client_last_request:
        # New client - set initial timestamp to 0 to ensure they get data
        client_last_request[client_id] = 0
    
    # Get the stored last request time for this client
    client_last_time = client_last_request[client_id]
    
    # Check if any data has been updated since the client's last request
    def is_data_updated():
        # For new clients (last_time=0), always return true
        if client_last_time == 0:
            return True
            
        # For existing clients, check if any data has been updated
        return (last_updates["messages"] > client_last_time or
                last_updates["users"] > client_last_time or
                last_updates["earnings"] > client_last_time or
                last_updates["status"] > client_last_time or
                last_updates["rtsp_urls"] > client_last_time or
                last_updates["private_chats"] > client_last_time)
    
    # If data is already updated or this is a new client, return immediately
    if is_data_updated():
        current_time = time.time()
        # Update the client's last request time
        client_last_request[client_id] = current_time
        
        # Log the data update for debugging
        logging.info(f"Sending updated data to client {client_id}")
        
        return jsonify({
            "messages": fake_messages,
            "users": fake_users,
            "earnings": fake_earnings,
            "status": fake_status,
            "rtsp_urls": fake_rtsp_urls,
            "private_chats": test_private_chat,
            "unread_private_messages": unread_private_messages,
            "timestamp": current_time
        })
    
    # For long polling, return a response that will be processed by the client
    # This avoids blocking the server thread
    return jsonify({
        "no_update": True,
        "retry_after": 1000,  # Retry after 1 second
        "timestamp": time.time()
    })

# Keep these routes for backward compatibility if needed
@flask_app.route("/api/messages")
def get_messages():
    last_updates["messages"] = time.time()
    socketio.emit('chat_update', {'type': 'messages'})
    return jsonify(fake_messages)

@flask_app.route("/api/users")
def get_users():
    last_updates["users"] = time.time()
    socketio.emit('chat_update', {'type': 'users'})
    return jsonify(fake_users)

@flask_app.route("/api/earnings")
def get_earnings():
    last_updates["earnings"] = time.time()
    socketio.emit('chat_update', {'type': 'earnings'})
    return jsonify(fake_earnings)

@flask_app.route("/api/status")
def get_status():
    last_updates["status"] = time.time()
    socketio.emit('chat_update', {'type': 'status'})
    return jsonify(fake_status)

@flask_app.route("/api/rtsp_urls")
def get_rtsp_urls():
    last_updates["rtsp_urls"] = time.time()
    socketio.emit('chat_update', {'type': 'rtsp_urls'})
    return jsonify(fake_rtsp_urls)

@flask_app.route("/api/private_chats")
def get_private_chats():
    last_updates["private_chats"] = time.time()
    socketio.emit('chat_update', {'type': 'private_chats'})
    return jsonify(test_private_chat)

@flask_app.route("/api/send_message", methods=["POST"])
def send_message():
    data = request.json
    if not data or "content" not in data:
        return jsonify({"error": "Invalid message data"}), 400
    
    # Get client_id from request if available
    client_id = request.args.get('client_id', None)
    
    new_message = {
        "sender": "me",
        "content": data["content"],
        "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M")
    }
    
    # Add to fake messages
    fake_messages.append(new_message)
    
    # Update the last update timestamp for messages
    current_time = time.time()
    last_updates["messages"] = current_time
    
    # Reset all client timestamps to force data refresh on next poll
    # But set them to slightly before current time to avoid full refresh
    for cid in client_last_request:
        # Don't reset the current client's timestamp if we know who it is
        if client_id and cid == client_id:
            continue
        # Set to a value that will trigger an update but not to 0
        client_last_request[cid] = current_time - 1
    
    # Notify clients about the update
    socketio.emit('chat_update', {'type': 'messages'})
    
    return jsonify({"success": True, "message": new_message})

@flask_app.route("/api/send_private_message", methods=["POST"])
def send_private_message():
    data = request.json
    if not data or "content" not in data or "recipient" not in data:
        return jsonify({"error": "Invalid message data"}), 400
    
    # Get client_id from request if available
    client_id = request.args.get('client_id', None)
    
    recipient = data["recipient"]
    content = data["content"]
    
    # Create new message
    new_message = {
        "sender": "me",
        "content": content,
        "timestamp": datetime.now().isoformat()
    }
    
    # Add to private chat
    if recipient not in test_private_chat:
        test_private_chat[recipient] = []
    
    test_private_chat[recipient].append(new_message)
    
    # Simulate response
    response_message = {
        "sender": recipient,
        "content": f"Thanks for your message: \"{content}\"",
        "timestamp": datetime.now().isoformat()
    }
    
    test_private_chat[recipient].append(response_message)
    
    # Increment unread message count for the recipient's response
    if recipient not in unread_private_messages:
        unread_private_messages[recipient] = 0
    unread_private_messages[recipient] += 1
    
    # Update the last update timestamp for private chats
    current_time = time.time()
    last_updates["private_chats"] = current_time
    
    # Reset all client timestamps to force data refresh on next poll
    for cid in client_last_request:
        # Don't reset the current client's timestamp if we know who it is
        if client_id and cid == client_id:
            continue
        # Set to a value that will trigger an update but not to 0
        client_last_request[cid] = current_time - 1
    
    # Notify clients about the update with additional data
    socketio.emit('chat_update', {
        'type': 'private_chats',
        'sender': recipient,
        'has_unread': True
    })
    
    return jsonify({
        "success": True,
        "messages": test_private_chat[recipient],
        "unread_count": unread_private_messages[recipient]
    })

@flask_app.route("/api/mark_messages_read", methods=["POST"])
def mark_messages_read():
    data = request.json
    if not data or "sender" not in data:
        return jsonify({"error": "Invalid data"}), 400
    
    # Get client_id from request if available
    client_id = request.args.get('client_id', None)
    
    sender = data["sender"]
    
    # Reset unread count for this sender
    if sender in unread_private_messages:
        unread_private_messages[sender] = 0
    
    # Update the last update timestamp for private chats
    current_time = time.time()
    last_updates["private_chats"] = current_time
    
    # Reset all client timestamps to force data refresh on next poll
    for cid in client_last_request:
        # Don't reset the current client's timestamp if we know who it is
        if client_id and cid == client_id:
            continue
        # Set to a value that will trigger an update but not to 0
        client_last_request[cid] = current_time - 1
    
    # Notify clients about the update with specific information
    socketio.emit('chat_update', {
        'type': 'private_chats',
        'sender': sender,
        'has_unread': False
    })
    
    return jsonify({
        "success": True,
        "sender": sender,
        "unread_count": 0
    })

@flask_app.route("/api/reset_session", methods=["POST"])
def reset_session():
    # Reset session earnings
    for earning in fake_earnings:
        earning["sess"] = 0
    
    # Update the last update timestamp for earnings
    last_updates["earnings"] = time.time()
    
    # Notify clients about the update
    socketio.emit('chat_update', {'type': 'earnings'})
    
    return jsonify({"success": True})

@flask_app.route("/api/update_status", methods=["POST"])
def update_status():
    data = request.json
    if not data:
        return jsonify({"error": "Invalid status data"}), 400
    
    # Update all platforms
    if "all" in data and data["all"] in ["online", "offline"]:
        new_status = data["all"]
        for platform in fake_status:
            fake_status[platform] = new_status
    
    # Update specific platform
    elif "platform" in data and "status" in data:
        platform = data["platform"]
        new_status = data["status"]
        if platform in fake_status:
            fake_status[platform] = new_status
    
    # Update the last update timestamp for status
    last_updates["status"] = time.time()
    
    # Notify clients about the update
    socketio.emit('chat_update', {'type': 'status'})
    
    return jsonify({"success": True, "status": fake_status})

@socketio.event
def my_event(message):
   session['receive_count'] = session.get('receive_count', 0) + 1
   emit('my_response',
     {'data': message['data'], 'count': session['receive_count']})

@socketio.event
def my_ping():
   emit('my_pong')

@socketio.event
def connect():
    """Handle client connection"""
    client_id = request.sid
    logging.info(f"Client connected: {client_id}")
    
    # Initialize new client with timestamp 0 to ensure they get all data
    client_last_request[client_id] = 0
    
    # Emit an immediate update event to trigger data fetch
    emit('chat_update', {'type': 'initial_connect'})
    
@socketio.event
def disconnect():
    """Handle client disconnection"""
    client_id = request.sid
    logging.info(f"Client disconnected: {client_id}")
    if client_id in client_last_request:
        del client_last_request[client_id]



@socketio.event
def get_queue():
    count=5
    while not qweb.empty() and count > 0: 
        count = count-1
        task=qweb.get(block=True)
        if task:
            logging.info('TASK INCOMING FOR WEB')
            logging.info(task)
            event = task['event']
            data = task['data']
            if event == 'OUTPUTCHANGE':
                if data['output'] in outputs.keys():
                    logging.info('CHANGE THE COLOR OF THE WEB BUTTON FOR '+str(data['output']))
                    bcfg = outputs[data['output']]['cfg']
                    if 'color.'+str(data['status']) in bcfg.keys():
                        emit('change_output', {'button': data['output'], 'color': bcfg['color.'+str(data['status'])] }, broadcast=True)
                    if 'title.'+str(data['status']) in bcfg.keys():
                        emit('change_output', {'button': data['output'], 'title': bcfg['title.'+str(data['status'])] }, broadcast=True)
            if event == 'INPUTSTATUSCHANGE':
               if data['input'] in inputs.keys():
                   logging.info('CHANGE THE COLOR OF THE WEB BUTTON FOR '+str(data['input']))
                   bcfg = inputs[data['input']]['cfg']
                   if data['status']:
                       emit('change_input', {'button': data['input'], 'color': bcfg['color.enabled'] }, broadcast=True)
                   else:
                       emit('change_input', {'button': data['input'], 'color': bcfg['color']}, broadcast=True)
            elif event == 'STATUSCHANGE':
                if 'status' in feedbacks.keys():
                    for b in feedbacks['status']:
                        bcfg = b['cfg']
                        if 'color.'+str(data['status']) in bcfg.keys():
                            emit('change_feedback', {'feedback': 'status', 'color': bcfg['color.'+str(data['status'])] }, broadcast=True)
                            #btn.config(bg=bcfg['color.'+str(data['status'])])
                        if 'title.'+str(data['status']) in bcfg.keys():
                            emit('change_feedback', {'feedback': 'status', 'title': bcfg['title.'+str(data['status'])] }, broadcast=True)
                            


# Function to clean up stale client entries
def cleanup_stale_clients():
    """Remove clients that haven't made a request in the last 10 minutes"""
    while True:
        try:
            current_time = time.time()
            stale_threshold = current_time - 600  # 10 minutes
            
            # Create a copy of the keys to avoid modifying during iteration
            client_ids = list(client_last_request.keys())
            
            for client_id in client_ids:
                if client_last_request[client_id] < stale_threshold:
                    logging.info(f"Removing stale client: {client_id}")
                    del client_last_request[client_id]
        except Exception as e:
            logging.error(f"Error in cleanup thread: {e}")
        
        # Sleep for 5 minutes before next cleanup
        time.sleep(300)

def run_flask_app(port=5000, daemon_mode=False):
    """Run Flask app with optional daemon mode"""
    if not check_port_available(port):
        logger.error(f"Port {port} is already in use")
        sys.exit(1)

    logger.info(f"Starting Flask app on port {port}")
   
    if daemon_mode and sys.platform != 'win32':
        create_daemon()
    
    # Start the cleanup thread
    cleanup_thread = threading.Thread(target=cleanup_stale_clients, daemon=True)
    cleanup_thread.start()
    
    flask_api.add_resource(PollAPI, '/update')
    flask_api.add_resource(AppData, '/data')
    #flask_app.run(host='0.0.0.0', port=port, debug=False, use_reloader=False)

    # for socketio
    socketio.run(flask_app, host='0.0.0.0', port=port, debug=True, use_reloader=False)


