import { spawnSync } from 'node:child_process';
import fs from 'node:fs';
import path3 from 'node:path';
import { builtinModules } from 'node:module';

// src/features/gen-html/infrastructure/local-server/storybook-config.ts

// src/features/retouch/domain/constants.ts
var RETOUCH_PORT_CANDIDATES = [4196, 4197, 4198, 4199];
var RETOUCH_CHANNEL_PICK_START = "juhonge/retouch/pick-start";
var RETOUCH_CHANNEL_PICK_RESULT = "juhonge/retouch/pick-result";
var RETOUCH_CHANNEL_PICK_CANCEL = "juhonge/retouch/pick-cancel";
var RETOUCH_CHANNEL_CLEAR_TARGET = "juhonge/retouch/clear-target";
var RETOUCH_CHANNEL_FLASH_TARGET = "juhonge/retouch/flash-target";
var RETOUCH_RESULT_MARKER = "__RETOUCH_RESULT__";
var RETOUCH_ERROR_MARKER = "__RETOUCH_ERROR__";
function normalizeOutputRelativePath(outputDirectory, filePath) {
  return path3.relative(outputDirectory, filePath).split(path3.sep).join("/");
}

// src/features/gen-html/domain/storybook.ts
var STORY_FILE_PATTERN = /\.stories\.[cm]?[jt]sx?$/i;
function isGenHtmlStoryFilePath(filePath) {
  return STORY_FILE_PATTERN.test(filePath);
}

// src/features/gen-html/infrastructure/runtime.ts
function listOutputFiles(outputDirectory) {
  if (!fs.existsSync(outputDirectory) || !fs.statSync(outputDirectory).isDirectory()) {
    return [];
  }
  const files = [];
  const visitDirectory = (directoryPath) => {
    const entries = fs.readdirSync(directoryPath, { withFileTypes: true }).sort((left, right) => left.name.localeCompare(right.name));
    for (const entry of entries) {
      const entryPath = path3.join(directoryPath, entry.name);
      if (entry.isDirectory()) {
        visitDirectory(entryPath);
        continue;
      }
      if (entry.isFile()) {
        files.push(normalizeOutputRelativePath(outputDirectory, entryPath));
      }
    }
  };
  visitDirectory(outputDirectory);
  return files.sort((left, right) => left.localeCompare(right));
}
function isGenHtmlPreviewDirectoryCandidate(outputDirectory) {
  return listOutputFiles(outputDirectory).some((filePath) => isGenHtmlStoryFilePath(filePath));
}
new Set(
  builtinModules.flatMap((moduleName) => [moduleName, moduleName.replace(/^node:/, ""), `node:${moduleName.replace(/^node:/, "")}`])
);
function getLocalServerPaths(paths) {
  const directoryPath = path3.join(paths.baseDir, "local-server");
  const generatedDirectoryPath = path3.join(directoryPath, "generated");
  const multipleDirectoryPath = path3.join(directoryPath, "multiple");
  const storybookConfigDirectoryPath = path3.join(directoryPath, ".storybook");
  return {
    directoryPath,
    generatedDirectoryPath,
    generatedRegistryPath: path3.join(directoryPath, "generated-registry.json"),
    multipleDirectoryPath,
    logPath: path3.join(directoryPath, "server.log"),
    packageJsonPath: path3.join(directoryPath, "package.json"),
    packageCustomJsonPath: path3.join(directoryPath, "package-custom.json"),
    previewTargetPath: path3.join(directoryPath, "preview-target.json"),
    refreshLockPath: path3.join(directoryPath, "refresh.lock"),
    scssModuleDeclarationPath: path3.join(directoryPath, "scss.d.ts"),
    serverLockPath: path3.join(directoryPath, "server.lock"),
    statePath: path3.join(directoryPath, "server-state.json"),
    storybookConfigDirectoryPath,
    storybookMainConfigPath: path3.join(storybookConfigDirectoryPath, "main.ts"),
    storybookManagerConfigPath: path3.join(storybookConfigDirectoryPath, "manager.tsx"),
    storybookPreviewConfigPath: path3.join(storybookConfigDirectoryPath, "preview.ts"),
    tsconfigPath: path3.join(directoryPath, "tsconfig.json")
  };
}
function writeJsonFile(filePath, value) {
  fs.mkdirSync(path3.dirname(filePath), { recursive: true });
  fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}
`, "utf8");
}

// src/features/gen-html/infrastructure/local-server/storybook-config.ts
function listGeneratedOutputDirectories(paths) {
  const { generatedDirectoryPath } = getLocalServerPaths(paths);
  if (!fs.existsSync(generatedDirectoryPath) || !fs.statSync(generatedDirectoryPath).isDirectory()) {
    return [];
  }
  return fs.readdirSync(generatedDirectoryPath, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => path3.join(generatedDirectoryPath, entry.name)).sort((left, right) => left.localeCompare(right));
}
function resolveValidGeneratedPreviewDirectories(paths) {
  return listGeneratedOutputDirectories(paths).filter((directoryPath) => isGenHtmlPreviewDirectoryCandidate(directoryPath));
}
function setLocalServerPreviewTarget(paths) {
  const localServerPaths = getLocalServerPaths(paths);
  const validDirectories = resolveValidGeneratedPreviewDirectories(paths);
  const nextTarget = validDirectories.length > 0 ? { directories: validDirectories } : {};
  writeJsonFile(localServerPaths.previewTargetPath, nextTarget);
}
function getStorybookExecutablePath(directoryPath) {
  return path3.join(directoryPath, "node_modules", ".bin", process.platform === "win32" ? "storybook.cmd" : "storybook");
}
function getScssModuleDeclarationContent() {
  return `declare module '*.module.scss' {
  const classes: Record<string, string>;
  export default classes;
}
`;
}
function getLocalServerTsconfigContent() {
  return `{
  "extends": "sales-frontend-typescript-config/nextjs.json",
  "compilerOptions": {
    "types": ["node"],
    "jsx": "react-jsx",
    "jsxImportSource": "react"
  },
  "include": [".storybook/**/*", "generated/**/*", "scss.d.ts"],
  "exclude": ["node_modules", "dist"]
}
`;
}
function getStorybookMainConfigContent() {
  return `import fs from 'node:fs';
import path from 'node:path';
import type { StorybookConfig } from '@storybook/react-vite';
import type { Alias } from 'vite';

const localServerRoot = path.resolve(__dirname, '..');
const localServerPackageJsonPath = path.resolve(localServerRoot, 'package.json');
const workspaceRoot = path.resolve(__dirname, '..', '..');
const dependencyAliasExcludeNames = new Set(['react', 'react-dom', 'sass', 'storybook']);
const dependencyAliasExcludePrefixes = ['@storybook/'];

function readLocalServerDependencies() {
  try {
    const raw = fs.readFileSync(localServerPackageJsonPath, 'utf8').trim();

    if (!raw) {
      return {};
    }

    const parsed = JSON.parse(raw) as { dependencies?: Record<string, string> };

    return parsed.dependencies && typeof parsed.dependencies === 'object' ? parsed.dependencies : {};
  } catch {
    return {};
  }
}

function isRecord(value: unknown): value is Record<string, unknown> {
  return typeof value === 'object' && value !== null && !Array.isArray(value);
}

function resolveExportTarget(value: unknown): string {
  if (typeof value === 'string' && value.trim()) {
    return value.trim();
  }

  if (Array.isArray(value)) {
    for (const entry of value) {
      const resolvedTarget = resolveExportTarget(entry);

      if (resolvedTarget) {
        return resolvedTarget;
      }
    }

    return '';
  }

  if (isRecord(value)) {
    return resolveExportTarget(value.import) || resolveExportTarget(value.default) || resolveExportTarget(value.require);
  }

  return '';
}

function escapeRegExp(value: string) {
  return value.replace(/[][\\\\^$.*+?(){}|]/g, '\\\\$&');
}

function buildDependencyAliases() {
  return Object.keys(readLocalServerDependencies()).reduce<Alias[]>((accumulator, dependencyName) => {
    if (dependencyAliasExcludeNames.has(dependencyName) || dependencyAliasExcludePrefixes.some((prefix) => dependencyName.startsWith(prefix))) {
      return accumulator;
    }

    const dependencyPath = path.resolve(localServerRoot, 'node_modules', ...dependencyName.split('/'));
    const dependencyPackageJsonPath = path.resolve(dependencyPath, 'package.json');

    if (fs.existsSync(dependencyPath)) {
      try {
        const dependencyPackageJson = JSON.parse(fs.readFileSync(dependencyPackageJsonPath, 'utf8')) as { exports?: Record<string, unknown> };
        const dependencyExports = isRecord(dependencyPackageJson.exports) ? dependencyPackageJson.exports : {};
        const rootExportTarget = resolveExportTarget(dependencyExports['.']);
        const rootReplacement = rootExportTarget ? path.resolve(dependencyPath, rootExportTarget) : dependencyPath;
        const dependencyNamePattern = escapeRegExp(dependencyName);

        accumulator.push({
          find: new RegExp(\`^\${dependencyNamePattern}$\`),
          replacement: rootReplacement
        });

        for (const [exportKey, exportValue] of Object.entries(dependencyExports)) {
          if (!exportKey.startsWith('./')) {
            continue;
          }

          const exportTarget = resolveExportTarget(exportValue);

          if (!exportTarget) {
            continue;
          }

          accumulator.push({
            find: new RegExp(\`^\${escapeRegExp(\`\${dependencyName}/\${exportKey.slice(2)}\`)}$\`),
            replacement: path.resolve(dependencyPath, exportTarget)
          });
        }
      } catch {
        accumulator.push({
          find: new RegExp(\`^\${escapeRegExp(dependencyName)}$\`),
          replacement: dependencyPath
        });
      }

      accumulator.push({
        find: new RegExp(\`^\${escapeRegExp(dependencyName)}/(.+)$\`),
        replacement: \`\${dependencyPath}/$1\`
      });
    }

    return accumulator;
  }, []);
}

function resolveStories() {
  return ['../generated/**/*.stories.@(js|jsx|ts|tsx)'];
}

const dependencyAliases = buildDependencyAliases();

const config: StorybookConfig = {
  stories: resolveStories(),
  addons: ['@storybook/addon-essentials'],
  framework: {
    name: '@storybook/react-vite',
    options: {}
  },
  docs: {
    autodocs: false
  },
  viteFinal: async (config) => {
    const currentAllow = Array.isArray(config.server?.fs?.allow) ? config.server.fs.allow : [];
    const currentResolveAlias = config.resolve?.alias;
    const currentResolveAliasArray = Array.isArray(currentResolveAlias)
      ? currentResolveAlias
      : Object.entries(currentResolveAlias || {}).map(([find, replacement]) => ({ find, replacement }));
    const currentEsbuild = config.esbuild && typeof config.esbuild === 'object' ? config.esbuild : {};
    const mergedAlias = [...dependencyAliases, ...currentResolveAliasArray];

    config.server = {
      ...(config.server || {}),
      fs: {
        ...(config.server?.fs || {}),
        allow: Array.from(new Set([...currentAllow, workspaceRoot]))
      }
    };
    config.resolve = {
      ...(config.resolve || {}),
      alias: mergedAlias
    };
    config.esbuild = {
      ...currentEsbuild,
      jsx: 'automatic',
      jsxImportSource: 'react'
    };

    return config;
  }
};

export default config;
`;
}
function getStorybookPreviewConfigContent() {
  return `import React from 'react';
import type { Preview } from '@storybook/react';
import { addons as previewAddons } from '@storybook/preview-api';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import 'sales-frontend-design-system/core-styles';

const queryClient = new QueryClient();

const fileNameBannerStyle: React.CSSProperties = {
  position: 'sticky',
  top: 0,
  zIndex: 2147483647,
  padding: '6px 12px',
  background: '#132128',
  color: '#c2f7ea',
  fontFamily: '"SF Mono", "JetBrains Mono", monospace',
  fontSize: '12px',
  lineHeight: 1.5,
  wordBreak: 'break-all',
  borderBottom: '1px solid rgba(194, 247, 234, 0.25)'
};

function LocalServerFileNameBanner({ fileName }: { fileName: string }) {
  if (!fileName) {
    return null;
  }

  return React.createElement('div', { style: fileNameBannerStyle }, '\uD30C\uC77C: ' + fileName);
}

function LocalServerPreviewShell({ children, fileName }: { children: React.ReactNode; fileName: string }) {
  React.useEffect(() => {
    document.documentElement.lang = 'ko';
    document.body.classList.add('stg');

    return () => {
      document.body.classList.remove('stg');
    };
  }, []);

  return React.createElement(
    QueryClientProvider,
    { client: queryClient },
    React.createElement(
      'div',
      { className: 'wrap', id: 'wrap' },
      React.createElement(LocalServerFileNameBanner, { fileName }),
      children,
      React.createElement('div', { id: 'ds-portal' })
    )
  );
}

function resolveStoryFileName(context: { parameters?: Record<string, unknown> }) {
  const fileName = context && context.parameters ? context.parameters.fileName : '';

  return typeof fileName === 'string' ? fileName : '';
}

const preview: Preview = {
  decorators: [
    (Story, context) =>
      React.createElement(
        LocalServerPreviewShell,
        { fileName: resolveStoryFileName(context) },
        React.createElement(Story)
      )
  ],
  parameters: {
    layout: 'fullscreen',
    controls: {
      matchers: {
        color: /(background|color)$/i,
        date: /Date$/i
      }
    },
    options: {
      storySort: {
        method: 'alphabetical'
      }
    }
  }
};

// \u2500\u2500 \uB9AC\uD130\uCE58 \uC5B4\uC2DC\uC2A4\uD2B8: DOM \uC120\uD0DD \uBAA8\uB4DC \u2500\u2500
// \uB9E4\uB2C8\uC800 \uD328\uB110\uC758 "DOM \uC120\uD0DD" \uC694\uCCAD\uC744 \uBC1B\uC544 \uCE94\uBC84\uC2A4 \uC694\uC18C\uB97C hover \uD558\uC774\uB77C\uC774\uD2B8 + \uD074\uB9AD\uC73C\uB85C \uC120\uD0DD\uD558\uACE0,
// \uC120\uD0DD \uC694\uC18C\uC758 \uCD94\uC0C1 \uC124\uBA85\uC744 \uB9E4\uB2C8\uC800\uB85C \uD68C\uC2E0\uD55C\uB2E4. (\uBE0C\uB77C\uC6B0\uC800 \uC804\uC6A9 \uCF54\uB4DC)
(function setupRetouchElementPicker() {
  if (typeof document === 'undefined') {
    return;
  }

  var channel = previewAddons.getChannel();
  var picking = false;
  var hoveredElement: HTMLElement | null = null;
  var previousOutline = '';
  var retouchTargetCounter = 0;

  // \uCE69 hover \uC2DC \uAE5C\uBE61\uC784\uC5D0 \uC0AC\uC6A9\uD560 keyframe \uC2A4\uD0C0\uC77C\uC744 \uD55C \uBC88\uB9CC \uC8FC\uC785\uD55C\uB2E4. (\uACE0\uB300\uBE44 \uC0C9 + \uAE00\uB85C\uC6B0\uB85C \uB208\uC5D0 \uC798 \uB744\uAC8C)
  var flashStyle = document.createElement('style');
  flashStyle.textContent = '@keyframes juhonge-retouch-flash { 0% { outline-color: #ff1744; box-shadow: 0 0 0 1px rgba(255,23,68,0.7), 0 0 8px 3px rgba(255,23,68,0.5); } 50% { outline-color: #ffea00; box-shadow: 0 0 0 2px rgba(255,234,0,0.9), 0 0 14px 5px rgba(255,234,0,0.6); } 100% { outline-color: #ff1744; box-shadow: 0 0 0 1px rgba(255,23,68,0.7), 0 0 8px 3px rgba(255,23,68,0.5); } } .juhonge-retouch-flash { outline: 2px solid #ff1744 !important; outline-offset: 2px; border-radius: 2px; animation: juhonge-retouch-flash 0.8s ease-in-out infinite; }';
  document.head.appendChild(flashStyle);

  function clearHighlight() {
    if (hoveredElement) {
      hoveredElement.style.outline = previousOutline;
      hoveredElement = null;
    }
  }

  // CSS module \uD574\uC2DC \uD074\uB798\uC2A4(_name_hash_line)\uB294 \uC77D\uAE30 \uC26C\uC6B4 name \uC73C\uB85C, emotion(css-xxx)\uC740 \uC81C\uAC70\uD55C\uB2E4.
  function toReadableClassName(token: string): string {
    if (!token || token.indexOf('css-') === 0) { return ''; }
    var moduleMatch = token.match(/^_(.+)_[a-z0-9]+_[0-9]+$/);
    return moduleMatch && moduleMatch[1] ? moduleMatch[1] : token;
  }

  function buildReadableClasses(classValue: string): string {
    return classValue
      .split(' ')
      .map(toReadableClassName)
      .filter(function (token) { return token; })
      .slice(0, 5)
      .join(' ');
  }

  // \uC120\uD0DD \uC694\uC18C\uC5D0 data-retouch-target-N \uB9C8\uD0B9 + \uC9C0\uC18D \uD558\uC774\uB77C\uC774\uD2B8\uB97C \uB0A8\uAE30\uACE0, \uD1A0\uD070/\uC124\uBA85\uC744 \uB9CC\uB4E0\uB2E4.
  function markAndDescribe(element: HTMLElement) {
    retouchTargetCounter += 1;
    var index = retouchTargetCounter;
    var tag = element.tagName.toLowerCase();
    var classes = buildReadableClasses(element.getAttribute('class') || '');
    var text = (element.textContent || '').trim();
    text = text.split(String.fromCharCode(10)).join(' ').slice(0, 40);

    element.setAttribute('data-retouch-target-' + index, 'true');
    element.style.outline = '2px solid #d97706';

    var descriptorParts = ['<' + tag + '>'];
    if (classes) { descriptorParts.push('class="' + classes + '"'); }
    if (text) { descriptorParts.push('text="' + text + '"'); }

    return {
      descriptor: descriptorParts.join(' '),
      index: index,
      token: '[' + tag + '-dom-\uD0C0\uAC9F' + index + ']'
    };
  }

  // \uD2B9\uC815 \uC120\uD0DD \uB300\uC0C1(data-retouch-target-N)\uC758 \uB9C8\uD0B9/\uD14C\uB450\uB9AC\uB97C \uC81C\uAC70\uD55C\uB2E4.
  function clearTargetMark(index: number) {
    var marked = document.querySelector('[data-retouch-target-' + index + ']') as HTMLElement | null;
    if (marked) {
      marked.classList.remove('juhonge-retouch-flash');
      marked.style.outline = '';
      marked.removeAttribute('data-retouch-target-' + index);
    }
  }

  // \uCE69 hover \uC2DC \uD574\uB2F9 \uB300\uC0C1\uC758 \uD14C\uB450\uB9AC\uB97C \uAE5C\uBE61\uC774\uACE0 \uD654\uBA74\uC5D0 \uBCF4\uC774\uB3C4\uB85D \uC2A4\uD06C\uB864\uD55C\uB2E4.
  function flashTargetMark(index: number, active: boolean) {
    var marked = document.querySelector('[data-retouch-target-' + index + ']') as HTMLElement | null;
    if (!marked) {
      return;
    }
    if (active) {
      marked.classList.add('juhonge-retouch-flash');
      marked.scrollIntoView({ behavior: 'smooth', block: 'center' });
    } else {
      marked.classList.remove('juhonge-retouch-flash');
    }
  }

  function onMouseMove(event: MouseEvent) {
    if (!picking) { return; }
    var target = event.target as HTMLElement | null;
    if (!target || target === hoveredElement) { return; }
    clearHighlight();
    hoveredElement = target;
    previousOutline = target.style.outline;
    target.style.outline = '2px solid #115e59';
  }

  function stopPicking() {
    picking = false;
    clearHighlight();
    document.removeEventListener('mousemove', onMouseMove, true);
    document.removeEventListener('click', onClick, true);
    document.removeEventListener('keydown', onKeyDown, true);
  }

  function onClick(event: MouseEvent) {
    if (!picking) { return; }
    event.preventDefault();
    event.stopPropagation();
    var target = event.target as HTMLElement | null;
    stopPicking();
    if (target) {
      channel.emit('${RETOUCH_CHANNEL_PICK_RESULT}', markAndDescribe(target));
    }
  }

  function onKeyDown(event: KeyboardEvent) {
    if (picking && event.key === 'Escape') {
      stopPicking();
      channel.emit('${RETOUCH_CHANNEL_PICK_CANCEL}');
    }
  }

  channel.on('${RETOUCH_CHANNEL_PICK_START}', function () {
    if (picking) { return; }
    picking = true;
    document.addEventListener('mousemove', onMouseMove, true);
    document.addEventListener('click', onClick, true);
    document.addEventListener('keydown', onKeyDown, true);
  });

  channel.on('${RETOUCH_CHANNEL_PICK_CANCEL}', stopPicking);
  channel.on('${RETOUCH_CHANNEL_CLEAR_TARGET}', function (index: number) {
    clearTargetMark(index);
  });
  channel.on('${RETOUCH_CHANNEL_FLASH_TARGET}', function (payload: { index: number; active: boolean }) {
    if (payload) {
      flashTargetMark(payload.index, payload.active);
    }
  });
})();

export default preview;
`;
}
function getStorybookManagerConfigContent() {
  return `import React from 'react';
import { addons, types, useChannel, useStorybookApi } from '@storybook/manager-api';
import { AddonPanel } from '@storybook/components';

var PORT_CANDIDATES = [${RETOUCH_PORT_CANDIDATES.join(", ")}];

function RetouchPanel() {
  var api = useStorybookApi();
  var lastImportPathRef = React.useRef('');
  var storyData = api.getCurrentStoryData();
  if (storyData && storyData.importPath) {
    lastImportPathRef.current = storyData.importPath;
  }
  var currentStoryId = storyData && storyData.id ? storyData.id : (storyData && storyData.importPath ? storyData.importPath : '');
  var lastStoryIdRef = React.useRef(currentStoryId);
  var promptRef = React.useRef<HTMLTextAreaElement | null>(null);

  var baseUrlState = React.useState<string | null>(null);
  var baseUrl = baseUrlState[0];
  var setBaseUrl = baseUrlState[1];
  var promptState = React.useState('');
  var prompt = promptState[0];
  var setPrompt = promptState[1];
  var busyState = React.useState(false);
  var busy = busyState[0];
  var setBusy = busyState[1];
  var pickingState = React.useState(false);
  var picking = pickingState[0];
  var setPicking = pickingState[1];
  var messageState = React.useState('');
  var message = messageState[0];
  var setMessage = messageState[1];
  var filesState = React.useState<Array<{ relativePath: string; absolutePath: string; before: string; after: string }>>([]);
  var files = filesState[0];
  var setFiles = filesState[1];
  var targetsState = React.useState<Array<{ token: string; descriptor: string; index: number }>>([]);
  var targets = targetsState[0];
  var setTargets = targetsState[1];
  var logState = React.useState('');
  var log = logState[0];
  var setLog = logState[1];

  var emit = useChannel({
    '${RETOUCH_CHANNEL_PICK_RESULT}': function (payload: { descriptor?: string; token?: string; index?: number } | string) {
      setPicking(false);
      // \uAD6C\uBC84\uC804(\uBB38\uC790\uC5F4) / \uC2E0\uBC84\uC804(\uAC1D\uCCB4) payload\uB97C \uBAA8\uB450 \uD5C8\uC6A9\uD55C\uB2E4.
      var token = typeof payload === 'string' ? payload : (payload && payload.token) || '';
      var descriptor = typeof payload === 'string' ? '' : (payload && payload.descriptor) || '';
      var index = typeof payload === 'string' ? 0 : (payload && typeof payload.index === 'number' ? payload.index : 0);
      if (token) {
        setPrompt(function (current) { return current ? current + ' ' + token : token; });
        setTargets(function (current) { return current.concat([{ token: token, descriptor: descriptor, index: index }]); });
        setMessage('\uC120\uD0DD\uB428: ' + token);
      }
    },
    '${RETOUCH_CHANNEL_PICK_CANCEL}': function () { setPicking(false); }
  });

  // \uCE69 hover \uC2DC \uC88C\uCE21\uC5D0\uC11C \uD574\uB2F9 DOM \uD14C\uB450\uB9AC\uB97C \uAE5C\uBE61\uC778\uB2E4.
  function handleFlashTarget(index, active) {
    emit('${RETOUCH_CHANNEL_FLASH_TARGET}', { index: index, active: active });
  }

  // \uD1A0\uD070\uC744 \uD604\uC7AC \uCEE4\uC11C \uC704\uCE58\uC5D0 \uC0BD\uC785\uD55C\uB2E4. (\uCEE4\uC11C \uC815\uBCF4\uAC00 \uC5C6\uC73C\uBA74 \uB05D\uC5D0 append, \uC911\uBCF5 \uD5C8\uC6A9)
  function handleReinsertTarget(token) {
    var element = promptRef.current;
    if (!element || typeof element.selectionStart !== 'number') {
      setPrompt(function (current) { return current ? current + ' ' + token : token; });
      setMessage('\uD1A0\uD070\uC744 \uD504\uB86C\uD504\uD2B8\uC5D0 \uB123\uC5C8\uC2B5\uB2C8\uB2E4: ' + token);
      return;
    }

    var value = element.value;
    var start = element.selectionStart;
    var end = element.selectionEnd;
    var before = value.slice(0, start);
    var after = value.slice(end);
    var needSpaceBefore = before.length > 0 && before.charAt(before.length - 1) !== ' ';
    var needSpaceAfter = after.length > 0 && after.charAt(0) !== ' ';
    var inserted = (needSpaceBefore ? ' ' : '') + token + (needSpaceAfter ? ' ' : '');
    var nextValue = before + inserted + after;
    var caret = (before + inserted).length;

    setPrompt(nextValue);
    setMessage('\uD1A0\uD070\uC744 \uCEE4\uC11C \uC704\uCE58\uC5D0 \uB123\uC5C8\uC2B5\uB2C8\uB2E4: ' + token);
    window.requestAnimationFrame(function () {
      if (promptRef.current) {
        promptRef.current.focus();
        promptRef.current.setSelectionRange(caret, caret);
      }
    });
  }

  // \uC120\uD0DD \uB300\uC0C1 \uC81C\uAC70: \uC88C\uCE21 \uB9C8\uD0B9/\uD14C\uB450\uB9AC \uC81C\uAC70 + \uD504\uB86C\uD504\uD2B8\uC758 \uD1A0\uD070 \uC81C\uAC70 + \uBAA9\uB85D\uC5D0\uC11C \uC81C\uAC70.
  function handleRemoveTarget(target) {
    emit('${RETOUCH_CHANNEL_CLEAR_TARGET}', target.index);
    setPrompt(function (current) { return current.split(target.token).join(' ').split(' ').filter(Boolean).join(' '); });
    setTargets(function (current) { return current.filter(function (item) { return item.index !== target.index; }); });
    setMessage('\uC120\uD0DD \uD574\uC81C: ' + target.token);
  }

  React.useEffect(function () {
    var cancelled = false;
    (async function () {
      for (var i = 0; i < PORT_CANDIDATES.length; i += 1) {
        var candidate = 'http://127.0.0.1:' + PORT_CANDIDATES[i];
        try {
          var res = await fetch(candidate + '/api/retouch/health', { cache: 'no-store' });
          if (res.ok) {
            if (!cancelled) { setBaseUrl(candidate); }
            return;
          }
        } catch (error) { /* \uB2E4\uC74C \uD6C4\uBCF4 \uC2DC\uB3C4 */ }
      }
      if (!cancelled) { setMessage('\uB9AC\uD130\uCE58 \uC11C\uBC84\uB97C \uCC3E\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4. ju-hong-e\uC5D0\uC11C /retouch-start \uB97C \uC2E4\uD589\uD558\uC138\uC694.'); }
    })();
    return function () { cancelled = true; };
  }, []);

  // \uC2A4\uD1A0\uB9AC\uAC00 \uBC14\uB00C\uBA74 \uC774\uC804 \uC2A4\uD1A0\uB9AC\uC5D0 \uBB36\uC778 \uC120\uD0DD/\uBCC0\uACBD\uC548\uC744 \uCD08\uAE30\uD654\uD55C\uB2E4.
  // (\uC88C\uCE21 \uB9C8\uD0B9\uC740 \uC2A4\uD1A0\uB9AC \uC7AC\uB80C\uB354\uB85C \uC0AC\uB77C\uC9C0\uBBC0\uB85C, \uD328\uB110\uC758 \uCE69/diff/\uB85C\uADF8\uB9CC \uC815\uB9AC\uD558\uBA74 \uC77C\uAD00\uC131\uC774 \uB9DE\uB294\uB2E4)
  React.useEffect(function () {
    if (lastStoryIdRef.current === currentStoryId) {
      return;
    }

    var previousStoryId = lastStoryIdRef.current;

    // \uC120\uD0DD\uD55C DOM(\uCE69) \uB610\uB294 \uC544\uC9C1 \uC801\uC6A9/\uCDE8\uC18C\uD558\uC9C0 \uC54A\uC740 diff\uAC00 \uC788\uC73C\uBA74 \uC774\uB3D9 \uC804\uC5D0 \uD655\uC778\uD55C\uB2E4.
    // diff(files)\uB294 AI\uAC00 \uC774\uBBF8 \uB514\uC2A4\uD06C\uC5D0 \uC4F4 \uBCC0\uACBD\uC758 \uC6D0\uBCF5(before \uC2A4\uB0C5\uC0F7)\uC744 \uD328\uB110 state\uB85C\uB9CC \uB4E4\uACE0 \uC788\uC73C\uBBC0\uB85C,
    // \uD655\uC778 \uC5C6\uC774 \uD3D0\uAE30\uD558\uBA74 [\uCDE8\uC18C(\uB418\uB3CC\uB9AC\uAE30)] \uACBD\uB85C\uAC00 \uC0AC\uB77C\uC9C4\uB2E4. (send \uC131\uACF5 \uC9C1\uD6C4\uC5D4 targets\uAC00 \uBE44\uACE0 files\uB9CC \uB0A8\uB294\uB2E4)
    var hasPendingTargets = targets.length > 0;
    var hasPendingDiff = files.length > 0;
    if (hasPendingTargets || hasPendingDiff) {
      var confirmMessage = hasPendingDiff
        ? '\uC544\uC9C1 \uC801\uC6A9/\uCDE8\uC18C\uD558\uC9C0 \uC54A\uC740 \uBCC0\uACBD\uC548(diff)\uC774 \uC788\uC2B5\uB2C8\uB2E4. \uB2E4\uB978 \uC2A4\uD1A0\uB9AC\uB85C \uC774\uB3D9\uD558\uBA74 diff\uAC00 \uC0AC\uB77C\uC838 [\uCDE8\uC18C(\uB418\uB3CC\uB9AC\uAE30)]\uB85C \uC6D0\uBCF5\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uC774\uB3D9\uD560\uAE4C\uC694?'
        : '\uC120\uD0DD\uD55C DOM\uC774 \uC788\uC2B5\uB2C8\uB2E4. \uB2E4\uB978 \uC2A4\uD1A0\uB9AC\uB85C \uC774\uB3D9\uD558\uBA74 \uC120\uD0DD/\uBCC0\uACBD\uC548\uC774 \uCD08\uAE30\uD654\uB429\uB2C8\uB2E4. \uC774\uB3D9\uD560\uAE4C\uC694?';
      var proceed = window.confirm(confirmMessage);
      if (!proceed) {
        if (previousStoryId) {
          api.selectStory(previousStoryId);
        }
        return;
      }
      // \uCE69 [x] \uC640 \uB3D9\uC77C\uD558\uAC8C: \uC88C\uCE21 \uB9C8\uD0B9 \uC81C\uAC70 + \uD504\uB86C\uD504\uD2B8\uC758 \uD1A0\uD070 \uC81C\uAC70.
      targets.forEach(function (target) { emit('${RETOUCH_CHANNEL_CLEAR_TARGET}', target.index); });
      setPrompt(function (current) {
        var next = current;
        targets.forEach(function (target) { next = next.split(target.token).join(' '); });
        return next.split(' ').filter(Boolean).join(' ');
      });
      setMessage('\uB2E4\uB978 \uC2A4\uD1A0\uB9AC\uB85C \uC774\uB3D9\uD574 \uC120\uD0DD\uD55C DOM \uBC0F \uBCC0\uACBD\uC548\uC744 \uCD08\uAE30\uD654\uD588\uC2B5\uB2C8\uB2E4.');
    }

    lastStoryIdRef.current = currentStoryId;
    setTargets([]);
    setFiles([]);
    setLog('');
  }, [currentStoryId]);

  function handleStartPick() {
    setPicking(true);
    setMessage('\uC88C\uCE21 \uD654\uBA74\uC5D0\uC11C \uC694\uC18C\uB97C \uD074\uB9AD\uD558\uC138\uC694. (Esc \uCDE8\uC18C)');
    emit('${RETOUCH_CHANNEL_PICK_START}');
  }

  async function handleSend() {
    if (!baseUrl) { setMessage('\uB9AC\uD130\uCE58 \uC11C\uBC84 \uC5F0\uACB0\uC744 \uCC3E\uB294 \uC911\uC785\uB2C8\uB2E4.'); return; }
    if (!prompt.trim()) { setMessage('\uD504\uB86C\uD504\uD2B8\uB97C \uC785\uB825\uD558\uC138\uC694.'); return; }
    var importPath = lastImportPathRef.current;
    if (!importPath) { setMessage('\uB300\uC0C1 \uC2A4\uD1A0\uB9AC\uB97C \uCC3E\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4. \uC2A4\uD1A0\uB9AC\uB97C \uBA3C\uC800 \uC120\uD0DD\uD558\uC138\uC694.'); return; }
    setBusy(true);
    setMessage('AI\uAC00 \uD3B8\uC9D1 \uC911\uC785\uB2C8\uB2E4\u2026 (\uC2E4\uC2DC\uAC04 \uB85C\uADF8)');
    setFiles([]);
    setLog('');

    try {
      var res = await fetch(baseUrl + '/api/retouch', {
        method: 'POST',
        headers: { 'content-type': 'application/json' },
        body: JSON.stringify({ importPath: importPath, prompt: prompt, selectedElements: targets })
      });

      if (!res.ok) {
        // 400/409 \uB4F1\uC740 JSON \uC73C\uB85C \uB0B4\uB824\uC628\uB2E4.
        var errText = await res.text();
        var errMessage = errText;
        try { var parsed = JSON.parse(errText); errMessage = parsed && parsed.message ? parsed.message : errText; } catch (ignore) { /* \uD3C9\uBB38 */ }
        setMessage('\uC694\uCCAD \uC2E4\uD328: ' + errMessage);
        return;
      }

      if (!res.body) {
        // \uC2A4\uD2B8\uB9BC \uBBF8\uC9C0\uC6D0 \uD658\uACBD \uD3F4\uBC31
        var fallbackText = await res.text();
        handleStreamComplete(fallbackText);
        return;
      }

      var reader = res.body.getReader();
      var decoder = new TextDecoder();
      var buffer = '';

      while (true) {
        var step = await reader.read();
        if (step.done) { break; }
        buffer += decoder.decode(step.value, { stream: true });
        // \uACB0\uACFC \uB9C8\uCEE4 \uC774\uC804\uAE4C\uC9C0\uB294 \uC9C4\uD589 \uB85C\uADF8\uB85C \uD45C\uC2DC\uD55C\uB2E4.
        var markerIndex = buffer.indexOf('${RETOUCH_RESULT_MARKER}');
        var errorIndex = buffer.indexOf('${RETOUCH_ERROR_MARKER}');
        var cut = markerIndex >= 0 ? markerIndex : (errorIndex >= 0 ? errorIndex : buffer.length);
        setLog(buffer.slice(0, cut));
      }

      handleStreamComplete(buffer);
    } catch (error) {
      setMessage(error instanceof Error ? error.message : String(error));
    } finally {
      setBusy(false);
    }
  }

  function handleStreamComplete(buffer: string) {
    var resultIndex = buffer.indexOf('${RETOUCH_RESULT_MARKER}');
    var errorIndex = buffer.indexOf('${RETOUCH_ERROR_MARKER}');

    if (errorIndex >= 0) {
      setLog(buffer.slice(0, errorIndex));
      try {
        var errorPayload = JSON.parse(buffer.slice(errorIndex + '${RETOUCH_ERROR_MARKER}'.length).trim());
        setMessage('\uC2E4\uD328: ' + (errorPayload && errorPayload.message ? errorPayload.message : '\uC54C \uC218 \uC5C6\uB294 \uC624\uB958'));
      } catch (parseError) {
        setMessage('\uC2E4\uD328: \uC751\uB2F5 \uD30C\uC2F1 \uC624\uB958');
      }
      return;
    }

    if (resultIndex >= 0) {
      setLog(buffer.slice(0, resultIndex));
      try {
        var result = JSON.parse(buffer.slice(resultIndex + '${RETOUCH_RESULT_MARKER}'.length).trim());
        setFiles(result.files || []);
        // \uC804\uC1A1\uC774 \uB05D\uB098\uBA74 \uC120\uD0DD \uB9C8\uD0B9\uC744 \uC815\uB9AC\uD55C\uB2E4. (HMR \uC7AC\uB80C\uB354\uB85C\uB3C4 \uC0AC\uB77C\uC9C0\uC9C0\uB9CC \uC989\uC2DC \uC815\uB9AC)
        targets.forEach(function (target) { emit('${RETOUCH_CHANNEL_CLEAR_TARGET}', target.index); });
        setTargets([]);
        if (!result.files || result.files.length === 0) {
          setMessage('\uBCC0\uACBD\uB41C \uD30C\uC77C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. (AI\uAC00 \uC218\uC815\uD558\uC9C0 \uC54A\uC558\uAC70\uB098 \uB3D9\uC77C)');
        } else {
          setMessage('AI\uAC00 \uC218\uC815\uC548\uC744 \uC88C\uCE21 \uD654\uBA74\uC5D0 \uBC18\uC601\uD588\uC2B5\uB2C8\uB2E4. \uC544\uB798 diff\uB97C \uD655\uC778\uD558\uACE0 [\uC801\uC6A9](\uC720\uC9C0) \uB610\uB294 [\uCDE8\uC18C](\uB418\uB3CC\uB9AC\uAE30)\uD558\uC138\uC694.');
        }
      } catch (parseError) {
        setMessage('\uACB0\uACFC \uD30C\uC2F1 \uC624\uB958');
      }
      return;
    }

    setMessage('\uC751\uB2F5\uC744 \uBC1B\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4.');
  }

  function handleEditAfter(index: number, value: string) {
    setFiles(function (current) {
      return current.map(function (file, i) { return i === index ? Object.assign({}, file, { after: value }) : file; });
    });
  }

  // \uC801\uC6A9/\uB418\uB3CC\uB9AC\uAE30 \uBAA8\uB450 \uAC19\uC740 \uC5D4\uB4DC\uD3EC\uC778\uD2B8\uB97C \uC4F0\uB418, \uC801\uC6A9\uC740 after(\uD3B8\uC9D1\uBCF8)\uB97C, \uB418\uB3CC\uB9AC\uAE30\uB294 before(\uC6D0\uBCF8)\uB97C \uB514\uC2A4\uD06C\uC5D0 \uC4F4\uB2E4.
  async function writeFiles(useBefore: boolean, doneMessage: string) {
    if (!baseUrl || files.length === 0) { return; }
    setBusy(true);
    setMessage(useBefore ? '\uC6D0\uBCF8\uC73C\uB85C \uB418\uB3CC\uB9AC\uB294 \uC911\uC785\uB2C8\uB2E4\u2026' : '\uC801\uC6A9 \uC911\uC785\uB2C8\uB2E4\u2026');
    try {
      var res = await fetch(baseUrl + '/api/retouch/apply', {
        method: 'POST',
        headers: { 'content-type': 'application/json' },
        body: JSON.stringify({
          files: files.map(function (file) { return { absolutePath: file.absolutePath, content: useBefore ? file.before : file.after }; })
        })
      });
      var body = await res.json();
      if (!res.ok) { throw new Error(body && body.message ? body.message : '\uC694\uCCAD \uC2E4\uD328'); }
      setMessage(doneMessage);
      setFiles([]);
    } catch (error) {
      setMessage(error instanceof Error ? error.message : String(error));
    } finally {
      setBusy(false);
    }
  }

  function handleApply() {
    writeFiles(false, '\uC801\uC6A9 \uC644\uB8CC \u2014 \uC88C\uCE21 \uD654\uBA74\uC774 \uAC31\uC2E0\uB429\uB2C8\uB2E4.');
  }

  function handleRevert() {
    writeFiles(true, '\uC6D0\uBCF8\uC73C\uB85C \uB418\uB3CC\uB838\uC2B5\uB2C8\uB2E4 \u2014 \uC88C\uCE21 \uD654\uBA74\uC774 \uC6D0\uC0C1\uBCF5\uAD6C\uB429\uB2C8\uB2E4.');
  }

  return (
    <div style={{ display: 'flex', flexDirection: 'column', height: '100%', padding: '12px', gap: '10px', boxSizing: 'border-box' }}>
      <div style={{ fontSize: '12px', color: baseUrl ? '#027a48' : '#b42318' }}>
        {baseUrl ? '\uB9AC\uD130\uCE58 \uC11C\uBC84 \uC5F0\uACB0\uB428' : '\uB9AC\uD130\uCE58 \uC11C\uBC84 \uBBF8\uC5F0\uACB0'}
      </div>
      <div style={{ flex: 1, overflow: 'auto', display: 'flex', flexDirection: 'column', gap: '10px' }}>
        {message ? <div style={{ fontSize: '13px', color: '#1e252b', lineHeight: 1.5 }}>{message}</div> : null}
        {log ? (
          <pre style={{ margin: 0, maxHeight: '220px', overflow: 'auto', background: '#0f1720', color: '#c2f7ea', padding: '10px', borderRadius: '8px', whiteSpace: 'pre-wrap', wordBreak: 'break-word', fontFamily: 'monospace', fontSize: '12px', lineHeight: 1.5 }}>{log}</pre>
        ) : null}
        {files.map(function (file, index) {
          return (
            <div key={file.absolutePath} style={{ border: '1px solid #d0d7de', borderRadius: '8px', padding: '8px' }}>
              <div style={{ fontSize: '12px', fontWeight: 700, marginBottom: '6px', wordBreak: 'break-all' }}>{file.relativePath}</div>
              <div style={{ fontSize: '11px', color: '#6e7781', marginBottom: '4px' }}>\uBCC0\uACBD \uD6C4 (\uD3B8\uC9D1 \uAC00\uB2A5)</div>
              <textarea
                value={file.after}
                onChange={function (event) { handleEditAfter(index, event.target.value); }}
                spellCheck={false}
                style={{ width: '100%', minHeight: '160px', fontFamily: 'monospace', fontSize: '12px', boxSizing: 'border-box' }}
              />
              <details style={{ marginTop: '6px' }}>
                <summary style={{ fontSize: '12px', cursor: 'pointer', color: '#6e7781' }}>\uBCC0\uACBD \uC804(\uC6D0\uBCF8) \uBCF4\uAE30</summary>
                <textarea
                  value={file.before}
                  readOnly
                  spellCheck={false}
                  style={{ width: '100%', minHeight: '120px', marginTop: '4px', fontFamily: 'monospace', fontSize: '12px', boxSizing: 'border-box', background: '#f6f8fa' }}
                />
              </details>
            </div>
          );
        })}
        {files.length > 0 ? (
          <div style={{ display: 'flex', gap: '8px' }}>
            <button type="button" disabled={busy} onClick={handleApply} style={{ padding: '6px 12px', cursor: 'pointer', fontWeight: 700 }}>\uC801\uC6A9(\uC720\uC9C0)</button>
            <button type="button" disabled={busy} onClick={handleRevert} style={{ padding: '6px 12px', cursor: 'pointer' }}>\uCDE8\uC18C(\uB418\uB3CC\uB9AC\uAE30)</button>
          </div>
        ) : null}
      </div>
      <div style={{ display: 'flex', flexDirection: 'column', gap: '8px', borderTop: '1px solid #d0d7de', paddingTop: '10px' }}>
        <div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
          <button type="button" onClick={handleStartPick} disabled={picking} style={{ padding: '4px 10px', cursor: 'pointer' }}>
            {picking ? 'DOM \uC120\uD0DD \uC911\u2026' : 'DOM \uC120\uD0DD'}
          </button>
        </div>
        {targets.length > 0 ? (
          <div style={{ display: 'flex', flexWrap: 'wrap', gap: '6px' }}>
            {targets.map(function (target) {
              return (
                <span key={target.index} onMouseEnter={function () { handleFlashTarget(target.index, true); }} onMouseLeave={function () { handleFlashTarget(target.index, false); }} style={{ display: 'inline-flex', alignItems: 'center', gap: '4px', padding: '3px 4px 3px 8px', borderRadius: '14px', background: 'rgba(217, 119, 6, 0.12)', border: '1px solid rgba(217, 119, 6, 0.4)', fontSize: '12px' }}>
                  <span style={{ fontFamily: 'monospace' }}>{target.token}</span>
                  <button type="button" title="\uC774 \uD1A0\uD070\uC744 \uD504\uB86C\uD504\uD2B8\uC5D0 \uB2E4\uC2DC \uC0BD\uC785" onClick={function () { handleReinsertTarget(target.token); }} style={{ padding: '1px 6px', cursor: 'pointer', borderRadius: '10px' }}>\uC7AC\uC0BD\uC785</button>
                  <button type="button" title="\uC120\uD0DD \uD574\uC81C (\uC88C\uCE21 \uD14C\uB450\uB9AC \uC81C\uAC70)" onClick={function () { handleRemoveTarget(target); }} style={{ padding: '1px 7px', cursor: 'pointer', borderRadius: '10px' }}>\u2715</button>
                </span>
              );
            })}
          </div>
        ) : null}
        <textarea
          ref={promptRef}
          value={prompt}
          onChange={function (event) { setPrompt(event.target.value); }}
          placeholder="\uC608) \uC774 \uBC84\uD2BC \uBC30\uACBD\uC744 \uBE68\uAC04\uC0C9\uC73C\uB85C \uBC14\uAFD4\uC918"
          style={{ width: '100%', minHeight: '70px', fontSize: '13px', boxSizing: 'border-box' }}
        />
        <button type="button" onClick={handleSend} disabled={busy} style={{ padding: '8px 12px', cursor: 'pointer', fontWeight: 700 }}>
          {busy ? '\uC2E4\uD589 \uC911\u2026' : '\uC804\uC1A1'}
        </button>
      </div>
    </div>
  );
}

addons.register('juhonge/retouch', function () {
  addons.add('juhonge/retouch/panel', {
    type: types.PANEL,
    title: '\uB9AC\uD130\uCE58',
    match: function (state) { return state.viewMode === 'story'; },
    render: function (renderOptions) {
      return (
        <AddonPanel active={Boolean(renderOptions.active)}>
          <RetouchPanel />
        </AddonPanel>
      );
    }
  });
});
`;
}
function getOpenTargetAttempts(target) {
  if (process.platform === "darwin") {
    return {
      chrome: [{ args: ["-a", "Google Chrome", target], command: "open" }],
      default: [{ args: [target], command: "open" }]
    };
  }
  if (process.platform === "win32") {
    return {
      chrome: [{ args: ["/c", "start", "", "chrome", target], command: "cmd" }],
      default: [{ args: ["/c", "start", "", target], command: "cmd" }]
    };
  }
  return {
    chrome: [
      { args: [target], command: "google-chrome" },
      { args: [target], command: "google-chrome-stable" },
      { args: [target], command: "chromium" },
      { args: [target], command: "chromium-browser" }
    ],
    default: [{ args: [target], command: "xdg-open" }]
  };
}
function tryOpenTarget(command, args) {
  const result = spawnSync(command, args, {
    stdio: "ignore",
    windowsHide: true
  });
  return !result.error && result.status === 0;
}
function openTarget(target, preferChrome) {
  const attempts = getOpenTargetAttempts(target);
  const orderedAttempts = preferChrome ? [...attempts.chrome, ...attempts.default] : attempts.default;
  for (const attempt of orderedAttempts) {
    if (tryOpenTarget(attempt.command, attempt.args)) {
      return true;
    }
  }
  return false;
}
function openLocalPreviewInBrowser(previewUrl) {
  return openTarget(previewUrl, true);
}
function openLocalServerPackageJson(packageJsonPath) {
  return openTarget(packageJsonPath, false);
}

export { getLocalServerTsconfigContent, getScssModuleDeclarationContent, getStorybookExecutablePath, getStorybookMainConfigContent, getStorybookManagerConfigContent, getStorybookPreviewConfigContent, openLocalPreviewInBrowser, openLocalServerPackageJson, setLocalServerPreviewTarget };
