#!/usr/bin/env python3
"""
YouTube Downloader (Web App) for BlackBerry QNX - SSL FIXED VERSION
- Compatible with TaskApp2.py (PORT declared, simple HTTP server)
- Uses pytube to download a single YouTube video to local directory
- Handles SSL certificate issues with fallback options
- ES5-only frontend (XMLHttpRequest), zero external web libs
"""

import http.server
import socketserver
import urllib.parse
import urllib.request
import json
import os
import ssl
import sys
import signal
import re

# ---- App identity & TaskApp2 integration ----
PORT = 8033  # TaskApp2 scans for this exact pattern
APP_NAME = "YouTube Downloader"

# ---- SSL Context with fallback options ----
def create_permissive_ssl_context():
    """Create a permissive SSL context that works on QNX"""
    try:
        # Create unverified context - this is what works on QNX
        ctx = ssl._create_unverified_context()
        print("SSL: Using unverified context (no certificate verification)")
        return ctx
    except Exception as e1:
        print(f"SSL: Unverified context failed ({e1}), trying manual setup...")
        try:
            # Manual permissive setup
            ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
            ctx.verify_mode = ssl.CERT_NONE
            ctx.check_hostname = False
            print("SSL: Using manual permissive context")
            return ctx
        except Exception as e2:
            print(f"SSL: Manual context failed ({e2}), using default...")
            # Last resort
            return ssl.create_default_context()

# Set up SSL context - start with permissive for QNX compatibility
ssl_context = create_permissive_ssl_context()

# Override ALL SSL context creation methods used by urllib/pytube
original_create_default_context = ssl.create_default_context
original_create_unverified_context = ssl._create_unverified_context

def create_qnx_ssl_context(*args, **kwargs):
    """Force all SSL contexts to be permissive for QNX compatibility"""
    return ssl_context

# Apply the override to all SSL context creation methods
ssl._create_default_https_context = create_qnx_ssl_context
ssl.create_default_context = create_qnx_ssl_context
ssl._create_unverified_context = create_qnx_ssl_context

# ---- pytube imports (external, expected to be installed by you) ----
try:
    from pytube import YouTube
    from pytube.exceptions import VideoUnavailable, RegexMatchError, PytubeError
    PYTUBE_AVAILABLE = True
except Exception as e:
    # Defer raising until first API call so HTML can still explain the issue
    PytubeImportError = e
    PYTUBE_AVAILABLE = False
else:
    PytubeImportError = None

# ---- YouTube search imports ----
try:
    from youtubesearchpython import VideosSearch
    SEARCH_AVAILABLE = True
    SEARCH_METHOD = "youtube-search-python"
except Exception as e:
    SearchImportError = e
    SEARCH_AVAILABLE = True  # We'll use fallback method
    SEARCH_METHOD = "direct-http"

# ---- Direct YouTube search fallback ----
def search_youtube_direct(query, limit=10):
    """Direct YouTube search using HTTP requests - QNX compatible"""
    try:
        # Construct YouTube search URL
        search_url = f"https://www.youtube.com/results?search_query={urllib.parse.quote(query)}"
        
        # Create request with headers
        headers = {
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
        }
        
        req = urllib.request.Request(search_url, headers=headers)
        
        # Use our SSL context
        with urllib.request.urlopen(req, context=ssl_context) as response:
            html = response.read().decode('utf-8')
        
        # Extract video data using regex
        # Look for ytInitialData JSON
        pattern = r'var ytInitialData = ({.*?});'
        match = re.search(pattern, html)
        
        if not match:
            # Try alternative pattern
            pattern = r'window\["ytInitialData"\] = ({.*?});'
            match = re.search(pattern, html)
        
        if not match:
            return []
        
        try:
            data = json.loads(match.group(1))
            return parse_youtube_data(data, limit)
        except json.JSONDecodeError:
            return []
            
    except Exception as e:
        print(f"Direct search error: {e}")
        return []

def parse_youtube_data(data, limit):
    """Parse YouTube search results from ytInitialData"""
    results = []
    
    try:
        # Navigate the complex YouTube data structure
        contents = data.get('contents', {}).get('twoColumnSearchResultsRenderer', {}).get('primaryContents', {}).get('sectionListRenderer', {}).get('contents', [])
        
        for section in contents:
            items = section.get('itemSectionRenderer', {}).get('contents', [])
            
            for item in items:
                if 'videoRenderer' in item:
                    video = item['videoRenderer']
                    
                    # Extract video information
                    video_id = video.get('videoId', '')
                    title = ''
                    duration = 'Unknown'
                    channel = 'Unknown Channel'
                    views = 'Unknown views'
                    published = 'Unknown'
                    
                    # Extract title
                    if 'title' in video and 'runs' in video['title']:
                        title = ''.join([run.get('text', '') for run in video['title']['runs']])
                    elif 'title' in video and 'simpleText' in video['title']:
                        title = video['title']['simpleText']
                    
                    # Extract duration
                    if 'lengthText' in video and 'simpleText' in video['lengthText']:
                        duration = video['lengthText']['simpleText']
                    
                    # Extract channel
                    if 'ownerText' in video and 'runs' in video['ownerText']:
                        channel = video['ownerText']['runs'][0].get('text', 'Unknown Channel')
                    
                    # Extract view count
                    if 'viewCountText' in video and 'simpleText' in video['viewCountText']:
                        views = video['viewCountText']['simpleText']
                    
                    # Extract published time
                    if 'publishedTimeText' in video and 'simpleText' in video['publishedTimeText']:
                        published = video['publishedTimeText']['simpleText']
                    
                    if video_id and title:
                        results.append({
                            'title': title,
                            'url': f'https://www.youtube.com/watch?v={video_id}',
                            'duration': duration,
                            'channel': channel,
                            'views': views,
                            'thumbnail': f'https://i.ytimg.com/vi/{video_id}/mqdefault.jpg',
                            'published': published
                        })
                        
                        if len(results) >= limit:
                            break
            
            if len(results) >= limit:
                break
                
    except Exception as e:
        print(f"Parse error: {e}")
    
    return results[:limit]

# ---- Utility: JSON response helper ----
def send_json(handler, status_code, payload):
    body = json.dumps(payload).encode("utf-8")
    handler.send_response(status_code)
    handler.send_header("Content-Type", "application/json")
    handler.send_header("Cache-Control", "no-store")
    handler.send_header("Content-Length", str(len(body)))
    handler.end_headers()
    handler.wfile.write(body)

# ---- HTTP Handler ----
class AppHandler(http.server.BaseHTTPRequestHandler):
    # Minimal logging (easier for TaskApp2 / pidin parsing)
    def log_message(self, fmt, *args):
        sys.stdout.write("%s - - [%s] %s\n" % (self.address_string(),
                                               self.log_date_time_string(),
                                               fmt % args))

    def do_GET(self):
        parsed = urllib.parse.urlparse(self.path)
        path = parsed.path

        if path == "/" or path == "/index.html":
            self.serve_html()
            return

        if path == "/api/download":
            self.handle_download(parsed.query)
            return

        if path == "/api/status":
            self.handle_status()
            return

        if path == "/api/search":
            self.handle_search(parsed.query)
            return

        if path == "/api/stream":
            self.handle_stream(parsed.query)
            return

        # Everything else: 404
        self.send_error(404, "Not Found")

    def handle_status(self):
        """Status endpoint to check SSL and pytube availability"""
        ssl_info = "permissive (QNX compatible)"
        if hasattr(ssl_context, 'verify_mode'):
            if ssl_context.verify_mode == ssl.CERT_NONE:
                ssl_info = "permissive (no verification)"
            elif ssl_context.verify_mode == ssl.CERT_REQUIRED:
                ssl_info = "secure (with verification)"
            else:
                ssl_info = "custom"
        
        status = {
            "app": APP_NAME,
            "port": PORT,
            "pytube_available": PYTUBE_AVAILABLE,
            "search_available": SEARCH_AVAILABLE,
            "ssl_context": ssl_info
        }
        send_json(self, 200, {"status": "ok", "info": status})

    def handle_search(self, query_string):
        """Search YouTube videos with fallback methods"""
        params = urllib.parse.parse_qs(query_string)
        query = params.get("q", [""])[0].strip()
        limit = int(params.get("limit", ["10"])[0])

        if not query:
            send_json(self, 400, {"status": "error", "error": "Missing 'q' parameter"})
            return

        if limit > 20:  # Reasonable limit
            limit = 20

        print(f"Searching YouTube for: {query} (limit: {limit}) using {SEARCH_METHOD}")
        
        formatted_results = []
        
        # Try youtube-search-python first if available
        if SEARCH_METHOD == "youtube-search-python":
            try:
                videos_search = VideosSearch(query, limit=limit)
                results = videos_search.result()
                
                # Format results for frontend
                for video in results.get('result', []):
                    formatted_results.append({
                        'title': video.get('title', 'Unknown Title'),
                        'url': video.get('link', ''),
                        'duration': video.get('duration', 'Unknown'),
                        'channel': video.get('channel', {}).get('name', 'Unknown Channel'),
                        'views': video.get('viewCount', {}).get('text', 'Unknown views'),
                        'thumbnail': video.get('thumbnails', [{}])[0].get('url', '') if video.get('thumbnails') else '',
                        'published': video.get('publishedTime', 'Unknown')
                    })
                
                print(f"Search completed via youtube-search-python: {len(formatted_results)} results")
                
            except Exception as e:
                print(f"youtube-search-python failed: {e}, trying direct method...")
                formatted_results = []
        
        # Use direct HTTP method if youtube-search-python failed or not available
        if not formatted_results:
            try:
                formatted_results = search_youtube_direct(query, limit)
                print(f"Search completed via direct HTTP: {len(formatted_results)} results")
            except Exception as e:
                print(f"Direct search failed: {e}")
                send_json(self, 500, {"status": "error", "error": f"All search methods failed: {str(e)}"})
                return
        
        # Send results
        send_json(self, 200, {
            "status": "ok",
            "query": query,
            "results": formatted_results,
            "count": len(formatted_results),
            "method": SEARCH_METHOD if formatted_results else "failed"
        })

    def handle_stream(self, query_string):
        """Get streaming URL for a YouTube video"""
        if not PYTUBE_AVAILABLE:
            send_json(self, 500, {
                "status": "error",
                "error": f"pytube not available: {PytubeImportError}"
            })
            return

        params = urllib.parse.parse_qs(query_string)
        url = params.get("url", [""])[0].strip()
        quality = params.get("quality", ["360p"])[0]  # Default to 360p for streaming

        if not url:
            send_json(self, 400, {"status": "error", "error": "Missing 'url' parameter"})
            return

        # Validate YouTube URL
        parsed = urllib.parse.urlparse(url)
        if parsed.scheme not in ("http", "https"):
            send_json(self, 400, {"status": "error", "error": "URL must be http or https"})
            return
        host = (parsed.netloc or "").lower()
        if ("youtube.com" not in host) and ("youtu.be" not in host):
            send_json(self, 400, {"status": "error", "error": "Only YouTube URLs are allowed"})
            return

        try:
            print(f"Getting stream info for: {url} (quality: {quality})")
            yt = YouTube(url)
            
            # Get available streams
            streams_info = []
            
            # Progressive streams (video + audio)
            progressive_streams = yt.streams.filter(progressive=True).order_by("resolution").desc()
            for stream in progressive_streams:
                streams_info.append({
                    'url': stream.url,
                    'quality': stream.resolution,
                    'type': 'progressive',
                    'mime_type': stream.mime_type,
                    'filesize': getattr(stream, 'filesize', 0)
                })
            
            # Audio-only streams
            audio_streams = yt.streams.filter(only_audio=True).order_by("abr").desc()
            for stream in audio_streams[:2]:  # Just top 2 audio streams
                streams_info.append({
                    'url': stream.url,
                    'quality': f"{stream.abr} audio",
                    'type': 'audio',
                    'mime_type': stream.mime_type,
                    'filesize': getattr(stream, 'filesize', 0)
                })
            
            # Find best stream for requested quality
            best_stream = None
            if quality == "best":
                best_stream = yt.streams.filter(progressive=True).order_by("resolution").desc().first()
            elif quality == "audio":
                best_stream = yt.streams.filter(only_audio=True).order_by("abr").desc().first()
            else:
                # Try to find specific quality
                best_stream = yt.streams.filter(progressive=True, res=quality).first()
                if not best_stream:
                    # Fallback to best available
                    best_stream = yt.streams.filter(progressive=True).order_by("resolution").desc().first()
            
            if not best_stream:
                send_json(self, 404, {"status": "error", "error": "No suitable stream found"})
                return
            
            response_data = {
                "status": "ok",
                "title": yt.title,
                "author": yt.author,
                "length_seconds": yt.length,
                "description": yt.description[:500] if yt.description else "",  # Truncate description
                "thumbnail": f"https://i.ytimg.com/vi/{yt.video_id}/mqdefault.jpg",
                "stream_url": best_stream.url,
                "stream_quality": getattr(best_stream, 'resolution', 'audio'),
                "stream_type": best_stream.mime_type,
                "available_streams": streams_info[:10]  # Limit to 10 streams
            }
            
            send_json(self, 200, response_data)
            print(f"Stream info retrieved: {best_stream.mime_type}, {getattr(best_stream, 'resolution', 'audio')}")
            
        except Exception as e:
            print(f"Stream error: {e}")
            send_json(self, 500, {"status": "error", "error": f"Failed to get stream info: {str(e)}"})

    def serve_html(self):
        html = """<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>YouTube Downloader (SSL Fixed)</title>
<style>
  body { font-family: Arial, sans-serif; background:#1a1a2e; color:#fff; margin:0; padding:20px; }
  .card { background:#222; border:2px solid #444; border-radius:8px; padding:16px; max-width:640px; margin:0 auto; }
  h1 { margin-top:0; font-size:22px; color:#4caf50; }
  label { display:block; margin:10px 0 6px; }
  input[type="text"] { width:100%; padding:10px; border-radius:6px; border:1px solid #555; background:#111; color:#fff; box-sizing:border-box; }
  .row { margin-top:12px; }
  button { background:#4caf50; border:2px solid #388e3c; color:#fff; padding:12px 18px; border-radius:8px; cursor:pointer; -webkit-appearance:none; margin-right:8px; }
  button:active { background:#388e3c; }
  button:disabled { background:#666; border-color:#555; cursor:not-allowed; }
  .status { margin-top:14px; background:#2b2b2b; padding:10px; border-radius:6px; min-height:40px; }
  .small { color:#aaa; font-size:12px; }
  .ok { color:#7CFC00; }
  .err { color:#ff6b6b; }
  .warn { color:#ffa500; }
  .info { background:#1e3a5f; padding:8px; border-radius:4px; margin:8px 0; font-size:12px; }
  .search-section { border-top:1px solid #444; margin-top:20px; padding-top:16px; }
  .search-results { max-height:300px; overflow-y:auto; margin-top:10px; }
  .video-item { background:#333; border:1px solid #555; border-radius:4px; padding:8px; margin:4px 0; cursor:pointer; }
  .video-item:hover { background:#444; }
  .video-title { font-weight:bold; color:#fff; font-size:14px; }
  .video-meta { color:#aaa; font-size:11px; margin-top:4px; }
  .video-duration { color:#4caf50; font-weight:bold; }
  .video-actions { margin-top:8px; }
  .video-actions button { font-size:11px; padding:4px 8px; margin-right:4px; }
  .stream-section { border-top:1px solid #444; margin-top:20px; padding-top:16px; display:none; }
  .video-player { width:100%; max-width:640px; height:360px; background:#000; border-radius:8px; }
  .video-info { background:#333; padding:12px; border-radius:4px; margin-top:10px; }
  .video-info h4 { margin:0 0 8px 0; color:#4caf50; }
  .video-description { color:#ccc; font-size:12px; line-height:1.4; max-height:60px; overflow:hidden; }
</style>
</head>
<body>
  <div class="card">
    <h1>YouTube Downloader (SSL Fixed)</h1>
    <p class="small">Enhanced SSL compatibility for QNX and embedded systems. Downloads to the app's working directory.</p>

    <div class="info">
      <strong>Status:</strong> <span id="appStatus">Checking...</span>
    </div>

    <label for="url">YouTube URL</label>
    <input id="url" type="text" placeholder="https://www.youtube.com/watch?v=..." value="https://www.youtube.com/watch?v=3on9TfhXIyc">

    <div class="row">
      <label><input type="checkbox" id="audioOnly"> Audio only</label>
    </div>

    <div class="row">
      <button id="downloadBtn" onclick="startDownload()">Download</button>
      <button onclick="checkStatus()">Check Status</button>
    </div>

    <div id="status" class="status">Ready to download. Paste a YouTube URL above or search below.</div>
    
    <!-- Search Section -->
    <div class="search-section">
      <h3 style="margin-top:0; color:#4caf50;">🔍 Search YouTube</h3>
      <label for="searchQuery">Search for videos</label>
      <input id="searchQuery" type="text" placeholder="Enter search terms..." value="">
      
      <div class="row">
        <button id="searchBtn" onclick="searchVideos()">Search</button>
        <button onclick="clearSearch()">Clear</button>
      </div>
      
      <div id="searchResults" class="search-results" style="display:none;"></div>
    </div>
    
    <!-- Stream Section -->
    <div id="streamSection" class="stream-section">
      <h3 style="margin-top:0; color:#4caf50;">📺 Video Player</h3>
      <video id="videoPlayer" class="video-player" controls>
        Your browser does not support the video tag.
      </video>
      <div id="videoInfo" class="video-info" style="display:none;">
        <h4 id="videoTitle"></h4>
        <div id="videoMeta" style="color:#aaa; font-size:12px; margin-bottom:8px;"></div>
        <div id="videoDescription" class="video-description"></div>
      </div>
      <div class="row" style="margin-top:10px;">
        <button onclick="closeStream()">Close Player</button>
        <select id="qualitySelect" onchange="changeQuality()">
          <option value="360p">360p</option>
          <option value="480p">480p</option>
          <option value="720p">720p</option>
          <option value="best">Best Quality</option>
          <option value="audio">Audio Only</option>
        </select>
      </div>
    </div>
    
    <p class="small">
      <strong>SSL:</strong> Automatic fallback for certificate issues.<br>
      <strong>Search:</strong> Uses youtube-search-python (no API key needed).<br>
      <strong>TaskApp2:</strong> Detected via <code>PORT = 8033</code>. ES5-compatible UI.
    </p>
  </div>

<script>
  // ES5 only
  var isDownloading = false;

  function setStatus(html, className) {
    var el = document.getElementById('status');
    el.innerHTML = html;
    el.className = 'status ' + (className || 'ok');
  }

  function setDownloadButton(enabled) {
    var btn = document.getElementById('downloadBtn');
    btn.disabled = !enabled;
    btn.textContent = enabled ? 'Download' : 'Downloading...';
    isDownloading = !enabled;
  }

  function checkStatus() {
    var xhr = new XMLHttpRequest();
    xhr.open('GET', '/api/status', true);
    xhr.onreadystatechange = function() {
      if (xhr.readyState === 4) {
        try {
          var res = JSON.parse(xhr.responseText);
          if (xhr.status === 200 && res.status === 'ok') {
            var info = res.info;
            var statusText = 'App: ' + info.app + ', Port: ' + info.port + 
                           ', PyTube: ' + (info.pytube_available ? 'Available' : 'Missing') +
                           ', Search: ' + (info.search_available ? 'Available' : 'Missing') +
                           ', SSL: ' + info.ssl_context;
            document.getElementById('appStatus').textContent = statusText;
            document.getElementById('appStatus').className = info.pytube_available ? 'ok' : 'err';
          } else {
            document.getElementById('appStatus').textContent = 'Error checking status';
            document.getElementById('appStatus').className = 'err';
          }
        } catch (e) {
          document.getElementById('appStatus').textContent = 'Status check failed';
          document.getElementById('appStatus').className = 'err';
        }
      }
    };
    xhr.send();
  }

  function startDownload() {
    if (isDownloading) return;

    var url = document.getElementById('url').value.trim();
    var audioOnly = document.getElementById('audioOnly').checked ? '1' : '0';
    
    if (!url) {
      setStatus('Please paste a YouTube URL.', 'err');
      return;
    }

    // Basic URL validation
    if (url.indexOf('youtube.com') === -1 && url.indexOf('youtu.be') === -1) {
      setStatus('Please enter a valid YouTube URL.', 'err');
      return;
    }

    setDownloadButton(false);
    setStatus('Starting download... This may take several minutes for longer videos (10min timeout).', 'warn');

    var xhr = new XMLHttpRequest();
    xhr.open('GET', '/api/download?url=' + encodeURIComponent(url) + '&audio_only=' + audioOnly, true);
    xhr.timeout = 600000; // 10 minute timeout for longer videos
    
    xhr.onreadystatechange = function() {
      if (xhr.readyState === 4) {
        setDownloadButton(true);
        try {
          var res = JSON.parse(xhr.responseText);
          if (xhr.status === 200 && res && res.status === 'ok') {
            var msg = '<strong>✓ Download Complete!</strong><br>' +
                     '<strong>Title:</strong> ' + res.title + '<br>' +
                     '<strong>Author:</strong> ' + res.author + '<br>' +
                     '<strong>File:</strong> <code>' + res.filename + '</code><br>' +
                     '<strong>Duration:</strong> ' + Math.floor(res.length_seconds / 60) + 'm ' + (res.length_seconds % 60) + 's';
            setStatus(msg, 'ok');
          } else {
            var err = (res && res.error) ? res.error : ('HTTP ' + xhr.status + ' error');
            setStatus('<strong>Download Failed:</strong><br>' + err, 'err');
          }
        } catch (e) {
          setStatus('<strong>Error:</strong> Could not parse server response.', 'err');
        }
      }
    };

    xhr.ontimeout = function() {
      setDownloadButton(true);
      setStatus('<strong>Timeout:</strong> Download took longer than 10 minutes. For very long videos, try audio-only mode or check your connection.', 'err');
    };

    xhr.onerror = function() {
      setDownloadButton(true);
      setStatus('<strong>Network Error:</strong> Could not connect to server.', 'err');
    };

    xhr.send();
  }

  // Search functionality
  var isSearching = false;

  function setSearchButton(enabled) {
    var btn = document.getElementById('searchBtn');
    btn.disabled = !enabled;
    btn.textContent = enabled ? 'Search' : 'Searching...';
    isSearching = !enabled;
  }

  function searchVideos() {
    if (isSearching) return;

    var query = document.getElementById('searchQuery').value.trim();
    if (!query) {
      setStatus('Please enter search terms.', 'err');
      return;
    }

    setSearchButton(false);
    setStatus('Searching YouTube...', 'warn');

    var xhr = new XMLHttpRequest();
    xhr.open('GET', '/api/search?q=' + encodeURIComponent(query) + '&limit=10', true);
    xhr.timeout = 30000; // 30 second timeout for search

    xhr.onreadystatechange = function() {
      if (xhr.readyState === 4) {
        setSearchButton(true);
        try {
          var res = JSON.parse(xhr.responseText);
          if (xhr.status === 200 && res && res.status === 'ok') {
            displaySearchResults(res.results, res.query);
            setStatus('Found ' + res.count + ' videos for "' + res.query + '"', 'ok');
          } else {
            var err = (res && res.error) ? res.error : ('HTTP ' + xhr.status + ' error');
            setStatus('<strong>Search Failed:</strong><br>' + err, 'err');
          }
        } catch (e) {
          setStatus('<strong>Error:</strong> Could not parse search response.', 'err');
        }
      }
    };

    xhr.ontimeout = function() {
      setSearchButton(true);
      setStatus('<strong>Search Timeout:</strong> Search took too long.', 'err');
    };

    xhr.onerror = function() {
      setSearchButton(true);
      setStatus('<strong>Search Error:</strong> Could not connect to server.', 'err');
    };

    xhr.send();
  }

  function displaySearchResults(results, query) {
    var container = document.getElementById('searchResults');
    
    if (!results || results.length === 0) {
      container.innerHTML = '<div style="padding:10px; color:#aaa;">No videos found for "' + query + '"</div>';
      container.style.display = 'block';
      return;
    }

    var html = '';
    for (var i = 0; i < results.length; i++) {
      var video = results[i];
      // Use data attributes instead of onclick to avoid quote escaping issues
      html += '<div class="video-item" data-url="' + video.url.replace(/"/g, '&quot;') + '" data-title="' + video.title.replace(/"/g, '&quot;') + '">';
      html += '<div class="video-title">' + video.title + '</div>';
      html += '<div class="video-meta">';
      html += '<span class="video-duration">' + video.duration + '</span> • ';
      html += video.channel + ' • ' + video.views + ' • ' + video.published;
      html += '</div>';
      html += '<div class="video-actions">';
      html += '<button onclick="selectVideoFromElement(this.parentElement.parentElement)">Select for Download</button>';
      html += '<button onclick="streamVideoFromElement(this.parentElement.parentElement)">▶ Stream</button>';
      html += '</div>';
      html += '</div>';
    }
    
    container.innerHTML = html;
    container.style.display = 'block';
  }

  function selectVideo(url, title) {
    document.getElementById('url').value = url;
    setStatus('Selected: ' + title, 'ok');
    // Scroll back to download section
    document.getElementById('url').focus();
  }

  function selectVideoFromElement(element) {
    var url = element.getAttribute('data-url');
    var title = element.getAttribute('data-title');
    selectVideo(url, title);
  }

  function clearSearch() {
    document.getElementById('searchQuery').value = '';
    document.getElementById('searchResults').style.display = 'none';
    setStatus('Search cleared.', 'ok');
  }

  // Streaming functionality
  var currentStreamUrl = '';
  var isStreaming = false;

  function streamVideoFromElement(element) {
    var url = element.getAttribute('data-url');
    var title = element.getAttribute('data-title');
    streamVideo(url, title);
  }

  function streamVideo(url, title) {
    if (isStreaming) {
      setStatus('Already loading a stream. Please wait...', 'warn');
      return;
    }

    isStreaming = true;
    currentStreamUrl = url;
    setStatus('Loading stream for: ' + title + '...', 'warn');

    var quality = document.getElementById('qualitySelect').value;
    var xhr = new XMLHttpRequest();
    xhr.open('GET', '/api/stream?url=' + encodeURIComponent(url) + '&quality=' + quality, true);
    xhr.timeout = 30000; // 30 second timeout for stream info

    xhr.onreadystatechange = function() {
      if (xhr.readyState === 4) {
        isStreaming = false;
        try {
          var res = JSON.parse(xhr.responseText);
          if (xhr.status === 200 && res && res.status === 'ok') {
            showVideoPlayer(res);
            setStatus('Now streaming: ' + res.title, 'ok');
          } else {
            var err = (res && res.error) ? res.error : ('HTTP ' + xhr.status + ' error');
            setStatus('<strong>Stream Failed:</strong><br>' + err, 'err');
          }
        } catch (e) {
          setStatus('<strong>Error:</strong> Could not parse stream response.', 'err');
        }
      }
    };

    xhr.ontimeout = function() {
      isStreaming = false;
      setStatus('<strong>Stream Timeout:</strong> Could not load stream info.', 'err');
    };

    xhr.onerror = function() {
      isStreaming = false;
      setStatus('<strong>Stream Error:</strong> Could not connect to server.', 'err');
    };

    xhr.send();
  }

  function showVideoPlayer(streamData) {
    var player = document.getElementById('videoPlayer');
    var section = document.getElementById('streamSection');
    var info = document.getElementById('videoInfo');
    
    // Set video source
    player.src = streamData.stream_url;
    
    // Show video info
    document.getElementById('videoTitle').textContent = streamData.title;
    document.getElementById('videoMeta').textContent = 
      'By ' + streamData.author + ' • ' + 
      Math.floor(streamData.length_seconds / 60) + 'm ' + (streamData.length_seconds % 60) + 's • ' +
      streamData.stream_quality + ' (' + streamData.stream_type + ')';
    document.getElementById('videoDescription').textContent = streamData.description || 'No description available.';
    
    // Show sections
    section.style.display = 'block';
    info.style.display = 'block';
    
    // Scroll to player
    section.scrollIntoView({ behavior: 'smooth' });
  }

  function changeQuality() {
    if (currentStreamUrl) {
      var title = document.getElementById('videoTitle').textContent;
      streamVideo(currentStreamUrl, title);
    }
  }

  function closeStream() {
    var player = document.getElementById('videoPlayer');
    var section = document.getElementById('streamSection');
    
    player.pause();
    player.src = '';
    section.style.display = 'none';
    currentStreamUrl = '';
    
    setStatus('Video player closed.', 'ok');
  }

  // Enter key support for search
  document.getElementById('searchQuery').onkeypress = function(e) {
    if (e.keyCode === 13) { // Enter key
      searchVideos();
    }
  };

  // Check status on page load
  window.onload = function() {
    checkStatus();
  };
</script>
</body>
</html>"""
        body = html.encode("utf-8")
        self.send_response(200)
        self.send_header("Content-Type", "text/html; charset=utf-8")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)

    def handle_download(self, query_string):
        # Validate pytube availability
        if not PYTUBE_AVAILABLE:
            send_json(self, 500, {
                "status": "error",
                "error": "pytube import failed: %s" % str(PytubeImportError)
            })
            return

        params = urllib.parse.parse_qs(query_string)
        url = params.get("url", [""])[0].strip()
        audio_only = params.get("audio_only", ["0"])[0] in ("1", "true", "yes")

        if not url:
            send_json(self, 400, {"status": "error", "error": "Missing 'url' parameter"})
            return

        # Basic allowlist check (optional hardening)
        # Accept main YouTube and youtu.be links.
        parsed = urllib.parse.urlparse(url)
        if parsed.scheme not in ("http", "https"):
            send_json(self, 400, {"status": "error", "error": "URL must be http or https"})
            return
        host = (parsed.netloc or "").lower()
        if ("youtube.com" not in host) and ("youtu.be" not in host):
            send_json(self, 400, {"status": "error", "error": "Only YouTube URLs are allowed"})
            return

        try:
            print(f"Attempting download: {url} (audio_only={audio_only})")
            yt = YouTube(url)
            
            if audio_only:
                stream = yt.streams.filter(only_audio=True).order_by("abr").desc().first()
                if stream is None:
                    raise RuntimeError("No audio stream available.")
            else:
                # Prefer a progressive stream (video+audio)
                prog = yt.streams.filter(progressive=True).order_by("resolution").desc().first()
                stream = prog or yt.streams.get_highest_resolution()

            if stream is None:
                raise RuntimeError("No suitable stream found.")

            print(f"Selected stream: {stream.mime_type}, {getattr(stream, 'resolution', 'audio')}")
            
            # Download to current working directory
            filename = stream.download()
            
            print(f"Download completed: {os.path.basename(filename)}")
            
            send_json(self, 200, {
                "status": "ok",
                "title": yt.title,
                "filename": os.path.basename(filename),
                "author": yt.author,
                "length_seconds": yt.length
            })
            
        except VideoUnavailable as e:
            print(f"VideoUnavailable: {e}")
            send_json(self, 404, {"status": "error", "error": "Video unavailable or restricted"})
        except RegexMatchError as e:
            print(f"RegexMatchError: {e}")
            send_json(self, 500, {"status": "error", "error": "Unable to parse video metadata (update pytube)"})
        except PytubeError as e:
            print(f"PytubeError: {e}")
            send_json(self, 500, {"status": "error", "error": "pytube error: %s" % str(e)})
        except ssl.SSLError as e:
            print(f"SSL Error: {e}")
            send_json(self, 502, {"status": "error", "error": "SSL error: %s. Try updating certificates or using a VPN." % str(e)})
        except Exception as e:
            print(f"Unexpected error: {e}")
            send_json(self, 500, {"status": "error", "error": str(e)})

# ---- Graceful shutdown (SIGTERM) ----
def _signal_handler(sig, frame):
    print("Shutting down %s..." % APP_NAME)
    sys.exit(0)

signal.signal(signal.SIGTERM, _signal_handler)

# ---- Threaded server (keeps UI responsive during downloads) ----
class ThreadingTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
    daemon_threads = True
    allow_reuse_address = True

def run_server(port=PORT):
    try:
        with ThreadingTCPServer(("", port), AppHandler) as httpd:
            print("Starting %s on port %d..." % (APP_NAME, port))
            print("Access at: http://localhost:%d" % port)
            print(f"PyTube available: {PYTUBE_AVAILABLE}")
            print(f"Search available: {SEARCH_AVAILABLE} (method: {SEARCH_METHOD})")
            if hasattr(ssl_context, 'verify_mode'):
                if ssl_context.verify_mode == ssl.CERT_NONE:
                    print("SSL: Using permissive mode (no certificate verification) - QNX compatible")
                else:
                    print("SSL: Using secure mode (with certificate verification)")
            else:
                print("SSL: Using unverified context - QNX compatible")
            try:
                httpd.serve_forever()
            except KeyboardInterrupt:
                print("\nShutting down %s..." % APP_NAME)
    except OSError as e:
        if e.errno == 48:  # Address already in use
            print(f"Error: Port {port} is already in use.")
        else:
            print(f"Error starting server: {e}")
        sys.exit(1)

if __name__ == "__main__":
    if not PYTUBE_AVAILABLE:
        print("WARNING: pytube is not available. Install it with: pip install pytube")
    if not SEARCH_AVAILABLE:
        print("WARNING: YouTube search is not available. Install it with: pip install youtube-search-python")
    run_server()
