import asyncio
import re
from pyrogram import Client, filters, idle
from pyrogram.types import Message
from aiohttp import web

# --- CONFIGURATION ---
API_ID = 29706288  # Replace with your API_ID
API_HASH = "32e67c7cffb983305c7b4fa6e52353cc" # Replace with your API_HASH
BOT_TOKEN = "7989178181:AAFsL4wgvWfNz8Ee_1iIVs0kJWtszwv_rOg" # Replace with your Bot Token
PORT = 9090
# ---------------------

# Increased workers to handle multiple refreshes simultaneously
bot = Client(
    "my_streamer_bot", 
    api_id=API_ID, 
    api_hash=API_HASH, 
    bot_token=BOT_TOKEN,
    workers=100,  
    sleep_threshold=10
)

file_store = {}

@bot.on_message(filters.video | filters.document)
async def handle_video(client: Client, message: Message):
    # Store the message to access file_id later
    # Format: chat_id_message_id
    unique_id = f"{message.chat.id}_{message.id}"
    file_store[unique_id] = message
    
    stream_link = f"https://tgstreamer.animixstream.ink/watch/{unique_id}"
    
    await message.reply_text(
        f"**✅ Link Generated!**\n\n"
        f"🔗 `{stream_link}`",
        quote=True
    )

async def handle_stream(request):
    unique_id = request.match_info.get('id')
    message = file_store.get(unique_id)
    
    # --- RECOVERY LOGIC START ---
    # If bot restarted, memory is empty. Fetch message from Telegram using the ID in the link.
    if not message and unique_id:
        try:
            # unique_id is formatted as "chatId_messageId"
            parts = unique_id.split('_')
            
            # Handle potential negative chat IDs properly
            if len(parts) == 2:
                chat_id = int(parts[0])
                message_id = int(parts[1])
                
                # Fetch message from Telegram API
                fetched_msg = await bot.get_messages(chat_id, message_id)
                
                if fetched_msg and (fetched_msg.video or fetched_msg.document):
                    message = fetched_msg
                    file_store[unique_id] = message # Cache it back to memory
        except Exception as e:
            print(f"Error restoring message: {e}")
    # --- RECOVERY LOGIC END ---

    if not message:
        return web.Response(status=404, text="404: Video not found, link expired, or message deleted.")

    # 1. GET FILE DETAILS
    file_size = message.video.file_size if message.video else message.document.file_size
    raw_mime = message.video.mime_type if message.video else message.document.mime_type
    raw_name = message.video.file_name if message.video else message.document.file_name
    
    # 2. SANITIZE HEADERS (Fixes 'Internal Server Error' & 'Newline detected')
    if not raw_mime: raw_mime = "video/mp4"
    if not raw_name: raw_name = "video.mp4"
    
    mime_type = raw_mime.replace('\r', '').replace('\n', '').strip()
    file_name = raw_name.replace('\r', '').replace('\n', '').replace('"', '').strip()

    # 3. HANDLE RANGE REQUESTS (Fixes 'Not loading on refresh')
    range_header = request.headers.get("Range")
    start = 0
    end = file_size - 1
    
    if range_header:
        # Browser requested a specific part (seeking/refreshing)
        try:
            matches = re.search(r"bytes=(\d+)-(\d*)", range_header)
            if matches:
                start = int(matches.group(1))
                if matches.group(2):
                    end = int(matches.group(2))
        except:
            pass # Invalid range? Just ignore and send full file.

    content_length = end - start + 1
    
    # 4. PREPARE RESPONSE
    # Status 206 is REQUIRED for video players to work properly
    response = web.StreamResponse(
        status=206 if range_header else 200,
        reason='Partial Content' if range_header else 'OK'
    )
    
    response.headers['Content-Type'] = mime_type
    response.headers['Accept-Ranges'] = 'bytes'
    response.headers['Content-Range'] = f'bytes {start}-{end}/{file_size}'
    response.headers['Content-Length'] = str(content_length)
    response.headers['Content-Disposition'] = f'inline; filename="{file_name}"'
    
    await response.prepare(request)

    # 5. STREAMING LOGIC
    CHUNK_SIZE = 1048576 # 1MB chunks
    chunk_start_index = start // CHUNK_SIZE
    offset_within_first_chunk = start % CHUNK_SIZE
    
    try:
        # Stream from Telegram starting at the specific chunk needed
        async for chunk in bot.stream_media(message, offset=chunk_start_index):
            
            # Trim the start of the first chunk if needed
            if offset_within_first_chunk > 0:
                chunk = chunk[offset_within_first_chunk:]
                offset_within_first_chunk = 0 
            
            # Stop if we have sent exactly what was requested
            if content_length > 0:
                if len(chunk) > content_length:
                    chunk = chunk[:content_length]
                
                await response.write(chunk)
                content_length -= len(chunk)
                
                if content_length <= 0:
                    break
            else:
                break
                
    except (ConnectionResetError, BrokenPipeError, asyncio.CancelledError, web.HTTPException):
        # NORMAL: User closed tab or refreshed. Stop silently.
        pass
    except Exception as e:
        # ABNORMAL: Only print real errors (not connection lost)
        if "Connection lost" not in str(e):
            print(f"Real Error: {e}")
        
    return response

async def web_server():
    app = web.Application()
    app.router.add_get('/watch/{id}', handle_stream)
    runner = web.AppRunner(app)
    await runner.setup()
    site = web.TCPSite(runner, '0.0.0.0', PORT)
    await site.start()
    print(f"🌐 Web Server running on port {PORT}")

async def main():
    await web_server()
    print("🤖 Bot Starting...")
    await bot.start()
    print("✅ Bot Started & Ready!")
    await idle()
    await bot.stop()

if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())