"""
UEFN Store — Installer
Browse the store, manage your library, and install assets directly into UEFN.
No dependencies required — runs with Python standard library only.

Get your API key at: https://www.uefnstore.com/dashboard/settings
"""

import os
import json
import zipfile
import tempfile
import threading
import webbrowser
import http.server
import socketserver

try:
    import unreal
    IN_UEFN = True
except ImportError:
    IN_UEFN = False

import urllib.request
import urllib.error

# ──────────────────────────────────────────────
# Configuration
# ──────────────────────────────────────────────
API_BASE = "https://www.uefnstore.com/api/script"
STORE_URL = "https://www.uefnstore.com"
CONFIG_FILE = os.path.join(os.path.expanduser("~"), ".uefnstore_config")
PORT = 48521


# ──────────────────────────────────────────────
# API Client
# ──────────────────────────────────────────────
class APIClient:
    def __init__(self):
        self.api_key = None
        self._load_config()

    def _load_config(self):
        if os.path.exists(CONFIG_FILE):
            try:
                with open(CONFIG_FILE, "r") as f:
                    data = json.load(f)
                    self.api_key = data.get("api_key")
            except (json.JSONDecodeError, IOError):
                self.api_key = None

    def _save_config(self):
        with open(CONFIG_FILE, "w") as f:
            json.dump({"api_key": self.api_key}, f)

    def _clear_config(self):
        self.api_key = None
        if os.path.exists(CONFIG_FILE):
            os.remove(CONFIG_FILE)

    def set_api_key(self, key):
        self.api_key = key.strip()
        self._save_config()

    def _request(self, method, endpoint, data=None):
        url = "{}/{}".format(API_BASE, endpoint)
        headers = {"Content-Type": "application/json"}
        if self.api_key:
            headers["X-API-Key"] = self.api_key

        body = json.dumps(data).encode() if data else None
        req = urllib.request.Request(url, data=body, headers=headers, method=method)

        class _RedirectHandler(urllib.request.HTTPRedirectHandler):
            def redirect_request(self, req, fp, code, msg, hdrs, newurl):
                return urllib.request.Request(
                    newurl, data=req.data, headers=dict(req.header_items()), method=req.get_method()
                )

        opener = urllib.request.build_opener(_RedirectHandler)
        try:
            resp = opener.open(req, timeout=30)
            return json.loads(resp.read().decode()), None
        except urllib.error.HTTPError as e:
            try:
                err = json.loads(e.read().decode())
                return None, err.get("error", "HTTP {}".format(e.code))
            except Exception:
                return None, "HTTP {}".format(e.code)
        except urllib.error.URLError as e:
            return None, "Connection error: {}".format(e.reason)
        except Exception as e:
            return None, str(e)

    def get_purchases(self):
        data, err = self._request("GET", "purchases")
        if err:
            return None, err
        return data.get("purchases", []), None

    def get_download_url(self, product_id, version_id):
        data, err = self._request("POST", "download", {"product_id": product_id, "version_id": version_id})
        if err:
            return None, err
        return data, None

    def logout(self):
        self._clear_config()

    @property
    def is_connected(self):
        return self.api_key is not None and self.api_key.startswith("uefn_")


# ──────────────────────────────────────────────
# Installer Logic
# ──────────────────────────────────────────────
def get_uefn_content_dir():
    if IN_UEFN:
        project = unreal.ValkyrieProjectLibrary.get_main_project()
        return unreal.ValkyrieProjectLibrary.get_project_content_dir(project)
    return None


def install_product(download_url, product_title, content_dir):
    slug = (
        product_title.lower()
        .replace(" ", "-")
        .encode("ascii", "ignore")
        .decode()
    )
    slug = "".join(c if c.isalnum() or c == "-" else "" for c in slug).strip("-")
    folder_name = slug or "product"
    dest_folder = os.path.join(content_dir, folder_name)

    tmp_path = os.path.join(tempfile.gettempdir(), "uefn_{}.zip".format(slug))
    urllib.request.urlretrieve(download_url, tmp_path)

    if tmp_path.lower().endswith(".zip"):
        with zipfile.ZipFile(tmp_path, "r") as zf:
            zf.extractall(dest_folder)
    else:
        raise RuntimeError("Only .zip is supported for auto-extraction.")

    os.remove(tmp_path)

    if IN_UEFN:
        unreal.log("[UEFN Store] Installed '{}' to {}".format(product_title, dest_folder))

    return dest_folder


# ──────────────────────────────────────────────
# API layer (used by the HTTP handler)
# ──────────────────────────────────────────────
class InstallerAPI:
    def __init__(self, content_dir=None):
        self.client = APIClient()
        self.content_dir = content_dir

    def get_status(self):
        project_name = None
        if self.content_dir:
            project_name = os.path.basename(os.path.dirname(self.content_dir))
        result = {
            "connected": self.client.is_connected,
            "uefn_project": project_name,
        }
        if self.client.is_connected:
            result["api_key"] = self.client.api_key
        return result

    def connect(self, api_key):
        if not api_key or not api_key.startswith("uefn_"):
            return {"success": False, "error": "API key must start with uefn_"}
        self.client.set_api_key(api_key)
        purchases, err = self.client.get_purchases()
        if err:
            self.client.logout()
            return {"success": False, "error": err}
        return {"success": True, **self.get_status()}

    def disconnect(self):
        self.client.logout()
        return {"success": True}

    def get_purchases(self):
        if not self.client.is_connected:
            return {"error": "Not connected"}
        purchases, err = self.client.get_purchases()
        if err:
            return {"error": err}
        return {"purchases": purchases}

    def do_install(self, product_id, version_id, title):
        if not self.client.is_connected:
            return {"success": False, "error": "Not connected"}
        dl_data, err = self.client.get_download_url(product_id, version_id)
        if err:
            return {"success": False, "error": err}
        target_dir = self.content_dir
        if not target_dir:
            target_dir = os.path.join(os.path.expanduser("~"), "UEFN_Downloads")
            os.makedirs(target_dir, exist_ok=True)
        try:
            dest = install_product(dl_data["url"], title, target_dir)
            return {"success": True, "folder": os.path.basename(dest)}
        except Exception as e:
            return {"success": False, "error": str(e)}


# ──────────────────────────────────────────────
# HTML UI
# ──────────────────────────────────────────────
HTML_PAGE = r"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>UEFN Store</title>
<style>
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
:root{
  --bg:#09090b;--bg-panel:#111114;--bg-card:#18181b;--bg-card-hover:#1e1e22;
  --bg-input:#0c0c0e;
  --border:#27272a;--border-hover:#3f3f46;
  --text:#fafafa;--text-dim:#a1a1aa;--text-muted:#52525b;
  --accent:#0071e3;--accent-hover:#0077ed;--accent-glow:rgba(0,113,227,.25);
  --green:#22c55e;--red:#ef4444;
  --radius:10px;
}
html,body{height:100%;overflow:hidden}
body{background:var(--bg);color:var(--text);font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',system-ui,sans-serif;font-size:14px;-webkit-font-smoothing:antialiased}

/* ── Layout ── */
.app{display:flex;flex-direction:column;height:100vh}

/* ── Title bar ── */
.titlebar{
  display:flex;align-items:center;justify-content:space-between;
  height:46px;padding:0 16px;
  background:var(--bg-panel);border-bottom:1px solid var(--border);
  user-select:none;flex-shrink:0;
}
.titlebar-brand{display:flex;align-items:center;gap:8px;font-weight:700;font-size:13px;letter-spacing:-.02em}
.titlebar-brand svg{width:18px;height:18px}
.titlebar-actions{display:flex;align-items:center;gap:6px}
.tb-btn{
  background:transparent;border:1px solid var(--border);color:var(--text-dim);
  padding:5px 12px;border-radius:7px;font-size:12px;font-weight:500;cursor:pointer;
  transition:all .15s;
}
.tb-btn:hover{background:var(--bg-card);border-color:var(--border-hover);color:var(--text)}

/* ── Main ── */
.main{display:flex;flex:1;overflow:hidden}

/* ── Store panel ── */
.store-panel{flex:1;display:flex;flex-direction:column;border-right:1px solid var(--border);min-width:0}
.store-nav{
  display:flex;align-items:center;gap:6px;
  padding:8px 12px;background:var(--bg-panel);border-bottom:1px solid var(--border);flex-shrink:0;
}
.nav-btn{
  background:var(--bg-card);border:1px solid var(--border);color:var(--text-dim);
  width:30px;height:30px;border-radius:7px;cursor:pointer;
  display:flex;align-items:center;justify-content:center;transition:all .15s;font-size:14px;
}
.nav-btn:hover{background:var(--bg-card-hover);color:var(--text)}
.nav-btn:disabled{opacity:.3;cursor:not-allowed}
.store-url{
  flex:1;height:30px;padding:0 10px;
  background:var(--bg-input);border:1px solid var(--border);border-radius:7px;
  color:var(--text-dim);font-size:12px;outline:none;
}
.store-url:focus{border-color:var(--accent)}
.store-notice{
  padding:6px 12px;background:rgba(0,113,227,.08);border-bottom:1px solid var(--border);
  font-size:11px;color:var(--text-muted);text-align:center;flex-shrink:0;
}
.store-frame{flex:1;border:none;background:var(--bg)}
.frame-error{
  flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:12px;
  color:var(--text-muted);font-size:13px;display:none;
}
.frame-error.show{display:flex}
.frame-error .open-link{
  display:inline-flex;align-items:center;gap:6px;
  padding:8px 20px;background:var(--accent);color:#fff;
  border-radius:8px;text-decoration:none;font-weight:600;font-size:13px;
  transition:background .15s;cursor:pointer;border:none;
}
.frame-error .open-link:hover{background:var(--accent-hover)}

/* ── Library panel ── */
.library-panel{width:340px;display:flex;flex-direction:column;background:var(--bg-panel);flex-shrink:0}
.library-header{
  display:flex;align-items:center;justify-content:space-between;
  padding:16px;border-bottom:1px solid var(--border);
}
.library-title{font-size:15px;font-weight:700;letter-spacing:-.01em}
.library-count{
  font-size:11px;font-weight:600;color:var(--accent);
  background:rgba(0,113,227,.1);padding:2px 8px;border-radius:20px;
}
.library-search{padding:8px 16px;border-bottom:1px solid var(--border)}
.library-search input{
  width:100%;height:32px;padding:0 10px 0 32px;
  background:var(--bg-input);border:1px solid var(--border);border-radius:8px;
  color:var(--text);font-size:12px;outline:none;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' fill='none' stroke='%2352525b' stroke-width='2' stroke-linecap='round'%3E%3Ccircle cx='6' cy='6' r='4.5'/%3E%3Cpath d='M9.5 9.5 13 13'/%3E%3C/svg%3E");
  background-repeat:no-repeat;background-position:10px center;
}
.library-search input:focus{border-color:var(--accent)}
.library-list{flex:1;overflow-y:auto;padding:8px}
.library-list::-webkit-scrollbar{width:5px}
.library-list::-webkit-scrollbar-track{background:transparent}
.library-list::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px}

/* ── Product card ── */
.product-card{
  padding:14px;margin-bottom:6px;
  background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);
  transition:border-color .15s,transform .1s;
  animation:cardIn .3s ease backwards;
}
.product-card:hover{border-color:var(--border-hover);transform:translateY(-1px)}
@keyframes cardIn{from{opacity:0;transform:translateY(6px)}}
.card-top{display:flex;align-items:flex-start;justify-content:space-between;gap:8px;margin-bottom:4px}
.card-title{font-size:13px;font-weight:600;color:var(--text);line-height:1.3}
.card-badge{
  font-size:10px;font-weight:700;color:var(--accent);text-transform:uppercase;
  letter-spacing:.05em;padding:2px 7px;background:rgba(0,113,227,.1);
  border-radius:20px;white-space:nowrap;flex-shrink:0;
}
.card-desc{font-size:11px;color:var(--text-muted);margin-bottom:10px;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
.card-bottom{display:flex;align-items:center;justify-content:space-between;gap:8px}
.card-meta{font-size:11px;color:var(--text-muted);font-family:'Cascadia Code','JetBrains Mono','Fira Code',monospace}

/* ── Buttons ── */
.btn{
  display:inline-flex;align-items:center;justify-content:center;gap:6px;
  padding:6px 16px;border:none;border-radius:7px;font-size:12px;font-weight:600;
  cursor:pointer;transition:all .15s;
}
.btn:active{transform:scale(.96)}
.btn-primary{background:var(--accent);color:#fff}
.btn-primary:hover:not(:disabled){background:var(--accent-hover);box-shadow:0 2px 12px var(--accent-glow)}
.btn-primary:disabled{opacity:.5;cursor:not-allowed}
.btn-installed{background:rgba(34,197,94,.1);color:var(--green);border:1px solid rgba(34,197,94,.15);cursor:default}
.btn-installed:hover{transform:none}
.btn-error{background:rgba(239,68,68,.1);color:var(--red);border:1px solid rgba(239,68,68,.15)}
.card-status{font-size:10px;color:var(--green);font-family:monospace;margin-top:6px;min-height:14px}

/* ── Spinner ── */
.spinner{width:14px;height:14px;border:2px solid transparent;border-top-color:currentColor;border-radius:50%;animation:spin .5s linear infinite;display:inline-block}
@keyframes spin{to{transform:rotate(360deg)}}

/* ── Status bar ── */
.statusbar{
  display:flex;align-items:center;gap:8px;
  height:32px;padding:0 16px;
  background:var(--bg-panel);border-top:1px solid var(--border);
  font-size:11px;color:var(--text-muted);flex-shrink:0;
}
.status-dot{width:6px;height:6px;border-radius:50%;flex-shrink:0}
.status-dot.on{background:var(--green);box-shadow:0 0 6px rgba(34,197,94,.4)}
.status-dot.off{background:#f59e0b;box-shadow:0 0 6px rgba(245,158,11,.3)}

/* ── Auth overlay ── */
.auth-overlay{
  position:fixed;inset:0;z-index:100;background:var(--bg);
  display:flex;align-items:center;justify-content:center;
}
.auth-overlay.hidden{display:none}
.auth-card{width:100%;max-width:380px;text-align:center;padding:40px}
.auth-logo{font-size:28px;font-weight:800;letter-spacing:-.03em;margin-bottom:6px}
.auth-logo span{color:var(--accent)}
.auth-sub{color:var(--text-dim);font-size:13px;margin-bottom:32px}
.auth-field{margin-bottom:16px;text-align:left}
.auth-field label{display:block;font-size:11px;font-weight:600;color:var(--text-dim);text-transform:uppercase;letter-spacing:.05em;margin-bottom:5px}
.auth-field input{
  width:100%;padding:10px 14px;
  background:var(--bg-panel);border:1px solid var(--border);border-radius:var(--radius);
  color:var(--text);font-family:monospace;font-size:13px;outline:none;transition:border-color .15s;
}
.auth-field input:focus{border-color:var(--accent)}
.auth-error{font-size:12px;color:var(--red);min-height:18px;margin-bottom:12px}
.auth-btn{width:100%;height:42px;font-size:14px;border-radius:var(--radius)}
.auth-help{margin-top:20px;font-size:11px;color:var(--text-muted);line-height:1.6}
.auth-help a{color:var(--accent);text-decoration:none}
.auth-help a:hover{text-decoration:underline}

/* ── Empty / loading ── */
.empty{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;color:var(--text-muted);font-size:13px;gap:8px;text-align:center;padding:20px}
.empty a{color:var(--accent);text-decoration:none}
.loading{display:flex;flex-direction:column;align-items:center;gap:10px;padding:40px;color:var(--text-dim)}
</style>
</head>
<body>
<div class="app">

  <!-- Auth overlay -->
  <div id="auth-overlay" class="auth-overlay">
    <div class="auth-card">
      <div class="auth-logo"><span>UEFN</span> Store</div>
      <div class="auth-sub">Install purchased assets directly into your UEFN projects</div>
      <div class="auth-field">
        <label for="api-key">API Key</label>
        <input type="text" id="api-key" placeholder="uefn_..." spellcheck="false" autocomplete="off">
      </div>
      <div id="auth-error" class="auth-error"></div>
      <button id="connect-btn" class="btn btn-primary auth-btn" onclick="doConnect()">Connect</button>
      <div class="auth-help">
        Get your key from <a href="https://www.uefnstore.com/dashboard/settings" target="_blank">uefnstore.com/dashboard/settings</a>
        <br>Your API key is stored locally and never shared.
      </div>
    </div>
  </div>

  <!-- Title bar -->
  <div class="titlebar">
    <div class="titlebar-brand">
      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m12 3-1.912 5.813a2 2 0 0 1-1.275 1.275L3 12l5.813 1.912a2 2 0 0 1 1.275 1.275L12 21l1.912-5.813a2 2 0 0 1 1.275-1.275L21 12l-5.813-1.912a2 2 0 0 1-1.275-1.275L12 3Z"/></svg>
      UEFN Store
    </div>
    <div class="titlebar-actions">
      <button class="tb-btn" onclick="loadPurchases()">Refresh</button>
      <button class="tb-btn" onclick="doDisconnect()">Disconnect</button>
    </div>
  </div>

  <!-- Main -->
  <div class="main">

    <!-- Store (left) -->
    <div class="store-panel">
      <div class="store-nav">
        <button class="nav-btn" onclick="storeBack()" id="btn-back" title="Back">&#8592;</button>
        <button class="nav-btn" onclick="storeForward()" id="btn-fwd" title="Forward">&#8594;</button>
        <button class="nav-btn" onclick="storeHome()" title="Home">&#8962;</button>
        <input type="text" class="store-url" id="store-url" value="uefnstore.com" readonly>
        <button class="nav-btn" onclick="window.open(STORE_URL,'_blank')" title="Open in browser">&#8599;</button>
      </div>
      <div class="store-notice">Browse only — to purchase, open in your browser using the &#8599; button above</div>
      <iframe id="store-frame" class="store-frame" src="STORE_URL_PLACEHOLDER" sandbox="allow-same-origin allow-scripts allow-popups allow-forms" loading="lazy"></iframe>
      <div id="frame-error" class="frame-error">
        <p>Could not load the store in this view.</p>
        <button class="open-link" onclick="window.open(STORE_URL,'_blank')">Open store in browser &#8599;</button>
      </div>
    </div>

    <!-- Library (right) -->
    <div class="library-panel">
      <div class="library-header">
        <span class="library-title">My Library</span>
        <span class="library-count" id="lib-count">0</span>
      </div>
      <div class="library-search">
        <input type="text" placeholder="Search products..." id="search-input" oninput="filterProducts()">
      </div>
      <div class="library-list" id="lib-list">
        <div class="loading">
          <div class="spinner" style="width:22px;height:22px"></div>
          <span>Loading library...</span>
        </div>
      </div>
    </div>

  </div>

  <!-- Status bar -->
  <div class="statusbar">
    <div id="status-dot" class="status-dot off"></div>
    <span id="status-text">Checking...</span>
  </div>

</div>

<script>
const $ = s => document.querySelector(s);
const STORE_URL = 'STORE_URL_PLACEHOLDER';
const storeFrame = $('#store-frame');
const apiKeyInput = $('#api-key');
let allPurchases = [];

apiKeyInput.addEventListener('keydown', e => { if (e.key === 'Enter') doConnect(); });

// Detect iframe load failure
storeFrame.addEventListener('load', () => {
  try {
    const loc = storeFrame.contentWindow.location.href;
    $('#store-url').value = loc.replace(/^https?:\/\//, '');
  } catch(e) {
    // Cross-origin = iframe loaded fine but we can't read URL
    $('#store-url').value = 'uefnstore.com';
  }
});
storeFrame.addEventListener('error', () => {
  storeFrame.style.display = 'none';
  $('#frame-error').classList.add('show');
});
// Fallback: if iframe is blocked by headers, show error after timeout
setTimeout(() => {
  try {
    // If we can access contentDocument and it's null/empty, iframe was blocked
    if (storeFrame.contentDocument && !storeFrame.contentDocument.body?.children.length) {
      storeFrame.style.display = 'none';
      $('#frame-error').classList.add('show');
    }
  } catch(e) {
    // Cross-origin error = iframe loaded fine
  }
}, 3000);

// ── Init ──
fetch('/api/status').then(r => r.json()).then(data => {
  updateStatus(data);
  if (data.connected) {
    $('#auth-overlay').classList.add('hidden');
    loadPurchases();
    // Auto-login into the site iframe
    if (data.api_key) {
      storeFrame.src = STORE_URL + '/api/auth/installer-login?key=' + encodeURIComponent(data.api_key);
    }
  }
}).catch(() => {});

// ── Store navigation ──
function storeHome() { storeFrame.src = STORE_URL; }
function storeBack() { try { storeFrame.contentWindow.history.back(); } catch(e){} }
function storeForward() { try { storeFrame.contentWindow.history.forward(); } catch(e){} }

// ── Auth ──
function updateStatus(data) {
  const dot = $('#status-dot');
  const text = $('#status-text');
  if (data.uefn_project) {
    dot.className = 'status-dot on';
    text.textContent = 'UEFN Project: ' + data.uefn_project;
  } else {
    dot.className = 'status-dot off';
    text.textContent = 'Standalone mode \u2014 files saved to ~/UEFN_Downloads';
  }
}

async function doConnect() {
  const key = apiKeyInput.value.trim();
  if (!key || !key.startsWith('uefn_')) {
    $('#auth-error').textContent = 'API key must start with uefn_';
    return;
  }
  $('#auth-error').textContent = '';
  const btn = $('#connect-btn');
  btn.disabled = true;
  btn.innerHTML = '<span class="spinner"></span> Connecting...';

  try {
    const res = await fetch('/api/connect', {
      method: 'POST',
      headers: {'Content-Type': 'application/json'},
      body: JSON.stringify({api_key: key})
    });
    const data = await res.json();
    if (data.success) {
      updateStatus(data);
      $('#auth-overlay').classList.add('hidden');
      loadPurchases();
      // Log into the site via API key — iframe navigates to login route
      storeFrame.src = STORE_URL + '/api/auth/installer-login?key=' + encodeURIComponent(key);
    } else {
      $('#auth-error').textContent = data.error || 'Connection failed';
    }
  } catch(e) {
    $('#auth-error').textContent = 'Connection error';
  }
  btn.disabled = false;
  btn.innerHTML = 'Connect';
}

async function doDisconnect() {
  await fetch('/api/disconnect', {method: 'POST'});
  apiKeyInput.value = '';
  $('#auth-error').textContent = '';
  $('#auth-overlay').classList.remove('hidden');
  $('#lib-list').innerHTML = '';
  $('#lib-count').textContent = '0';
}

// ── Library ──
async function loadPurchases() {
  const container = $('#lib-list');
  container.innerHTML = '<div class="loading"><div class="spinner" style="width:22px;height:22px"></div><span>Loading...</span></div>';

  try {
    const res = await fetch('/api/purchases');
    const data = await res.json();
    if (data.error) {
      container.innerHTML = '<div class="empty">' + esc(data.error) + '</div>';
      return;
    }
    allPurchases = data.purchases || [];
    $('#lib-count').textContent = allPurchases.length;
    renderProducts(allPurchases);
  } catch(e) {
    container.innerHTML = '<div class="empty">Failed to load library</div>';
  }
}

function renderProducts(purchases) {
  const container = $('#lib-list');
  if (purchases.length === 0) {
    container.innerHTML = '<div class="empty"><p>No products found</p><a href="' + STORE_URL + '" target="_blank">Browse the store</a></div>';
    return;
  }

  container.innerHTML = '';
  purchases.forEach((p, i) => {
    const product = p.product || {};
    const version = p.latest_version;
    const card = document.createElement('div');
    card.className = 'product-card';
    card.style.animationDelay = (i * 40) + 'ms';
    card.dataset.title = (product.title || '').toLowerCase();

    let meta = '';
    if (version) {
      meta = 'v' + esc(version.version);
      if (version.file_size) meta += ' \u00B7 ' + (version.file_size / 1048576).toFixed(1) + ' MB';
    }

    let installBtn = '';
    if (version) {
      installBtn = '<button class="btn btn-primary" data-pid="' + esc(product.id) + '" data-vid="' + esc(version.id) + '" data-title="' + esc(product.title) + '" onclick="doInstall(this)">Install</button>';
    } else {
      installBtn = '<span style="font-size:11px;color:var(--text-muted)">No version</span>';
    }

    card.innerHTML =
      '<div class="card-top">' +
        '<div class="card-title">' + esc(product.title || 'Unknown') + '</div>' +
        (product.category ? '<div class="card-badge">' + esc(product.category) + '</div>' : '') +
      '</div>' +
      (product.short_description ? '<div class="card-desc">' + esc(product.short_description) + '</div>' : '') +
      '<div class="card-bottom">' +
        '<div class="card-meta">' + meta + '</div>' +
        installBtn +
      '</div>' +
      '<div class="card-status" id="status-' + esc(product.id) + '"></div>';

    container.appendChild(card);
  });
}

function filterProducts() {
  const q = $('#search-input').value.toLowerCase().trim();
  if (!q) { renderProducts(allPurchases); return; }
  renderProducts(allPurchases.filter(p =>
    (p.product?.title || '').toLowerCase().includes(q) ||
    (p.product?.category || '').toLowerCase().includes(q)
  ));
}

async function doInstall(btn) {
  const pid = btn.dataset.pid;
  const vid = btn.dataset.vid;
  const title = btn.dataset.title;
  const statusEl = document.getElementById('status-' + pid);

  btn.disabled = true;
  btn.innerHTML = '<span class="spinner"></span> Installing...';
  if (statusEl) statusEl.textContent = '';

  try {
    const res = await fetch('/api/install', {
      method: 'POST',
      headers: {'Content-Type': 'application/json'},
      body: JSON.stringify({product_id: pid, version_id: vid, title: title})
    });
    const data = await res.json();
    if (data.success) {
      btn.className = 'btn btn-installed';
      btn.innerHTML = '\u2713 Installed';
      btn.disabled = true;
      if (statusEl && data.folder) statusEl.textContent = '\u2192 ' + data.folder + '/';
    } else {
      btn.className = 'btn btn-error';
      btn.innerHTML = 'Retry';
      btn.disabled = false;
      if (statusEl) { statusEl.style.color = 'var(--red)'; statusEl.textContent = data.error || 'Failed'; }
    }
  } catch(e) {
    btn.className = 'btn btn-error';
    btn.innerHTML = 'Retry';
    btn.disabled = false;
    if (statusEl) { statusEl.style.color = 'var(--red)'; statusEl.textContent = 'Connection error'; }
  }
}

// Heartbeat — tells the server we're still alive
setInterval(() => { fetch('/api/heartbeat').catch(() => {}); }, 3000);

function esc(s) {
  if (!s) return '';
  return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
</script>
</body>
</html>"""


# ──────────────────────────────────────────────
# HTTP Server
# ──────────────────────────────────────────────
import time

class InstallerHandler(http.server.BaseHTTPRequestHandler):
    api = None
    last_heartbeat = 0

    def log_message(self, fmt, *args):
        if IN_UEFN:
            unreal.log("[UEFN Store] " + (fmt % args))

    def _json(self, data, status=200):
        body = json.dumps(data).encode()
        self.send_response(status)
        self.send_header("Content-Type", "application/json")
        self.send_header("Content-Length", str(len(body)))
        self.send_header("Cache-Control", "no-store")
        self.end_headers()
        self.wfile.write(body)

    def _body(self):
        length = int(self.headers.get("Content-Length", 0))
        return json.loads(self.rfile.read(length).decode()) if length else {}

    def do_GET(self):
        if self.path == "/":
            InstallerHandler.last_heartbeat = time.time()
            html = HTML_PAGE.replace("STORE_URL_PLACEHOLDER", STORE_URL).encode()
            self.send_response(200)
            self.send_header("Content-Type", "text/html; charset=utf-8")
            self.send_header("Content-Length", str(len(html)))
            self.end_headers()
            self.wfile.write(html)
        elif self.path == "/api/heartbeat":
            InstallerHandler.last_heartbeat = time.time()
            self._json({"ok": True})
        elif self.path == "/api/status":
            self._json(self.api.get_status())
        elif self.path == "/api/purchases":
            self._json(self.api.get_purchases())
        else:
            self.send_error(404)

    def do_POST(self):
        if self.path == "/api/connect":
            self._json(self.api.connect(self._body().get("api_key", "")))
        elif self.path == "/api/disconnect":
            self._json(self.api.disconnect())
        elif self.path == "/api/install":
            data = self._body()
            self._json(self.api.do_install(
                data.get("product_id"), data.get("version_id"), data.get("title", "product")
            ))
        elif self.path == "/api/shutdown":
            self._json({"success": True})
            threading.Thread(target=self.server.shutdown, daemon=True).start()
        else:
            self.send_error(404)


# ──────────────────────────────────────────────
# Entry Point
# ──────────────────────────────────────────────
_server_instance = None

def main(content_dir=None):
    global _server_instance

    if _server_instance is not None:
        try:
            _server_instance.shutdown()
            _server_instance.server_close()
        except Exception:
            pass

    api = InstallerAPI(content_dir=content_dir)

    InstallerHandler.api = api

    socketserver.ThreadingTCPServer.allow_reuse_address = True
    server = socketserver.ThreadingTCPServer(("127.0.0.1", PORT), InstallerHandler)
    server.daemon_threads = True
    _server_instance = server

    InstallerHandler.last_heartbeat = time.time()

    def watchdog():
        """Shut down the server if no heartbeat received for 10 seconds."""
        while True:
            time.sleep(5)
            if time.time() - InstallerHandler.last_heartbeat > 10:
                msg = "[UEFN Store] Browser closed — shutting down."
                if IN_UEFN:
                    unreal.log(msg)
                else:
                    print(msg)
                server.shutdown()
                break

    threading.Thread(target=watchdog, daemon=True).start()

    url = "http://127.0.0.1:{}".format(PORT)
    msg = "[UEFN Store] Installer running at {}".format(url)
    if IN_UEFN:
        unreal.log(msg)
    else:
        print(msg)

    webbrowser.open(url)

    try:
        server.serve_forever(poll_interval=0.5)
    except KeyboardInterrupt:
        pass
    finally:
        try:
            server.server_close()
        except Exception:
            pass


if __name__ == "__main__":
    _content_dir = get_uefn_content_dir()

    if IN_UEFN:
        threading.Thread(target=main, args=(_content_dir,), daemon=True).start()
    else:
        main(_content_dir)
