---
import Base from '../../layouts/Base.astro';
import { ymd } from '../../lib/format';
import { mediaHtml } from '../../lib/media';
import { fixBody, textOf, abs, AUTHOR, PUBLISHER, SITE, SITE_NAME } from '../../lib/seo';
import { SERIES_PAGES, allCards, withDeepDiveThumb } from '../../lib/records';
import RecordCard from '../../components/RecordCard.astro';
import { getCollection, render } from 'astro:content';
import { existsSync, readFileSync } from 'node:fs';
import aiConsult from '../../data/aiConsult.json';
import { linkifyHtml, emphasizeHtml, linkLogsHtml } from '../../lib/links.mjs';
import { roomyTables } from '../../lib/tables.mjs';
import emphasis from '../../data/emphasis.json';

// 記事ページは2種類から作る
// - Studio から移した記事：CMS の本文 HTML（src/data/records/。tools/import_all.py で作る）
// - これから Claude が書く記事：Markdown（src/content/records/）
export async function getStaticPaths() {
  const studio = Object.values(import.meta.glob('../../data/records/*.json', { eager: true, import: 'default' })) as any[];
  const md = await getCollection('records');
  return [
    ...studio.map((r) => ({ params: { slug: r.slug }, props: { kind: 'studio', rec: r } })),
    ...md.map((e) => ({ params: { slug: e.data.slug }, props: { kind: 'md', entry: e } })),
  ];
}
const { kind, entry, rec } = Astro.props;
const esc = (s) => s.replace(/&/g, '&amp;').replace(/</g, '&lt;');
const tocOf = (items) => items.length ? `<nav class="toc"><ul>${items.map((t) => `<li><a href="#${t.id}">${esc(t.text)}</a></li>`).join('')}</ul></nav>` : '';

let r, body = '', Content = null, toc = '';
if (kind === 'studio') {
  r = rec;
  body = fixBody(mediaHtml(rec.body.replace('<!--TOC-->', tocOf(rec.toc))), rec.title);
} else {
  const d = entry.data;
  r = { title: d.title, description: d.meta_description || '', slug: d.slug, day: `LOG${d.day}`, series: d.series,
        published: d.date.toISOString(), cover: d.cover ?? null, thumb: d.cover ? null : `/thumbs/LOG${d.day}.html`, tags: d.tags || [] };
  const rendered = await render(entry);
  Content = rendered.Content;
  toc = tocOf(rendered.headings.filter((h) => h.depth === 2).map((h) => ({ id: h.slug, text: h.text })));
  // 目次は最初の見出しの直前に置く（冒頭の書き出しを先に読んでもらうため。article_rules.md「冒頭の書き出し」）
  const html = entry.rendered?.html;
  if (html) {
    const at = toc ? html.search(/<h2[\s>]/) : -1;
    body = !toc ? html : at >= 0 ? html.slice(0, at) + toc + html.slice(at) : toc + html;
  }
}
// 本文の URL・サイトの住所をリンクにする（中の住所は同じタブ、外のサイトは新しいタブ。src/lib/links.mjs）
// 記事のいちばん大事な一文を太字にする（src/data/emphasis.json）
// 本文の記事番号（LOG334 など）を、その記事へのリンクにする（同じ記事へは最初の1か所だけ・自分へはつながない）。
// 古い呼び方の DAY200 は LOG200 に直す。LOG343（DAY から LOG への呼び方の変更を書いた記事）は DAY を例として書いているので直さない
const LOGS = Object.fromEntries((await allCards()).map((c) => [c.day, c.slug]));
const isEnglish = /-en$/.test(r.slug);
body = linkifyHtml(linkLogsHtml(emphasizeHtml(body, (emphasis as Record<string, string[]>)[r.slug] || []), LOGS, r.slug, isEnglish, r.slug !== 'studio-shutdown-cloudflare-redirect', r.slug === 'day39'));
// 表を窮屈にしない（本文より広い表は横スクロール・短いセルは折り返さない・長いセルは最小の幅を持たせる。src/lib/tables.mjs）
body = roomyTables(body);

// 検索エンジン向けの情報
// 考察記事は、問いを文字で組んだサムネイルを記事の上と SNS の画像にも使う（Gemini の絵より優先）
r = withDeepDiveThumb(r);
// SNS・検索結果に出す画像：表紙の画像 → サムネイルを書き出した画像（public/og/。tools/render_og.py）→ サイト共通の画像
const ogFromThumb = r.thumb ? r.thumb.replace(/^\/thumbs\//, '/og/').replace(/\.html$/, '.jpg') : null;
const ogImage = r.cover || (ogFromThumb && existsSync(`public${ogFromThumb}`) ? ogFromThumb : undefined);
const isEn = rec?.lang === 'EN' || /-en$/.test(r.slug);
const description = r.description || textOf(kind === 'studio' ? rec.body : entry.body, 120);
const allSlugs = new Set([
  ...Object.values(import.meta.glob('../../data/records/*.json', { eager: true, import: 'default' })).map((x: any) => x.slug),
  ...(await getCollection('records')).map((e) => e.data.slug),
]);
const pair = isEn ? r.slug.replace(/-en$/, '') : `${r.slug}-en`;
const alternates = allSlugs.has(pair) ? [{ lang: isEn ? 'ja' : 'en', path: `/record/${pair}` }] : [];
// 英語の考察記事（Consideration）は、考察記事のシリーズページ（series5）につなぐ
const seriesKey = r.series === 'Consideration' ? '考察記事' : r.series;
const seriesPage = Object.entries(SERIES_PAGES).find(([, c]) => c.series === seriesKey);
const seriesName = seriesPage ? seriesPage[1].title.split(' | ').pop() : r.series;

// 同じシリーズの記事（一覧と同じく LOG 番号の大きい順）。前後の記事と、この記事の近くの6本
const sameSeries = (await allCards()).filter((c) => c.series === r.series);
const pos = sameSeries.findIndex((c) => c.slug === r.slug);
const newer = pos > 0 ? sameSeries[pos - 1] : null;
const older = pos >= 0 && pos < sameSeries.length - 1 ? sameSeries[pos + 1] : null;
const others = sameSeries.filter((c) => c.slug !== r.slug);
const start = Math.max(0, Math.min((pos < 0 ? 0 : pos) - 3, others.length - 6));
const nearby = others.slice(start, start + 6);
// AI活用の記事（研究を回すために作った仕組みの記録）の末尾に、相談の案内を出す。
// Markdown の記事は先頭の項目 ai_consult: true、Studio から移した記事は src/data/aiConsult.json の slug で決める
const showConsult = kind === 'md' ? entry.data.ai_consult === true : (aiConsult as string[]).includes(r.slug);
const url = `${SITE}/record/${r.slug}`;
// 長いコード・ログ・データを外に出した記事（LOG364 から。tools/split_files.py）は、
// 全文（Markdown。Studio から移した記事は HTML）を head に、ファイルの一覧を構造化データ（hasPart）に出して、AI がたどれるようにする
const filesIndexPath = `public/files/${r.day}/index.json`;
const filesIndex = existsSync(filesIndexPath) ? JSON.parse(readFileSync(filesIndexPath, 'utf-8')) : null;
const codeLang = { csharp: 'C#', json: 'JSON', yaml: 'YAML', markdown: 'Markdown', python: 'Python', text: 'Text' };
const jsonLd = [
  {
    '@context': 'https://schema.org', '@type': 'BlogPosting',
    headline: r.title, description, url, mainEntityOfPage: url,
    inLanguage: isEn ? 'en' : 'ja',
    datePublished: r.published, dateModified: rec?.updated || r.published,
    image: ogImage ? [abs(ogImage)] : undefined,
    articleSection: r.series, keywords: r.tags?.length ? r.tags.join(', ') : undefined,
    author: AUTHOR, publisher: PUBLISHER,
    isPartOf: { '@type': 'Blog', name: SITE_NAME, url: SITE + '/' },
    hasPart: filesIndex ? [
      { '@type': 'DigitalDocument', name: `${r.day} 記事の全文（${filesIndex.article_full_type === 'text/html' ? 'HTML' : 'Markdown'}）`, encodingFormat: filesIndex.article_full_type || 'text/markdown', url: abs(filesIndex.article_full || filesIndex.article_full_markdown) },
      ...filesIndex.files.map((f) => f.lang === 'csharp' || f.lang === 'yaml' || /\.(cs|prefab|asset|mat)\.txt$/.test(f.file)
        ? { '@type': 'SoftwareSourceCode', name: f.subheading || f.heading || f.file, programmingLanguage: codeLang[f.lang] || f.lang, url: abs(f.url) }
        : { '@type': 'Dataset', name: [f.heading, f.subheading].filter(Boolean).join(' / ') || f.file, url: abs(f.url) }),
    ] : undefined,
  },
  {
    '@context': 'https://schema.org', '@type': 'BreadcrumbList',
    itemListElement: [
      { '@type': 'ListItem', position: 1, name: SITE_NAME, item: SITE + '/' },
      ...(seriesPage ? [{ '@type': 'ListItem', position: 2, name: seriesPage[1].title, item: `${SITE}/${seriesPage[0]}` }] : []),
      { '@type': 'ListItem', position: seriesPage ? 3 : 2, name: r.title, item: url },
    ],
  },
];
---
<Base title={`${r.title} | ${SITE_NAME}`} description={description} ogImage={ogImage} path={`/record/${r.slug}`}
  lang={isEn ? 'en' : 'ja'} ogType="article" published={r.published} modified={rec?.updated || r.published}
  alternates={alternates} jsonLd={jsonLd}>
  {filesIndex && <link slot="head" rel="alternate" type={filesIndex.article_full_type || 'text/markdown'} title="記事の全文（コードとログを含む）" href={filesIndex.article_full || filesIndex.article_full_markdown} />}
  <article class="rec">
    <nav class="crumbs" aria-label="現在地">
      <a href="/">{SITE_NAME}</a><span aria-hidden="true">›</span>{seriesPage ? <a href={`/${seriesPage[0]}`}>{seriesName}</a> : <span>{seriesName}</span>}
    </nav>
    {r.thumb ? <div class="hero"><iframe src={r.thumb} sandbox="allow-scripts" title={r.title} scrolling="no" tabindex="-1"></iframe></div> : r.cover && <img class="hero-img" src={r.cover} alt={r.title} width="1920" height="1080" />}
    <div class="meta"><span class="day">{r.day}</span>{seriesPage ? <a class="tag" href={`/${seriesPage[0]}`}>{r.series}</a> : <span class="tag">{r.series}</span>}<span class="date">{ymd(r.published)}</span></div>
    <h1 class="title">{r.title}</h1>
    {kind === 'studio'
      ? <div class="richText" set:html={body} />
      : body ? <div class="richText" set:html={body} /> : <div class="richText"><Fragment set:html={toc} /><Content /></div>}
    {showConsult && (
      <aside class="consult" aria-label="ご相談について">
        <p>この記事の仕組みは、フィジカルAIの研究をひとりで回すために作ったものです。同じような仕組みを仕事に取り入れたい方は、メッセージからご相談ください。</p>
        <a class="btn-dark" href="/message?topic=ai">メッセージで相談する<i class="ms">arrow_forward</i></a>
      </aside>
    )}
    {(newer || older) && (
      <nav class="pager" aria-label="同じシリーズの前後の記事">
        {older ? <a class="pg pg-prev" href={`/record/${older.slug}`}><span class="pg-k">← 前の記事　{older.day}</span><span class="pg-t">{older.title}</span></a> : <span />}
        {newer ? <a class="pg pg-next" href={`/record/${newer.slug}`}><span class="pg-k">次の記事　{newer.day} →</span><span class="pg-t">{newer.title}</span></a> : <span />}
      </nav>
    )}
  </article>
  {nearby.length > 0 && (
    <section class="related">
      <div class="panel">
        <h2 class="sec-h">{seriesName}の記録 | Series</h2>
        <div class="grid3">{nearby.map((c) => <RecordCard item={c} />)}</div>
        {seriesPage && <div class="rel-more"><a class="btn-dark" href={`/${seriesPage[0]}`}>{seriesName}の記録をすべて見る（{sameSeries.length}本）<i class="ms">arrow_forward</i></a></div>}
      </div>
    </section>
  )}
  {filesIndex && (
    <script is:inline>
      // 長いコード・ログの枠：押したときだけ全文を読み込んで、先頭の数行の所に出す（LOG364 から）
      document.addEventListener('click', async (e) => {
        const b = e.target.closest && e.target.closest('.filebox .fb-open');
        if (!b) return;
        const box = b.closest('.filebox'), code = box.querySelector('.fb-pre code');
        if (box.dataset.full === '1') { box.classList.toggle('fb-closed'); b.textContent = box.classList.contains('fb-closed') ? '全文をここに表示' : 'たたむ'; return; }
        b.disabled = true; b.textContent = '読み込み中…';
        try {
          const res = await fetch(box.dataset.src);
          code.textContent = await res.text();
          box.dataset.full = '1'; box.classList.add('fb-full'); b.textContent = 'たたむ';
        } catch (err) { b.textContent = '読み込めませんでした（別のタブで開いてください）'; }
        b.disabled = false;
      });
    </script>
  )}
</Base>

<style is:global>
  .rec { width: 620px; max-width: calc(100% - 48px); margin: 0 auto; padding: 104px 0 64px; }
  .rec .hero { aspect-ratio: 16 / 9; position: relative; border-radius: 4px; overflow: hidden; background: #0d0d0d; }
  .rec .hero iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; pointer-events: none; }
  .rec .hero-img { width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; border-radius: 4px; display: block; }
  .rec .crumbs { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 8px; margin: 0 0 16px; font: 400 13px/20px var(--sans); color: var(--ink-3); }
  .rec .crumbs a { color: var(--ink-2); text-decoration: underline; text-underline-offset: 2px; }
  .rec .crumbs a:hover { color: var(--ink); }
  .rec a.tag:hover { background: #e4e4e4; color: var(--ink-2); }
  .pager { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin: 64px 0 0; padding-top: 32px; border-top: 1px solid #ddd; }
  .pg { display: flex; flex-direction: column; gap: 8px; padding: 16px; border-radius: 8px; background: #fff; box-shadow: 0 8px 16px rgba(0,0,0,.08); }
  .pg:hover .pg-t { text-decoration: underline; }
  .pg-next .pg-k { text-align: right; }
  .pg-k { font: 400 12px/18px var(--sans); color: var(--ink-3); }
  .pg-t { font: 600 14px/1.6 var(--sans); color: var(--ink); overflow-wrap: anywhere; }
  .rel-more { display: flex; justify-content: center; margin-top: 40px; }
  .rel-more .btn-dark { height: auto; min-height: 40px; padding: 8px 20px; white-space: normal; text-align: center; }
  @media (max-width: 599px) { .pager { grid-template-columns: 1fr; } .pg-next .pg-k { text-align: left; } }
  .consult { margin: 56px 0 0; padding: 24px; border-radius: 8px; background: #f0f0f0; }
  .consult p { margin: 0 0 16px; font: 400 16px/28px var(--sans); color: var(--ink); }
  .consult .btn-dark { height: auto; min-height: 40px; padding: 8px 20px; white-space: normal; }
  .rec .meta { display: flex; align-items: center; gap: 12px; margin-top: 16px; }
  .rec .day { font: 400 28px/28px var(--sans); }
  .rec .title { margin: 12px 0 40px; font: 600 28px/42px var(--sans); overflow-wrap: anywhere; }

  .richText { color: var(--ink); font-family: var(--sans); }
  .richText p { font-size: 18px; line-height: 36px; margin: 8px 0 12px; overflow-wrap: anywhere; }
  .richText h2 { font-size: 24px; line-height: 36px; font-weight: 600; margin: 40px 0 16px; }
  .richText h3 { font-size: 20px; line-height: 30px; font-weight: 600; margin: 36px 0 16px; padding-left: 12px; border-left: 2px solid var(--ink); }
  .richText h4 { font-size: 18px; line-height: 30px; font-weight: 600; margin: 24px 0 8px; }
  .richText h5 { font-size: 14px; line-height: 1.8; font-weight: 400; color: #555; margin: 48px 0 0; }
  .richText hr { border: 0; border-top: 1px solid #ddd; margin: 10px 0; }
  .richText strong { font-weight: 600; }
  .richText a { color: inherit; text-decoration: underline; }
  .richText pre { background: #eee; border-radius: 4px; padding: 25px 35px; margin: 4px 0; overflow-x: auto;
    font: 400 16px/24px var(--mono); white-space: pre-wrap; overflow-wrap: anywhere; }
  .richText pre code { font: inherit; }
  .richText :not(pre) > code { background: #eee; padding: 0 5px; margin: 2px; border: 1px solid rgba(0,0,0,.1); border-radius: 6px; font: 400 18px/36px var(--sans); overflow-wrap: anywhere; }
  .richText blockquote { margin: 10px 0; padding: 10px 15px; border-left: 3px solid rgba(0,0,0,.15); color: #333; }
  .richText blockquote p { font: 400 16px/28.8px 'Noto Sans JP', sans-serif; margin: 10px 0; }
  .richText figure { margin: 16px 0; }
  .richText figure img { width: 100%; height: auto; }
  .richText figcaption:empty { display: none; }
  .richText ul, .richText ol { padding-left: 1.5em; }
  .richText li { font-size: 18px; line-height: 36px; margin: 10px 0; }
  /* 表は本文の幅に押し込まず、はみ出す分は横にスクロールする（縦に1文字ずつ折り返して読めなくなっていた。LOG355） */
  .richText .table { margin: 10px 0; overflow-x: auto; -webkit-overflow-scrolling: touch; }
  .richText table { border-collapse: collapse; width: auto; min-width: 100%; font-size: 14px; }
  /* 表は見出しの行を少し濃く、線をはっきり、本文の行は白に（ページの地色と見分けられるように） */
  .richText th, .richText td { border: 1px solid #d2d2d2; vertical-align: top; text-align: left; }
  .richText th { background: #e9e9e9; padding: 24px; font: 600 16px/24px var(--sans); }
  .richText td { background: #fff; padding: 16px; font: 400 18px/27px var(--sans); }
  .richText th p, .richText td p { margin: 0; font: inherit; }
  .richText .embed { margin: 16px 0; }
  .richText .embed iframe { display: block; width: 100%; height: 150px; border: 0; }
  .richText .video { margin: 16px 0; }
  .richText .video video { display: block; width: 100%; height: auto; }
  .richText .yt { aspect-ratio: 16 / 9; margin: 16px 0; }
  /* 長いコード・ログの枠（LOG364 から。tools/split_files.py） */
  .richText .filebox { margin: 8px 0 16px; border: 1px solid #d2d2d2; border-radius: 4px; background: #fff; }
  .richText .fb-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 12px; padding: 12px 16px; border-bottom: 1px solid #e4e4e4; }
  .richText .fb-name { font: 600 14px/20px var(--mono); overflow-wrap: anywhere; text-decoration: none; border-bottom: 1px solid currentColor; }
  .richText .fb-meta { font: 400 13px/20px var(--sans); color: #666; }
  .richText .filebox .fb-pre { margin: 0; border-radius: 0; padding: 12px 16px; font-size: 13px; line-height: 20px; max-height: 180px; overflow: hidden; position: relative; }
  .richText .filebox.fb-full .fb-pre { max-height: 70vh; overflow: auto; }
  .richText .filebox.fb-full.fb-closed .fb-pre { max-height: 180px; overflow: hidden; }
  .richText .fb-act { display: flex; flex-wrap: wrap; gap: 8px 16px; align-items: center; padding: 10px 16px; border-top: 1px solid #e4e4e4; font: 400 14px/20px var(--sans); }
  .richText .fb-open { font: 600 14px/20px var(--sans); padding: 6px 12px; border: 1px solid var(--ink); border-radius: 4px; background: var(--ink); color: #fff; cursor: pointer; }
  .richText .fb-open:disabled { opacity: .6; cursor: default; }
  .richText .fb-all { padding: 8px 16px; background: #f6f6f6; }
  .richText .fb-all p { font-size: 16px; line-height: 28px; margin: 8px 0; }
  .richText .fb-all li { font-size: 16px; line-height: 28px; margin: 4px 0; }
  .richText .fb-all .fb-lh { font: 600 16px/28px var(--sans); margin: 16px 0 4px; }
  .richText .fb-list .fb-li { font-size: 14px; line-height: 22px; margin: 2px 0; overflow-wrap: anywhere; }
  .richText .yt iframe { width: 100%; height: 100%; border: 0; }

  .toc { background: #f0f0f0; border-radius: 4px; padding: 20px 20px 8px 8px; margin: 0 0 24px; }
  .toc ul { list-style: none; margin: 0; padding: 0; }
  .toc li { position: relative; padding-left: 32px; margin: 0 0 16px; font: 400 16px/32px var(--sans); color: var(--ink-2); }
  .toc a { text-decoration: none; color: inherit; }

  /* 本文より広いもの（表・画像）が画面の幅を押し広げないようにする */
  .richText { max-width: 100%; min-width: 0; }
  .richText a, .richText code, .richText li, .richText td, .richText th, .richText blockquote, .richText h2, .richText h3 { overflow-wrap: anywhere; word-break: break-word; }
  /* 表の中は、英数字の語（ファイル名・数値）を途中で折らない。見出しの行は日本語も途中で折らない */
  .richText td, .richText td code, .richText td a, .richText th, .richText th code { overflow-wrap: normal; word-break: normal; }
  .richText .nw, .richText .nw * { white-space: nowrap; }
  .richText .cw { min-width: 9em; }
  .richText pre code { white-space: pre-wrap; }
  .richText img { max-width: 100%; height: auto; }
  @media (max-width: 767px) {
    .rec { max-width: calc(100% - 48px); padding-top: 72px; }
    .rec .title { font-size: 24px; line-height: 36px; }
  }
</style>
