import argparse
import os
import httpx
import uvicorn
import json
import urllib.request
import time
import subprocess
import sys
from datetime import datetime
from fastapi import FastAPI, HTTPException, Request
from fastapi.responses import HTMLResponse, StreamingResponse, Response
from pydantic import BaseModel
from typing import List

# Command Line Arguments Configuration
parser = argparse.ArgumentParser(description="VLM Edge Studio WebApp Bridge")
parser.add_argument("--video-dir", required=True, help="Directory path where video MP4 files are hosted")
parser.add_argument("--aaf-server", default="http://127.0.0.1:8000", help="AAF Server backend Base URL")
parser.add_argument("--host", default="0.0.0.0", help="Host binding address for this web application")
parser.add_argument("--port", type=int, default=8080, help="Port binding for this web application")
parser.add_argument("--verbose", action="store_true", default=False, help="Enable verbose terminal dumping")

args, _ = parser.parse_known_args()

TARGET_MODEL = "Qwen2.5-VL-7B-Instruct"
CONFIG_PATH = "/usr/share/eiq/aaf-connector/server_config.json"
SERVICE_NAME = "eiq-aaf-connector.service"

# --- MODEL CONFIG & SERVICE CONTROL FUNCTIONS ---

def ensure_model_enabled(target_model: str, config_path: str) -> bool:
    """Updates server_config.json so only the target model is set to enabled=True."""
    if not os.path.exists(config_path):
        print(f"[Warning] Config file not found at {config_path}.")
        return False

    try:
        with open(config_path, "r") as f:
            config = json.load(f)

        config_changed = False
        available_models = config.get("available_models", [])

        if isinstance(available_models, list):
            for model_info in available_models:
                model_id = model_info.get("name") or model_info.get("id")
                should_be_enabled = (model_id == target_model)
                if model_info.get("enabled") != should_be_enabled:
                    model_info["enabled"] = should_be_enabled
                    config_changed = True

        if config_changed:
            print(f"[Config] Enabling '{target_model}' in server_config.json...")
            with open(config_path, "w") as f:
                json.dump(config, f, indent=4)
            return True

        print(f"[Config] '{target_model}' is already set as enabled.")
    except Exception as e:
        print(f"[Error] Failed updating config file: {e}")

    return False


def is_service_running(service_name: str) -> bool:
    try:
        res = subprocess.run(
            ["systemctl", "is-active", service_name],
            stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True
        )
        return res.stdout.strip() == "active"
    except Exception:
        return False


def restart_aaf_service(service_name: str):
    print(f"[Service] Restarting '{service_name}'...")
    try:
        subprocess.run(["systemctl", "restart", service_name], check=True)
    except subprocess.CalledProcessError as e:
        print(f"[Error] Failed to restart service: {e}")
        sys.exit(1)


def wait_for_model_ready(target_model: str, server_base_url: str, timeout: int = 300) -> bool:
    models_url = f"{server_base_url}/v1/models"
    print(f"[Service] Waiting for '{target_model}' to load on NPU (can take several minutes)...", end="", flush=True)
    start_time = time.time()

    while time.time() - start_time < timeout:
        try:
            req = urllib.request.Request(models_url, headers={"Accept": "application/json"})
            with urllib.request.urlopen(req, timeout=2.0) as resp:
                if resp.status == 200:
                    data = json.loads(resp.read().decode('utf-8'))
                    loaded_models = [m.get("id") or m.get("name") for m in data.get("data", [])]
                    if target_model in loaded_models or len(loaded_models) > 0:
                        print("\n[Service] AAF Connector is ready and Vision model endpoint is active!")
                        return True
        except Exception:
            pass

        print(".", end="", flush=True)
        time.sleep(2.0)

    print("\n[Error] Timed out waiting for AAF Connector model initialization.")
    return False


def prepare_aaf_backend():
    config_updated = ensure_model_enabled(TARGET_MODEL, CONFIG_PATH)
    service_active = is_service_running(SERVICE_NAME)

    if config_updated or not service_active:
        if not service_active:
            print(f"[Service] '{SERVICE_NAME}' is not active. Starting service...")
        restart_aaf_service(SERVICE_NAME)

    if not wait_for_model_ready(TARGET_MODEL, args.aaf_server, timeout=600):
        print(f"[Error] Unable to reach target model '{TARGET_MODEL}' on AAF Server ({args.aaf_server}).")
        sys.exit(1)


# --- FASTAPI WEB APPLICATION ---
app = FastAPI(title="Gateworks AI VLM Chat")

if not os.path.isdir(args.video_dir):
    raise RuntimeError(f"Provided video directory target does not exist: {args.video_dir}")

class ChatMessage(BaseModel):
    role: str
    content: str

class MultiTurnPayload(BaseModel):
    video_name: str
    history: List[ChatMessage]

def get_timestamp():
    return datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]

# --- STREAMING VIDEO ROUTE WITH HTTP RANGE SUPPORT ---
# Change "async def" to "def"
@app.get("/stream/video/{filename}", tags=["Media"])
def stream_video(filename: str, request: Request):
    video_path = os.path.join(args.video_dir, filename)
    if not os.path.isfile(video_path):
        raise HTTPException(status_code=404, detail="Video file not found.")

    file_size = os.path.getsize(video_path)
    range_header = request.headers.get("range")

    if not range_header:
        def iterfile():
            with open(video_path, mode="rb") as file_like:
                yield from file_like
        return StreamingResponse(
            iterfile(),
            media_type="video/mp4",
            headers={"Accept-Ranges": "bytes", "Content-Length": str(file_size)}
        )

    try:
        range_str = range_header.replace("bytes=", "")
        start_str, end_str = range_str.split("-")
        start = int(start_str) if start_str else 0
        end = int(end_str) if end_str else file_size - 1
    except Exception:
        raise HTTPException(status_code=416, detail="Invalid byte range requested")

    if start >= file_size or end >= file_size:
        raise HTTPException(status_code=416, detail="Requested range not satisfiable")

    chunk_size = (end - start) + 1

    def send_bytes():
        with open(video_path, "rb") as f:
            f.seek(start)
            bytes_left = chunk_size
            while bytes_left > 0:
                read_bytes = min(1024 * 512, bytes_left)
                data = f.read(read_bytes)
                if not data:
                    break
                bytes_left -= len(data)
                yield data

    headers = {
        "Content-Range": f"bytes {start}-{end}/{file_size}",
        "Accept-Ranges": "bytes",
        "Content-Length": str(chunk_size),
        "Content-Type": "video/mp4",
    }
    return StreamingResponse(send_bytes(), status_code=206, headers=headers)

@app.get("/api/videos", tags=["Media"])
async def get_available_videos():
    try:
        if not os.path.exists(args.video_dir):
            return []
        files = os.listdir(args.video_dir)
        valid_extensions = (".mp4", ".mov", ".mkv", ".avi")
        return [f for f in files if f.lower().endswith(valid_extensions)]
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))


@app.get("/api/metrics")
async def proxy_metrics():
    headers = {"Accept": "application/json", "User-Agent": "AAFConnector/1.0"}
    async with httpx.AsyncClient() as client:
        try:
            url = f"{args.aaf_server}/metrics/"
            response = await client.get(url, params={"model_name": TARGET_MODEL}, headers=headers, timeout=3.0)
            return response.json()
        except Exception:
            return {
                TARGET_MODEL: {
                    "llm_average_token_per_second": 0.0,
                    "llm_first_infer_duration": 0.0,
                    "generated_token_num": 0
                }
            }


@app.post("/api/analyze")
async def proxy_analysis_stream(payload: MultiTurnPayload):
    absolute_video_target_path = os.path.join(args.video_dir, payload.video_name)
    start_time = time.time()

    if len(payload.history) > 1:
        flattened_text = "Here is the conversation history context for this execution sequence:\n"
        for msg in payload.history[:-1]:
            label = "User Question" if msg.role == "user" else "Your Previous Response"
            flattened_text += f"[{label}]: {msg.content}\n"
        flattened_text += f"\n[New Follow-up Question to Answer]: {payload.history[-1].content}"
    else:
        flattened_text = payload.history[0].content

    aaf_payload = {
        "model": TARGET_MODEL,
        "stream": True,
        "messages": [{
            "role": "user",
            "content": [
                {"type": "text", "text": flattened_text},
                {"type": "video_url", "video_url": {"url": absolute_video_target_path}}
            ]
        }]
    }

    print("\n" + "═"*70)
    print(f"[{get_timestamp()}] [CONVERSATIONAL INFERENCE DISPATCH]")
    print(f"  Model ID      : {TARGET_MODEL}")
    print(f"  Target Path   : {absolute_video_target_path}")
    print(f"  Turn Count    : {len(payload.history)} turns processed in session state.")
    
    if args.verbose:
        print("─"*70)
        print("[RAW OUTGOING JSON PAYLOAD SENT TO AAF SERVER]:")
        print(json.dumps(aaf_payload, indent=2))
        
    print("═"*70 + "\n")

    def raw_socket_generator():
        target_endpoint = f"{args.aaf_server}/v1/chat/completions"
        data_bytes = json.dumps(aaf_payload).encode('utf-8')
        
        req = urllib.request.Request(
            target_endpoint,
            data=data_bytes,
            headers={
                "Content-Type": "application/json",
                "Accept": "application/json",
                "User-Agent": "AAFConnector/1.0"
            },
            method="POST"
        )
        
        first_token_received = False
        
        try:
            with urllib.request.urlopen(req, timeout=300.0) as response:
                while True:
                    line_bytes = response.readline()
                    if not line_bytes:
                        break  
                    
                    line_str = line_bytes.decode('utf-8', errors='ignore')
                    trimmed = line_str.strip()
                    
                    if trimmed:
                        yield f"{trimmed}\n".encode('utf-8')
                        
                        if trimmed.startswith('data: '):
                            data_content = trimmed[5:].strip()
                            if data_content == "[DONE]":
                                continue
                                
                            try:
                                parsed = json.loads(data_content)
                                token = parsed["choices"][0]["delta"].get("content", "")
                                if token:
                                    if not first_token_received:
                                        ttft_duration = time.time() - start_time
                                        print(f"[{get_timestamp()}] [TTFT / DECODE PHASE]: {ttft_duration:.2f}s.")
                                        print(f"[{get_timestamp()}] [STREAMING TEXT TOKENS]: ", end="")
                                        first_token_received = True
                                    
                                    print(token, end="", flush=True)
                            except Exception:
                                pass
                        
        except urllib.error.HTTPError as http_err:
            err_body = http_err.read().decode('utf-8', errors='ignore')
            yield f"data: {{\"error\": \"AAF Server Engine error {http_err.code}: {err_body}\"}}\n\n".encode('utf-8')
        except Exception as e:
            yield f"data: {{\"error\": \"Direct socket pipeline fault: {str(e)}\"}}\n\n".encode('utf-8')
        finally:
            duration = time.time() - start_time
            print("\n" + "═"*70)
            print(f"[{get_timestamp()}] [INFERENCE COMPLETED] Turn Runtime: {duration:.2f}s")
            print("═"*70 + "\n")

    return StreamingResponse(raw_socket_generator(), media_type="text/event-stream")


# User Interface (HTML Layer with Debug Output)
@app.get("/", response_class=HTMLResponse)
async def serve_index():
    html_content = r"""
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>VLM Edge Studio Analyzer</title>
        <script src="https://cdn.tailwindcss.com"></script>
        <style>
            .skeleton-pulse {
                background: linear-gradient(-90deg, #1e293b 0%, #334155 50%, #1e293b 100%);
                background-size: 400% 400%;
                animation: pulse 1.5s ease-in-out infinite;
            }
            @keyframes pulse {
                0% { background-position: 100% 50%; }
                100% { background-position: 0% 50%; }
            }
        </style>
    </head>
    <body class="bg-gray-900 text-gray-100 min-h-screen p-6">
        <div class="max-w-6xl mx-auto space-y-6">
            <header class="border-b border-gray-800 pb-4 flex justify-between items-center">
                <div>
                    <h1 class="text-2xl font-bold tracking-wide text-indigo-400">VLM Edge Platform Interface</h1>
                    <p id="metricsPanel" class="text-xs text-gray-400 mt-1 font-mono">Metrics: Waiting for pipeline activity...</p>
                </div>
                <div class="flex items-center space-x-3">
                    <span class="text-xs font-mono bg-gray-800 border border-gray-700 rounded px-2.5 py-1 text-indigo-300">Target Profile: __MODEL_NAME_PLACEHOLDER__</span>
                    <button id="clearChatBtn" class="bg-red-900/40 hover:bg-red-800 border border-red-700 text-red-200 text-xs py-1.5 px-3 rounded transition-colors">Clear Chat History</button>
                </div>
            </header>

            <div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
                <div class="lg:col-span-2 space-y-4">
                    <div class="flex items-center space-x-4">
                        <label class="font-medium text-sm">Select Stream Source:</label>
                        <select id="videoSelect" class="flex-1 bg-gray-800 border border-gray-700 rounded p-2 focus:outline-none focus:border-indigo-500"></select>
                    </div>
                    
                    <div class="bg-black rounded-lg overflow-hidden aspect-video relative flex items-center justify-center border border-gray-800">
                        <video id="videoPlayer" controls preload="auto" playsinline muted class="w-full h-full hidden"></video>
                        <div id="videoPlaceholder" class="text-gray-500 text-sm">No Active Video Stream Sample Loaded</div>
                    </div>
                    
                    <!-- DEBUGGER STATUS BAR -->
                    <div class="bg-gray-950 p-3 rounded border border-gray-800 font-mono text-xs space-y-1">
                        <div class="text-gray-400 font-bold">🔍 Video Player Debug Console:</div>
                        <div id="debugStatus" class="text-indigo-300">Initializing video pipeline...</div>
                        <div id="debugUrl" class="text-gray-500 break-all">URL: None</div>
                    </div>
                </div>

                <div class="flex flex-col h-[540px]">
                    <div class="bg-gray-800 border border-gray-700 rounded-lg p-4 flex-1 flex flex-col min-h-0 relative overflow-hidden">
                        <div class="flex justify-between items-center mb-3 flex-none">
                            <h2 class="text-sm font-semibold tracking-wider text-gray-400 uppercase">Conversational History Log</h2>
                            <div id="busySpinner" class="hidden h-4 w-4 animate-spin rounded-full border-2 border-indigo-500 border-t-transparent"></div>
                        </div>
                        <div id="chatHistoryLog" class="flex-1 space-y-4 text-sm overflow-y-auto bg-gray-900 p-3 rounded border border-gray-750 font-mono min-h-0">
                            <div class="text-gray-500 text-xs italic">System initialized. Awaiting prompt loop...</div>
                        </div>
                    </div>
                    
                    <div class="space-y-2 mt-4 flex-none">
                        <textarea id="promptInput" rows="2" class="w-full bg-gray-800 border border-gray-700 rounded-lg p-3 text-sm focus:outline-none focus:border-indigo-500 resize-none placeholder-gray-500" placeholder="Ask a follow-up question..."></textarea>
                        <button id="submitBtn" class="w-full bg-indigo-600 hover:bg-indigo-500 disabled:bg-gray-700 disabled:cursor-not-allowed text-white font-medium py-2.5 px-4 rounded-lg transition-colors flex items-center justify-center space-x-2">
                            <span id="btnText">Execute Analysis Prompt</span>
                        </button>
                    </div>
                </div>
            </div>
        </div>

        <script>
            const videoSelect = document.getElementById('videoSelect');
            const videoPlayer = document.getElementById('videoPlayer');
            const videoPlaceholder = document.getElementById('videoPlaceholder');
            const debugStatus = document.getElementById('debugStatus');
            const debugUrl = document.getElementById('debugUrl');
            
            const promptInput = document.getElementById('promptInput');
            const submitBtn = document.getElementById('submitBtn');
            const btnText = document.getElementById('btnText');
            const chatHistoryLog = document.getElementById('chatHistoryLog');
            const metricsPanel = document.getElementById('metricsPanel');
            const busySpinner = document.getElementById('busySpinner');
            const clearChatBtn = document.getElementById('clearChatBtn');

            let chatHistoryBuffer = [];

            // Add Event Listeners for Video Debugging
            videoPlayer.addEventListener('loadstart', () => logDebug("Loading video stream..."));
            videoPlayer.addEventListener('loadeddata', () => logDebug("First video frame decoded successfully!"));
            videoPlayer.addEventListener('canplay', () => logDebug("Video canplay ready state reached."));
            videoPlayer.addEventListener('error', (e) => {
                const errCode = videoPlayer.error ? videoPlayer.error.code : 'unknown';
                logDebug(`HTML5 Video Error Code ${errCode}: Failed to stream file.`, true);
            });

            function logDebug(msg, isError = false) {
                console.log(`[Video Debug]: ${msg}`);
                debugStatus.textContent = msg;
                debugStatus.className = isError ? "text-red-400 font-bold" : "text-indigo-300";
            }

            async function initializeApp() {
                try {
                    const videoRes = await fetch('/api/videos');
                    const videos = await videoRes.json();
                    
                    videoSelect.innerHTML = "";
                    videos.forEach(v => videoSelect.add(new Option(v, v)));
                    
                    if (videos.length > 0) {
                        handleVideoChange(videos[0]);
                    } else {
                        logDebug("No video files found in target directory.", true);
                    }
                } catch (e) {
                    logDebug(`Initialization fault: ${e.message}`, true);
                }
            }

            function handleVideoChange(filename) {
                resetChatHistory();
                if (!filename) {
                    videoPlayer.classList.add('hidden');
                    videoPlaceholder.classList.remove('hidden');
                    return;
                }
                
                videoPlaceholder.classList.add('hidden');
                videoPlayer.classList.remove('hidden');
                
                const streamUrl = `/stream/video/${encodeURIComponent(filename)}`;
                debugUrl.textContent = `Endpoint: ${streamUrl}`;
                logDebug(`Requesting stream for: ${filename}`);
                
                videoPlayer.src = streamUrl;
                videoPlayer.load();
            }

            function appendMessageBlock(role, text, isSkeleton = false) {
                const block = document.createElement('div');
                block.className = `p-2.5 rounded border ${role === 'user' ? 'bg-indigo-950/40 border-indigo-900/60 ml-6' : 'bg-gray-800/60 border-gray-700/50 mr-6'} ${isSkeleton ? 'skeleton-pulse min-h-[40px]' : ''}`;
                
                const senderLabel = document.createElement('div');
                senderLabel.className = `text-[10px] font-bold uppercase tracking-wider mb-1 ${role === 'user' ? 'text-indigo-400' : 'text-gray-400'}`;
                senderLabel.textContent = role === 'user' ? '● User Prompt' : '● Model Response';
                
                const contentText = document.createElement('div');
                contentText.className = "whitespace-pre-wrap leading-relaxed break-words text-sm font-mono text-gray-100";
                if (!isSkeleton) contentText.textContent = text;
                
                block.appendChild(senderLabel);
                block.appendChild(contentText);
                chatHistoryLog.appendChild(block);
                chatHistoryLog.scrollTop = chatHistoryLog.scrollHeight;
                return contentText;
            }

            async function updateMetrics(clientLatencySec) {
                try {
                    const res = await fetch('/api/metrics');
                    const root = await res.json();
                    const metrics = Object.values(root)[0];
                    if (metrics) {
                        const tps = metrics.llm_average_token_per_second?.toFixed(1) || "0.0";
                        const ttft = metrics.llm_first_infer_duration?.toFixed(2) || "0.00";
                        const tokens = metrics.generated_token_num || 0;
                        metricsPanel.textContent = `Metrics: ${tps} tok/s • TTFT: ${ttft}s • ${tokens} tokens • Latency: ${clientLatencySec.toFixed(2)}s`;
                    }
                } catch (e) {}
            }

            function resetChatHistory() {
                chatHistoryBuffer = [];
                chatHistoryLog.innerHTML = `<div class="text-gray-500 text-xs italic">Conversation wiped. Ready for prompt input...</div>`;
                promptInput.value = "what is happening in this video?";
            }

            videoSelect.addEventListener('change', (e) => handleVideoChange(e.target.value));
            clearChatBtn.addEventListener('click', resetChatHistory);

            submitBtn.addEventListener('click', async () => {
                const prompt = promptInput.value.trim();
                const video_name = videoSelect.value;
                
                if (!prompt || !video_name) return;

                const clientStartTime = performance.now();

                appendMessageBlock('user', prompt);
                chatHistoryBuffer.push({ role: 'user', content: prompt });
                
                promptInput.value = "";
                submitBtn.disabled = true;
                btnText.textContent = "Processing Inference...";
                busySpinner.classList.remove('hidden');
                
                const liveResponseNode = appendMessageBlock('assistant', "Connecting...", true);
                
                try {
                    const response = await fetch('/api/analyze', {
                        method: 'POST',
                        headers: { 'Content-Type': 'application/json' },
                        body: JSON.stringify({ video_name, history: chatHistoryBuffer })
                    });

                    if (!response.ok) throw new Error("Server engine pipeline connection fault.");

                    liveResponseNode.parentElement.classList.remove('skeleton-pulse');
                    liveResponseNode.textContent = "";

                    const reader = response.body.getReader();
                    const decoder = new TextDecoder();
                    let buffer = "";
                    let fullModelResponse = "";

                    while (true) {
                        const { value, done } = await reader.read();
                        if (done) break;
                        
                        buffer += decoder.decode(value, { stream: true });
                        const lines = buffer.split('\n');
                        buffer = lines.pop(); 
                        
                        for (const line of lines) {
                            const trimmed = line.trim();
                            
                            if (!trimmed || !trimmed.startsWith('data: ')) continue;
                            
                            const dataStr = trimmed.slice(5).trim();
                            if (dataStr === '[DONE]') continue;
                            
                            try {
                                const json = JSON.parse(dataStr);
                                if(json.error) {
                                    liveResponseNode.textContent += `\n[AAF Error]: ${json.error}`;
                                    continue;
                                }
                                
                                const contentToken = json.choices?.[0]?.delta?.content || "";
                                if (contentToken) {
                                    fullModelResponse += contentToken;
                                    liveResponseNode.textContent = fullModelResponse;
                                    chatHistoryLog.scrollTop = chatHistoryLog.scrollHeight;
                                }
                            } catch(e) {}
                        }
                    }
                    
                    chatHistoryBuffer.push({ role: 'assistant', content:fullModelResponse });

                    const clientLatencySec = (performance.now() - clientStartTime) / 1000;
                    setTimeout(() => updateMetrics(clientLatencySec), 500);

                } catch (err) {
                    liveResponseNode.parentElement.classList.remove('skeleton-pulse');
                    liveResponseNode.textContent = `\n[Pipeline Runtime Exception]: ${err.message}`;
                } finally {
                    submitBtn.disabled = false;
                    btnText.textContent = "Execute Analysis Prompt";
                    busySpinner.classList.add('hidden');
                }
            });

            initializeApp();
        </script>
    </body>
    </html>
    """
    return HTMLResponse(content=html_content.replace("__MODEL_NAME_PLACEHOLDER__", TARGET_MODEL))


if __name__ == "__main__":
    prepare_aaf_backend()
    uvicorn.run(app, host=args.host, port=args.port)
