"""zuzu run — local dev server with live preview."""

from __future__ import annotations

import json
import os
import sys
import time
import subprocess
import threading
from http.server import HTTPServer, SimpleHTTPRequestHandler
from pathlib import Path
from typing import Any

_spec: dict[str, Any] = {}
_app_file: str = ""
_last_mtime: float = 0


def _run_app(app_file: str) -> dict[str, Any]:
    """Execute the app file and capture the JSON spec."""
    abs_path = os.path.abspath(app_file)
    app_dir = os.path.dirname(abs_path)
    # Ensure zuzu is importable: add the SDK root to PYTHONPATH
    sdk_root = str(Path(__file__).parent.parent.parent)
    existing_pp = os.environ.get("PYTHONPATH", "")
    python_path = f"{sdk_root}:{app_dir}:{existing_pp}" if existing_pp else f"{sdk_root}:{app_dir}"
    env = {**os.environ, "ZUZU_OUTPUT_PATH": "/tmp/zuzu_dev_spec.json", "PYTHONPATH": python_path}
    try:
        result = subprocess.run(
            [sys.executable, abs_path],
            env=env,
            capture_output=True,
            text=True,
            timeout=10,
            cwd=app_dir,
        )
        if result.returncode != 0:
            return {"error": result.stderr or "App failed to run"}

        spec_path = "/tmp/zuzu_dev_spec.json"
        if os.path.exists(spec_path):
            with open(spec_path) as f:
                return json.load(f)
        # Fallback: parse stdout
        if result.stdout.strip():
            return json.loads(result.stdout)
        return {"error": "No output from app"}
    except subprocess.TimeoutExpired:
        return {"error": "App timed out (10s limit)"}
    except Exception as e:
        return {"error": str(e)}


_last_db_mtime: float = 0


def _watch_and_reload() -> None:
    """Watch the app file AND db.sqlite for changes and re-run."""
    global _spec, _last_mtime, _last_db_mtime
    db_path = os.path.join(os.path.dirname(os.path.abspath(_app_file)), "db.sqlite")
    while True:
        try:
            app_mtime = os.path.getmtime(_app_file)
            db_mtime = os.path.getmtime(db_path) if os.path.exists(db_path) else 0

            if app_mtime != _last_mtime or db_mtime != _last_db_mtime:
                changed = "app" if app_mtime != _last_mtime else "data"
                _last_mtime = app_mtime
                _last_db_mtime = db_mtime
                if changed == "app":
                    print(f"  Reloading {_app_file}...")
                _spec = _run_app(_app_file)
                if "error" in _spec:
                    print(f"  Error: {_spec['error']}")
                elif changed == "app":
                    pages = len(_spec.get("pages", []))
                    records = len(_spec.get("app", {}).get("records", []))
                    print(f"  OK — {records} records, {pages} pages")
        except Exception as e:
            print(f"  Watch error: {e}")
        time.sleep(2)


class _Handler(SimpleHTTPRequestHandler):
    """HTTP handler that serves the app spec and renderer."""

    def do_GET(self) -> None:
        if self.path == "/api/spec":
            self._json_response(_spec)
        elif self.path == "/api/data" or self.path.startswith("/api/data/"):
            self._handle_data()
        elif self.path.startswith("/api/aggregate/"):
            self._handle_aggregate()
        elif self.path == "/" or self.path == "/index.html":
            self._serve_static("index.html", "text/html")
        elif self.path.startswith("/assets/"):
            filename = self.path.lstrip("/")
            ct = "application/javascript" if filename.endswith(".js") else "text/css" if filename.endswith(".css") else "application/octet-stream"
            self._serve_static(filename, ct)
        else:
            # SPA fallback: serve index.html for non-API paths
            self._serve_static("index.html", "text/html")

    def do_POST(self) -> None:
        if self.path.startswith("/api/data/"):
            self._handle_data_write()
        else:
            self.send_error(404)

    def do_PATCH(self) -> None:
        if self.path.startswith("/api/data/"):
            parts = self.path.split("/")
            if len(parts) >= 5:
                table_name = parts[3]
                record_id = parts[4]
                content_length = int(self.headers.get("Content-Length", 0))
                body = json.loads(self.rfile.read(content_length))
                try:
                    import sqlite3
                    db_path = os.path.join(os.path.dirname(os.path.abspath(_app_file)), "db.sqlite")
                    conn = sqlite3.connect(db_path)
                    sets = ", ".join(f"{k} = ?" for k in body)
                    values = list(body.values()) + [record_id]
                    conn.execute(f"UPDATE {table_name} SET {sets}, updated_at = datetime('now') WHERE id = ?", values)
                    conn.commit()
                    conn.close()
                    self._json_response({"ok": True})
                except Exception as e:
                    self._json_response({"error": str(e)})
            else:
                self._json_response({"error": "specify id"})
        else:
            self.send_error(404)

    def do_DELETE(self) -> None:
        if self.path.startswith("/api/data/"):
            parts = self.path.split("/")
            if len(parts) >= 5:
                table_name = parts[3]
                record_id = parts[4]
                try:
                    import sqlite3
                    db_path = os.path.join(os.path.dirname(os.path.abspath(_app_file)), "db.sqlite")
                    conn = sqlite3.connect(db_path)
                    conn.execute(f"DELETE FROM {table_name} WHERE id = ?", [record_id])
                    conn.commit()
                    conn.close()
                    self._json_response({"ok": True})
                except Exception as e:
                    self._json_response({"error": str(e)})
            else:
                self._json_response({"error": "specify id: /api/data/{table}/{id}"})
        else:
            self.send_error(404)

    def do_OPTIONS(self) -> None:
        """Handle CORS preflight."""
        self.send_response(200)
        self.send_header("Access-Control-Allow-Origin", "*")
        self.send_header("Access-Control-Allow-Methods", "GET, POST, PATCH, DELETE, OPTIONS")
        self.send_header("Access-Control-Allow-Headers", "Content-Type")
        self.end_headers()

    def _json_response(self, data: Any) -> None:
        body = json.dumps(data, ensure_ascii=False).encode()
        self.send_response(200)
        self.send_header("Content-Type", "application/json")
        self.send_header("Content-Length", str(len(body)))
        self.send_header("Access-Control-Allow-Origin", "*")
        self.end_headers()
        self.wfile.write(body)

    def _handle_data(self) -> None:
        """Read data from a record's SQLite table."""
        parts = self.path.split("/")
        if len(parts) >= 4:
            table_name = parts[3]
            try:
                import sqlite3
                db_path = os.path.join(os.path.dirname(os.path.abspath(_app_file)), "db.sqlite")
                if not os.path.exists(db_path):
                    self._json_response([])
                    return
                conn = sqlite3.connect(db_path)
                conn.row_factory = sqlite3.Row
                rows = conn.execute(f"SELECT * FROM {table_name}").fetchall()
                conn.close()
                self._json_response([dict(r) for r in rows])
            except Exception as e:
                self._json_response({"error": str(e)})
        else:
            self._json_response({"error": "specify table name: /api/data/{table}"})

    def _handle_aggregate(self) -> None:
        """Handle aggregate queries: /api/aggregate/{table}?func=SUM&field=amount"""
        from urllib.parse import urlparse, parse_qs
        parsed = urlparse(self.path)
        parts = parsed.path.split("/")
        if len(parts) >= 4:
            table_name = parts[3]
            params = parse_qs(parsed.query)
            func = params.get("func", ["COUNT"])[0].upper()
            field = params.get("field", ["*"])[0]
            where_field = params.get("where_field", [None])[0]
            where_value = params.get("where_value", [None])[0]
            try:
                import sqlite3
                db_path = os.path.join(os.path.dirname(os.path.abspath(_app_file)), "db.sqlite")
                if not os.path.exists(db_path):
                    self._json_response({"value": 0})
                    return
                conn = sqlite3.connect(db_path)
                col = field if field != "*" else "*"
                sql = f"SELECT COALESCE({func}({col}), 0) FROM {table_name}"
                sql_params: list[Any] = []
                if where_field and where_value:
                    sql += f" WHERE {where_field} = ?"
                    sql_params.append(where_value)
                row = conn.execute(sql, sql_params).fetchone()
                conn.close()
                self._json_response({"value": row[0] if row else 0})
            except Exception as e:
                self._json_response({"value": 0, "error": str(e)})
        else:
            self._json_response({"error": "specify table"})

    def _handle_data_write(self) -> None:
        """Write data to a record's SQLite table."""
        parts = self.path.split("/")
        if len(parts) >= 4:
            table_name = parts[3]
            content_length = int(self.headers.get("Content-Length", 0))
            body = json.loads(self.rfile.read(content_length))
            try:
                import sqlite3
                db_path = os.path.join(os.path.dirname(os.path.abspath(_app_file)), "db.sqlite")
                conn = sqlite3.connect(db_path)
                conn.row_factory = sqlite3.Row
                columns = ", ".join(body.keys())
                placeholders = ", ".join("?" for _ in body)
                values = list(body.values())
                row = conn.execute(f"INSERT INTO {table_name} ({columns}) VALUES ({placeholders}) RETURNING *", values).fetchone()
                conn.commit()
                result = dict(row) if row else {"ok": True}
                conn.close()
                self._json_response(result)
            except Exception as e:
                self._json_response({"error": str(e)})
        else:
            self._json_response({"error": "specify table"})

    def _serve_static(self, filename: str, content_type: str) -> None:
        """Serve a file from the static/ directory (Vite build output)."""
        static_dir = Path(__file__).parent / "static"
        file_path = static_dir / filename
        # Fallback to legacy renderer.html if static build doesn't exist
        if not file_path.exists() and filename == "index.html":
            file_path = Path(__file__).parent / "renderer.html"
        if not file_path.exists():
            self.send_error(404)
            return
        body = file_path.read_bytes()
        self.send_response(200)
        self.send_header("Content-Type", f"{content_type}; charset=utf-8")
        self.send_header("Content-Length", str(len(body)))
        self.send_header("Cache-Control", "no-cache" if filename == "index.html" else "public, max-age=31536000, immutable")
        self.end_headers()
        self.wfile.write(body)

    def log_message(self, format: str, *args: Any) -> None:
        """Suppress default access logs."""
        pass


def serve(app_file: str, port: int = 8000) -> None:
    """Start the local dev server."""
    global _spec, _app_file, _last_mtime

    _app_file = app_file
    _last_mtime = os.path.getmtime(app_file)

    print(f"  zuzu dev server")
    print(f"  App: {app_file}")
    print(f"  Running initial build...")

    _spec = _run_app(app_file)
    if "error" in _spec:
        print(f"  Error: {_spec['error']}")
    else:
        pages = len(_spec.get("pages", []))
        records = len(_spec.get("app", {}).get("records", []))
        print(f"  OK — {records} records, {pages} pages")

    # Start file watcher
    watcher = threading.Thread(target=_watch_and_reload, daemon=True)
    watcher.start()

    print(f"")
    print(f"  → http://localhost:{port}")
    print(f"  Watching for changes... (Ctrl+C to stop)")
    print()

    server = HTTPServer(("", port), _Handler)
    try:
        server.serve_forever()
    except KeyboardInterrupt:
        print("\n  Stopped.")
        server.server_close()


# ── The renderer HTML ────────────────────────────────────────────────────

_RENDERER_HTML = """<!DOCTYPE html>
<html lang="en" class="dark">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>zuzu</title>
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
  darkMode: 'class',
  theme: {
    extend: {
      fontFamily: { mono: ['JetBrains Mono', 'monospace'], display: ['DM Sans', 'sans-serif'] },
    }
  }
}
</script>
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<style>
  body { font-family: 'DM Sans', sans-serif; background: #0a0a0a; color: #e5e5e5; }
  .mono { font-family: 'JetBrains Mono', monospace; }
</style>
</head>
<body class="min-h-screen">

<div id="app"></div>

<script>
let currentPage = null;
let spec = null;
let tableData = {};

async function loadSpec() {
  const res = await fetch('/api/spec');
  spec = await res.json();
  if (spec.error) {
    document.getElementById('app').innerHTML = `
      <div class="max-w-xl mx-auto mt-20 p-6 border border-red-500/30 rounded-xl bg-red-500/5">
        <h2 class="text-red-400 font-semibold mb-2">Error</h2>
        <pre class="text-sm text-red-300/70 whitespace-pre-wrap mono">${spec.error}</pre>
      </div>`;
    return;
  }
  // Load data for all records
  if (spec.app?.records) {
    for (const rec of spec.app.records) {
      try {
        const res = await fetch(`/api/data/${rec.table}`);
        tableData[rec.table] = await res.json();
      } catch { tableData[rec.table] = []; }
    }
  }
  if (!currentPage && spec.pages?.length) currentPage = spec.pages[0].name;
  render();
}

function render() {
  if (!spec) return;
  const app = spec.app || {};
  const pages = spec.pages || [];

  let html = '';

  // Header
  html += `<header class="border-b border-white/10 bg-black/50 backdrop-blur sticky top-0 z-50">
    <div class="max-w-5xl mx-auto px-4 h-12 flex items-center justify-between">
      <span class="font-semibold text-sm">${app.name || 'zuzu app'}</span>
      <div class="flex gap-1">
        ${pages.map(p => `
          <button onclick="switchPage('${p.name}')"
            class="px-3 py-1 text-xs rounded-md transition-colors ${p.name === currentPage
              ? 'bg-white/10 text-white' : 'text-white/40 hover:text-white/70'}"
          >${p.name}</button>
        `).join('')}
      </div>
    </div>
  </header>`;

  // Current page content
  const page = pages.find(p => p.name === currentPage);
  if (page) {
    html += '<main class="max-w-5xl mx-auto px-4 py-6">';
    html += renderPage(page);
    html += '</main>';
  }

  document.getElementById('app').innerHTML = html;
}

function switchPage(name) {
  currentPage = name;
  render();
}

function renderPage(page) {
  if (page.type === 'list') return renderListPage(page);
  if (page.type === 'detail') return renderDetailPage(page);
  if (page.type === 'form') return renderFormPage(page);
  if (page.type === 'custom') return renderCustomPage(page);
  return `<p class="text-white/40">Unknown page type: ${page.type}</p>`;
}

function renderListPage(page) {
  const data = tableData[page.record] || [];
  const config = page.config || {};
  const titleField = config.title || Object.keys(data[0] || {}).find(k => k !== 'id' && k !== 'created_at' && k !== 'updated_at') || 'id';
  const subtitleField = config.subtitle;
  const badgeField = config.badge;

  if (data.length === 0) {
    return `<div class="text-center py-16">
      <p class="text-white/30 text-sm">No records yet.</p>
      <button onclick="switchPage('Add ${page.record.charAt(0).toUpperCase() + page.record.slice(1)}')"
        class="mt-4 px-4 py-2 text-xs bg-white/10 rounded-lg hover:bg-white/20 transition-colors">
        + Add first record
      </button>
    </div>`;
  }

  let html = `<div class="space-y-2">`;
  for (const row of data) {
    const badge = badgeField && row[badgeField]
      ? `<span class="text-[10px] mono px-2 py-0.5 rounded-full bg-white/10 text-white/50">${row[badgeField]}</span>`
      : '';
    html += `
      <div class="flex items-center justify-between p-3 rounded-lg border border-white/5 hover:border-white/10 hover:bg-white/[0.02] transition-colors cursor-pointer"
        onclick="viewDetail('${page.record}', ${row.id})">
        <div>
          <p class="text-sm font-medium">${row[titleField] || '—'}</p>
          ${subtitleField ? `<p class="text-xs text-white/40 mt-0.5">${row[subtitleField] || ''}</p>` : ''}
        </div>
        <div class="flex items-center gap-2">
          ${badge}
          <span class="text-white/20 text-xs">→</span>
        </div>
      </div>`;
  }
  html += '</div>';
  return html;
}

function renderDetailPage(page) {
  return `<p class="text-white/40 text-sm">Select a record from the list to view details.</p>`;
}

function renderFormPage(page) {
  const rec = spec.app.records.find(r => r.table === page.record);
  if (!rec) return '<p class="text-white/40">Record not found</p>';

  let html = `<form onsubmit="submitForm(event, '${page.record}')" class="space-y-4 max-w-md">`;
  for (const [name, field] of Object.entries(rec.fields)) {
    if (field.type === 'choice' && field.choices) {
      html += `<div>
        <label class="block text-xs text-white/50 mb-1 mono">${name}</label>
        <select name="${name}" class="w-full bg-white/5 border border-white/10 rounded-lg px-3 py-2 text-sm focus:border-white/30 focus:outline-none">
          ${field.choices.map(c => `<option value="${c}">${c}</option>`).join('')}
        </select>
      </div>`;
    } else if (field.type === 'number') {
      html += `<div>
        <label class="block text-xs text-white/50 mb-1 mono">${name}</label>
        <input name="${name}" type="number" step="any" class="w-full bg-white/5 border border-white/10 rounded-lg px-3 py-2 text-sm focus:border-white/30 focus:outline-none">
      </div>`;
    } else if (field.type === 'boolean') {
      html += `<div class="flex items-center gap-2">
        <input name="${name}" type="checkbox" class="rounded bg-white/5 border-white/10">
        <label class="text-xs text-white/50 mono">${name}</label>
      </div>`;
    } else if (field.type === 'date') {
      html += `<div>
        <label class="block text-xs text-white/50 mb-1 mono">${name}</label>
        <input name="${name}" type="date" class="w-full bg-white/5 border border-white/10 rounded-lg px-3 py-2 text-sm focus:border-white/30 focus:outline-none">
      </div>`;
    } else {
      html += `<div>
        <label class="block text-xs text-white/50 mb-1 mono">${name}</label>
        <input name="${name}" type="text" class="w-full bg-white/5 border border-white/10 rounded-lg px-3 py-2 text-sm focus:border-white/30 focus:outline-none">
      </div>`;
    }
  }
  html += `<button type="submit" class="px-4 py-2 bg-white/10 hover:bg-white/20 rounded-lg text-sm font-medium transition-colors">
    Add Record
  </button>`;
  html += '</form>';
  return html;
}

function renderCustomPage(page) {
  if (!page.components) return '';
  return page.components.map(renderComponent).join('');
}

function renderComponent(comp) {
  switch (comp.type) {
    case 'title':
      return `<h2 class="text-xl font-semibold mb-4" style="font-family:'DM Sans'">${comp.text}</h2>`;

    case 'text':
      return `<p class="text-sm text-white/60 mb-3">${comp.text}</p>`;

    case 'metric': {
      const delta = comp.delta ? `<span class="text-xs ${String(comp.delta).startsWith('-') ? 'text-red-400' : 'text-green-400'}">${comp.delta}</span>` : '';
      return `<div class="inline-block mr-4 mb-4 p-4 rounded-xl border border-white/5 bg-white/[0.02] min-w-[140px]">
        <p class="text-2xl font-semibold mono">${comp.value}</p>
        <p class="text-xs text-white/40 mt-1">${comp.label} ${delta}</p>
      </div>`;
    }

    case 'chart':
      return `<div class="p-4 rounded-xl border border-white/5 bg-white/[0.02] mb-4">
        <p class="text-xs text-white/30 mono mb-2">${comp.kind || 'bar'} chart: ${comp.x} × ${comp.y}</p>
        <div class="h-32 flex items-end gap-1">
          ${renderChartBars(comp)}
        </div>
      </div>`;

    case 'list': {
      const data = tableData[comp.source] || comp.data || [];
      if (data.length === 0) return `<p class="text-white/30 text-sm py-4">${comp.empty || 'No items.'}</p>`;
      return `<div class="space-y-1 mb-4">${data.map(row => {
        const t = comp.title ? row[comp.title] : JSON.stringify(row);
        const s = comp.subtitle ? row[comp.subtitle] : '';
        return `<div class="flex items-center justify-between p-2 rounded-lg hover:bg-white/[0.03] transition-colors">
          <div><p class="text-sm">${t}</p>${s ? `<p class="text-xs text-white/40">${s}</p>` : ''}</div>
        </div>`;
      }).join('')}</div>`;
    }

    case 'grid':
      return `<div class="overflow-x-auto mb-4"><p class="text-xs text-white/30 mono">grid component (table view)</p></div>`;

    case 'section':
      return `<h3 class="text-xs text-white/40 uppercase tracking-wider mono mt-6 mb-3">${comp.title}</h3>`;

    case 'button':
      return `<button class="px-4 py-2 bg-white/10 hover:bg-white/20 rounded-lg text-sm font-medium transition-colors mb-2 mr-2">${comp.label}</button>`;

    case 'badge':
      return `<span class="inline-block px-2 py-0.5 text-xs rounded-full bg-white/10 text-white/50 mono mb-2">${comp.text}</span>`;

    case 'progress':
      const pct = Math.round((comp.value / comp.max) * 100);
      return `<div class="mb-4">
        <div class="h-2 rounded-full bg-white/5 overflow-hidden">
          <div class="h-full rounded-full bg-purple-500/70" style="width:${pct}%"></div>
        </div>
        <p class="text-xs text-white/30 mono mt-1">${comp.value}/${comp.max}</p>
      </div>`;

    default:
      return `<p class="text-xs text-white/20 mono">[${comp.type}]</p>`;
  }
}

function renderChartBars(comp) {
  const data = tableData[comp.source] || [];
  if (data.length === 0) return '<p class="text-xs text-white/20">No data</p>';

  // Group by x, aggregate y
  const groups = {};
  for (const row of data) {
    const key = row[comp.x] || 'other';
    if (!groups[key]) groups[key] = 0;
    if (comp.aggregate === 'count') groups[key]++;
    else if (comp.aggregate === 'sum') groups[key] += (parseFloat(row[comp.y]) || 0);
    else groups[key] += (parseFloat(row[comp.y]) || 0);
  }

  const max = Math.max(...Object.values(groups), 1);
  return Object.entries(groups).map(([key, val]) => {
    const h = Math.max(4, (val / max) * 100);
    return `<div class="flex-1 flex flex-col items-center gap-1">
      <div class="w-full bg-purple-500/50 rounded-t" style="height:${h}%"></div>
      <span class="text-[9px] text-white/30 mono truncate max-w-full">${key}</span>
    </div>`;
  }).join('');
}

async function submitForm(event, table) {
  event.preventDefault();
  const form = event.target;
  const data = Object.fromEntries(new FormData(form));
  // Convert number fields
  const rec = spec.app.records.find(r => r.table === table);
  if (rec) {
    for (const [k, field] of Object.entries(rec.fields)) {
      if (field.type === 'number' && data[k]) data[k] = parseFloat(data[k]);
      if (field.type === 'boolean') data[k] = data[k] === 'on' ? 1 : 0;
    }
  }
  await fetch(`/api/data/${table}`, {
    method: 'POST',
    headers: {'Content-Type': 'application/json'},
    body: JSON.stringify(data),
  });
  form.reset();
  // Reload data
  const res = await fetch(`/api/data/${table}`);
  tableData[table] = await res.json();
  render();
}

function viewDetail(table, id) {
  // For now, just show an alert with the data
  const row = (tableData[table] || []).find(r => r.id === id);
  if (row) alert(JSON.stringify(row, null, 2));
}

// Auto-refresh every 2s
setInterval(async () => {
  const res = await fetch('/api/spec');
  const newSpec = await res.json();
  if (JSON.stringify(newSpec) !== JSON.stringify(spec)) {
    spec = newSpec;
    // Reload data
    if (spec.app?.records) {
      for (const rec of spec.app.records) {
        try {
          const r = await fetch(`/api/data/${rec.table}`);
          tableData[rec.table] = await r.json();
        } catch {}
      }
    }
    render();
  }
}, 2000);

loadSpec();
</script>
</body>
</html>"""
