// サイトの入口。ほとんどのページは作っておいた静的ファイル（dist/）をそのまま返す。
// ここで処理するのは3つだけ:
// - /pixelstomotors/…   旧 URL の形。/pixelstomotors を外した URL へ 301 で転送する
// - /media/{ファイル名}  記事・4コマの画像と動画を R2（p2m-media）から返す（GitHub には入れないため）
// - /api/message         メッセージフォームの送信を R2（p2m-messages）に保存し、メールで知らせる
// - /api/hk-event        更新を知らせるハコブくんが「出た・押された・あとで見に来た」を R2（p2m-messages の hk-events/）に1件ずつ残す
// - /api/hk-stats        その記録を日ごと・更新ごとに数えて見せる（数だけ。誰かは分からない）
// - /api/ev              サイトの使われ方（1回のページ表示ごとの、読んだ深さ・押したもの・迷い）を R2（p2m-messages の ev/）に残す
// - /api/ev-stats        その記録を週ごと・ページごとに数えて見せる（数だけ。誰かは分からない）
// - ページ・llms.txt・全文の Markdown など  静的ファイルを返す。検索エンジン・AI のクローラーが読みに来たら、R2（p2m-messages の cr/）に1件ずつ残す
// - /api/cr-stats        その記録を、どのクローラーが・いつ・どのページを読んだかで数えて見せる（2026-10-25）
// - /api/kpi-stats       主に見る3つの数字（AI の依頼読み・AI のリンクからの来訪・サイト名の検索）を日ごと・週ごとに見せる（worker/kpi.js。2026-11-06）
// - /mcp/{鍵}            サイトの計測を Claude につなぐ部品（MCP サーバー。worker/mcp.js。2026-11-06）
// - 5分ごとの scheduled  SNS の予約投稿の時刻が来ていたら GitHub Actions を起こす（worker/sns_cron.js）。前の日の3つの数字がまだなければ集計して残す（worker/kpi.js）
//
// 守り（2026-10-23）
// - 数の表（/api/hk-stats・/api/ev-stats・/api/cr-stats）は、鍵（Cloudflare の秘密の値 STATS_KEY）を付けたときだけ見られる。鍵がなければ 404
// - 送信の口（/api/message・/api/hk-event・/api/ev）は、このサイトのページから送られたものだけ受け取る
// - 同じ送信元からの送りすぎは、Cloudflare の流量制限（wrangler.jsonc の ratelimits）で断る。送信元の IP アドレスは数えるためだけに使い、記録しない
// - フォームには人には見えない欄（website）を置き、そこが埋まった送信は保存せずに受け取ったふりをする（自動投稿の対策）
// - このファイルが返す応答には、内容の種類を推測させない指定（nosniff）を付ける。ページ側の指定は public/_headers
import { EmailMessage } from 'cloudflare:email';
import { snsCron } from './sns_cron.js';
import { kpiCron, kpiRange, todayJst, addDays } from './kpi.js';
import { handleMcp } from './mcp.js';
import { gscRefresh } from './gsc.js';

export default {
  async fetch(request, env, ctx) {
    const url = new URL(request.url);
    if (url.pathname.startsWith('/mcp/')) return secured(await handleMcp(request, env, url, { evStats, crStats, hkStats }));
    if (url.pathname.startsWith('/api/') || url.pathname.startsWith('/media/') || url.pathname === '/pixelstomotors' || url.pathname.startsWith('/pixelstomotors/')) {
      return secured(await route(request, env, url));
    }
    // 古い住所・まちがった住所への訪問を、今のページへ恒久転送する（2026-10-30。クローラーの「返せなかったページ」から）
    const moved = movedTo(url.pathname);
    if (moved) {
      const to = new URL(moved, url);
      to.search = url.search;
      // クローラーの記録にも残す（返した結果 301。どの古い住所に来たかが分かるように）
      try {
        const bot = crawlerOf(request, url);
        if (bot) ctx.waitUntil(crRecord(request, env, url, bot, 301).catch(() => {}));
      } catch { /* 数え損ねても転送はする */ }
      return secured(Response.redirect(to.toString(), 301));
    }
    let res = await env.ASSETS.fetch(request);
    // 記事の全文ページ（/record/{slug}.md。2026-11-01）は、検索では記事ページを正とする（canonical）。文字コードも付ける
    const full = res.ok && url.pathname.match(/^\/record\/([^/]+)\.md$/);
    if (full) {
      res = new Response(res.body, res);
      res.headers.set('Content-Type', 'text/markdown; charset=utf-8');
      res.headers.set('Link', `<https://pixelstomotors.com/record/${full[1]}>; rel="canonical"`);
    }
    // 検索エンジン・AI のクローラーが読みに来たか（記録の失敗でページを止めない）
    // 記事の「AIで読む」から来た全文ページ（?via=chatgpt など）は、名乗りが一覧にない読み手（Gemini など）も「その他」として残す（2026-11-01）
    try {
      // TOP の「このサイトの楽しみ方」から来た /llms.txt?via=… も同じく数える（2026-11-03）
      const via = (/^\/record\/[^/]+\.md$/.test(url.pathname) || (/^\/record\/[^/.]+$/.test(url.pathname) && url.searchParams.get('via') === 'gemini') || url.pathname === '/llms.txt' || url.pathname === '/llms' || url.pathname === '/llms.html') && AI_VIA.has(url.searchParams.get('via') || '') ? url.searchParams.get('via') : '';
      const bot = crawlerOf(request, url) || (via ? 'その他' : null);
      if (bot) ctx.waitUntil(crRecord(request, env, url, bot, res.status, via).catch(() => {}));
    } catch { /* 数え損ねてもページは返す */ }
    return res;
  },
  // 5分ごと（wrangler.jsonc の triggers）：SNS の予約投稿の時刻が来ていたら、GitHub Actions を起こす（worker/sns_cron.js）
  async scheduled(event, env, ctx) {
    ctx.waitUntil(snsCron(env, event.scheduledTime).then((m) => console.log('sns-cron:', m), (e) => console.error('sns-cron:', e.message)));
    // 前の日の分を作ったとき（1日1回）だけ、Search Console からサイト名の検索の数も取り直す（worker/gsc.js）
    ctx.waitUntil(kpiCron(env).then(async (m) => {
      console.log(m);
      if (m.startsWith('kpi: made')) console.log('gsc:', JSON.stringify(await gscRefresh(env)));
    }).catch((e) => console.error('kpi-cron:', e.message)));
  },
};
