// Hatchway CLI - Built with Rollup
import { execSync, spawn } from 'node:child_process';
import { EventEmitter } from 'node:events';
import { platform, arch } from 'node:os';
import { existsSync, mkdirSync, chmodSync } from 'node:fs';
import { resolve } from 'node:path';
import http from 'http';
import httpProxy from 'http-proxy';
import zlib from 'zlib';

const CLOUDFLARED_GITHUB = 'https://github.com/cloudflare/cloudflared/releases/latest/download';
/**
 * Get the local bin directory for cloudflared installation
 */
function getBinDir() {
    const homeDir = process.env.HOME || process.env.USERPROFILE || '/tmp';
    const binDir = resolve(homeDir, '.hatchway', 'bin');
    if (!existsSync(binDir)) {
        mkdirSync(binDir, { recursive: true });
    }
    return binDir;
}
/**
 * Detect the appropriate cloudflared binary name for this platform
 */
function getCloudflaredBinaryName() {
    const plat = platform();
    const architecture = arch();
    if (plat === 'darwin') {
        if (architecture === 'arm64') {
            return 'cloudflared-darwin-arm64.tgz';
        }
        return 'cloudflared-darwin-amd64.tgz';
    }
    else if (plat === 'linux') {
        if (architecture === 'arm64') {
            return 'cloudflared-linux-arm64';
        }
        return 'cloudflared-linux-amd64';
    }
    else if (plat === 'win32') {
        return 'cloudflared-windows-amd64.exe';
    }
    throw new Error(`Unsupported platform: ${plat} ${architecture}`);
}
/**
 * Check if cloudflared is already installed (globally or locally)
 */
function checkExistingInstallation() {
    // Check global installation
    try {
        execSync('cloudflared --version', { stdio: 'ignore' });
        return 'cloudflared';
    }
    catch {
        // Not installed globally
    }
    // Check local installation
    const binDir = getBinDir();
    const localPath = resolve(binDir, 'cloudflared');
    if (existsSync(localPath)) {
        return localPath;
    }
    return null;
}
/**
 * Download and install cloudflared binary
 */
async function downloadCloudflared() {
    const binDir = getBinDir();
    const binaryName = getCloudflaredBinaryName();
    const downloadUrl = `${CLOUDFLARED_GITHUB}/${binaryName}`;
    const plat = platform();
    console.log(`📦 Downloading cloudflared from ${downloadUrl}...`);
    if (plat === 'darwin') {
        // macOS - download and extract tarball
        const tarPath = resolve(binDir, 'cloudflared.tgz');
        const extractDir = resolve(binDir, 'cloudflared-extract');
        execSync(`curl -L "${downloadUrl}" -o "${tarPath}"`, { stdio: 'inherit' });
        // Create extraction directory
        if (!existsSync(extractDir)) {
            mkdirSync(extractDir, { recursive: true });
        }
        // Extract
        execSync(`tar -xzf "${tarPath}" -C "${extractDir}"`, { stdio: 'inherit' });
        // Find the cloudflared binary in extracted files
        const extractedBinary = resolve(extractDir, 'cloudflared');
        const targetPath = resolve(binDir, 'cloudflared');
        // Move to bin directory
        execSync(`mv "${extractedBinary}" "${targetPath}"`, { stdio: 'inherit' });
        // Cleanup
        execSync(`rm -rf "${tarPath}" "${extractDir}"`, { stdio: 'ignore' });
        // Make executable
        chmodSync(targetPath, 0o755);
        return targetPath;
    }
    else if (plat === 'linux') {
        // Linux - download binary directly
        const targetPath = resolve(binDir, 'cloudflared');
        execSync(`curl -L "${downloadUrl}" -o "${targetPath}"`, { stdio: 'inherit' });
        chmodSync(targetPath, 0o755);
        return targetPath;
    }
    else if (plat === 'win32') {
        // Windows - download .exe
        const targetPath = resolve(binDir, 'cloudflared.exe');
        execSync(`curl -L "${downloadUrl}" -o "${targetPath}"`, { stdio: 'inherit' });
        return targetPath;
    }
    throw new Error(`Unsupported platform: ${plat}`);
}
/**
 * Ensure cloudflared is installed and return the path to the binary
 */
async function ensureCloudflared(silent = false) {
    // Check if already installed
    const existing = checkExistingInstallation();
    if (existing) {
        if (!silent) {
            console.log(`✅ cloudflared found: ${existing}`);
        }
        return existing;
    }
    if (!silent) {
        console.log('📦 cloudflared not found, installing...');
    }
    try {
        const path = await downloadCloudflared();
        if (!silent) {
            console.log(`✅ cloudflared installed to: ${path}`);
        }
        return path;
    }
    catch (error) {
        console.error('❌ Failed to install cloudflared:', error);
        throw new Error('Failed to install cloudflared. Please install manually: https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/installation/');
    }
}

// src/lib/selection/hmr-proxy-script.ts
var SELECTION_SCRIPT = `
(function() {

  // Selection state - DORMANT by default
  let isInspectorActive = false;
  let inspectorStyle = null;
  let highlightedElement = null;
  let highlightOverlay = null;
  let mouseHandler = null;
  let clickHandler = null;

  function getProxyPrefix() {
    try {
      var parts = window.location.pathname.split('/').filter(Boolean);
      var projectsIndex = parts.indexOf('projects');
      if (projectsIndex === -1) {
        return null;
      }

      var projectId = parts[projectsIndex + 1];
      if (!projectId) {
        return null;
      }

      return '/api/projects/' + projectId + '/proxy?path=';
    } catch (error) {
      console.warn('\u26A0\uFE0F [Hatchway CSS] Unable to derive proxy prefix:', error);
      return null;
    }
  }

  var proxyPrefix = getProxyPrefix();

  function rewriteStylesheetHref(link) {
    if (!proxyPrefix || !link) {
      return false;
    }

    var href = link.getAttribute('href');
    if (!href) {
      return false;
    }

    var trimmed = href.trim();

    if (
      trimmed.indexOf('proxy?path=') !== -1 ||
      trimmed.indexOf('http://') === 0 ||
      trimmed.indexOf('https://') === 0 ||
      trimmed.indexOf('//') === 0 ||
      trimmed.indexOf('data:') === 0
    ) {
      return false;
    }

    if (trimmed.charAt(0) === '/') {
      var proxiedHref = proxyPrefix + encodeURIComponent(trimmed);
      if (link.getAttribute('href') !== proxiedHref) {
        link.setAttribute('href', proxiedHref);
        console.log('\u{1F3A8} [Hatchway CSS] rewrote stylesheet href to proxy:', proxiedHref);
        return true;
      }
    }

    return false;
  }

  // Debug helper: track when stylesheets are added/loaded inside the iframe
  function monitorStylesheets() {
    const loggedLinks = new WeakSet();
    const loggedStyles = new WeakSet();

    const logLink = (link, phase) => {
      if (!link) return;
      const href = link.getAttribute('href') || '(no href)';
      console.log('\u{1F3A8} [Hatchway CSS]', phase + ' stylesheet:', href);
    };

    const logStyle = (style, phase) => {
      if (!style) return;
      const sample = (style.textContent || '')
        .replace(/s+/g, ' ')
        .trim()
        .slice(0, 140);
      console.log('\u{1F3A8} [Hatchway CSS]', phase + ' inline style', sample);
    };

    const attachLinkListeners = (link, phase) => {
      if (!link || loggedLinks.has(link)) return;
      loggedLinks.add(link);
      const rewritten = rewriteStylesheetHref(link);
      const phaseLabel = rewritten ? phase + ' (rewritten)' : phase;
      logLink(link, phaseLabel);

      link.addEventListener(
        'load',
        () => {
          rewriteStylesheetHref(link);
          logLink(link, 'loaded');
        },
        { once: true }
      );

      link.addEventListener(
        'error',
        () => {
          rewriteStylesheetHref(link);
          logLink(link, 'error loading');
        },
        { once: true }
      );
    };

    const recordStyleElement = (style, phase) => {
      if (!style || loggedStyles.has(style)) return;
      loggedStyles.add(style);
      logStyle(style, phase);
    };

    // Observe new link/style nodes appended to the document
    const observer = new MutationObserver((mutations) => {
      mutations.forEach((mutation) => {
        mutation.addedNodes.forEach((node) => {
          if (!(node instanceof HTMLElement)) return;

          if (node.tagName === 'LINK' && (node.getAttribute('rel') || '').includes('stylesheet')) {
            attachLinkListeners(node, 'added');
          }

          if (node.tagName === 'STYLE') {
            recordStyleElement(node, 'added');
          }
        });
      });
    });

    try {
      const head = document.head || document.documentElement;
      if (head) {
        observer.observe(head, { childList: true, subtree: true });
      }
    } catch (e) {
      console.warn('\u26A0\uFE0F [Hatchway CSS] Failed to observe stylesheet mutations:', e);
    }

    // Log existing stylesheet/link elements when script runs
    document
      .querySelectorAll('link[rel~="stylesheet"], style')
      .forEach((node) => {
        if (node.tagName === 'LINK') {
          attachLinkListeners(node, 'existing');
        } else if (node.tagName === 'STYLE') {
          recordStyleElement(node, 'existing');
        }
      });

    // Capture late load events (when link is in DOM before listener added)
    document.addEventListener(
      'load',
      (event) => {
        const target = event.target;
        if (
          target instanceof HTMLLinkElement &&
          (target.getAttribute('rel') || '').includes('stylesheet')
        ) {
          attachLinkListeners(target, 'load event');
        }
      },
      true
    );
  }

  monitorStylesheets();

  // Create highlight overlay
  function createHighlightOverlay() {
    if (highlightOverlay) return highlightOverlay;

    const overlay = document.createElement('div');
    overlay.id = '__hatchway-highlight';
    overlay.style.cssText = \`
      position: absolute;
      pointer-events: none;
      border: 2px solid #7553FF;
      background: rgba(117, 83, 255, 0.1);
      z-index: 999999;
      transition: all 0.1s ease;
      box-shadow: 0 0 0 1px rgba(117, 83, 255, 0.3), 0 0 20px rgba(117, 83, 255, 0.4);
    \`;
    document.body.appendChild(overlay);
    highlightOverlay = overlay;
    return overlay;
  }

  // Remove highlight overlay
  function removeHighlightOverlay() {
    if (highlightOverlay) {
      highlightOverlay.remove();
      highlightOverlay = null;
    }
  }

  // Highlight element on hover
  function highlightElement(element) {
    if (!element || !isInspectorActive) {
      removeHighlightOverlay();
      return;
    }

    const rect = element.getBoundingClientRect();
    const overlay = createHighlightOverlay();

    overlay.style.left = rect.left + window.scrollX + 'px';
    overlay.style.top = rect.top + window.scrollY + 'px';
    overlay.style.width = rect.width + 'px';
    overlay.style.height = rect.height + 'px';

    highlightedElement = element;
  }

  // Generate unique CSS selector for element
  function generateSelector(element) {
    // Strategy 1: data-testid (best)
    const testId = element.getAttribute('data-testid');
    if (testId) {
      return \`[data-testid="\${testId}"]\`;
    }

    // Strategy 2: ID (good)
    if (element.id) {
      return \`#\${element.id}\`;
    }

    // Strategy 3: Class + tag (ok) - but skip classes with colons (Tailwind responsive)
    const classes = Array.from(element.classList)
      .filter(c => !c.match(/^(hover:|focus:|active:|group-|animate-|transition-)/))
      .filter(c => !c.includes(':')) // Skip Tailwind responsive classes
      .slice(0, 3) // Limit to first 3 classes
      .join('.');

    if (classes) {
      const tagName = element.tagName.toLowerCase();

      try {
        // Check if unique enough
        const selector = \`\${tagName}.\${classes}\`;
        const matches = document.querySelectorAll(selector);

        if (matches.length === 1) {
          return selector;
        }

        // Add nth-child if multiple matches
        const parent = element.parentElement;
        if (parent) {
          const siblings = Array.from(parent.children);
          const index = siblings.indexOf(element) + 1;
          return \`\${selector}:nth-child(\${index})\`;
        }

        return selector;
      } catch (err) {
        console.warn('Invalid selector, falling back to path:', err);
      }
    }

    // Strategy 4: Full path (fallback)
    return getFullPath(element);
  }

  // Get full CSS path to element
  function getFullPath(element) {
    const path = [];
    let current = element;

    while (current && current !== document.body) {
      let selector = current.tagName.toLowerCase();

      if (current.id) {
        selector += \`#\${current.id}\`;
        path.unshift(selector);
        break;
      }

      const parent = current.parentElement;
      if (parent) {
        const siblings = Array.from(parent.children).filter(
          child => child.tagName === current.tagName
        );

        if (siblings.length > 1) {
          const index = siblings.indexOf(current) + 1;
          selector += \`:nth-of-type(\${index})\`;
        }
      }

      path.unshift(selector);
      current = current.parentElement;
    }

    return path.join(' > ');
  }

  // Capture element data and click position
  function captureElementData(element, clickEvent) {
    const rect = element.getBoundingClientRect();

    return {
      selector: generateSelector(element),
      tagName: element.tagName.toLowerCase(),
      className: element.className,
      id: element.id,
      textContent: element.textContent?.trim().slice(0, 100),
      innerHTML: element.innerHTML?.slice(0, 200),
      attributes: Array.from(element.attributes).reduce((acc, attr) => {
        acc[attr.name] = attr.value;
        return acc;
      }, {}),
      boundingRect: {
        top: rect.top,
        left: rect.left,
        width: rect.width,
        height: rect.height,
      },
      clickPosition: {
        x: clickEvent.clientX,
        y: clickEvent.clientY,
      },
      computedStyles: {
        backgroundColor: window.getComputedStyle(element).backgroundColor,
        color: window.getComputedStyle(element).color,
        fontSize: window.getComputedStyle(element).fontSize,
        fontFamily: window.getComputedStyle(element).fontFamily,
      }
    };
  }

  // Mouse move handler (hover preview)
  function handleMouseMove(e) {
    if (!isInspectorActive) return;

    const element = e.target;
    if (element && element !== highlightedElement) {
      highlightElement(element);
    }
  }

  // Click handler (select element)
  function handleClick(e) {
    console.log('\u{1F5B1}\uFE0F Click detected, selection mode:', isInspectorActive);

    if (!isInspectorActive) return;

    e.preventDefault();
    e.stopPropagation();

    const element = e.target;
    const data = captureElementData(element, e);

    console.log('\u{1F3AF} Element captured:', data);
    console.log('   Click position:', data.clickPosition);
    console.log('\u{1F4E4} Sending postMessage to parent...');

    // Send to parent window
    window.parent.postMessage({
      type: 'hatchway:element-selected',
      data,
    }, '*');

    console.log('\u2705 Message sent to parent');

    // Disable selection mode after selection
    setInspectorActive(false);
  }

  // Activate/deactivate inspector (DORMANT PATTERN)
  function setInspectorActive(active) {
    isInspectorActive = active;

    if (active) {
      // Add inspector styles ONLY when activated
      if (!inspectorStyle) {
        inspectorStyle = document.createElement('style');
        inspectorStyle.textContent = \`
          .inspector-active * {
            cursor: crosshair !important;
          }
          .inspector-highlight {
            outline: 2px solid #7553FF !important;
            outline-offset: -2px !important;
            background-color: rgba(117, 83, 255, 0.1) !important;
          }
        \`;
        document.head.appendChild(inspectorStyle);
      }

      document.body.classList.add('inspector-active');

      // Add event listeners ONLY when activated
      if (!mouseHandler) {
        mouseHandler = handleMouseMove;
        clickHandler = handleClick;
        document.addEventListener('mousemove', mouseHandler, true);
        document.addEventListener('click', clickHandler, true);
        console.log('\u2705 Inspector event listeners attached');
      }
    } else {
      document.body.classList.remove('inspector-active');

      // Remove highlight
      if (highlightedElement) {
        highlightedElement = null;
      }
      removeHighlightOverlay();

      // Remove event listeners when deactivated
      if (mouseHandler) {
        document.removeEventListener('mousemove', mouseHandler, true);
        document.removeEventListener('click', clickHandler, true);
        mouseHandler = null;
        clickHandler = null;
        console.log('\u{1F9F9} Inspector event listeners removed');
      }

      // Remove styles
      if (inspectorStyle) {
        inspectorStyle.remove();
        inspectorStyle = null;
      }
    }

  }

  // Listen for activation/deactivation from parent
  window.addEventListener('message', (e) => {
    if (e.data.type === 'hatchway:toggle-selection-mode') {
      setInspectorActive(e.data.enabled);
    }
  });

  // Announce ready to parent
  window.parent.postMessage({ type: 'hatchway:ready' }, '*');
})();
`;

/**
 * Injection Proxy for Remote Runner Support
 *
 * This proxy sits between the dev server and the Cloudflare tunnel,
 * injecting the element selection script into HTML responses.
 *
 * This enables the "select element" feature to work when:
 * - Frontend is hosted remotely (e.g., Vercel)
 * - Runner is running locally on user's machine
 * - Traffic flows through Cloudflare tunnel
 *
 * Without this proxy, the selection script can't be injected because
 * the iframe loads from a different origin (the tunnel URL).
 */
const DEFAULT_PROXY_PORT = 4000;
/**
 * Create an injection proxy that forwards requests to a dev server
 * while injecting the selection script into HTML responses.
 */
async function createInjectionProxy(options) {
    const { targetPort, proxyPort = DEFAULT_PROXY_PORT, onError, log = console.log } = options;
    const targetUrl = `http://localhost:${targetPort}`;
    // Create proxy with WebSocket support
    const proxy = httpProxy.createProxyServer({
        target: targetUrl,
        ws: true,
        selfHandleResponse: true, // We need to modify HTML responses
        changeOrigin: true,
    });
    const server = http.createServer((req, res) => {
        // Forward the request through proxy
        proxy.web(req, res, {}, (err) => {
            if (err) {
                onError?.(err);
                // Try to send error response if headers not sent
                if (!res.headersSent) {
                    res.writeHead(502, { 'Content-Type': 'text/plain' });
                    res.end(`Proxy error: ${err.message}`);
                }
            }
        });
    });
    // Handle WebSocket upgrades (critical for HMR in Vite/Next.js/etc)
    server.on('upgrade', (req, socket, head) => {
        proxy.ws(req, socket, head, {}, (err) => {
            if (err) {
                onError?.(err);
                socket.destroy();
            }
        });
    });
    // Intercept responses to inject script into HTML
    proxy.on('proxyRes', (proxyRes, req, res) => {
        const contentType = proxyRes.headers['content-type'] || '';
        const isHtml = contentType.includes('text/html');
        if (!isHtml) {
            // Pass through non-HTML responses unchanged
            // Copy all headers
            res.writeHead(proxyRes.statusCode || 200, proxyRes.headers);
            proxyRes.pipe(res);
            return;
        }
        // Handle HTML - collect chunks and inject script
        const chunks = [];
        const encoding = proxyRes.headers['content-encoding'];
        proxyRes.on('data', (chunk) => {
            chunks.push(chunk);
        });
        proxyRes.on('end', () => {
            try {
                let body = Buffer.concat(chunks);
                // Decompress if needed
                if (encoding === 'gzip') {
                    try {
                        body = zlib.gunzipSync(body);
                    }
                    catch {
                        // If decompression fails, pass through unchanged
                        const headers = { ...proxyRes.headers };
                        res.writeHead(proxyRes.statusCode || 200, headers);
                        res.end(Buffer.concat(chunks));
                        return;
                    }
                }
                else if (encoding === 'deflate') {
                    try {
                        body = zlib.inflateSync(body);
                    }
                    catch {
                        const headers = { ...proxyRes.headers };
                        res.writeHead(proxyRes.statusCode || 200, headers);
                        res.end(Buffer.concat(chunks));
                        return;
                    }
                }
                else if (encoding === 'br') {
                    try {
                        body = zlib.brotliDecompressSync(body);
                    }
                    catch {
                        const headers = { ...proxyRes.headers };
                        res.writeHead(proxyRes.statusCode || 200, headers);
                        res.end(Buffer.concat(chunks));
                        return;
                    }
                }
                let html = body.toString('utf-8');
                // Inject selection script before </body> or </html> or at end
                const scriptTag = `<script>${SELECTION_SCRIPT}</script>`;
                if (html.toLowerCase().includes('</body>')) {
                    html = html.replace(/<\/body>/i, `${scriptTag}</body>`);
                }
                else if (html.toLowerCase().includes('</html>')) {
                    html = html.replace(/<\/html>/i, `${scriptTag}</html>`);
                }
                else {
                    // Fallback: append at end
                    html += scriptTag;
                }
                // Prepare response
                const responseBody = Buffer.from(html, 'utf-8');
                // Copy headers, update content-length, remove encoding (we decompressed)
                const headers = { ...proxyRes.headers };
                delete headers['content-encoding'];
                delete headers['transfer-encoding'];
                headers['content-length'] = responseBody.length;
                res.writeHead(proxyRes.statusCode || 200, headers);
                res.end(responseBody);
            }
            catch (err) {
                onError?.(err instanceof Error ? err : new Error(String(err)));
                // On error, try to pass through original response
                if (!res.headersSent) {
                    res.writeHead(proxyRes.statusCode || 500, proxyRes.headers);
                    res.end(Buffer.concat(chunks));
                }
            }
        });
        proxyRes.on('error', (err) => {
            onError?.(err);
            if (!res.headersSent) {
                res.writeHead(502, { 'Content-Type': 'text/plain' });
                res.end(`Proxy response error: ${err.message}`);
            }
        });
    });
    // Handle proxy errors
    proxy.on('error', (err, req, res) => {
        onError?.(err);
        if (res && 'writeHead' in res && !res.headersSent) {
            res.writeHead(502, { 'Content-Type': 'text/plain' });
            res.end(`Proxy error: ${err.message}`);
        }
    });
    // Start server and return promise
    return new Promise((resolve, reject) => {
        server.on('error', (err) => {
            if (err.code === 'EADDRINUSE') {
                reject(new Error(`Injection proxy port ${proxyPort} is already in use`));
            }
            else {
                reject(err);
            }
        });
        server.listen(proxyPort, '127.0.0.1', () => {
            log(`[injection-proxy] Started on port ${proxyPort} → localhost:${targetPort}`);
            resolve({
                port: proxyPort,
                server,
                close: () => new Promise((resolveClose) => {
                    // Add timeout to prevent hanging if connections don't close
                    const CLOSE_TIMEOUT_MS = 2000;
                    let resolved = false;
                    const timeoutId = setTimeout(() => {
                        if (!resolved) {
                            resolved = true;
                            log(`[injection-proxy] Force closing after ${CLOSE_TIMEOUT_MS}ms timeout`);
                            // Force destroy the server if it hasn't closed
                            try {
                                server.closeAllConnections?.();
                            }
                            catch {
                                // closeAllConnections may not be available in older Node versions
                            }
                            resolveClose();
                        }
                    }, CLOSE_TIMEOUT_MS);
                    proxy.close(() => {
                        server.close(() => {
                            if (!resolved) {
                                resolved = true;
                                clearTimeout(timeoutId);
                                log(`[injection-proxy] Stopped`);
                            }
                            resolveClose();
                        });
                    });
                }),
            });
        });
    });
}
/**
 * Check if a port is available
 */
async function isPortAvailable(port) {
    return new Promise((resolve) => {
        const server = http.createServer();
        server.once('error', () => resolve(false));
        server.once('listening', () => {
            server.close(() => resolve(true));
        });
        server.listen(port, '127.0.0.1');
    });
}
/**
 * Find an available port starting from the given port
 */
async function findAvailablePort(startPort, maxAttempts = 10) {
    for (let i = 0; i < maxAttempts; i++) {
        const port = startPort + i;
        if (await isPortAvailable(port)) {
            return port;
        }
    }
    throw new Error(`No available port found starting from ${startPort}`);
}

/** Default port for the injection proxy */
const DEFAULT_INJECTION_PROXY_PORT = 4000;
class TunnelManager extends EventEmitter {
    constructor() {
        super(...arguments);
        this.tunnels = new Map();
        this.cloudflaredPath = null;
        this.silent = false; // Suppress console output
    }
    /**
     * Set silent mode (for TUI)
     */
    setSilent(silent) {
        this.silent = silent;
    }
    /**
     * Conditional logging
     */
    log(...args) {
        if (!this.silent) {
            console.log(...args);
        }
    }
    /**
     * Create a tunnel for a specific port
     * Returns the public tunnel URL
     *
     * The tunnel is created through an injection proxy that adds the element
     * selection script to HTML responses. This enables the "select element"
     * feature to work when the frontend is hosted remotely.
     */
    async createTunnel(port, maxRetries = 5) {
        // Check if tunnel already exists for this port
        if (this.tunnels.has(port)) {
            const existing = this.tunnels.get(port);
            this.log(`🔗 Tunnel already exists for port ${port}: ${existing.url}`);
            return existing.url;
        }
        // Ensure cloudflared is installed
        if (!this.cloudflaredPath) {
            this.cloudflaredPath = await ensureCloudflared(this.silent);
        }
        // Step 1: Start injection proxy
        // This proxy injects the element selection script into HTML responses
        let injectionProxy;
        let proxyPort = DEFAULT_INJECTION_PROXY_PORT;
        try {
            // Find an available port for the proxy
            proxyPort = await findAvailablePort(DEFAULT_INJECTION_PROXY_PORT);
            injectionProxy = await createInjectionProxy({
                targetPort: port,
                proxyPort,
                onError: (err) => this.log(`[injection-proxy] Error: ${err.message}`),
                log: (...args) => this.log(...args),
            });
            this.log(`✅ Injection proxy started: localhost:${proxyPort} → localhost:${port}`);
        }
        catch (err) {
            // Fallback: tunnel directly to dev server (selection won't work but preview will)
            this.log(`⚠️  Injection proxy failed to start: ${err instanceof Error ? err.message : String(err)}`);
            this.log(`⚠️  Falling back to direct tunnel (element selection will not work)`);
            proxyPort = port; // Fall back to direct connection
        }
        // Step 2: Create tunnel to proxy port (or dev server if proxy failed)
        const tunnelTargetPort = injectionProxy ? proxyPort : port;
        // Try creating tunnel with smart retries
        const errors = [];
        for (let attempt = 1; attempt <= maxRetries; attempt++) {
            try {
                return await this._createTunnelAttempt(port, tunnelTargetPort, injectionProxy);
            }
            catch (error) {
                const errorMsg = error?.message || String(error);
                errors.push(errorMsg);
                console.error(`Tunnel creation attempt ${attempt}/${maxRetries} failed:`, errorMsg);
                // Check if this is a permanent error (fail fast)
                if (this._isPermanentError(errorMsg)) {
                    // Clean up injection proxy on permanent failure
                    if (injectionProxy) {
                        await injectionProxy.close().catch(() => { });
                    }
                    throw new Error(`Permanent failure: ${errorMsg}`);
                }
                if (attempt === maxRetries) {
                    // Clean up injection proxy on final failure
                    if (injectionProxy) {
                        await injectionProxy.close().catch(() => { });
                    }
                    throw new Error(`Failed to create tunnel after ${maxRetries} attempts: ${errors.join('; ')}`);
                }
                // Exponential backoff with jitter to prevent thundering herd
                const baseDelay = Math.pow(2, attempt) * 1000; // 2s, 4s, 8s, 16s
                const jitter = Math.random() * 1000; // 0-1s random jitter
                const delay = baseDelay + jitter;
                this.log(`Retrying in ${Math.round(delay)}ms...`);
                await new Promise(resolve => setTimeout(resolve, delay));
            }
        }
        // Clean up injection proxy on failure
        if (injectionProxy) {
            await injectionProxy.close().catch(() => { });
        }
        throw new Error('Tunnel creation failed after all retries');
    }
    /**
     * Check if an error is permanent (no point retrying)
     */
    _isPermanentError(errorMsg) {
        const permanentErrors = [
            'port already in use',
            'cloudflared not found',
            'permission denied',
            'cannot find',
            'enoent',
            'eacces',
        ];
        const lowerMsg = errorMsg.toLowerCase();
        return permanentErrors.some(err => lowerMsg.includes(err));
    }
    /**
     * Extract tunnel URL from cloudflared output
     */
    _extractTunnelUrl(output) {
        // Format: "Your quick Tunnel has been created! Visit it at: https://xxx.trycloudflare.com"
        // Or just: "https://xxx.trycloudflare.com"
        const match = output.match(/https:\/\/[a-zA-Z0-9-]+\.trycloudflare\.com/);
        return match ? match[0] : null;
    }
    /**
     * Verify tunnel is actually responding (async, non-blocking)
     * This runs in background and only logs results
     */
    async _verifyTunnelReady(url, maxWaitMs = 15000) {
        const startTime = Date.now();
        const checkInterval = 1000; // Check every 1 second (less aggressive)
        this.log(`🔍 [Background] Verifying tunnel is ready: ${url}`);
        while (Date.now() - startTime < maxWaitMs) {
            try {
                const controller = new AbortController();
                const timeoutId = setTimeout(() => controller.abort(), 2000);
                const response = await fetch(url, {
                    method: 'HEAD',
                    signal: controller.signal,
                });
                clearTimeout(timeoutId);
                // Any response (even errors) means tunnel is connected
                // We just need to verify it's resolving and routing
                if (response.status < 500 || response.ok) {
                    const elapsed = Date.now() - startTime;
                    this.log(`✅ [Background] Tunnel verified in ${elapsed}ms`);
                    return true;
                }
            }
            catch (error) {
                // Expected while DNS propagates or tunnel initializes
                // Will keep retrying silently
            }
            await new Promise(resolve => setTimeout(resolve, checkInterval));
        }
        this.log(`⏱️  [Background] Verification timeout after ${maxWaitMs}ms (tunnel may still work)`);
        return false;
    }
    /**
     * Single attempt to create a tunnel
     * @param devServerPort - The original dev server port (used as key in tunnels map)
     * @param tunnelTargetPort - The port to tunnel to (proxy port or dev server port if proxy failed)
     * @param injectionProxy - Optional injection proxy instance for cleanup
     */
    async _createTunnelAttempt(devServerPort, tunnelTargetPort, injectionProxy) {
        return new Promise((resolve, reject) => {
            const isUsingProxy = tunnelTargetPort !== devServerPort;
            this.log(`[tunnel] Creating tunnel for port ${tunnelTargetPort}${isUsingProxy ? ` (proxy for dev server on ${devServerPort})` : ''}...`);
            // Direct binary execution with unbuffered streams
            const proc = spawn(this.cloudflaredPath, [
                'tunnel',
                '--url', `http://localhost:${tunnelTargetPort}`,
                '--no-autoupdate',
            ], {
                cwd: process.cwd(),
                stdio: ['ignore', 'pipe', 'pipe'],
            });
            // Set streams to unbuffered mode immediately for responsive output
            if (proc.stdout) {
                proc.stdout.setEncoding('utf8');
                proc.stdout.resume();
            }
            if (proc.stderr) {
                proc.stderr.setEncoding('utf8');
                proc.stderr.resume();
            }
            this.log(`[tunnel] Cloudflared spawned with PID: ${proc.pid}`);
            let resolved = false;
            let tunnelUrl = null;
            let tunnelRegistered = false;
            const timeout = setTimeout(() => {
                if (!resolved) {
                    proc.kill();
                    reject(new Error('Tunnel creation timeout (30s)'));
                }
            }, 30000);
            // Shared handler for both stdout and stderr
            const handleOutput = async (data) => {
                const output = data.toString();
                // Step 1: Extract URL
                if (!tunnelUrl) {
                    const url = this._extractTunnelUrl(output);
                    if (url) {
                        tunnelUrl = url;
                        this.log(`✅ Tunnel URL received: ${url} → localhost:${tunnelTargetPort}${isUsingProxy ? ` → localhost:${devServerPort}` : ''}`);
                        // Store with dev server port as key, but include proxy info
                        this.tunnels.set(devServerPort, {
                            url,
                            port: devServerPort,
                            proxyPort: tunnelTargetPort,
                            process: proc,
                            injectionProxy,
                        });
                    }
                }
                // Step 2: Wait for tunnel registration (edge connection established)
                if (tunnelUrl && !tunnelRegistered && output.includes('Registered tunnel connection')) {
                    tunnelRegistered = true;
                    this.log(`✅ Tunnel registered with Cloudflare edge (DNS propagating)`);
                    // Wait 3 seconds after registration for DNS to propagate
                    this.log(`⏳ Waiting 3 seconds for DNS to fully propagate...`);
                    await new Promise(r => setTimeout(r, 3000));
                    // Step 3: Return only after registration + 3 second buffer
                    if (!resolved) {
                        resolved = true;
                        clearTimeout(timeout);
                        this.log(`✅ Tunnel ready: ${tunnelUrl}`);
                        if (isUsingProxy) {
                            this.log(`✅ Element selection enabled via injection proxy`);
                        }
                        // Note: Backend verification skipped for localhost tunnels
                        // The tunnel connects localhost to Cloudflare - backend can't verify it
                        // Frontend will verify DNS before loading in iframe
                        resolve(tunnelUrl);
                    }
                }
            };
            proc.stdout.on('data', handleOutput);
            proc.stderr.on('data', (data) => {
                const output = data.toString();
                // Log errors (cloudflared uses stderr for all output)
                // Only show real errors, not shutdown messages
                const lower = output.toLowerCase();
                if ((lower.includes('error') || lower.includes('fatal')) &&
                    !lower.includes('context canceled') &&
                    !lower.includes('connection terminated') &&
                    !lower.includes('no more connections active')) {
                    this.log(`[cloudflared:${devServerPort}] ${output.trim()}`);
                }
                // Check for tunnel URL in stderr too
                handleOutput(data);
            });
            proc.on('exit', (code, signal) => {
                this.log(`Tunnel exited for port ${devServerPort} with code ${code} signal ${signal}`);
                // Clean up injection proxy when tunnel exits
                const tunnel = this.tunnels.get(devServerPort);
                if (tunnel?.injectionProxy) {
                    tunnel.injectionProxy.close().catch(() => { });
                }
                this.tunnels.delete(devServerPort);
                this.emit('tunnel-closed', devServerPort);
            });
            proc.on('error', (error) => {
                if (!resolved) {
                    clearTimeout(timeout);
                    reject(error);
                }
            });
        });
    }
    /**
     * Close tunnel for a specific port
     */
    async closeTunnel(port) {
        const tunnel = this.tunnels.get(port);
        if (!tunnel) {
            this.log(`No tunnel found for port ${port}`);
            return;
        }
        this.log(`🔗 Closing tunnel for port ${port}...`);
        // Close injection proxy first (if exists) with timeout
        if (tunnel.injectionProxy) {
            try {
                // Use Promise.race to enforce timeout - injection proxy close can hang
                // if there are active HTTP keep-alive connections
                const PROXY_CLOSE_TIMEOUT_MS = 3000;
                await Promise.race([
                    tunnel.injectionProxy.close(),
                    new Promise((resolve) => setTimeout(() => {
                        this.log(`⚠️  Injection proxy close timed out after ${PROXY_CLOSE_TIMEOUT_MS}ms, continuing...`);
                        resolve();
                    }, PROXY_CLOSE_TIMEOUT_MS))
                ]);
                this.log(`✅ Injection proxy closed for port ${port}`);
            }
            catch (err) {
                this.log(`⚠️  Error closing injection proxy: ${err instanceof Error ? err.message : String(err)}`);
            }
        }
        return new Promise((resolve) => {
            const timeout = setTimeout(() => {
                // Force kill if not dead after 1 second
                if (!tunnel.process.killed) {
                    tunnel.process.kill('SIGKILL');
                }
                this.tunnels.delete(port);
                resolve();
            }, 1000);
            tunnel.process.once('exit', () => {
                clearTimeout(timeout);
                this.tunnels.delete(port);
                resolve();
            });
            // Send SIGTERM
            tunnel.process.kill('SIGTERM');
        });
    }
    /**
     * Close all active tunnels
     */
    async closeAll() {
        this.log(`🔗 Closing ${this.tunnels.size} active tunnel(s)...`);
        const ports = Array.from(this.tunnels.keys());
        await Promise.all(ports.map(port => this.closeTunnel(port)));
    }
    /**
     * Get tunnel URL for a specific port (if exists)
     */
    getTunnelUrl(port) {
        const tunnel = this.tunnels.get(port);
        return tunnel ? tunnel.url : null;
    }
    /**
     * Get all active tunnels
     */
    getActiveTunnels() {
        return Array.from(this.tunnels.values()).map(({ port, url }) => ({ port, url }));
    }
}
// Singleton instance
const tunnelManager = new TunnelManager();

export { TunnelManager, tunnelManager };
//# sourceMappingURL=manager-DjVI7erc.js.map
