/* ============ PRO STUDIO — flagship code generator ============ */
/* global React, PrzelomAudio, JSZip, Prism */

const { useState, useEffect, useRef, useMemo } = React;

// ============================================================
// Streaming SSE → onDelta callback per token + full message at end
// ============================================================
async function streamStepfun({
  messages,
  max_tokens,
  signal,
  onDelta,
  onReasoning,
  quotaHeaders,
}) {
  const headers = {
    "Content-Type": "application/json",
    ...(quotaHeaders || {}),
  };
  const res = await fetch("/api/ai/stream", {
    method: "POST",
    headers,
    body: JSON.stringify({ messages }),
    signal,
  });
  if (res.status === 402) {
    let info = null;
    try {
      info = await res.json();
    } catch {}
    const err = new Error(info?.message || "Wyczerpany darmowy limit.");
    err.code = "QUOTA_EXCEEDED";
    err.reason = info?.reason;
    err.remaining = info?.remaining;
    throw err;
  }
  if (res.status === 451) {
    let info = null;
    try {
      info = await res.json();
    } catch {}
    const err = new Error(
      info?.message ||
        "Model odmówił odpowiedzi — filtr treści zablokował zapytanie. Przeformułuj pytanie lub usuń wrażliwe fragmenty.",
    );
    err.code = "CONTENT_BLOCKED";
    throw err;
  }
  if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);

  const reader = res.body.getReader();
  const decoder = new TextDecoder();
  let buffer = "";
  let fullContent = "";
  let fullReasoning = "";
  let finishReason = null;
  let usage = null;

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    buffer += decoder.decode(value, { stream: true });
    const lines = buffer.split("\n");
    buffer = lines.pop() || "";
    for (const line of lines) {
      if (!line.startsWith("data:")) continue;
      const json = line.slice(5).trim();
      if (!json || json === "[DONE]") continue;
      try {
        const obj = JSON.parse(json);
        const choice = obj.choices?.[0] || {};
        const delta = choice.delta || {};
        if (delta.content) {
          fullContent += delta.content;
          onDelta?.(delta.content, fullContent);
        }
        if (delta.reasoning) {
          fullReasoning += delta.reasoning;
          onReasoning?.(delta.reasoning, fullReasoning);
        }
        if (choice.finish_reason) finishReason = choice.finish_reason;
        if (obj.usage) usage = obj.usage;
      } catch {}
    }
  }
  return {
    content: fullContent,
    reasoning: fullReasoning,
    finishReason,
    usage,
  };
}

// ============================================================
// Freemium quota — helper do budowania headerów per operacja
// ============================================================
function buildQuotaHeaders(opType, opId) {
  const h = {
    "X-Op-Id": opId,
    "X-Op-Type": opType,
  };
  if (window.VISITOR_TOKEN) h["X-Visitor-Token"] = window.VISITOR_TOKEN;
  if (window.VISITOR_ID) h["X-Visitor-Id"] = window.VISITOR_ID;
  if (window.PRO_TOKEN) h["X-Pro-Token"] = window.PRO_TOKEN;
  if (window.AUTH?.token) h["X-Auth-Token"] = window.AUTH.token;
  return h;
}

function newOpId() {
  if (window.crypto?.randomUUID) return window.crypto.randomUUID();
  return "op_" + Math.random().toString(36).slice(2) + Date.now().toString(36);
}

async function fetchQuota() {
  if (window.VISITOR_READY) await window.VISITOR_READY;
  const headers = {};
  if (window.VISITOR_TOKEN) headers["X-Visitor-Token"] = window.VISITOR_TOKEN;
  if (window.VISITOR_ID) headers["X-Visitor-Id"] = window.VISITOR_ID;
  if (window.PRO_TOKEN) headers["X-Pro-Token"] = window.PRO_TOKEN;
  if (window.AUTH?.token) headers["X-Auth-Token"] = window.AUTH.token;
  // Timeout 8s — quota endpoint nie powinien wisieć dłużej, jeśli wisi
  // (Redis cold start, network blip), wolimy odpalić generację bez quoty
  // niż zablokować UI na ekranie ładowania.
  const ctrl = new AbortController();
  const timeoutId = setTimeout(() => ctrl.abort(), 8000);
  try {
    const res = await fetch("/api/quota", { headers, signal: ctrl.signal });
    if (!res.ok) {
      throw new Error(`quota fetch failed (HTTP ${res.status})`);
    }
    return await res.json();
  } finally {
    clearTimeout(timeoutId);
  }
}

// ============================================================
// Robusterowy JSON extractor — wycina JSON z dowolnego output (markdown, prefix, etc)
// ============================================================
function extractJSON(text) {
  if (!text || typeof text !== "string") return null;
  // 1) Strip markdown code blocks
  let cleaned = text
    .replace(/^```(?:json|js|javascript)?\s*/gim, "")
    .replace(/```\s*$/gim, "")
    .trim();
  // 2) Try direct parse
  try {
    return JSON.parse(cleaned);
  } catch {}
  // 3) Find first { i ostatni } (greedy match outermost JSON)
  const firstBrace = cleaned.indexOf("{");
  const lastBrace = cleaned.lastIndexOf("}");
  if (firstBrace === -1 || lastBrace === -1 || lastBrace <= firstBrace)
    return null;
  const slice = cleaned.slice(firstBrace, lastBrace + 1);
  try {
    return JSON.parse(slice);
  } catch {}
  // 4) Try fixing common issues: trailing commas, single quotes, line comments
  let fixed = slice
    // Strip // line comments (model czasem dorzuca)
    .replace(/\/\/[^\n\r]*/g, "")
    // Strip /* block comments */
    .replace(/\/\*[\s\S]*?\*\//g, "")
    // Trailing comma przed } lub ]
    .replace(/,(\s*[}\]])/g, "$1")
    // Single-quoted keys → double-quoted
    .replace(/([{,]\s*)'([^']*?)'(\s*:)/g, '$1"$2"$3')
    // Single-quoted values — escape any inner double-quotes przed zamianą
    .replace(/(:\s*)'([^']*?)'/g, (_, pre, val) => {
      return pre + '"' + val.replace(/"/g, '\\"') + '"';
    });
  try {
    return JSON.parse(fixed);
  } catch {}
  // 5) Last resort: spróbuj wyciąć tylko część do ostatniego poprawnego } -
  // model mógł obciąć w połowie generacji, ale początek może być valid.
  for (let cut = lastBrace; cut > firstBrace + 10; cut--) {
    if (cleaned[cut] !== "}") continue;
    try {
      return JSON.parse(cleaned.slice(firstBrace, cut + 1));
    } catch {}
  }
  return null;
}

// ============================================================
// CLIENT-SIDE AUTOFIX — naprawia mechaniczne błędy bez LLM call
// 80% typowych pomyłek modelu naprawia się tutaj instant
// ============================================================
function autoFixProject(project) {
  if (!project?.files) return project;
  const fixes = [];
  const files = project.files.map((f) => {
    let content = f.content || "";
    const path = f.path;

    // Wszystkie typy: wykryj nieprawidłowy named import dla Next.js default-exported modules
    const NEXT_DEFAULT_IMPORTS = [
      "next/link",
      "next/image",
      "next/dynamic",
      "next/script",
      "next/head",
      "next/router",
    ];
    NEXT_DEFAULT_IMPORTS.forEach((mod) => {
      const name = mod.split("/").pop();
      const cap = name[0].toUpperCase() + name.slice(1);
      // import { Link } from 'next/link' → import Link from 'next/link'
      const re = new RegExp(
        `import\\s*\\{\\s*${cap}\\s*\\}\\s*from\\s*['"]${mod}['"]`,
        "g",
      );
      if (re.test(content)) {
        content = content.replace(re, `import ${cap} from '${mod}'`);
        fixes.push(`${path}: \`{ ${cap} }\` → default import`);
      }
    });

    // Next.js: 'use client' w layout.tsx jest BŁĘDEM (layout musi być Server Component)
    if (
      (path === "app/layout.tsx" ||
        path === "app/layout.js" ||
        path === "app/layout.jsx") &&
      /^['"]use client['"];?\s*$/m.test(content)
    ) {
      content = content.replace(/^['"]use client['"];?\s*\n/m, "");
      fixes.push(
        `${path}: usunięto 'use client' (layout musi być Server Component)`,
      );
    }

    // React standalone (Babel): wykryj broken imports
    if (
      project.type === "react" &&
      (path.endsWith(".jsx") || path.endsWith(".js"))
    ) {
      // import React from 'react' → const {} = React;
      if (/^import\s+React/m.test(content)) {
        content = content.replace(
          /^import\s+React.*?from\s*['"]react['"];?\s*\n/m,
          "// React loaded globally via UMD\n",
        );
        fixes.push(`${path}: usunięto \`import React\` (loaded globally)`);
      }
      // import { useState } from 'react' → const {useState} = React;
      const namedReact =
        /^import\s*\{\s*([^}]+)\s*\}\s*from\s*['"]react['"];?\s*\n/m;
      const m = content.match(namedReact);
      if (m) {
        content = content.replace(
          namedReact,
          `const { ${m[1].trim()} } = React;\n`,
        );
        fixes.push(`${path}: \`import { ${m[1].trim()} }\` → const = React`);
      }
    }

    return { ...f, content };
  });

  // package.json — sprawdź czy zawiera używane biblioteki
  if (project.type === "nextjs") {
    const pkgFile = files.find((f) => f.path === "package.json");
    if (pkgFile) {
      try {
        const pkg = JSON.parse(pkgFile.content);
        pkg.dependencies = pkg.dependencies || {};
        const deps = pkg.dependencies;
        const allCode = files.map((f) => f.content).join("\n");
        const REQUIRED = {
          "lucide-react": /from\s*['"]lucide-react['"]/,
          "@supabase/supabase-js": /from\s*['"]@supabase\/supabase-js['"]/,
          "@supabase/ssr": /from\s*['"]@supabase\/ssr['"]/,
          stripe: /from\s*['"]stripe['"]/,
          "tailwindcss-animate": /tailwindcss-animate/,
          clsx: /from\s*['"]clsx['"]/,
          "tailwind-merge": /from\s*['"]tailwind-merge['"]/,
          zod: /from\s*['"]zod['"]/,
          "react-hot-toast": /from\s*['"]react-hot-toast['"]/,
          "framer-motion": /from\s*['"]framer-motion['"]/,
          "react-router-dom": /from\s*['"]react-router-dom['"]/,
          "@tanstack/react-query": /from\s*['"]@tanstack\/react-query['"]/,
          "react-icons": /from\s*['"]react-icons\//,
          recharts: /from\s*['"]recharts['"]/,
          "react-hook-form": /from\s*['"]react-hook-form['"]/,
          "@hookform/resolvers": /from\s*['"]@hookform\/resolvers/,
          "date-fns": /from\s*['"]date-fns['"]/,
          axios: /from\s*['"]axios['"]/,
        };
        const VERSIONS = {
          "lucide-react": "^0.474.0",
          "@supabase/supabase-js": "^2.45.0",
          "@supabase/ssr": "^0.5.0",
          stripe: "^17.7.0",
          "tailwindcss-animate": "^1.0.7",
          clsx: "^2.1.0",
          "tailwind-merge": "^2.5.0",
          zod: "^3.23.0",
          "react-hot-toast": "^2.4.0",
          "framer-motion": "^11.11.0",
          "react-router-dom": "^6.28.0",
          "@tanstack/react-query": "^5.59.0",
          "react-icons": "^5.4.0",
          recharts: "^2.13.0",
          "react-hook-form": "^7.53.0",
          "@hookform/resolvers": "^3.9.0",
          "date-fns": "^4.1.0",
          axios: "^1.7.0",
        };
        let pkgChanged = false;
        for (const [lib, re] of Object.entries(REQUIRED)) {
          if (re.test(allCode) && !deps[lib] && !pkg.devDependencies?.[lib]) {
            deps[lib] = VERSIONS[lib];
            fixes.push(
              `package.json: dodano brakującą zależność "${lib}": "${VERSIONS[lib]}"`,
            );
            pkgChanged = true;
          }
        }
        // Wymuś poprawne wersje krytycznych pakietów (model często daje stare/zmyślone wersje)
        if (deps["lucide-react"] && !deps["lucide-react"].includes("0.4")) {
          deps["lucide-react"] = "^0.474.0";
          fixes.push(
            `package.json: poprawiono wersję lucide-react na ^0.474.0`,
          );
          pkgChanged = true;
        }
        if (
          deps["stripe"] &&
          /\^[0-9]\./.test(deps["stripe"]) &&
          !deps["stripe"].includes("17")
        ) {
          deps["stripe"] = "^17.7.0";
          fixes.push(`package.json: poprawiono wersję stripe na ^17.7.0`);
          pkgChanged = true;
        }
        // auth-helpers-nextjs jest deprecated → @supabase/ssr
        if (deps["@supabase/auth-helpers-nextjs"]) {
          delete deps["@supabase/auth-helpers-nextjs"];
          deps["@supabase/ssr"] = "^0.5.0";
          fixes.push(
            `package.json: zamiana deprecated @supabase/auth-helpers-nextjs → @supabase/ssr`,
          );
          pkgChanged = true;
        }
        if (pkgChanged) {
          pkgFile.content = JSON.stringify(pkg, null, 2);
        }
      } catch {}
    }

    // Czy istnieje postcss.config.js gdy używamy Tailwind?
    const hasTailwind = files.some(
      (f) => /tailwind/i.test(f.content) || f.path.includes("tailwind"),
    );
    const hasPostcss = files.some(
      (f) => f.path === "postcss.config.js" || f.path === "postcss.config.mjs",
    );
    if (hasTailwind && !hasPostcss) {
      files.push({
        path: "postcss.config.js",
        content: `module.exports = {\n  plugins: {\n    tailwindcss: {},\n    autoprefixer: {},\n  },\n};\n`,
      });
      fixes.push(`dodano brakujący postcss.config.js`);
    }

    // .env.example
    const hasEnv = files.some(
      (f) => f.path === ".env.example" || f.path === ".env.local.example",
    );
    const usesEnv = files.some((f) => /process\.env\./.test(f.content));
    if (usesEnv && !hasEnv) {
      const envVars = new Set();
      files.forEach((f) => {
        const matches = f.content.matchAll(/process\.env\.([A-Z_]+)/g);
        for (const m of matches) envVars.add(m[1]);
      });
      if (envVars.size > 0) {
        const envContent =
          [...envVars]
            .sort()
            .map((v) => `${v}=`)
            .join("\n") + "\n";
        files.push({ path: ".env.example", content: envContent });
        fixes.push(`dodano .env.example z ${envVars.size} zmiennymi`);
      }
    }
  }

  return { ...project, files, autofixes: fixes };
}

// ============================================================
// Architect prompt — szybki call, tworzy plan projektu
// ============================================================
const ARCHITECT_PROMPT = `Jesteś szybkim architektem. NIE myśl długo. Zwróć od razu JSON.

ZASADY:
- Zacznij odpowiedź od { i zakończ na }. Bez markdown, bez wstępu, bez tekstu po JSON.
- Wybierz "html" dla narzędzi/kalkulatorów (preview działa). "react" dla SPA. "nextjs" TYLKO gdy user wprost mówi SaaS.
- Maksymalnie 8-14 plików.

FORMAT:
{"name":"kebab-case","title":"Nazwa","type":"html","stack":["Tech"],"description":"Krótko","files_planned":[{"path":"index.html","purpose":"entry"}]}`;

// ============================================================
// Builder prompt — generuje pliki w streaming JSON format
// ============================================================
function buildBuilderPrompt(plan) {
  const typeRules = {
    html: `TYP HTML — JEDEN PLIK INDEX.HTML
- Wszystko w jednym index.html (CSS w <style>, JS w <script>) — działa po otwarciu w przeglądarce
- Brak buildera, brak npm
- Możesz dorzucić osobne style.css / app.js, ale wtedy linkuj je względnie ./style.css
- Brak external CDN dla bibliotek POZA: nieaktualne, działa zawsze: vanilla JS + CSS`,

    react: `TYP REACT — STANDALONE Z BABEL W IFRAME
- index.html ładuje React UMD + Babel z unpkg (NIE jsdelivr — w naszym preview działa unpkg)
- WYMAGANE w index.html w <head>:
  <script src="https://unpkg.com/react@18.3.1/umd/react.development.js"></script>
  <script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.development.js"></script>
  <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
- app.jsx jako <script type="text/babel" src="./app.jsx"> — ALE LEPIEJ: wstaw kod app jako <script type="text/babel"> inline w index.html (działa bez serwera)
- NIE używaj importów ES modules (import React from 'react') — to NIE działa w Babel standalone. Używaj globalnego React. (np. const {useState} = React;)
- Brak npm, brak buildera
- Komponenty pisz jako function ComponentName(props) { return <div>...</div>; }`,

    nextjs: `TYP NEXT.JS — APP ROUTER (Next.js 16+)
KRYTYCZNE ZASADY (zapobiegają build errorom):
1. **layout.tsx MUSI być Server Component** — NIGDY nie dodawaj 'use client' w layout. State (np. cart) wynieś do osobnego <Providers> client component który layout opakowuje.
2. **import Link from 'next/link'** (DEFAULT export) — NIGDY \`import { Link } from 'next/link'\` (to BŁĄD typu).
3. **import default vs named** dla Next/React:
   - Default: Link, Image, dynamic, NextRequest (z jednej z nawigacji), Inter
   - Named: usePathname, useRouter, redirect, notFound, useState, useEffect, NextResponse
4. **Supabase init: LAZY!** NIE rób \`export const supabase = createClient(URL, KEY)\` na top-level pliku — to crashuje SSG gdy env vars są pustE. Zamiast tego:
   \`\`\`ts
   export function getSupabaseClient() {
     return createClient(process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!);
   }
   \`\`\`
   I używaj \`const supabase = getSupabaseClient()\` wewnątrz funkcji/komponentów.
5. **package.json** MUSI zawierać KAŻDĄ używaną bibliotekę:
   - Jeśli importujesz lucide-react: dodaj "lucide-react": "^0.474.0"
   - Jeśli używasz tailwindcss-animate w tailwind.config: dodaj plugin do dependencies
   - Stripe: "stripe": "^17.7.0" (latest 2026)
   - Supabase: "@supabase/supabase-js": "^2.45.0", "@supabase/ssr": "^0.5.0" (NIE auth-helpers, te są deprecated)
   - Next.js: "next": "^16.0.0", "react": "^19.0.0", "react-dom": "^19.0.0"
6. **postcss.config.js** wymagany jeśli używasz Tailwind:
   \`\`\`js
   module.exports = { plugins: { tailwindcss: {}, autoprefixer: {} } };
   \`\`\`
7. **tailwind.config** — content paths zawiera ./app, ./components, ./pages
8. **globals.css** w app/ z @tailwind base; @tailwind components; @tailwind utilities;
9. **Routes z dynamic rendering**: jeśli strona używa cookies/headers/runtime data, dodaj \`export const dynamic = 'force-dynamic';\` żeby nie crashowała SSG
10. **.env.example** zawsze z placeholder values
11. **README.md** z setup instrukcjami: \`npm install\` → setup env → \`npm run dev\``,
  };

  return `Jesteś senior developerem. NIE myśl długo. Pisz od razu produkcyjny, działający kod.

PLAN: ${JSON.stringify(plan)}

ZWRÓĆ WYŁĄCZNIE VALID JSON. Zacznij od { skończ na }. Bez markdown bloku. Bez wstępu. Bez komentarzy poza JSON.

FORMAT:
{"files":[{"path":"app/page.tsx","content":"export default function Page() { ... }"}]}

ESCAPING (KRYTYCZNE):
- W content używaj \\n dla newline, \\" dla cudzysłowów, \\\\ dla backslash
- Sprawdź że JSON jest VALID — niezamknięte stringi są częstym błędem

${typeRules[plan.type] || typeRules.html}

OGÓLNE ZASADY:
- Pełen działający kod, NIE pseudokod, NIE "// reszta"
- Komentarze tylko gdzie naprawdę ważne
- Nowoczesne API (ES2022+, fetch zamiast XHR)
- Estetyka: nowoczesna, dark mode preferowany, responsywna (mobile-first)
- Dostępność: semantyczne HTML, alt na obrazkach, aria-label gdzie trzeba
- Validacja inputów po stronie klienta`;
}

// ============================================================
// REVIEWER prompt — ostatni pass naprawiający typowe błędy
// ============================================================
function buildReviewerPrompt(project) {
  return `Jesteś senior reviewerem kodu. Dostajesz wygenerowany projekt. Twoje zadanie: znaleźć i NAPRAWIĆ typowe błędy. Zwróć WYŁĄCZNIE valid JSON z tym samym formatem co input — pełna lista plików (z poprawkami gdzie potrzebne).

PROJEKT TYPU "${project.type}" — sprawdź TE BŁĘDY:

${
  project.type === "nextjs"
    ? `1. **'use client' w layout.tsx** → USUŃ. Layout musi być Server Component. Jeśli layout potrzebuje state/effects, stwórz osobny components/Providers.tsx z 'use client' i opakuj nim children w layoucie.
2. **\`import { Link } from 'next/link'\`** → ZAMIEŃ na \`import Link from 'next/link'\` (default export!)
3. **\`import { Image } from 'next/image'\`** → ZAMIEŃ na \`import Image from 'next/image'\`
4. **Top-level supabase init** (export const supabase = createClient(...)) crashuje SSG → ZAMIEŃ na lazy: \`export function getSupabase() { return createClient(...) }\`
5. **package.json**: czy zawiera WSZYSTKIE biblioteki używane w importach? Wykryj brakujące i dodaj. Wersje: lucide-react ^0.474.0, stripe ^17.7.0, @supabase/supabase-js ^2.45.0, @supabase/ssr ^0.5.0
6. **tailwindcss-animate**: jeśli w tailwind.config jest \`require("tailwindcss-animate")\` ale nie ma w package.json — dodaj "tailwindcss-animate": "^1.0.7"
7. **postcss.config.js** jeśli używamy Tailwind, sprawdz że istnieje
8. **dynamic = 'force-dynamic'** dla stron używających cookies/runtime data, żeby nie crashowały build SSG
9. **.env.example** sprawdź czy istnieje z placeholder values
`
    : project.type === "react"
      ? `1. **import React from 'react'** → ZAMIEŃ na const {useState, useEffect} = React; (Babel standalone NIE wspiera ES modules)
2. **<script type="module">** → ZAMIEŃ na <script type="text/babel">
3. **export default** w app.jsx → USUŃ (w Babel standalone nie ma modules)
4. **import Component from './x'** w app.jsx → USUŃ, daj wszystko w jednym pliku app.jsx
5. **CDN URLs**: tylko unpkg.com (nasz preview działa z unpkg). NIE używaj jsdelivr lub cdnjs.
`
      : `1. Inline CSS/JS w jednym index.html jeśli to proste
2. Sprawdz że <!DOCTYPE html> jest na początku
3. <meta charset="utf-8"> i <meta name="viewport" content="width=device-width,initial-scale=1">
4. Brak external CDN dla podstawowych funkcji (vanilla JS wystarczy)
`
}

WSZYSTKIE TYPY: sprawdz że JSON jest VALID — wszystkie strings poprawnie escaped, brak orphan trailing commas.

INPUT (projekt do review):
${JSON.stringify({ type: project.type, files: project.files }).slice(0, 30000)}

ZWRÓĆ JSON: {"files":[...]} z poprawionymi plikami. Zachowaj wszystkie pliki nawet jeśli nie wymagały zmian. Jeśli nic nie wymagało zmian, zwróć identyczny JSON.`;
}

// ============================================================
// MODIFY prompt — modyfikacja istniejącego projektu (delta change)
// ============================================================
function buildModifyPrompt(project, userRequest, history = []) {
  // StepFun step-3.5-flash-2603 ma 256k token context — bezpiecznie zmieścimy
  // ~200k chars input. Truncacja tylko dla ekstremalnie dużych projektów.
  const MAX_PAYLOAD = 200000;
  let filesPayload = project.files;
  const total = JSON.stringify(filesPayload).length;
  let truncated = false;
  if (total > MAX_PAYLOAD) {
    // Sortuj: najpierw entry/main pliki (priorytet), potem code, na końcu config/styling
    filesPayload = [...filesPayload].sort((a, b) => {
      const score = (f) =>
        /^(app|src|pages)\/(layout|page|index|main|App)\.(tsx|jsx|js|ts|html)$/.test(
          f.path,
        )
          ? 0
          : /\.(jsx|tsx|js|ts)$/.test(f.path)
            ? 1
            : /\.(css|html)$/.test(f.path)
              ? 2
              : 3;
      return score(a) - score(b);
    });
    while (
      filesPayload.length > 1 &&
      JSON.stringify(filesPayload).length > MAX_PAYLOAD
    ) {
      filesPayload = filesPayload.slice(0, -1);
      truncated = true;
    }
  }

  // Skondensowana historia poprzednich modyfikacji w tej sesji.
  // Daje modelowi continuity — wie co user już prosił, czego nie powtarzać,
  // i może referencjonować poprzednie decyzje ("wróć do poprzedniego layoutu").
  const historyBlock = history.length
    ? `\nHISTORIA TEJ SESJI (ostatnie ${history.length} ${history.length === 1 ? "modyfikacja" : "modyfikacji"} — od najstarszej):\n${history
        .map(
          (h, i) =>
            `${i + 1}. User: "${h.request}"\n   → ${h.changes || "(brak notatek)"}`,
        )
        .join("\n")}\n`
    : "";

  const truncationNote = truncated
    ? `\n⚠ UWAGA: projekt ma ${project.files.length} plików — w kontekście masz tylko ${filesPayload.length} najważniejszych. Jeśli user prosi o zmianę w pliku którego NIE WIDZISZ, poproś o uściślenie (zwróć {"files":[],"changes":["Nie widzę pliku X — proszę o pełną nazwę lub kontekst"]}).\n`
    : "";

  return `Jesteś senior developerem. Modyfikujesz ISTNIEJĄCY projekt na życzenie użytkownika.

PROJEKT TYPU: ${project.type}
TYTUŁ: ${project.title}
STACK: ${(project.stack || []).join(", ")}
${truncationNote}${historyBlock}
AKTUALNE PLIKI (${filesPayload.length}/${project.files.length}):
${JSON.stringify({ files: filesPayload }, null, 0)}

NOWE ŻYCZENIE UŻYTKOWNIKA:
"${userRequest}"

ZASADY:
1. Zwróć WYŁĄCZNIE valid JSON. Zacznij od { skończ na }. Bez markdown bloku.
2. FORMAT: {"files":[{"path":"app/page.tsx","content":"..."}], "changes":["opisz co zmieniłeś"]}
3. Zwracaj TYLKO PLIKI KTÓRE SIĘ ZMIENIŁY (lub nowe). Niezmienione pliki POMIJAJ — NIE zwracaj ich.
4. Jeśli plik trzeba USUNĄĆ: zwróć {"path":"...", "content": null, "deleted": true}
5. Każdy zwrócony plik musi mieć PEŁNĄ treść (nie diff, nie patch — cały content nowej wersji).
6. Pole "changes" — krótki opis 1-3 zdania co zmieniłeś (po polsku).
7. ESCAPING: \\n dla newline, \\" dla cudzysłowu, \\\\ dla backslash.
8. KONTEKST: masz pełną historię poprzednich modyfikacji. Jeśli user mówi "cofnij" / "wróć" / "to było lepiej zanim" — referuj do nich konkretnie i przywróć stan sprzed danej modyfikacji.
9. SPÓJNOŚĆ: jeśli zmieniasz import/funkcję używaną w wielu plikach, zaktualizuj WSZYSTKIE miejsca które są w "AKTUALNE PLIKI" — nie zostawiaj broken referencji.
10. JEŚLI ŻYCZENIE NIEJASNE: zwróć {"files":[],"changes":["Pytanie: <konkretne pytanie>"]} zamiast zgadywać.

ZASADY KODU:
${
  project.type === "nextjs"
    ? `- Layout zostaje Server Component (NIE dodawaj 'use client' do layout.tsx)
- import default dla Link, Image, dynamic z next/*
- Lazy supabase init (NIE top-level createClient)
- Wersje pakietów: lucide-react ^0.474.0, stripe ^17.7.0, @supabase/ssr ^0.5.0`
    : project.type === "react"
      ? `- React UMD globalny (const {useState} = React;)
- <script type="text/babel">
- Bez import ES modules`
      : `- Pełen samodzielny HTML/CSS/JS`
}

NIE myśl długo, zacznij od { i pisz.`;
}

// Merguje pliki: zmienione/nowe z modify wynika ZASTĘPUJĄ stare; pliki z deleted:true są usuwane
// Sanity check ścieżki: bez traversal, bez absolute, rozsądna długość, allowed znaki.
// Zmiana renderowana w iframe przez LivePreview, więc invalid ścieżki nigdy nie szkodzą
// na hoście — ale chronią przed mylącymi entries w drzewie i przypadkowym nadpisaniem
// "../../package.json" gdy user pobierze ZIP.
function isValidProjectPath(path) {
  if (typeof path !== "string" || path.length === 0 || path.length > 200)
    return false;
  if (path.startsWith("/") || /^[a-zA-Z]:/.test(path)) return false;
  const segments = path.split("/");
  for (const seg of segments) {
    if (seg === ".." || seg === "." || seg === "") return false;
    if (!/^[a-zA-Z0-9._\-@[\]() ]+$/.test(seg)) return false;
  }
  return true;
}

function mergeProjectFiles(currentFiles, modifications) {
  const map = new Map(currentFiles.map((f) => [f.path, f]));
  for (const mod of modifications) {
    if (!mod?.path || !isValidProjectPath(mod.path)) {
      console.warn("[PRO Studio] skipping invalid path:", mod?.path);
      continue;
    }
    if (mod.deleted) {
      map.delete(mod.path);
    } else {
      map.set(mod.path, { path: mod.path, content: String(mod.content ?? "") });
    }
  }
  return [...map.values()];
}

// ============================================================
// Parse partial JSON streaming — wykrywa zakończone pliki na bieżąco
// ============================================================
function extractFilesFromPartialJSON(text) {
  if (!text) return [];
  // Try full parse first (z extractJSON dla resilience)
  const fullObj = extractJSON(text);
  if (fullObj?.files && Array.isArray(fullObj.files)) {
    // Dedupe by path
    const seen = new Set();
    return fullObj.files.filter((f) => {
      if (!f?.path || seen.has(f.path)) return false;
      seen.add(f.path);
      return true;
    });
  }
  // Partial: szukamy wzorca "path": "...", "content": "..."
  const files = [];
  const seen = new Set();
  const re =
    /"path"\s*:\s*"([^"]+)"\s*,\s*"content"\s*:\s*"((?:[^"\\]|\\.)*?)"\s*}/g;
  let m;
  while ((m = re.exec(text)) !== null) {
    try {
      const path = m[1];
      if (seen.has(path)) continue;
      seen.add(path);
      const content = JSON.parse('"' + m[2] + '"'); // unescape
      files.push({ path, content });
    } catch {}
  }
  return files;
}

// ============================================================
// File icons
// ============================================================
function fileIcon(path) {
  const ext = path.split(".").pop().toLowerCase();
  if (["html", "htm"].includes(ext)) return "🌐";
  if (["css", "scss"].includes(ext)) return "🎨";
  if (["js", "mjs", "cjs", "jsx"].includes(ext)) return "📜";
  if (["ts", "tsx"].includes(ext)) return "🔷";
  if (["json"].includes(ext)) return "📋";
  if (["md", "txt"].includes(ext)) return "📄";
  if (["py"].includes(ext)) return "🐍";
  if (["env", "gitignore"].includes(ext) || path.includes(".env")) return "🔐";
  return "📄";
}
function langForFile(path) {
  const ext = path.split(".").pop().toLowerCase();
  const map = {
    js: "javascript",
    jsx: "jsx",
    ts: "typescript",
    tsx: "tsx",
    html: "markup",
    css: "css",
    json: "json",
    md: "markdown",
    sh: "bash",
  };
  return map[ext] || "javascript";
}

// ============================================================
// Build preview HTML — standalone (independent of DOM).
// Used by LivePreview (memoized) AND openPreviewFullscreen (direct call).
// Returns null for nextjs (no live preview) or empty/missing-index projects.
// ============================================================
function buildPreviewHTML(project) {
  if (!project || !project.files || project.files.length === 0) return null;
  if (project.type === "nextjs") return null;

  const files = project.files;
  const findFile = (matcher) => files.find((f) => matcher(f.path));

  // CSP wstrzykiwany do <head> każdego preview — drugi pas bezpieczeństwa.
  // Iframe ma już sandbox bez allow-same-origin (no parent access),
  // CSP dodatkowo blokuje fetch/websocket/img tracking exfiltration.
  const CSP_META = `<meta http-equiv="Content-Security-Policy" content="default-src 'self' 'unsafe-inline' 'unsafe-eval' data: blob: https://unpkg.com https://cdn.jsdelivr.net https://cdnjs.cloudflare.com https://fonts.googleapis.com https://fonts.gstatic.com; connect-src 'none'; frame-ancestors 'self';">`;
  const injectCSP = (html) => {
    if (!html) return html;
    if (html.includes('http-equiv="Content-Security-Policy"')) return html;
    if (/<head[^>]*>/i.test(html)) {
      return html.replace(/<head[^>]*>/i, (m) => m + "\n" + CSP_META);
    }
    return CSP_META + "\n" + html;
  };

  if (project.type === "html") {
    const indexHtml = findFile(
      (p) => p === "index.html" || p.endsWith("/index.html"),
    );
    if (!indexHtml) return null;
    let html = indexHtml.content;

    // Inline external CSS
    const cssFiles = files.filter((f) => f.path.endsWith(".css"));
    cssFiles.forEach((cssFile) => {
      const tag = `<link[^>]*href=["']${cssFile.path.replace(/^\.?\//, "")}["'][^>]*>`;
      const re = new RegExp(tag, "g");
      html = html.replace(re, `<style>${cssFile.content}</style>`);
    });
    // Inline external JS
    const jsFiles = files.filter((f) => /\.(js|mjs)$/.test(f.path));
    jsFiles.forEach((jsFile) => {
      const tag = `<script[^>]*src=["']${jsFile.path.replace(/^\.?\//, "")}["'][^>]*></script>`;
      const re = new RegExp(tag, "g");
      html = html.replace(re, `<script>${jsFile.content}</script>`);
    });
    return injectCSP(html);
  }

  if (project.type === "react") {
    const indexHtml = findFile(
      (p) => p === "index.html" || p.endsWith("/index.html"),
    );
    const cssFiles = files.filter((f) => f.path.endsWith(".css"));
    const jsxFiles = files.filter((f) => /\.(jsx|js)$/.test(f.path));

    const cssBlock = cssFiles
      .map((f) => `<style>${f.content}</style>`)
      .join("\n");
    const jsxBlock = jsxFiles.map((f) => f.content).join("\n\n");

    if (indexHtml) {
      let html = indexHtml.content;
      if (
        !html.includes("react.development.js") &&
        !html.includes("react.production")
      ) {
        html = html.replace(
          "</head>",
          `<script src="https://unpkg.com/react@18.3.1/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone@7.29.0/babel.min.js"></script>
${cssBlock}
</head>`,
        );
      }
      html = html.replace(
        "</body>",
        `<script type="text/babel" data-presets="react,typescript">
${jsxBlock}
</script>
</body>`,
      );
      return injectCSP(html);
    }
    return injectCSP(`<!doctype html><html><head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>${project.title || "Preview"}</title>
<script src="https://unpkg.com/react@18.3.1/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone@7.29.0/babel.min.js"></script>
${cssBlock}
</head><body><div id="root"></div>
<script type="text/babel" data-presets="react">
${jsxBlock}
</script></body></html>`);
  }

  return null;
}

// ============================================================
// Live preview iframe — renders HTML/CSS/JS lub React via Babel standalone
// ============================================================
function LivePreview({ project }) {
  const iframeRef = useRef(null);
  const [refreshKey, setRefreshKey] = useState(0);

  const previewHTML = useMemo(
    () => buildPreviewHTML(project),
    // eslint-disable-next-line react-hooks/exhaustive-deps
    [project, refreshKey],
  );

  if (!project) {
    return (
      <div className="ps-preview-empty">
        <div className="ps-preview-empty-icon">▣</div>
        <div className="ps-preview-empty-title">Live Preview</div>
        <div className="ps-preview-empty-sub">
          Wpisz prompt po lewej stronie i kliknij <strong>GENERUJ</strong>.
          <br />
          Tutaj zobaczysz live preview wygenerowanej aplikacji.
        </div>
      </div>
    );
  }

  if (project.type === "nextjs") {
    return (
      <div className="ps-preview-empty">
        <div className="ps-preview-empty-icon">⚡</div>
        <div className="ps-preview-empty-title">
          Next.js — preview wymaga deploya
        </div>
        <div className="ps-preview-empty-sub">
          Pobierz ZIP, zainstaluj <code>npm install</code> i uruchom{" "}
          <code>npm run dev</code>.<br />
          Albo zmień typ projektu na "react" / "html" w nowym promcie aby
          zobaczyć live.
        </div>
      </div>
    );
  }

  if (!previewHTML) {
    return (
      <div className="ps-preview-empty">
        <div className="ps-preview-empty-icon">⏳</div>
        <div className="ps-preview-empty-title">Generowanie…</div>
        <div className="ps-preview-empty-sub">
          Czekam na pierwsze pliki od buildera.
        </div>
      </div>
    );
  }

  return (
    <div className="ps-preview-frame">
      <div className="ps-preview-bar">
        <div className="ps-preview-dots">
          <span></span>
          <span></span>
          <span></span>
        </div>
        <div className="ps-preview-url">file:///{project.name}/</div>
        <button
          className="ps-preview-refresh"
          onClick={() => setRefreshKey((k) => k + 1)}
          title="Odśwież"
        >
          ⟳
        </button>
      </div>
      <iframe
        key={refreshKey}
        ref={iframeRef}
        className="ps-iframe"
        srcDoc={previewHTML}
        sandbox="allow-scripts allow-forms allow-popups allow-popups-to-escape-sandbox"
        referrerPolicy="no-referrer"
        title="preview"
      />
    </div>
  );
}

// ============================================================
// Code editor (read-only z syntax highlight, klik = zmiana pliku)
// ============================================================
function CodeView({ project, activeFile, onSelectFile }) {
  const codeRef = useRef(null);
  const fileObj = project?.files?.find((f) => f.path === activeFile);

  useEffect(() => {
    if (codeRef.current && window.Prism) {
      window.Prism.highlightElement(codeRef.current);
    }
  }, [fileObj?.content, activeFile]);

  if (!project) return null;

  return (
    <div className="ps-code">
      <div className="ps-tree">
        {project.files.map((f) => (
          <div
            key={f.path}
            className={`ps-tree-item ${f.path === activeFile ? "active" : ""}`}
            onClick={() => onSelectFile(f.path)}
          >
            <span className="ps-tree-icon">{fileIcon(f.path)}</span>
            <span className="ps-tree-path">{f.path}</span>
            <span className="ps-tree-size">{f.content.length}b</span>
          </div>
        ))}
      </div>
      <div className="ps-code-pane">
        {fileObj ? (
          <>
            <div className="ps-code-header">{fileObj.path}</div>
            <pre className="ps-code-pre">
              <code
                ref={codeRef}
                className={`language-${langForFile(fileObj.path)}`}
              >
                {fileObj.content}
              </code>
            </pre>
          </>
        ) : (
          <div className="ps-code-empty">Wybierz plik z listy po lewej</div>
        )}
      </div>
    </div>
  );
}

// ============================================================
// SESSION STORAGE — ostatnie 5 sesji per pro user
// ============================================================
const PS_SESSIONS_KEY = "proStudio:sessions";
const PS_MAX_SESSIONS = 5;
const PS_DEFAULT_OPENER = {
  role: "a",
  text: "👋 Witaj w PRO Studio. Opisz aplikację którą chcesz wygenerować — od prostego narzędzia HTML, przez React SPA, po fullstack Next.js. Zobaczysz live preview side-by-side.",
};
function loadSessions() {
  try {
    return JSON.parse(localStorage.getItem(PS_SESSIONS_KEY) || "[]");
  } catch {
    return [];
  }
}
function saveSessions(sessions) {
  try {
    localStorage.setItem(
      PS_SESSIONS_KEY,
      JSON.stringify(sessions.slice(0, PS_MAX_SESSIONS)),
    );
  } catch {}
}
function newSessionId() {
  return (
    "s_" + Date.now().toString(36) + Math.random().toString(36).slice(2, 6)
  );
}

// ============================================================
// PRO STUDIO MAIN
// ============================================================
function ProStudioApp() {
  // Tier flags:
  //  - isPro = ma plan PRO lub EPIC (paid) — używane do UI (PRO chip), unlock UX
  //  - isUnlimited = TYLKO EPIC — pomija quota check (PRO też ma limity 20/100/tydz)
  const _tierInfo = window.AUTH?.getActiveTier?.() || { tier: "free" };
  const isPro = _tierInfo.tier === "pro" || _tierInfo.tier === "epic";
  const isUnlimited = _tierInfo.tier === "epic";
  const [quota, setQuota] = useState(null); // { gen_remaining, ref_remaining, gen_total, ref_total, pro }
  const [quotaLoaded, setQuotaLoaded] = useState(false);
  const [sessions, setSessions] = useState(() => loadSessions());
  const [currentSessionId, setCurrentSessionId] = useState(null);
  const [historyOpen, setHistoryOpen] = useState(false);
  const [confirmDeleteId, setConfirmDeleteId] = useState(null);
  const confirmTimerRef = useRef(null);
  const [prompt, setPrompt] = useState("");
  const [chatLog, setChatLog] = useState([PS_DEFAULT_OPENER]);
  const [generating, setGenerating] = useState(false);
  const [project, setProject] = useState(null);
  const [activeFile, setActiveFile] = useState(null);
  const [activeTab, setActiveTab] = useState("preview"); // preview | code
  const [phase, setPhase] = useState(""); // architekt | builder | reviewer | done
  const [progressPct, setProgressPct] = useState(0);
  const [reasoningStream, setReasoningStream] = useState("");
  const abortRef = useRef(null);
  const chatRef = useRef(null);

  // Fetch quota on mount + auto-refresh (gdy timer refill aktywny)
  useEffect(() => {
    let cancelled = false;
    let refreshTimer = null;

    const doFetch = async () => {
      try {
        if (window.VISITOR_READY) await window.VISITOR_READY;
        const q = await fetchQuota();
        if (cancelled) return;
        setQuota(q);
        setQuotaLoaded(true);
      } catch (e) {
        console.warn("[PRO Studio] quota fetch failed:", e);
        if (!cancelled) setQuotaLoaded(true);
      }
    };

    doFetch();
    // Auto-refresh: co 30s podczas aktywnego timera refill (żeby złapać moment refila)
    refreshTimer = setInterval(doFetch, 30000);
    return () => {
      cancelled = true;
      if (refreshTimer) clearInterval(refreshTimer);
    };
  }, []);

  const refreshQuota = async () => {
    try {
      const q = await fetchQuota();
      setQuota(q);
    } catch {}
  };

  // Countdown timer — odlicza co sekundę gdy refill_at jest ustawiony
  const [countdownTick, setCountdownTick] = useState(0);
  useEffect(() => {
    if (!quota?.refill_at) return;
    const id = setInterval(() => setCountdownTick((t) => t + 1), 1000);
    return () => clearInterval(id);
  }, [quota?.refill_at]);

  // Format countdown HH:MM:SS
  const refillCountdown = (() => {
    if (!quota?.refill_at) return null;
    const now = Date.now();
    const left = Math.max(0, Math.floor((quota.refill_at - now) / 1000));
    if (left === 0) {
      // Czas się skończył — ale klient może być wolniejszy niż serwer.
      // Wymuszamy refresh.
      return "0:00:00";
    }
    const h = Math.floor(left / 3600);
    const m = Math.floor((left % 3600) / 60);
    const s = left % 60;
    return `${h}:${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
  })();

  // Po wygaśnięciu countdownu — refresh quoty (prawdziwy refill po stronie serwera)
  useEffect(() => {
    if (!quota?.refill_at) return;
    const left = quota.refill_at - Date.now();
    if (left <= 0) {
      refreshQuota();
      return;
    }
    const id = setTimeout(refreshQuota, left + 200);
    return () => clearTimeout(id);
  }, [quota?.refill_at]);

  // Auto-save current session whenever project lub chatLog changes
  useEffect(() => {
    if (!currentSessionId) return;
    if (chatLog.length <= 1 && !project) return; // pusta — nie zapisuj
    const title = (
      project?.title ||
      prompt.slice(0, 50) ||
      "Nowy projekt"
    ).slice(0, 60);
    const entry = {
      id: currentSessionId,
      title,
      createdAt:
        sessions.find((s) => s.id === currentSessionId)?.createdAt ||
        Date.now(),
      lastUsedAt: Date.now(),
      prompt,
      project,
      chatLog,
      phase,
    };
    setSessions((prev) => {
      const filtered = prev.filter((s) => s.id !== currentSessionId);
      const updated = [entry, ...filtered].slice(0, PS_MAX_SESSIONS);
      saveSessions(updated);
      return updated;
    });
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [project, chatLog, currentSessionId]);

  const startNewProject = () => {
    // Abort aktywną generację — bez tego stary stream modyfikuje state nowej sesji
    try {
      abortRef.current?.abort();
    } catch {}
    PrzelomAudio.sounds.click();
    setGenerating(false);
    setCurrentSessionId(newSessionId());
    setChatLog([PS_DEFAULT_OPENER]);
    setProject(null);
    setActiveFile(null);
    setPrompt("");
    setProgressPct(0);
    setPhase("");
    setReasoningStream("");
    setHistoryOpen(false);
    setActiveTab("preview");
  };

  const loadSession = (sessionId) => {
    const s = sessions.find((x) => x.id === sessionId);
    if (!s) return;
    PrzelomAudio.sounds.click();
    setCurrentSessionId(s.id);
    setChatLog(s.chatLog || [PS_DEFAULT_OPENER]);
    setProject(s.project || null);
    setActiveFile(s.project?.files?.[0]?.path || null);
    setPrompt("");
    setProgressPct(0);
    setPhase(s.project ? "done" : "");
    setHistoryOpen(false);
    setActiveTab(s.project ? "preview" : "preview");
  };

  const deleteSession = (sessionId, e) => {
    e?.stopPropagation();
    // 2-click confirm: pierwszy klik → uzbrojony stan, drugi w 5s → faktyczne usunięcie.
    // (Bez native confirm — zgodnie z CLAUDE.md i hint w MCP guidelines.)
    if (confirmDeleteId !== sessionId) {
      setConfirmDeleteId(sessionId);
      if (confirmTimerRef.current) clearTimeout(confirmTimerRef.current);
      confirmTimerRef.current = setTimeout(() => {
        setConfirmDeleteId(null);
        confirmTimerRef.current = null;
      }, 5000);
      return;
    }
    if (confirmTimerRef.current) {
      clearTimeout(confirmTimerRef.current);
      confirmTimerRef.current = null;
    }
    setConfirmDeleteId(null);
    setSessions((prev) => {
      const updated = prev.filter((s) => s.id !== sessionId);
      saveSessions(updated);
      return updated;
    });
    if (currentSessionId === sessionId) startNewProject();
  };

  // Inicjalizuj nową sesję przy pierwszym mount jeśli nie ma żadnej
  useEffect(() => {
    if (!currentSessionId) {
      setCurrentSessionId(newSessionId());
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  useEffect(() => {
    if (chatRef.current)
      chatRef.current.scrollTop = chatRef.current.scrollHeight;
  }, [chatLog, reasoningStream]);

  // Hard paywall TYLKO gdy free user wyczerpał wszystko I nie ma aktywnego projektu
  const isQuotaExhausted =
    !isUnlimited &&
    quotaLoaded &&
    quota &&
    quota.gen_remaining === 0 &&
    quota.ref_remaining === 0;
  if (isQuotaExhausted && !project) {
    const tier = quota?.tier || "free";
    const isProTier = tier === "pro";
    return (
      <div className="ps-locked">
        <div className="ps-locked-icon">🔒</div>
        <div className="ps-locked-title">
          {isProTier
            ? "Wyczerpałeś tygodniowy limit PRO"
            : "Wyczerpałeś darmowy limit"}
        </div>
        <div className="ps-locked-sub">
          Wykorzystałeś {quota.gen_total} generacji i {quota.ref_total} poprawek
          {isProTier ? " w tym tygodniu." : "."}
          <br />
          {isProTier
            ? "Limity zresetują się przy najbliższym refill, lub przejdź na EPIC dla nieograniczonego dostępu."
            : "Aktywuj subskrypcję PRO żeby zwiększyć limit do 20/100 tygodniowo."}
        </div>
      </div>
    );
  }

  const stop = () => {
    // tylko abort — reszta naturalnie idzie przez catch+finally w generate()
    try {
      abortRef.current?.abort();
    } catch {}
    PrzelomAudio.sounds.click();
  };

  // ============ MODYFIKACJA istniejącego projektu ============
  const modifyProject = async (userRequest, tStart, opHeaders) => {
    setPhase("modify");
    setProgressPct(15);

    // Zbuduj historię TEJ sesji z chatLog: pary (user request, assistant change-notes).
    // Model dostaje pełen kontekst poprzednich modyfikacji — wie co user już prosił,
    // może odwoływać się ("wróć do poprzedniego layoutu") i nie powtarzać tych samych zmian.
    const history = [];
    let pendingUserReq = null;
    for (const m of chatLog) {
      if (m.role === "u" && m.text) {
        pendingUserReq = m.text;
      } else if (m.role === "a" && pendingUserReq && m.phase === "done") {
        // Wyciągnij sekcję "📝 ..." (change notes) jeśli jest, inaczej cały tekst
        const changeMatch = m.text.match(/📝\s+([^\n]+)/);
        history.push({
          request: pendingUserReq,
          changes: changeMatch ? changeMatch[1] : null,
        });
        pendingUserReq = null;
      }
    }
    // Limituj do ostatnich 8 — żeby kontekst nie eksplodował
    const recentHistory = history.slice(-8);

    setChatLog((l) => [
      ...l,
      {
        role: "a",
        text: `🔧 Modyfikuję projekt: **${project.files.length} plików** w kontekście${
          recentHistory.length
            ? ` + historia ${recentHistory.length} poprzednich zmian`
            : ""
        }. Czekaj…`,
        phase: "modify",
      },
    ]);
    setReasoningStream("");

    let lastMods = 0;
    let firstModAt = null;
    const modifyStart = Date.now();
    let watchdogShown = false;
    const watchdog = setInterval(() => {
      if (firstModAt) return;
      if (Date.now() - modifyStart > 30000 && !watchdogShown) {
        watchdogShown = true;
        setChatLog((l) => [
          ...l,
          {
            role: "a",
            text: "⏳ Model dalej myśli nad zmianami — to normalne dla większych modyfikacji. Daj mu jeszcze ~60s lub kliknij STOP.",
            phase: "modify-slow",
          },
        ]);
      }
    }, 5000);
    let modifyResult;
    try {
      modifyResult = await streamStepfun({
        quotaHeaders: opHeaders,
        messages: [
          {
            role: "system",
            content: buildModifyPrompt(project, userRequest, recentHistory),
          },
          { role: "user", content: userRequest },
        ],
        max_tokens: 64000,
        signal: abortRef.current.signal,
        onReasoning: (_, full) => setReasoningStream(full.slice(-300)),
        onDelta: (_, full) => {
          if (!firstModAt) firstModAt = Date.now();
          // Live update — pokazuj zmienione pliki na bieżąco
          const partial = extractFilesFromPartialJSON(full);
          if (partial.length > lastMods) {
            lastMods = partial.length;
            // Merge na żywo: pokażemy efekt tymczasowo
            const merged = mergeProjectFiles(project.files, partial);
            setProject((p) => (p ? { ...p, files: merged } : p));
            setProgressPct(15 + Math.min(70, partial.length * 8));
          }
        },
      });
    } finally {
      clearInterval(watchdog);
    }

    // Final parse
    const parsed =
      extractJSON(modifyResult.content) || extractJSON(modifyResult.reasoning);
    let modifications = [];
    let changeNotes = [];
    if (parsed?.files) {
      modifications = Array.isArray(parsed.files) ? parsed.files : [];
      changeNotes = Array.isArray(parsed.changes) ? parsed.changes : [];
    } else {
      // Fallback do partial extract
      const partial = extractFilesFromPartialJSON(modifyResult.content);
      if (partial.length > 0) modifications = partial;
    }

    if (modifications.length === 0) {
      throw new Error(
        "Nie udało się wygenerować modyfikacji (finish: " +
          (modifyResult.finishReason || "?") +
          "). Spróbuj prostszego promptu lub kliknij ZMIEŃ jeszcze raz.",
      );
    }

    // Merge wynik z aktualnymi plikami
    const newFiles = mergeProjectFiles(project.files, modifications);
    const updatedProject = { ...project, files: newFiles };
    setProject(updatedProject);
    setProgressPct(90);

    // Autofix też po modyfikacji (gdyby model zepsuł import etc)
    setPhase("reviewer");
    setChatLog((l) => [
      ...l,
      {
        role: "a",
        text: "🔍 Sprawdzam zmiany…",
        phase: "reviewer",
      },
    ]);
    const autofixed = autoFixProject(updatedProject);
    setProject(autofixed);
    setProgressPct(100);

    // Podsumowanie zmian — pokaż user co się stało
    const changedPaths = modifications.map((m) =>
      m.deleted ? `🗑 ${m.path}` : `✏ ${m.path}`,
    );
    const elapsed = ((Date.now() - tStart) / 1000).toFixed(1);
    setChatLog((l) => [
      ...l,
      {
        role: "a",
        text:
          `✅ Zaktualizowano **${modifications.length} plik${modifications.length === 1 ? "" : "ów"}** w ${elapsed}s.\n\n` +
          (changeNotes.length > 0 ? `📝 ${changeNotes.join(" ")}\n\n` : "") +
          `Zmienione:\n${changedPaths.slice(0, 8).join("\n")}` +
          (changedPaths.length > 8
            ? `\n…i ${changedPaths.length - 8} więcej`
            : "") +
          (autofixed.autofixes?.length
            ? `\n\n🛠 Auto-fix po modyfikacji: ${autofixed.autofixes.length}`
            : ""),
        phase: "done",
      },
    ]);
    setPhase("done");
    PrzelomAudio.sounds.levelup();
  };

  const generate = async () => {
    if (!prompt.trim() || generating) return;
    const userPrompt = prompt;

    // ============ TRYB MODYFIKACJI vs NOWY PROJEKT ============
    const isModify = !!(project && project.files && project.files.length > 0);

    // Race protection: jeśli quota nie zostala jeszcze zfetched, fetchni teraz (block)
    if (!isUnlimited && !quotaLoaded) {
      try {
        const q = await fetchQuota();
        setQuota(q);
        setQuotaLoaded(true);
      } catch {
        /* zostaw — server gate i tak złapie */
      }
    }

    PrzelomAudio.sounds.boot();
    const tStart = Date.now();

    // Pre-flight check: każdy tier oprócz EPIC ma limity → zablokuj zanim coś zaczniesz
    if (!isUnlimited && quota) {
      const tier = quota.tier || "free";
      const isProTier = tier === "pro";
      const refillTxt =
        quota.refill_at && refillCountdown
          ? ` Refill za ${refillCountdown}.`
          : "";
      const upgradeTxt = isProTier
        ? "Przejdź na EPIC żeby mieć bez limitu."
        : "Aktywuj PRO żeby zwiększyć limit do 20/100 tygodniowo.";
      if (isModify && quota.ref_remaining === 0) {
        setChatLog((l) => [
          ...l,
          { role: "u", text: userPrompt },
          {
            role: "a",
            text: `🔒 Wyczerpałeś poprawki (${quota.ref_total}/${quota.ref_total}).${refillTxt} ${upgradeTxt}`,
            phase: "quota-block",
          },
        ]);
        setPrompt("");
        return;
      }
      if (!isModify && quota.gen_remaining === 0) {
        setChatLog((l) => [
          ...l,
          { role: "u", text: userPrompt },
          {
            role: "a",
            text: `🔒 Wyczerpałeś generacje (${quota.gen_total}/${quota.gen_total}).${refillTxt} ${upgradeTxt}`,
            phase: "quota-block",
          },
        ]);
        setPrompt("");
        return;
      }
    }

    setGenerating(true);
    setProgressPct(0);
    setReasoningStream("");
    setChatLog((l) => [...l, { role: "u", text: userPrompt }]);
    setPrompt("");
    abortRef.current = new AbortController();

    // Wspólne op_id i headery dla całej operacji (architect/builder/reviewer dzielą jedno ID)
    const opId = newOpId();
    const opType = isModify ? "refine" : "generate";
    const opHeaders = buildQuotaHeaders(opType, opId);

    if (isModify) {
      try {
        await modifyProject(userPrompt, tStart, opHeaders);
        refreshQuota();
      } catch (e) {
        if (e.name === "AbortError" || abortRef.current?.signal?.aborted) {
          setChatLog((l) => [
            ...l,
            {
              role: "a",
              text: "⏸ Zatrzymano modyfikację. Stan projektu nienaruszony.",
            },
          ]);
        } else if (e.code === "QUOTA_EXCEEDED") {
          setChatLog((l) => [
            ...l,
            {
              role: "a",
              text: "🔒 " + e.message + " Aktywuj PRO żeby kontynuować.",
              phase: "quota-block",
            },
          ]);
          refreshQuota();
        } else {
          console.error("[PRO Studio] modify error:", e);
          setChatLog((l) => [
            ...l,
            {
              role: "a",
              text:
                "❌ Błąd modyfikacji: " +
                e.message +
                "\n\nMożesz spróbować ponownie z innym promptem albo kliknąć NOWY PROJEKT (☰) jeśli wolisz zacząć od zera.",
            },
          ]);
        }
      } finally {
        setGenerating(false);
        setReasoningStream("");
      }
      return;
    }

    // ============ NOWY PROJEKT — full pipeline ============
    setProject(null);
    setActiveFile(null);

    try {
      // ============ FAZA 1: ARCHITECT ============
      setPhase("architekt");
      setProgressPct(5);
      setChatLog((l) => [
        ...l,
        {
          role: "a",
          text: "🧠 Architekt analizuje wymagania…",
          phase: "architekt",
        },
      ]);

      const archResult = await streamStepfun({
        quotaHeaders: opHeaders,
        messages: [
          { role: "system", content: ARCHITECT_PROMPT },
          { role: "user", content: prompt },
        ],
        // Step 3.5 to reasoning model — potrzebuje budżetu na thinking + content
        max_tokens: 8000,
        signal: abortRef.current.signal,
        onReasoning: (_, full) => setReasoningStream(full.slice(-300)),
      });

      // Próbuj content. Jeśli pusty (model spalił całość na reasoning) — szukaj JSON-a w reasoning
      let plan =
        extractJSON(archResult.content) || extractJSON(archResult.reasoning);

      if (!plan) {
        // Fallback: zbuduj minimalny plan z promptu samego użytkownika
        const safeName =
          prompt
            .toLowerCase()
            .replace(/[^a-z0-9]+/g, "-")
            .slice(0, 30) || "projekt";
        plan = {
          name: safeName,
          title: prompt.slice(0, 60),
          type: "html",
          stack: ["HTML", "CSS", "JS"],
          description: prompt,
          files_planned: [{ path: "index.html", purpose: "entry" }],
        };
        setChatLog((l) => [
          ...l,
          {
            role: "a",
            text:
              "⚠ Architekt nie zwrócił czytelnego JSON-a (`finish: " +
              (archResult.finishReason || "?") +
              "`). Używam fallbacku: prosta strona HTML.",
            phase: "architekt-fallback",
          },
        ]);
      }
      // Validacja minimalna
      if (!plan.type || !["html", "react", "nextjs"].includes(plan.type))
        plan.type = "html";
      if (
        !Array.isArray(plan.files_planned) ||
        plan.files_planned.length === 0
      ) {
        plan.files_planned = [{ path: "index.html", purpose: "entry" }];
      }
      if (!plan.title) plan.title = prompt.slice(0, 60) || "Projekt";
      if (!plan.name)
        plan.name = (plan.title || "projekt")
          .toLowerCase()
          .replace(/[^a-z0-9]+/g, "-")
          .slice(0, 30);

      setProgressPct(20);
      setChatLog((l) => [
        ...l,
        {
          role: "a",
          text: `📋 Plan gotowy: **${plan.title}** (${plan.type})\n\nStack: ${(plan.stack || []).join(", ")}\nPlanowanych plików: ${(plan.files_planned || []).length}\n\n${plan.description || ""}`,
          phase: "architekt-done",
        },
      ]);

      // Stwórz pusty projekt — będziemy do niego dodawać pliki w trakcie streaming
      setProject({
        name: plan.name,
        title: plan.title,
        type: plan.type,
        stack: plan.stack,
        description: plan.description,
        files: [],
      });

      // ============ FAZA 2: BUILDER (streaming) ============
      setPhase("builder");
      setProgressPct(25);
      setChatLog((l) => [
        ...l,
        {
          role: "a",
          text: "⚙ Builder generuje kod (real-time)…",
          phase: "builder",
        },
      ]);
      setReasoningStream("");

      let lastFilesCount = 0;
      let firstContentAt = null;
      const builderStart = Date.now();
      // Watchdog: jeśli content nie zaczyna się w 30s → poinformuj użytkownika
      let watchdogShown = false;
      const watchdog = setInterval(() => {
        if (firstContentAt) return;
        if (Date.now() - builderStart > 30000 && !watchdogShown) {
          watchdogShown = true;
          setChatLog((l) => [
            ...l,
            {
              role: "a",
              text: "⏳ Model dalej myśli — to normalne dla większych projektów. Daj mu jeszcze ~60s lub kliknij STOP żeby anulować.",
              phase: "builder-slow",
            },
          ]);
        }
      }, 5000);
      let builderResult;
      try {
        builderResult = await streamStepfun({
          quotaHeaders: opHeaders,
          messages: [
            { role: "system", content: buildBuilderPrompt(plan) },
            {
              role: "user",
              content: `Wygeneruj wszystkie pliki dla: ${plan.title}.\n\nKontekst od użytkownika: ${prompt}`,
            },
          ],
          max_tokens: 64000,
          signal: abortRef.current.signal,
          onReasoning: (_, full) => setReasoningStream(full.slice(-400)),
          onDelta: (_, full) => {
            if (!firstContentAt) firstContentAt = Date.now();
            // Próba wyciągnięcia gotowych plików z partial JSON
            const files = extractFilesFromPartialJSON(full);
            if (files.length > lastFilesCount) {
              lastFilesCount = files.length;
              setProject((p) => (p ? { ...p, files } : p));
              // Auto-select first file when avail
              setActiveFile((af) => af || (files[0]?.path ?? null));
              // Progress: 25% + 65% × (files / planned)
              const target = Math.max(
                plan.files_planned?.length || 10,
                files.length,
              );
              const pct = 25 + Math.min(65, (files.length / target) * 65);
              setProgressPct(Math.floor(pct));
            }
          },
        });
      } finally {
        clearInterval(watchdog);
      }

      // Final parse — w priorytecie pełen JSON, fallback do partial extract, fallback do reasoning
      let finalProject = extractJSON(builderResult.content);
      if (!finalProject || !Array.isArray(finalProject.files)) {
        // Spróbuj partial extract z content
        let files = extractFilesFromPartialJSON(builderResult.content);
        if (files.length === 0) {
          // Ostateczny fallback — może JSON jest w reasoning
          const fromReasoning = extractJSON(builderResult.reasoning);
          if (fromReasoning?.files?.length) {
            finalProject = fromReasoning;
          } else {
            files = extractFilesFromPartialJSON(builderResult.reasoning);
            if (files.length > 0) finalProject = { files };
          }
        } else {
          finalProject = { files };
        }
      }
      if (!finalProject?.files?.length) {
        throw new Error(
          "Builder nie zwrócił żadnego pliku (finish: " +
            (builderResult.finishReason || "?") +
            "). Spróbuj prostszego promptu albo kliknij GENERUJ jeszcze raz.",
        );
      }

      // Pokaż wstępne pliki
      setProject((p) => ({ ...p, files: finalProject.files || [] }));
      setActiveFile(finalProject.files?.[0]?.path || null);
      setProgressPct(90);

      // ============ FAZA 3: REVIEWER (auto-fix + opcjonalny LLM polish) ============
      setPhase("reviewer");
      setChatLog((l) => [
        ...l,
        {
          role: "a",
          text: "🔍 Reviewer sprawdza kod (typowe błędy, wersje pakietów, importy)…",
          phase: "reviewer",
        },
      ]);

      // 1. Client-side autofix — instant, bez LLM call
      const projectForReview = {
        type: plan.type,
        name: plan.name,
        title: plan.title,
        stack: plan.stack,
        description: plan.description,
        files: finalProject.files,
      };
      const autofixed = autoFixProject(projectForReview);
      setProject((p) => ({ ...p, files: autofixed.files }));
      if (autofixed.autofixes?.length > 0) {
        setChatLog((l) => [
          ...l,
          {
            role: "a",
            text: `🛠 Auto-naprawiono ${autofixed.autofixes.length} typowych błędów:\n${autofixed.autofixes
              .slice(0, 8)
              .map((f) => "• " + f)
              .join(
                "\n",
              )}${autofixed.autofixes.length > 8 ? `\n• …i ${autofixed.autofixes.length - 8} więcej` : ""}`,
            phase: "reviewer-autofix",
          },
        ]);
      }
      setProgressPct(95);

      // 2. (Opcjonalnie) LLM REVIEWER — tylko dla nextjs (najbardziej złożone, najwięcej błędów)
      // Skip dla małych projektów (mniej niż 4 pliki) i dla html (pojedynczy plik = nic do review)
      const useLLMReviewer =
        plan.type === "nextjs" && autofixed.files.length >= 4;
      if (useLLMReviewer) {
        try {
          const reviewerResult = await streamStepfun({
            quotaHeaders: opHeaders,
            messages: [
              { role: "system", content: buildReviewerPrompt(autofixed) },
              {
                role: "user",
                content: "Przejrzyj projekt, napraw błędy, zwróć JSON.",
              },
            ],
            max_tokens: 32000,
            signal: abortRef.current.signal,
            onReasoning: (_, full) => setReasoningStream(full.slice(-300)),
          });
          const reviewed =
            extractJSON(reviewerResult.content) ||
            extractJSON(reviewerResult.reasoning);
          if (reviewed?.files?.length > 0) {
            // Drugi pass autofix na wynik reviewera
            const finalAutofixed = autoFixProject({
              ...autofixed,
              files: reviewed.files,
            });
            setProject((p) => ({ ...p, files: finalAutofixed.files }));
            setChatLog((l) => [
              ...l,
              {
                role: "a",
                text: "✓ Reviewer LLM przeszedł — kod sprawdzony i poprawiony.",
                phase: "reviewer-done",
              },
            ]);
          }
        } catch (rerr) {
          if (rerr.name !== "AbortError") {
            // Reviewer fail nie zatrzymuje całości — autofix już naprawił najważniejsze
            console.warn("[PRO Studio] Reviewer LLM failed:", rerr);
          }
        }
      }

      // ============ FAZA 4: DONE ============
      const elapsed = ((Date.now() - tStart) / 1000).toFixed(1);
      setPhase("done");
      setProgressPct(100);
      setChatLog((l) => [
        ...l,
        {
          role: "a",
          text: `✅ Wygenerowano **${finalProject.files.length} plików** w ${elapsed}s. Kliknij PREVIEW żeby zobaczyć live, CODE żeby przeglądać pliki, ⬇ żeby pobrać ZIP.`,
          phase: "done",
        },
      ]);
      setActiveTab("preview");
      PrzelomAudio.sounds.levelup();
      refreshQuota();
    } catch (e) {
      // Aborted by user (STOP button) — nie traktuj jako błąd
      if (e.name === "AbortError" || abortRef.current?.signal?.aborted) {
        setChatLog((l) => [
          ...l,
          { role: "a", text: "⏸ Zatrzymano generację. Możesz zacząć od nowa." },
        ]);
      } else if (e.code === "QUOTA_EXCEEDED") {
        setChatLog((l) => [
          ...l,
          {
            role: "a",
            text: "🔒 " + e.message + " Aktywuj PRO żeby kontynuować.",
            phase: "quota-block",
          },
        ]);
        refreshQuota();
      } else if (e.message?.includes("HTTP")) {
        // Network/proxy error
        setChatLog((l) => [
          ...l,
          {
            role: "a",
            text: `❌ Problem z połączeniem do AI (${e.message}). Sprawdź czy proxy server działa, kliknij GENERUJ żeby spróbować ponownie.`,
          },
        ]);
      } else if (
        e.message?.includes("zwrócił niepoprawny") ||
        e.message?.includes("nie zwrócił żadnego pliku")
      ) {
        // Model output issue — daj actionable hint
        setChatLog((l) => [
          ...l,
          {
            role: "a",
            text: `⚠ ${e.message}\n\nTo zdarza się gdy model "się zamyśli" i nie zdąży wypisać kodu. Spróbuj:\n• kliknąć GENERUJ jeszcze raz (różne wyniki za każdym razem)\n• prostszego promptu (np. "kalkulator napiwków" zamiast "fullstack SaaS z 20 features")`,
          },
        ]);
      } else {
        // Unknown error — pokaż surowy
        console.error("[PRO Studio] generation error:", e);
        setChatLog((l) => [
          ...l,
          {
            role: "a",
            text:
              "❌ Niespodziewany błąd: " +
              e.message +
              "\n\nKliknij GENERUJ żeby spróbować ponownie.",
          },
        ]);
      }
    } finally {
      setGenerating(false);
      setReasoningStream("");
    }
  };

  const downloadZip = async () => {
    if (!project) return;
    if (typeof JSZip === "undefined") {
      setChatLog((l) => [
        ...l,
        {
          role: "a",
          text: "⚠ Biblioteka ZIP (JSZip) jeszcze nie załadowana lub została zablokowana. Odśwież stronę i spróbuj ponownie.",
        },
      ]);
      return;
    }
    PrzelomAudio.sounds.pickup();
    try {
      const zip = new JSZip();
      project.files.forEach((f) => zip.file(f.path, f.content));
      zip.file(
        "README.md",
        `# ${project.title}\n\nWygenerowane przez ZAZA · PRO Studio\n\n## Stack\n${(project.stack || []).map((s) => "- " + s).join("\n")}\n\n## Setup\n\n${
          project.type === "html"
            ? "Otwórz `index.html` w przeglądarce — działa od razu."
            : project.type === "react"
              ? "Otwórz `index.html` w przeglądarce — React + Babel ładuje się z CDN."
              : "```bash\nnpm install\nnpm run dev\n```"
        }\n\n— Wygenerowano: ${new Date().toISOString()}\n`,
      );
      const blob = await zip.generateAsync({ type: "blob" });
      const url = URL.createObjectURL(blob);
      const a = document.createElement("a");
      a.href = url;
      a.download = `${project.name}.zip`;
      a.click();
      URL.revokeObjectURL(url);
    } catch (e) {
      console.error("[PRO Studio] ZIP error:", e);
      setChatLog((l) => [
        ...l,
        {
          role: "a",
          text:
            "❌ Nie udało się spakować projektu: " +
            (e.message || "nieznany błąd"),
        },
      ]);
    }
  };

  const openPreviewFullscreen = () => {
    if (!project) return;
    if (project.type === "nextjs") {
      setChatLog((l) => [
        ...l,
        {
          role: "a",
          text: "ℹ Next.js wymaga deploya — pobierz ZIP, `npm install`, `npm run dev`. Live preview niedostępny dla projektów Next.js.",
        },
      ]);
      return;
    }
    // Buduj HTML niezależnie od DOM — działa nawet gdy aktywny jest tab CODE
    // (iframe nie istnieje w drzewie) lub gdy LivePreview nie zdążył się zrenderować.
    const html = buildPreviewHTML(project);
    if (!html) {
      setChatLog((l) => [
        ...l,
        {
          role: "a",
          text: "⚠ Nie mogę zbudować preview — brak `index.html` w projekcie albo pliki nie są jeszcze gotowe.",
        },
      ]);
      return;
    }
    // Blob URL ma unique origin — wygenerowany kod NIE może czytać localStorage parenta.
    // noopener,noreferrer odcina window.opener.
    const blob = new Blob([html], { type: "text/html" });
    const url = URL.createObjectURL(blob);
    const win = window.open(url, "_blank", "noopener,noreferrer");
    if (!win) {
      // Popup blocker — fallback: download jako plik HTML, user otworzy ręcznie
      URL.revokeObjectURL(url);
      const a = document.createElement("a");
      a.href = "data:text/html;charset=utf-8," + encodeURIComponent(html);
      a.download = `${project.name || "preview"}.html`;
      a.click();
      setChatLog((l) => [
        ...l,
        {
          role: "a",
          text:
            "ℹ Przeglądarka zablokowała popup — pobrałem preview jako plik HTML. Otwórz `" +
            (project.name || "preview") +
            ".html` ręcznie (dwuklik).",
        },
      ]);
      return;
    }
    PrzelomAudio.sounds.click();
    setTimeout(() => URL.revokeObjectURL(url), 60000);
  };

  const phaseColor =
    phase === "architekt"
      ? "#33ffe1"
      : phase === "builder"
        ? "#ffd24a"
        : phase === "modify"
          ? "#ff66cc"
          : phase === "reviewer"
            ? "#b58aff"
            : phase === "done"
              ? "#5aff5a"
              : "var(--fg-dim)";

  // Tryb modyfikacji aktywny gdy projekt już istnieje
  const isModifyMode = !!(project && project.files && project.files.length > 0);

  return (
    <div className="ps-root">
      {/* HISTORY SIDEBAR (slide-in overlay) */}
      {historyOpen && (
        <div
          className="ps-history-overlay"
          onClick={() => setHistoryOpen(false)}
        >
          <div
            className="ps-history-panel"
            onClick={(e) => e.stopPropagation()}
          >
            <div className="ps-history-head">
              <span className="ps-history-title">
                SESJE · ostatnie {PS_MAX_SESSIONS}
              </span>
              <button
                className="ps-history-x"
                onClick={() => setHistoryOpen(false)}
              >
                ×
              </button>
            </div>
            <button className="ps-history-new" onClick={startNewProject}>
              <span style={{ fontSize: 18, marginRight: 8 }}>+</span> NOWY
              PROJEKT
            </button>
            <div className="ps-history-list">
              {sessions.length === 0 && (
                <div className="ps-history-empty">
                  Brak sesji. Kliknij NOWY PROJEKT aby zacząć.
                </div>
              )}
              {sessions.map((s) => {
                const isActive = s.id === currentSessionId;
                const dt = new Date(s.lastUsedAt).toLocaleString("pl-PL", {
                  dateStyle: "short",
                  timeStyle: "short",
                });
                return (
                  <div
                    key={s.id}
                    className={`ps-history-item ${isActive ? "active" : ""}`}
                    onClick={() => loadSession(s.id)}
                  >
                    <div className="ps-history-item-title">{s.title}</div>
                    <div className="ps-history-item-meta">
                      <span>
                        {s.project
                          ? `${s.project.type} · ${s.project.files?.length || 0} plików`
                          : "draft"}
                      </span>
                      <span>{dt}</span>
                    </div>
                    <button
                      className="ps-history-del"
                      onClick={(e) => deleteSession(s.id, e)}
                      title={
                        confirmDeleteId === s.id
                          ? "Kliknij ponownie aby potwierdzić"
                          : "Usuń"
                      }
                      style={
                        confirmDeleteId === s.id
                          ? {
                              color: "#ff5566",
                              fontWeight: "bold",
                              fontSize: 11,
                              letterSpacing: "0.5px",
                            }
                          : undefined
                      }
                    >
                      {confirmDeleteId === s.id ? "POTWIERDŹ?" : "×"}
                    </button>
                  </div>
                );
              })}
            </div>
            <div className="ps-history-foot">
              {sessions.length}/{PS_MAX_SESSIONS} sesji · auto-save włączony
            </div>
          </div>
        </div>
      )}

      {/* LEWY PANEL — chat & prompt */}
      <div className="ps-left">
        <div className="ps-head">
          <button
            className="ps-hamburger"
            onClick={() => {
              setHistoryOpen((o) => !o);
              PrzelomAudio.sounds.click();
            }}
            title="Historia sesji + nowy projekt"
          >
            ☰
          </button>
          <span className="ps-head-badge">★ PRO STUDIO</span>
          <span className="ps-head-status" style={{ color: phaseColor }}>
            {generating ? `● ${phase}` : project ? "● gotowy" : "● czekam"}
          </span>
          {(() => {
            const tier =
              quota?.tier || window.AUTH?.getActiveTier?.()?.tier || "free";
            if (!quota) return null;
            const isEpic = tier === "epic" || quota.unlimited;
            const isProTier = tier === "pro";
            const isFree = tier === "free";
            const exhausted =
              !isEpic && quota.gen_remaining === 0 && quota.ref_remaining === 0;
            const baseStyle = {
              marginLeft: "auto",
              padding: "3px 8px",
              fontSize: 11,
              fontFamily: "JetBrains Mono, monospace",
              background: "rgba(0,0,0,0.4)",
              borderRadius: 4,
              letterSpacing: "0.5px",
            };
            if (isEpic) {
              return (
                <span
                  style={{
                    ...baseStyle,
                    color: "#ff66cc",
                    border: "1px solid #ff66cc",
                  }}
                >
                  ★ EPIC · bez limitu
                </span>
              );
            }
            if (isProTier) {
              return (
                <span
                  style={{
                    ...baseStyle,
                    color: exhausted ? "#ff5566" : "#ffd24a",
                    border: exhausted
                      ? "1px solid #ff5566"
                      : "1px solid #ffd24a",
                  }}
                  title={`PRO: ${quota.gen_remaining}/${quota.gen_total} generacji + ${quota.ref_remaining}/${quota.ref_total} poprawek tygodniowo`}
                >
                  ★ PRO · gen {quota.gen_remaining}/{quota.gen_total} · popr{" "}
                  {quota.ref_remaining}/{quota.ref_total}
                  {refillCountdown && (
                    <span style={{ marginLeft: 8, opacity: 0.85 }}>
                      · ⟳ {refillCountdown}
                    </span>
                  )}
                </span>
              );
            }
            // FREE
            return (
              <span
                className="ps-quota-chip"
                style={{
                  ...baseStyle,
                  color: exhausted ? "#ff5566" : "#caff33",
                  border: exhausted
                    ? "1px solid #ff5566"
                    : "1px solid rgba(202, 255, 51, 0.5)",
                }}
                title={`FREE: ${quota.gen_remaining}/${quota.gen_total} generacji + ${quota.ref_remaining}/${quota.ref_total} poprawek dziennie`}
              >
                FREE · gen {quota.gen_remaining}/{quota.gen_total} · popr{" "}
                {quota.ref_remaining}/{quota.ref_total}
                {refillCountdown && (
                  <span style={{ marginLeft: 8, opacity: 0.85 }}>
                    · ⟳ {refillCountdown}
                  </span>
                )}
              </span>
            );
          })()}
        </div>

        <div className="ps-chat" ref={chatRef}>
          {chatLog.map((m, i) => (
            <div key={i} className={`ps-msg ps-msg-${m.role}`}>
              <div className="ps-msg-bubble">
                {m.text.split(/\*\*(.*?)\*\*/g).map((part, j) =>
                  j % 2 === 1 ? (
                    <strong key={j} style={{ color: "#ffd24a" }}>
                      {part}
                    </strong>
                  ) : (
                    <React.Fragment key={j}>{part}</React.Fragment>
                  ),
                )}
              </div>
            </div>
          ))}
          {generating && reasoningStream && (
            <div className="ps-msg ps-msg-thinking">
              <div className="ps-msg-bubble ps-msg-thinking-bubble">
                <span className="ps-thinking-tag">myślę…</span>
                {reasoningStream}
              </div>
            </div>
          )}
        </div>

        {generating && (
          <div className="ps-progress">
            <div className="ps-progress-bar">
              <div
                className="ps-progress-fill"
                style={{ width: progressPct + "%" }}
              />
            </div>
            <div className="ps-progress-info">
              <span>{phase || "—"}</span>
              <span>{progressPct}%</span>
            </div>
          </div>
        )}

        <div className="ps-input-row">
          {isModifyMode && (
            <div className="ps-mode-tag">
              <span>
                ✏ TRYB MODYFIKACJI · {project.files.length} plików w kontekście
              </span>
              <button
                className="ps-mode-new"
                onClick={startNewProject}
                title="Zacznij nowy projekt od zera"
              >
                + NOWY
              </button>
            </div>
          )}
          <textarea
            className="ps-input"
            placeholder={
              isModifyMode
                ? "Modyfikuj projekt: 'zmień kolor na czerwony', 'dodaj sekcję cennik', 'wymień Stripe na BLIK'…"
                : "Opisz aplikację: 'kalkulator BMI z animacjami', 'todo app w React', 'SaaS habit tracker'…"
            }
            value={prompt}
            onChange={(e) => setPrompt(e.target.value)}
            onKeyDown={(e) => {
              if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) generate();
            }}
            disabled={generating}
            rows={3}
          />
          <div className="ps-input-actions">
            {generating ? (
              <button className="ps-btn ps-btn-stop" onClick={stop}>
                STOP
              </button>
            ) : (
              <button
                className={`ps-btn ${isModifyMode ? "ps-btn-modify" : "ps-btn-go"}`}
                onClick={generate}
                disabled={!prompt.trim()}
              >
                {isModifyMode ? "ZMIEŃ ⌘↵" : "GENERUJ ⌘↵"}
              </button>
            )}
          </div>
        </div>
      </div>

      {/* PRAWY PANEL — preview / code */}
      <div className="ps-right">
        <div className="ps-tabs">
          <button
            className={`ps-tab ${activeTab === "preview" ? "active" : ""}`}
            onClick={() => setActiveTab("preview")}
            disabled={!project}
          >
            ▣ PREVIEW
          </button>
          <button
            className={`ps-tab ${activeTab === "code" ? "active" : ""}`}
            onClick={() => setActiveTab("code")}
            disabled={!project}
          >
            ◇ CODE {project ? `(${project.files.length})` : ""}
          </button>
          <div className="ps-tab-spacer" />
          {project && (
            <>
              <button
                className="ps-tab-action"
                onClick={openPreviewFullscreen}
                title="Otwórz w nowej karcie"
              >
                ↗
              </button>
              <button
                className="ps-tab-action ps-tab-download"
                onClick={downloadZip}
                title="Pobierz ZIP"
              >
                ⬇ ZIP
              </button>
            </>
          )}
        </div>

        <div className="ps-content">
          {activeTab === "preview" && <LivePreview project={project} />}
          {activeTab === "code" && (
            <CodeView
              project={project}
              activeFile={activeFile}
              onSelectFile={setActiveFile}
            />
          )}
        </div>

        {project && (
          <div className="ps-meta">
            <span className="ps-meta-item">{project.type}</span>
            <span className="ps-meta-item">{project.files.length} plików</span>
            <span className="ps-meta-item">
              {(project.stack || []).slice(0, 3).join(" · ")}
            </span>
          </div>
        )}
      </div>
    </div>
  );
}

// ============================================================
// EXPORTS
// ============================================================
Object.assign(window, { ProStudioApp });
