/// Playground command — Monaco editor with live preview.

use std::fs;
use std::path::Path;
use std::time::Instant;

/// The playground HTML page with Monaco editor + live preview.
const PLAYGROUND_HTML: &str = r##"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DreamStack Playground</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<style>
  * { margin: 0; padding: 0; box-sizing: border-box; }

  :root {
    --bg: #0a0a12;
    --surface: #12121e;
    --surface-2: #1a1a2e;
    --border: #2a2a3e;
    --text: #e4e4ef;
    --text-dim: #888899;
    --accent: #818cf8;
    --accent-glow: rgba(129,140,248,0.15);
    --green: #34d399;
    --red: #f87171;
    --yellow: #fbbf24;
  }

  html, body { height: 100%; background: var(--bg); color: var(--text); font-family: 'Inter', sans-serif; overflow: hidden; }

  /* Header */
  .header {
    height: 52px;
    background: var(--surface);
    border-bottom: 1px solid var(--border);
    display: flex;
    align-items: center;
    padding: 0 20px;
    gap: 16px;
    z-index: 100;
  }
  .header .logo {
    font-size: 16px;
    font-weight: 700;
    background: linear-gradient(135deg, var(--accent), #a78bfa);
    -webkit-background-clip: text;
    -webkit-text-fill-color: transparent;
    letter-spacing: -0.5px;
  }
  .header .sep { width: 1px; height: 24px; background: var(--border); }
  .header .status {
    font-size: 12px;
    font-family: 'JetBrains Mono', monospace;
    color: var(--text-dim);
    display: flex;
    align-items: center;
    gap: 8px;
  }
  .header .status .dot {
    width: 7px; height: 7px;
    border-radius: 50%;
    background: var(--green);
    box-shadow: 0 0 6px rgba(52,211,153,0.5);
    transition: background 0.3s, box-shadow 0.3s;
  }
  .header .status .dot.error {
    background: var(--red);
    box-shadow: 0 0 6px rgba(248,113,113,0.5);
  }
  .header .status .dot.compiling {
    background: var(--yellow);
    box-shadow: 0 0 6px rgba(251,191,36,0.5);
    animation: pulse 0.6s ease-in-out infinite;
  }
  @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } }

  .header .actions { margin-left: auto; display: flex; gap: 8px; }
  .header .btn {
    padding: 6px 14px;
    border-radius: 6px;
    border: 1px solid var(--border);
    background: var(--surface-2);
    color: var(--text);
    font-family: 'JetBrains Mono', monospace;
    font-size: 11px;
    cursor: pointer;
    transition: all 0.15s;
  }
  .header .btn:hover { border-color: var(--accent); background: var(--accent-glow); }
  .header .btn.primary { background: var(--accent); border-color: var(--accent); color: #fff; }
  .header .btn.primary:hover { opacity: 0.9; }

  /* Layout */
  .container {
    display: flex;
    height: calc(100vh - 52px);
  }
  .editor-pane {
    width: 50%;
    min-width: 300px;
    position: relative;
    border-right: 1px solid var(--border);
  }
  .preview-pane {
    flex: 1;
    position: relative;
    background: #fff;
  }
  #previewRoot {
    width: 100%;
    height: 100%;
    overflow: auto;
  }

  /* Resize handle */
  .resize-handle {
    position: absolute;
    right: -3px;
    top: 0;
    width: 6px;
    height: 100%;
    cursor: col-resize;
    z-index: 10;
    background: transparent;
    transition: background 0.2s;
  }
  .resize-handle:hover, .resize-handle.active {
    background: var(--accent);
  }

  /* Error panel */
  .error-panel {
    position: absolute;
    bottom: 0;
    left: 0;
    right: 0;
    max-height: 40%;
    background: rgba(10,10,18,0.97);
    border-top: 2px solid var(--red);
    overflow-y: auto;
    z-index: 20;
    display: none;
    font-family: 'JetBrains Mono', monospace;
    font-size: 12px;
    padding: 16px 20px;
    color: var(--red);
    line-height: 1.6;
    white-space: pre-wrap;
  }
  .error-panel.visible { display: block; }

  /* Monaco loader */
  #editor { width: 100%; height: 100%; }
</style>
</head>
<body>

<div class="header">
  <span class="logo">DreamStack</span>
  <span class="sep"></span>
  <div class="status">
    <span class="dot" id="statusDot"></span>
    <span id="statusText">Ready</span>
  </div>
  <div class="actions">
    <button class="btn" onclick="formatCode()">Format</button>
    <button class="btn primary" onclick="compileNow()">Compile ⌘↵</button>
  </div>
</div>

<div class="container">
  <div class="editor-pane" id="editorPane">
    <div id="editor"></div>
    <div class="resize-handle" id="resizeHandle"></div>
    <div class="error-panel" id="errorPanel"></div>
  </div>
  <div class="preview-pane" id="previewPane">
    <div id="previewRoot"></div>
  </div>
</div>

<script src="https://cdn.jsdelivr.net/npm/monaco-editor@0.45.0/min/vs/loader.js"></script>
<script>
  // ── State ──
  let editor;
  let compileTimer = null;
  const DEBOUNCE_MS = 400;

  // ── Monaco Setup ──
  require.config({ paths: { vs: 'https://cdn.jsdelivr.net/npm/monaco-editor@0.45.0/min/vs' }});

  require(['vs/editor/editor.main'], function () {
    // Register DreamStack language
    monaco.languages.register({ id: 'dreamstack' });
    monaco.languages.setMonarchTokensProvider('dreamstack', {
      keywords: ['let','view','when','match','on','effect','perform','handle','import','export',
                 'if','else','every','type','where','stream','from','layout','component','for','in'],
      typeKeywords: ['Int','Float','String','Bool','Signal','Derived','Array','Stream','View'],
      operators: ['=','>','<','>=','<=','==','!=','+','-','*','/','%','|>','->','!','&&','||'],
      tokenizer: {
        root: [
          [/\/\/.*$/, 'comment'],
          [/"([^"\\]|\\.)*"/, 'string'],
          [/\d+\.\d+/, 'number.float'],
          [/\d+/, 'number'],
          [/[a-zA-Z_]\w*/, {
            cases: {
              '@keywords': 'keyword',
              '@typeKeywords': 'type',
              '@default': 'identifier'
            }
          }],
          [/[{}()\[\]]/, 'delimiter.bracket'],
          [/[,;:]/, 'delimiter'],
          [/[=><!\+\-\*\/%|&]+/, 'operator'],
          [/\s+/, 'white'],
        ]
      }
    });

    // Define DreamStack theme
    monaco.editor.defineTheme('dreamstack-dark', {
      base: 'vs-dark',
      inherit: true,
      rules: [
        { token: 'keyword', foreground: '818cf8', fontStyle: 'bold' },
        { token: 'type', foreground: '34d399' },
        { token: 'string', foreground: 'fbbf24' },
        { token: 'number', foreground: 'f472b6' },
        { token: 'number.float', foreground: 'f472b6' },
        { token: 'comment', foreground: '555566', fontStyle: 'italic' },
        { token: 'operator', foreground: '94a3b8' },
        { token: 'delimiter', foreground: '64748b' },
        { token: 'identifier', foreground: 'e4e4ef' },
      ],
      colors: {
        'editor.background': '#0a0a12',
        'editor.foreground': '#e4e4ef',
        'editor.lineHighlightBackground': '#1a1a2e',
        'editorLineNumber.foreground': '#3a3a4e',
        'editorLineNumber.activeForeground': '#818cf8',
        'editor.selectionBackground': '#818cf833',
        'editorCursor.foreground': '#818cf8',
        'editorIndentGuide.background': '#1e1e30',
      }
    });

    // Create editor
    editor = monaco.editor.create(document.getElementById('editor'), {
      value: INITIAL_SOURCE,
      language: 'dreamstack',
      theme: 'dreamstack-dark',
      fontFamily: "'JetBrains Mono', monospace",
      fontSize: 14,
      lineHeight: 24,
      padding: { top: 16 },
      minimap: { enabled: false },
      scrollBeyondLastLine: false,
      renderLineHighlight: 'all',
      cursorBlinking: 'smooth',
      cursorSmoothCaretAnimation: 'on',
      smoothScrolling: true,
      tabSize: 2,
      wordWrap: 'on',
      automaticLayout: true,
    });

    // Auto-compile on change
    editor.onDidChangeModelContent(() => {
      clearTimeout(compileTimer);
      compileTimer = setTimeout(compileNow, DEBOUNCE_MS);
    });

    // Keyboard shortcut: Cmd/Ctrl + Enter
    editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter, compileNow);

    // Initial compile
    compileNow();
  });

  // ── Compile ──
  let compiling = false;
  async function compileNow() {
    if (compiling || !editor) return;
    compiling = true;

    const dot = document.getElementById('statusDot');
    const text = document.getElementById('statusText');
    const errorPanel = document.getElementById('errorPanel');

    dot.className = 'dot compiling';
    text.textContent = 'Compiling...';

    const source = editor.getValue();
    const start = performance.now();

    try {
      const res = await fetch('/compile', {
        method: 'POST',
        headers: { 'Content-Type': 'text/plain' },
        body: source,
      });

      const ms = (performance.now() - start).toFixed(0);
      const data = await res.json();

      if (data.type === 'full') {
        const root = document.getElementById('previewRoot');
        // Parse HTML and extract body + scripts
        const parser = new DOMParser();
        const doc = parser.parseFromString(data.html, 'text/html');
        // Attach or reuse shadow root
        if (!root.shadowRoot) root.attachShadow({ mode: 'open' });
        const shadow = root.shadowRoot;
        shadow.innerHTML = '';
        // Copy styles into shadow
        doc.querySelectorAll('style').forEach(s => shadow.appendChild(s.cloneNode(true)));
        // Copy body content into shadow
        const wrapper = document.createElement('div');
        wrapper.innerHTML = doc.body.innerHTML;
        shadow.appendChild(wrapper);
        // Execute scripts in main window context (where DS signals live)
        doc.querySelectorAll('script').forEach(s => {
          try { new Function(s.textContent)(); } catch(e) { console.warn('Script error:', e); }
        });
        dot.className = 'dot';
        text.textContent = `Full compile ${ms}ms`;
        errorPanel.classList.remove('visible');
      } else if (data.type === 'patch') {
        if (data.js && data.js.length > 0) {
          try {
            new Function(data.js)();
          } catch (e) { console.warn('Patch eval error:', e); }
        }
        dot.className = 'dot';
        text.textContent = `Patched ${ms}ms ⚡`;
        errorPanel.classList.remove('visible');
      } else if (data.type === 'error') {
        dot.className = 'dot error';
        text.textContent = `Error (${ms}ms)`;
        errorPanel.textContent = data.message;
        errorPanel.classList.add('visible');
      }
    } catch (e) {
      dot.className = 'dot error';
      text.textContent = 'Network error';
      errorPanel.textContent = e.message;
      errorPanel.classList.add('visible');
    }

    compiling = false;
  }

  function formatCode() {
    if (editor) editor.getAction('editor.action.formatDocument')?.run();
  }

  // ── Resize Handle ──
  const handle = document.getElementById('resizeHandle');
  const editorPane = document.getElementById('editorPane');
  let resizing = false;

  handle.addEventListener('mousedown', (e) => {
    resizing = true;
    handle.classList.add('active');
    document.body.style.cursor = 'col-resize';
    document.body.style.userSelect = 'none';
    e.preventDefault();
  });

  window.addEventListener('mousemove', (e) => {
    if (!resizing) return;
    const pct = (e.clientX / window.innerWidth) * 100;
    const clamped = Math.max(25, Math.min(75, pct));
    editorPane.style.width = clamped + '%';
  });

  window.addEventListener('mouseup', () => {
    resizing = false;
    handle.classList.remove('active');
    document.body.style.cursor = '';
    document.body.style.userSelect = '';
  });
</script>
</body>
</html>
"##;

pub fn cmd_playground(file: Option<&Path>, port: u16) {
    println!("🎮 DreamStack Playground");
    println!("   http://localhost:{port}");
    println!();

    // Load initial source from file or use default
    let initial_source = if let Some(path) = file {
        match fs::read_to_string(path) {
            Ok(s) => {
                println!("   loaded: {}", path.display());
                s
            }
            Err(e) => {
                eprintln!("⚠️  Could not read {}: {e}", path.display());
                default_playground_source()
            }
        }
    } else {
        default_playground_source()
    };

    // Build the playground HTML with the initial source injected
    let escaped_source = initial_source
        .replace('\\', "\\\\")
        .replace('`', "\\`")
        .replace("${", "\\${");
    let playground_html = PLAYGROUND_HTML.replace(
        "INITIAL_SOURCE",
        &format!("String.raw`{}`", escaped_source),
    );

    let server = tiny_http::Server::http(format!("0.0.0.0:{port}")).unwrap();
    println!("✅ Playground running at http://localhost:{port}");
    println!("   Press Ctrl+C to stop");
    println!();

    let base_dir = file.and_then(|f| f.parent()).unwrap_or(Path::new("."));
    let _base_dir = base_dir.to_path_buf();
    let mut inc_compiler = ds_incremental::IncrementalCompiler::new();

    for mut request in server.incoming_requests() {
        let url = request.url().to_string();

        if url == "/compile" && request.method() == &tiny_http::Method::Post {
            // Read the body
            let mut body = String::new();
            let reader = request.as_reader();
            match reader.read_to_string(&mut body) {
                Ok(_) => {}
                Err(e) => {
                    let resp = tiny_http::Response::from_string(format!("Read error: {e}"))
                        .with_status_code(400);
                    let _ = request.respond(resp);
                    continue;
                }
            }

            let start = Instant::now();
            match inc_compiler.compile(&body) {
                ds_incremental::IncrementalResult::Full(html) => {
                    let ms = start.elapsed().as_millis();
                    println!("   ✅ full compile in {ms}ms ({} bytes)", html.len());
                    let json = format!(r#"{{"type":"full","html":{}}}"#, json_escape(&html));
                    let response = tiny_http::Response::from_string(&json)
                        .with_header(
                            tiny_http::Header::from_bytes(
                                &b"Content-Type"[..],
                                &b"application/json; charset=utf-8"[..],
                            ).unwrap(),
                        )
                        .with_header(
                            tiny_http::Header::from_bytes(
                                &b"Access-Control-Allow-Origin"[..],
                                &b"*"[..],
                            ).unwrap(),
                        );
                    let _ = request.respond(response);
                }
                ds_incremental::IncrementalResult::Patch(js) => {
                    let ms = start.elapsed().as_millis();
                    if js.is_empty() {
                        println!("   ⚡ unchanged ({ms}ms)");
                    } else {
                        println!("   ⚡ incremental patch in {ms}ms ({} bytes)", js.len());
                    }
                    let json = format!(r#"{{"type":"patch","js":{}}}"#, json_escape(&js));
                    let response = tiny_http::Response::from_string(&json)
                        .with_header(
                            tiny_http::Header::from_bytes(
                                &b"Content-Type"[..],
                                &b"application/json; charset=utf-8"[..],
                            ).unwrap(),
                        )
                        .with_header(
                            tiny_http::Header::from_bytes(
                                &b"Access-Control-Allow-Origin"[..],
                                &b"*"[..],
                            ).unwrap(),
                        );
                    let _ = request.respond(response);
                }
                ds_incremental::IncrementalResult::Error(e) => {
                    println!("   ❌ compile error");
                    let json = format!(r#"{{"type":"error","message":{}}}"#, json_escape(&e));
                    let response = tiny_http::Response::from_string(&json)
                        .with_status_code(400)
                        .with_header(
                            tiny_http::Header::from_bytes(
                                &b"Content-Type"[..],
                                &b"application/json; charset=utf-8"[..],
                            ).unwrap(),
                        );
                    let _ = request.respond(response);
                }
            }
        } else {
            // Serve the playground HTML
            let response = tiny_http::Response::from_string(&playground_html)
                .with_header(
                    tiny_http::Header::from_bytes(
                        &b"Content-Type"[..],
                        &b"text/html; charset=utf-8"[..],
                    ).unwrap(),
                );
            let _ = request.respond(response);
        }
    }
}

fn default_playground_source() -> String {
    r#"let count = 0
let label = "Hello, DreamStack!"

on click -> count = count + 1

view main = column [
  text label
  text count
  button "Increment" { click: count += 1 }
]
"#.to_string()
}

/// Escape a string for embedding in JSON.
pub fn json_escape(s: &str) -> String {
    let mut out = String::with_capacity(s.len() + 2);
    out.push('"');
    for c in s.chars() {
        match c {
            '"' => out.push_str("\\\""),
            '\\' => out.push_str("\\\\"),
            '\n' => out.push_str("\\n"),
            '\r' => out.push_str("\\r"),
            '\t' => out.push_str("\\t"),
            c if c < '\x20' => out.push_str(&format!("\\u{:04x}", c as u32)),
            c => out.push(c),
        }
    }
    out.push('"');
    out
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_json_escape_basic() {
        assert_eq!(json_escape("hello"), "\"hello\"");
        assert_eq!(json_escape("line1\nline2"), "\"line1\\nline2\"");
        assert_eq!(json_escape("say \"hi\""), "\"say \\\"hi\\\"\"");
        assert_eq!(json_escape("back\\slash"), "\"back\\\\slash\"");
        assert_eq!(json_escape("tab\there"), "\"tab\\there\"");
    }

    #[test]
    fn test_json_escape_empty() {
        assert_eq!(json_escape(""), "\"\"");
    }
}

