import type { ReactNode } from 'react';
import { useEffect, useMemo, useRef, useState } from 'react';
import { Marked, Renderer } from 'marked';
import DOMPurify from 'dompurify';
import mermaid from 'mermaid';
import hljs from 'highlight.js';
import { updateLocalFileContent } from '../../api';
import { EmbedBlock } from '../embed/EmbedBlock';
import { OUTPUT_PATH_REGEX, linkifyOutputPathsInEscapedHtml } from '../../lib/output-path-detect';

mermaid.initialize({ startOnLoad: false, theme: 'default' });

// --- MDXG outline helpers ---
interface OutlineEntry { depth: 1 | 2 | 3; text: string; slug: string; }

function slugify(text: string): string {
  const cleaned = text
    .replace(/<[^>]+>/g, '')
    .toLowerCase()
    .trim()
    .replace(/[\s_]+/g, '-')
    .replace(/[^\w\-぀-ヿ一-鿿]/g, '')
    .replace(/-+/g, '-')
    .replace(/^-+|-+$/g, '');
  return cleaned || 'section';
}

function buildSlugger() {
  const counts = new Map<string, number>();
  return (text: string): string => {
    const base = slugify(text);
    const n = counts.get(base) ?? 0;
    counts.set(base, n + 1);
    return n === 0 ? base : `${base}-${n}`;
  };
}

function extractOutline(content: string, parser: Marked): OutlineEntry[] {
  const tokens = parser.lexer(content);
  const headings: OutlineEntry[] = [];
  const slugger = buildSlugger();
  for (const t of tokens) {
    if (t.type === 'heading') {
      const depth = (t as { depth?: number }).depth;
      const text = (t as { text?: string }).text ?? '';
      if (depth === 1 || depth === 2 || depth === 3) {
        headings.push({ depth, text, slug: slugger(text) });
      }
    }
  }
  return headings;
}

interface FilePreviewProps {
  name: string;
  content: string;
  imageSrc: string;
  /** Markdown 内の相対パス画像を解決するためのベース URL (省略可) */
  markdownImageBaseUrl?: string;
  onClose: () => void;
  taskId?: number;
  section?: string;
  filePath?: string;
  editable?: boolean;
}

// --- CSV ---
function renderCsv(csv: string) {
  const rows = csv.trim().split(/\r?\n/).map(r => r.split(','));
  if (rows.length === 0) return null;
  return (
    <div className="overflow-x-auto">
      <table className="w-full text-xs border-collapse">
        <tbody>
          {rows.slice(0, 120).map((r, i) => (
            <tr key={i}>
              {r.slice(0, 20).map((c, j) => (
                <td key={j} className={`border border-slate-200 px-2 py-1 ${i === 0 ? 'bg-slate-100 font-bold' : 'bg-white'}`}>
                  {c}
                </td>
              ))}
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}

// --- Markdown (marked) ---
function escapeHtml(s: string): string {
  return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}

function buildMdRenderer(opts: { imageBaseUrl?: string; slugger?: (text: string) => string }): Renderer {
  const { imageBaseUrl, slugger } = opts;
  const renderer = new Renderer();
  renderer.link = function ({ href, title, text }: { href: string; title?: string | null; text: string }) {
    const titleAttr = title ? ` title="${title}"` : '';
    // Markdown link whose destination is an `output/...` workspace
    // path: route the click through OutputPreviewProvider's
    // delegation instead of opening a new tab. Visible text is the
    // markdown label.
    const isOutputHref = OUTPUT_PATH_REGEX.test(href);
    OUTPUT_PATH_REGEX.lastIndex = 0;
    if (isOutputHref) {
      return `<a class="output-path-link" data-output-path="${href.replace(/"/g, '&quot;')}"${titleAttr} role="button" tabindex="0">${text}</a>`;
    }
    return `<a href="${href}"${titleAttr} target="_blank" rel="noopener noreferrer">${text}</a>`;
  };
  // Bare `output/...` paths in paragraph / list / blockquote text.
  // Marked passes pre-escaped HTML strings here, so the linkifier
  // runs on safe content.
  //
  // CRITICAL: when the text token has nested inline children (strong,
  // em, codespan, link), `tokens` is set and we MUST defer to
  // parser.parseInline. Otherwise the inline formatting is silently
  // dropped — `- **bold** \`code\`` would render as literal `**bold**
  // \`code\`` instead of formatted.
  renderer.text = function ({ tokens, text }: { tokens?: unknown[]; text: string }) {
    if (tokens && tokens.length > 0) {
      const self = this as unknown as { parser: { parseInline(tokens: unknown[]): string } };
      return self.parser.parseInline(tokens);
    }
    return linkifyOutputPathsInEscapedHtml(text);
  };
  // Inline `output/foo.md` in single-backtick spans. Fenced code
  // blocks go through `renderer.code` (below) which deliberately
  // doesn't linkify — fenced code is usually a copy-paste sample,
  // not a real reference.
  renderer.codespan = function ({ text }: { text: string }) {
    return `<code>${linkifyOutputPathsInEscapedHtml(text)}</code>`;
  };
  renderer.code = function ({ text, lang }: { text: string; lang?: string }) {
    if (lang === 'mermaid') {
      return `<pre class="mermaid">${escapeHtml(text)}</pre>`;
    }
    let highlighted: string;
    try {
      if (lang && hljs.getLanguage(lang)) {
        highlighted = hljs.highlight(text, { language: lang, ignoreIllegals: true }).value;
      } else {
        highlighted = hljs.highlightAuto(text).value;
      }
    } catch {
      highlighted = escapeHtml(text);
    }
    const langLabel = lang ? `<span class="mdxg-lang">${escapeHtml(lang)}</span>` : '';
    const langClass = lang ? `language-${escapeHtml(lang)}` : '';
    return `<pre>${langLabel}<button class="mdxg-copy" data-copy="1" type="button" aria-label="copy">copy</button><code class="hljs ${langClass}">${highlighted}</code></pre>`;
  };
  if (slugger) {
    renderer.heading = function ({ tokens, depth, text }: { tokens: unknown[]; depth: number; text: string }) {
      const self = this as unknown as { parser: { parseInline(tokens: unknown[]): string } };
      const inner = self.parser.parseInline(tokens);
      if (depth > 3) {
        return `<h${depth}>${inner}</h${depth}>`;
      }
      const slug = slugger(text);
      return `<h${depth} id="${slug}"><a class="mdxg-anchor" href="#${slug}" aria-hidden="true">#</a>${inner}</h${depth}>`;
    };
  }
  if (imageBaseUrl) {
    renderer.image = function ({ href, title, text }: { href: string; title?: string | null; text: string }) {
      let resolvedHref = href;
      if (href && !href.startsWith('http://') && !href.startsWith('https://') && !href.startsWith('data:')) {
        const cleanPath = href.replace(/^\.\//, '');
        resolvedHref = `${imageBaseUrl}${encodeURIComponent(cleanPath)}`;
      }
      const titleAttr = title ? ` title="${title}"` : '';
      return `<img src="${resolvedHref}" alt="${text}"${titleAttr} style="max-width:100%" />`;
    };
  }
  return renderer;
}

/** embed マーカーでコンテンツを分割する */
const EMBED_SPLIT_RE = /\[\[embed:([\w-]+)\]\]/g;

interface ContentSegment {
  type: 'markdown' | 'embed';
  value: string; // markdown: テキスト, embed: refId
}

function splitContentByEmbeds(content: string): ContentSegment[] {
  const segments: ContentSegment[] = [];
  let lastIndex = 0;
  let match: RegExpExecArray | null;
  while ((match = EMBED_SPLIT_RE.exec(content)) !== null) {
    if (match.index > lastIndex) {
      segments.push({ type: 'markdown', value: content.slice(lastIndex, match.index) });
    }
    segments.push({ type: 'embed', value: match[1] });
    lastIndex = match.index + match[0].length;
  }
  if (lastIndex < content.length) {
    segments.push({ type: 'markdown', value: content.slice(lastIndex) });
  }
  return segments;
}

/** 単一の Markdown 断片をレンダリングする内部コンポーネント */
function MarkdownSegment({ html }: { html: string }): JSX.Element {
  const ref = useRef<HTMLDivElement>(null);

  useEffect(() => {
    if (ref.current) {
      mermaid.run({ nodes: ref.current.querySelectorAll('.mermaid') }).catch(() => {});
    }
  }, [html]);

  return <div ref={ref} dangerouslySetInnerHTML={{ __html: html }} />;
}

const DOMPURIFY_CONFIG = {
  // `data-output-path`, `role`, `tabindex` added for the output-path
  // linkifier — defaults already permit `data-*`, but being explicit
  // guards against future config tightening, same as MarkdownText.
  ADD_ATTR: ['data-copy', 'aria-hidden', 'aria-label', 'id', 'target', 'rel', 'data-output-path', 'role', 'tabindex'] as string[],
};

interface MarkdownPreviewProps {
  content: string;
  imageBaseUrl?: string;
  taskId?: number;
  /** true で目次サイドバー + リーダースタイル (MDXG) を有効化。チャット吹き出し等では false 推奨。 */
  showOutline?: boolean;
}

export function MarkdownPreview({ content, imageBaseUrl, taskId, showOutline = false }: MarkdownPreviewProps): JSX.Element {
  const truncated = content.slice(0, 100000);
  const containerRef = useRef<HTMLDivElement>(null);
  const [activeSlug, setActiveSlug] = useState<string>('');

  // 目次抽出 (showOutline=true のときのみ)
  const outline = useMemo<OutlineEntry[]>(() => {
    if (!showOutline) return [];
    try {
      const parser = new Marked({ gfm: true });
      return extractOutline(truncated, parser);
    } catch {
      return [];
    }
  }, [truncated, showOutline]);

  // HTML 生成
  const segments = useMemo(() => {
    EMBED_SPLIT_RE.lastIndex = 0;
    const slugger = showOutline ? buildSlugger() : undefined;
    const renderer = buildMdRenderer({ imageBaseUrl, slugger });
    const parser = new Marked({ gfm: true, renderer });

    const hasEmbed = taskId != null && EMBED_SPLIT_RE.test(truncated);
    EMBED_SPLIT_RE.lastIndex = 0;
    if (!hasEmbed) {
      const html = DOMPurify.sanitize(parser.parse(truncated, { async: false }) as string, DOMPURIFY_CONFIG);
      return [{ type: 'markdown' as const, html }];
    }
    return splitContentByEmbeds(truncated).map(seg => {
      if (seg.type === 'embed') return { type: 'embed' as const, refId: seg.value };
      const html = DOMPurify.sanitize(parser.parse(seg.value, { async: false }) as string, DOMPURIFY_CONFIG);
      return { type: 'markdown' as const, html };
    });
  }, [truncated, imageBaseUrl, taskId, showOutline]);

  // コピーボタン + アンカーリンクのイベント delegation
  useEffect(() => {
    const root = containerRef.current;
    if (!root) return;
    const handler = (e: MouseEvent) => {
      const target = e.target as HTMLElement;
      const btn = target.closest<HTMLButtonElement>('button.mdxg-copy');
      if (btn) {
        const pre = btn.closest('pre');
        const code = pre?.querySelector('code');
        if (code) {
          navigator.clipboard.writeText(code.textContent ?? '').then(() => {
            btn.classList.add('copied');
            const original = btn.textContent;
            btn.textContent = '✓';
            setTimeout(() => {
              btn.classList.remove('copied');
              btn.textContent = original ?? 'copy';
            }, 1200);
          }).catch(() => {});
        }
        return;
      }
      const anchor = target.closest<HTMLAnchorElement>('a.mdxg-anchor');
      if (anchor) {
        e.preventDefault();
        const id = anchor.getAttribute('href')?.slice(1);
        if (id) {
          root.querySelector(`#${CSS.escape(id)}`)?.scrollIntoView({ behavior: 'smooth', block: 'start' });
        }
      }
    };
    root.addEventListener('click', handler);
    return () => root.removeEventListener('click', handler);
  }, [segments]);

  // scroll-spy: 表示中の見出しを active に
  useEffect(() => {
    if (!showOutline || outline.length === 0) return;
    const root = containerRef.current;
    if (!root) return;
    const targets = Array.from(root.querySelectorAll<HTMLElement>('h1[id], h2[id], h3[id]'));
    if (targets.length === 0) return;
    const observer = new IntersectionObserver(
      entries => {
        const visible = entries.filter(e => e.isIntersecting);
        if (visible.length > 0) {
          visible.sort((a, b) => a.boundingClientRect.top - b.boundingClientRect.top);
          setActiveSlug(visible[0].target.id);
        }
      },
      { rootMargin: '0px 0px -70% 0px', threshold: 0 }
    );
    targets.forEach(t => observer.observe(t));
    if (!activeSlug && targets[0]) setActiveSlug(targets[0].id);
    return () => observer.disconnect();
  }, [segments, showOutline, outline.length]);

  const handleOutlineClick = (slug: string) => (e: React.MouseEvent) => {
    e.preventDefault();
    const root = containerRef.current;
    root?.querySelector(`#${CSS.escape(slug)}`)?.scrollIntoView({ behavior: 'smooth', block: 'start' });
  };

  const content_el = (
    <div ref={containerRef} className={`${showOutline ? 'prose prose-slate max-w-none mdxg-reader' : 'prose prose-sm max-w-none'} min-w-0 break-words [&_a]:[overflow-wrap:anywhere] [&_a]:break-all [&_code]:[overflow-wrap:anywhere] [&_pre]:max-w-full [&_pre]:overflow-x-auto`}>
      {segments.map((seg, i) => {
        if (seg.type === 'embed') {
          return <EmbedBlock key={`embed-${seg.refId}-${i}`} refId={seg.refId} taskId={taskId!} />;
        }
        return <MarkdownSegment key={`md-${i}`} html={seg.html} />;
      })}
    </div>
  );

  if (!showOutline || outline.length < 2) {
    return content_el;
  }

  return (
    <div className="flex gap-4 items-start">
      <aside className="mdxg-outline hidden md:block flex-shrink-0 sticky top-0 max-h-[68vh] overflow-y-auto pr-2 border-r border-hairline" style={{ width: '200px' }}>
        <div className="text-2xs font-semibold text-slate-500 uppercase tracking-wide px-2 py-1.5">目次</div>
        <nav>
          {outline.map(h => (
            <a
              key={h.slug}
              href={`#${h.slug}`}
              onClick={handleOutlineClick(h.slug)}
              className={`depth-${h.depth} ${activeSlug === h.slug ? 'active' : ''}`}
              title={h.text}
            >
              {h.text}
            </a>
          ))}
        </nav>
      </aside>
      <div className="flex-1 min-w-0">{content_el}</div>
    </div>
  );
}

// --- Print / PDF helpers ---
const PRINT_STYLE = `
  body {
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Hiragino Kaku Gothic ProN", "Yu Gothic", Meiryo, sans-serif;
    color: #1e293b;
    line-height: 1.7;
    max-width: 760px;
    margin: 32px auto;
    padding: 0 24px;
    font-size: 14px;
    -webkit-print-color-adjust: exact;
    print-color-adjust: exact;
  }
  h1, h2, h3, h4, h5, h6 { color: #0f172a; font-weight: 600; margin-top: 1.6em; margin-bottom: 0.5em; line-height: 1.3; }
  h1 { font-size: 1.9em; border-bottom: 1px solid #e2e8f0; padding-bottom: 0.3em; }
  h2 { font-size: 1.5em; border-bottom: 1px solid #e2e8f0; padding-bottom: 0.2em; }
  h3 { font-size: 1.25em; }
  h4 { font-size: 1.05em; }
  p, ul, ol, blockquote { margin: 0.7em 0; }
  ul, ol { padding-left: 1.5em; }
  li { margin: 0.2em 0; }
  a { color: #2563eb; text-decoration: none; }
  a:hover { text-decoration: underline; }
  a.mdxg-anchor { display: none; }
  a.output-path-link { color: #2563eb; cursor: text; }
  code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; background: #f1f5f9; padding: 0.1em 0.35em; border-radius: 4px; font-size: 0.9em; }
  pre { background: #f8fafc; color: #0f172a; padding: 14px 16px; border-radius: 6px; overflow-x: auto; font-size: 0.85em; border: 1px solid #e2e8f0; position: relative; white-space: pre-wrap; word-break: break-word; }
  pre code { background: transparent; color: inherit; padding: 0; font-size: 1em; }
  pre .mdxg-copy, pre .mdxg-lang { display: none; }
  table { border-collapse: collapse; width: 100%; margin: 1em 0; font-size: 0.9em; }
  th, td { border: 1px solid #e2e8f0; padding: 6px 10px; text-align: left; vertical-align: top; }
  th { background: #f8fafc; font-weight: 600; }
  blockquote { border-left: 3px solid #cbd5e1; margin: 1em 0; padding: 0.3em 1em; color: #475569; background: #f8fafc; }
  img { max-width: 100%; height: auto; display: block; margin: 0.5em 0; }
  hr { border: 0; border-top: 1px solid #e2e8f0; margin: 1.5em 0; }
  .mermaid-rendered { margin: 1em 0; text-align: center; }
  .mermaid-rendered svg { max-width: 100%; height: auto; }
  .embed-placeholder { border: 1px dashed #cbd5e1; background: #f8fafc; color: #64748b; padding: 8px 12px; border-radius: 6px; margin: 0.7em 0; font-size: 0.85em; }
  /* hljs minimal light theme */
  .hljs-comment, .hljs-quote { color: #6a737d; font-style: italic; }
  .hljs-keyword, .hljs-selector-tag, .hljs-literal, .hljs-section, .hljs-link { color: #d73a49; }
  .hljs-function .hljs-keyword { color: #d73a49; }
  .hljs-subst { color: #24292e; }
  .hljs-string, .hljs-attr, .hljs-template-tag, .hljs-template-variable { color: #032f62; }
  .hljs-title, .hljs-name, .hljs-type, .hljs-attribute, .hljs-symbol, .hljs-bullet, .hljs-addition, .hljs-variable, .hljs-template-tag, .hljs-template-variable { color: #6f42c1; }
  .hljs-number, .hljs-meta { color: #005cc5; }
  .hljs-built_in, .hljs-builtin-name, .hljs-class .hljs-title { color: #e36209; }
  .hljs-deletion { color: #b31d28; background: #ffeef0; }
  .hljs-regexp, .hljs-link { color: #032f62; }
  @media print {
    body { margin: 0; padding: 12mm; max-width: 100%; }
    a { color: inherit; text-decoration: none; }
    a.output-path-link { color: inherit; }
    pre { background: #f8fafc !important; border: 1px solid #e2e8f0; }
    h1, h2, h3, h4, h5, h6 { page-break-after: avoid; break-after: avoid; }
    pre, table, blockquote, .mermaid-rendered, img { page-break-inside: avoid; break-inside: avoid; }
  }
`;

const EMBED_MARKER_RE = /\[\[embed:([\w-]+)\]\]/g;

async function buildPrintHtml(content: string, opts: { title: string; imageBaseUrl?: string }): Promise<string> {
  const truncated = content.slice(0, 100000);
  const slugger = buildSlugger();
  const absoluteImageBaseUrl = opts.imageBaseUrl
    ? (opts.imageBaseUrl.startsWith('http') ? opts.imageBaseUrl : `${window.location.origin}${opts.imageBaseUrl}`)
    : undefined;
  const renderer = buildMdRenderer({ imageBaseUrl: absoluteImageBaseUrl, slugger });
  const parser = new Marked({ gfm: true, renderer });
  const renderedHtml = DOMPurify.sanitize(parser.parse(truncated, { async: false }) as string, DOMPURIFY_CONFIG);

  // Replace [[embed:xxx]] markers with placeholders BEFORE parsing into DOM
  // (marked passes them through as literal text inside paragraphs).
  EMBED_MARKER_RE.lastIndex = 0;
  const withEmbedPlaceholders = renderedHtml.replace(
    EMBED_MARKER_RE,
    (_, refId) => `<div class="embed-placeholder">📎 Embedded: <code>${escapeHtml(String(refId))}</code></div>`,
  );

  // Parse to a temporary DOM so we can swap mermaid <pre> blocks for rendered SVG.
  const doc = new DOMParser().parseFromString(`<body>${withEmbedPlaceholders}</body>`, 'text/html');
  const mermaidBlocks = Array.from(doc.body.querySelectorAll<HTMLPreElement>('pre.mermaid'));
  for (let i = 0; i < mermaidBlocks.length; i++) {
    const block = mermaidBlocks[i];
    const source = (block.textContent ?? '').trim();
    if (!source) continue;
    try {
      const id = `mermaid-print-${Date.now()}-${i}`;
      const { svg } = await mermaid.render(id, source);
      const wrapper = doc.createElement('div');
      wrapper.className = 'mermaid-rendered';
      wrapper.innerHTML = svg;
      block.replaceWith(wrapper);
    } catch {
      // Leave the original <pre> in place if render fails.
    }
  }

  const bodyHtml = doc.body.innerHTML;
  const safeTitle = escapeHtml(opts.title);

  return `<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>${safeTitle}</title>
<style>${PRINT_STYLE}</style>
</head>
<body>
${bodyHtml}
<script>
(function () {
  function triggerPrint() {
    try { window.focus(); } catch (e) {}
    setTimeout(function () { window.print(); }, 150);
  }
  function waitForImages(done) {
    var imgs = Array.prototype.slice.call(document.images);
    if (imgs.length === 0) { done(); return; }
    var remaining = imgs.length;
    var settled = false;
    function finish() {
      if (settled) return;
      settled = true;
      done();
    }
    imgs.forEach(function (img) {
      if (img.complete) {
        remaining--;
        if (remaining === 0) finish();
        return;
      }
      var onDone = function () {
        remaining--;
        if (remaining === 0) finish();
      };
      img.addEventListener('load', onDone, { once: true });
      img.addEventListener('error', onDone, { once: true });
    });
    setTimeout(finish, 3000);
  }
  function ready() { waitForImages(triggerPrint); }
  if (document.readyState === 'complete') ready();
  else window.addEventListener('load', ready);
})();
</script>
</body>
</html>`;
}

// --- JSONL ---
function badgeClass(color: 'green' | 'orange' | 'red' | 'gray'): string {
  if (color === 'green')  return 'bg-green-100 text-green-800';
  if (color === 'orange') return 'bg-amber-100 text-amber-800';
  if (color === 'red')    return 'bg-red-100 text-red-800';
  return 'bg-slate-100 text-slate-600';
}

function outcomeColor(value: string): 'green' | 'orange' | 'red' | 'gray' {
  if (value === 'success') return 'green';
  if (value === 'ssrf_blocked' || value === 'pdf_blocked' || value === 'binary_blocked') return 'orange';
  if (value === 'error' || value === 'http_error' || value === 'invalid_url') return 'red';
  return 'gray';
}

function formatCell(key: string, value: unknown): ReactNode {
  if (value === null || value === undefined) return <span className="text-slate-300">—</span>;

  if (key === 'url' && typeof value === 'string') {
    return (
      <a href={value} target="_blank" rel="noopener noreferrer"
         className="text-blue-600 underline break-all max-w-[300px] block">
        {value.length > 60 ? `${value.slice(0, 60)}…` : value}
      </a>
    );
  }

  if (key === 'timestamp' && typeof value === 'string') {
    try {
      return new Date(value).toLocaleTimeString('ja-JP', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
    } catch { return String(value); }
  }

  if (key === 'outcome' && typeof value === 'string') {
    const color = outcomeColor(value);
    return <span className={`px-1.5 py-0.5 rounded-full text-[10px] font-bold ${badgeClass(color)}`}>{value}</span>;
  }

  if (key === 'status') {
    if (typeof value === 'string') {
      const color: 'green' | 'red' | 'gray' = value === 'success' ? 'green' : value === 'error' ? 'red' : 'gray';
      return <span className={`px-1.5 py-0.5 rounded-full text-[10px] font-bold ${badgeClass(color)}`}>{value}</span>;
    }
    if (typeof value === 'number') {
      const color: 'green' | 'red' | 'gray' = value >= 200 && value < 300 ? 'green' : value >= 400 ? 'red' : 'gray';
      return <span className={`px-1.5 py-0.5 rounded-full text-[10px] font-bold ${badgeClass(color)}`}>{String(value)}</span>;
    }
  }

  if (key === 'exitCode') {
    const color: 'green' | 'red' = value === 0 ? 'green' : 'red';
    return <span className={`px-1.5 py-0.5 rounded-full text-[10px] font-bold ${badgeClass(color)}`}>{String(value)}</span>;
  }

  if (Array.isArray(value)) return value.join(' ');

  const str = typeof value === 'string' ? value : JSON.stringify(value);
  return str.length > 80 ? `${str.slice(0, 80)}…` : str;
}

function renderJsonl(content: string): JSX.Element {
  const lines = content.trim().split('\n').filter(Boolean).slice(0, 1000);
  const records: Record<string, unknown>[] = [];
  for (const line of lines) {
    try { records.push(JSON.parse(line) as Record<string, unknown>); }
    catch { /* skip invalid lines */ }
  }

  if (records.length === 0) {
    return <p className="text-slate-400 text-sm">表示できるレコードがありません</p>;
  }

  const columns = [...new Set(records.flatMap(r => Object.keys(r)))];

  return (
    <div className="overflow-x-auto">
      <table className="w-full border-collapse text-xs">
        <thead>
          <tr className="bg-slate-100">
            {columns.map(col => (
              <th key={col} className="px-3 py-2 text-left text-xs font-bold text-slate-600 border-b border-slate-200 whitespace-nowrap">
                {col}
              </th>
            ))}
          </tr>
        </thead>
        <tbody>
          {records.map((record, i) => (
            <tr key={i} className={i % 2 === 0 ? 'bg-white' : 'bg-slate-50'}>
              {columns.map(col => (
                <td key={col} className="px-3 py-1.5 border-b border-slate-100 align-top">
                  {formatCell(col, record[col])}
                </td>
              ))}
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}

// --- FilePreview ---
export function FilePreview({ name, content, imageSrc, markdownImageBaseUrl, onClose, taskId, section, filePath, editable }: FilePreviewProps) {
  const [mode, setMode] = useState<'view' | 'edit'>('view');
  const [editContent, setEditContent] = useState(content);
  const [saving, setSaving] = useState(false);
  const [error, setError] = useState('');
  const [currentContent, setCurrentContent] = useState(content);
  const [printing, setPrinting] = useState(false);

  const canEdit = editable && taskId != null && section && filePath;
  const isMarkdownFile = /\.(md|markdown)$/i.test(name);

  const handlePrint = async () => {
    if (printing) return;
    setPrinting(true);
    setError('');
    try {
      const html = await buildPrintHtml(currentContent, { title: name, imageBaseUrl: markdownImageBaseUrl });
      const win = window.open('', '_blank');
      if (!win) {
        setError('印刷ウィンドウを開けませんでした。ポップアップブロックを解除してください。');
        return;
      }
      win.document.open();
      win.document.write(html);
      win.document.close();
    } catch (err) {
      setError(err instanceof Error ? err.message : '印刷の準備に失敗しました');
    } finally {
      setPrinting(false);
    }
  };

  const handleSave = async () => {
    if (!taskId || !section || !filePath) return;
    setSaving(true);
    setError('');
    try {
      await updateLocalFileContent(taskId, section, filePath, editContent);
      setCurrentContent(editContent);
      setMode('view');
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Failed to save');
    } finally {
      setSaving(false);
    }
  };

  const body = (() => {
    if (mode === 'edit') {
      return (
        <div className="flex flex-col gap-2">
          <textarea
            className="w-full min-h-[60vh] font-mono text-xs p-3 border border-hairline rounded-md resize-none focus:outline-none focus:ring-2 focus:ring-accent-ring transition-shadow"
            value={editContent}
            onChange={e => setEditContent(e.target.value)}
          />
          {error && <p className="text-red-600 text-xs">{error}</p>}
          <div className="flex gap-2 justify-end">
            <button
              onClick={() => { setMode('view'); setError(''); }}
              className="px-3 h-8 text-xs rounded-md border border-hairline bg-white text-slate-700 hover:bg-surface transition-colors"
            >
              キャンセル
            </button>
            <button
              onClick={handleSave}
              disabled={saving}
              className="px-3 h-8 text-xs font-semibold rounded-md bg-accent text-accent-fg hover:bg-accent-deep disabled:opacity-50 transition-colors"
            >
              {saving ? '保存中...' : '保存'}
            </button>
          </div>
        </div>
      );
    }

    // view mode
    if (imageSrc) {
      if (/\.html?$/i.test(name)) {
        return (
          <iframe
            src={imageSrc}
            sandbox="allow-scripts allow-same-origin"
            className="w-full rounded-lg border-0"
            style={{ height: '80vh' }}
            title={name}
          />
        );
      }
      if (/\.pdf$/i.test(name)) {
        return (
          <embed
            src={imageSrc}
            type="application/pdf"
            className="w-full rounded-lg"
            style={{ height: '80vh' }}
          />
        );
      }
      return (
        <div className="flex justify-center">
          <img src={imageSrc} alt={name} className="max-w-full max-h-[80vh] object-contain rounded-lg" />
        </div>
      );
    }
    if (/\.(md|markdown)$/i.test(name)) return <MarkdownPreview content={currentContent} imageBaseUrl={markdownImageBaseUrl} showOutline taskId={taskId} />;
    if (/\.csv$/i.test(name))           return renderCsv(currentContent);
    if (/\.jsonl$/i.test(name))         return renderJsonl(currentContent);
    return <pre className="text-xs whitespace-pre-wrap break-all">{currentContent.slice(0, 100000)}</pre>;
  })();

  // All preview types share the wide modal so HTML / PDF / images / tables get
  // the same generous viewport as Markdown (previously non-MD was ~40% narrower).
  const modalWidth = 'min(1400px, 96vw)';

  return (
    <div className="fixed inset-0 bg-black/40 flex items-center justify-center z-50 p-[env(safe-area-inset-top)_env(safe-area-inset-right)_env(safe-area-inset-bottom)_env(safe-area-inset-left)]" onClick={onClose}>
      <div
        className="bg-white rounded-md border border-hairline shadow-md flex flex-col overflow-hidden"
        style={{ width: modalWidth, maxHeight: 'min(90vh, calc(100dvh - env(safe-area-inset-top, 0px) - env(safe-area-inset-bottom, 0px) - 24px))' }}
        onClick={e => e.stopPropagation()}
        role="dialog"
        aria-modal="true"
      >
        <div className="flex justify-between items-center px-4 py-2.5 border-b border-hairline flex-shrink-0 sticky top-0 bg-white z-10 gap-2">
          <div className="font-mono text-xs text-slate-700 truncate" title={name}>{name}</div>
          <div className="flex items-center gap-1 flex-shrink-0">
            {isMarkdownFile && mode === 'view' && (
              <button
                onClick={handlePrint}
                disabled={printing}
                title="ブラウザの印刷ダイアログから PDF として保存または印刷"
                className="inline-flex items-center gap-1 px-2.5 h-7 text-2xs font-medium rounded-md border border-hairline bg-white text-slate-700 hover:bg-surface disabled:opacity-50 transition-colors"
              >
                <svg className="w-3 h-3" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
                  <path d="M4 5V2h8v3M4 11H2.5A1.5 1.5 0 0 1 1 9.5v-3A1.5 1.5 0 0 1 2.5 5h11A1.5 1.5 0 0 1 15 6.5v3a1.5 1.5 0 0 1-1.5 1.5H12M4 9.5h8v4.5H4z" />
                </svg>
                {printing ? '準備中...' : 'PDF / 印刷'}
              </button>
            )}
            {canEdit && mode === 'view' && (
              <button
                onClick={() => { setEditContent(currentContent); setMode('edit'); }}
                className="inline-flex items-center gap-1 px-2.5 h-7 text-2xs font-medium rounded-md border border-hairline bg-white text-slate-700 hover:bg-surface transition-colors"
              >
                <svg className="w-3 h-3" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
                  <path d="M11.5 2.5l2 2L5 13l-2.5.5L3 11l8.5-8.5z" />
                </svg>
                編集
              </button>
            )}
            <button
              onClick={onClose}
              className="inline-flex items-center justify-center w-7 h-7 rounded-md text-slate-400 hover:text-slate-700 hover:bg-surface-2 transition-colors"
              aria-label="プレビューを閉じる"
            >
              <svg className="w-3.5 h-3.5" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round">
                <path d="M4 4l8 8M12 4l-8 8" />
              </svg>
            </button>
          </div>
        </div>
        <div className="p-4 overflow-auto flex-1">
          {mode === 'view' && error && (
            <div className="mb-2 px-3 py-2 bg-red-50 border border-red-200 text-red-700 text-xs rounded">{error}</div>
          )}
          {body}
        </div>
      </div>
    </div>
  );
}
