// 主に見る3つの数字を、日ごとに集計して残す（2026-11-06）。
// サイトの今のねらいは「AI に調べられることで、人の来訪や知名度がいつ増えはじめるか」を見ること。
// そのために見る数字を3つに絞った（エイイチ 2026-11-06）：
//   1. aiReads      AI の利用者の依頼で読みに来た数（ChatGPT-User・Claude-User・Perplexity-User・MistralAI-User）
//   2. aiReferrals  AI のサービス（chatgpt.com など）のリンクから来た人の数（人の操作があった表示だけ）
//   3. nameSearch   サイト名で検索して来た人の数（Search Console の数。ここでは集計できないので、別に記録したものを返す）
//
// 置き場所は R2（p2m-messages）の kpi/{日付}.json。日本時間で1日が終わったら、5分ごとの scheduled が前の日の分を作る。
// 元の記録（cr/{日付}/・ev/{日付}/）は読むだけで、変えない。
// 作り直すときは kpi/{日付}.json を消すか、KPI_VERSION を上げる（版が古いものは読むときに作り直す）。

export const KPI_VERSION = 1;
export const KPI_FIRST_DAY = '2026-10-26'; // 人と機械を分け始めた日（それより前の来訪の数は人の数として使えない）

// 1. 利用者の依頼で読む AI（学習・検索のための巡回は入れない）
const AI_USER_BOTS = new Set(['ChatGPT-User', 'Claude-User', 'Perplexity-User', 'MistralAI-User']);
// 読んだ数に入れないページ（読む前の確かめ）
const AI_READ_SKIP = new Set(['/robots.txt']);

// 2. AI のサービスの住所（referrer のホスト）と、リンクに付く印（utm_source）
const AI_HOSTS = {
  'chatgpt.com': 'ChatGPT', 'chat.openai.com': 'ChatGPT',
  'perplexity.ai': 'Perplexity', 'www.perplexity.ai': 'Perplexity',
  'claude.ai': 'Claude',
  'gemini.google.com': 'Gemini', 'bard.google.com': 'Gemini',
  'copilot.microsoft.com': 'Copilot', 'copilot.cloud.microsoft': 'Copilot',
  'chat.deepseek.com': 'DeepSeek', 'grok.com': 'Grok', 'you.com': 'You.com',
  'felo.ai': 'Felo', 'www.genspark.ai': 'Genspark', 'genspark.ai': 'Genspark', 'phind.com': 'Phind', 'www.phind.com': 'Phind',
};
const AI_UTM = {
  'chatgpt.com': 'ChatGPT', 'chatgpt': 'ChatGPT', 'openai': 'ChatGPT',
  'perplexity': 'Perplexity', 'perplexity.ai': 'Perplexity',
  'claude.ai': 'Claude', 'claude': 'Claude',
  'gemini': 'Gemini', 'gemini.google.com': 'Gemini',
  'copilot.com': 'Copilot', 'copilot': 'Copilot', 'copilot.microsoft.com': 'Copilot',
};
export const aiSourceOf = (r, us) => AI_HOSTS[r] || AI_UTM[us] || '';

const jstDay = (ms) => new Date(ms + 9 * 3600e3).toISOString().slice(0, 10);
export const todayJst = () => jstDay(Date.now());
export const addDays = (day, n) => { const d = new Date(day + 'T00:00:00Z'); d.setUTCDate(d.getUTCDate() + n); return d.toISOString().slice(0, 10); };
export const weekOf = (day) => { const d = new Date(day + 'T00:00:00Z'); d.setUTCDate(d.getUTCDate() - ((d.getUTCDay() + 6) % 7)); return d.toISOString().slice(0, 10); };
const inc = (o, k, n = 1) => { o[k] = (o[k] || 0) + n; return o; };
const topOf = (o, n) => Object.entries(o).sort((a, b) => b[1] - a[1]).slice(0, n).map(([k, v]) => ({ key: k, count: v }));

async function listAll(env, prefix) {
  const out = [];
  let cursor;
  do {
    const r = await env.MESSAGES.list({ prefix, cursor, include: ['customMetadata'], limit: 1000 });
    for (const o of r.objects) out.push(o);
    cursor = r.truncated ? r.cursor : undefined;
  } while (cursor);
  return out;
}

// 1日分を元の記録から数える
export async function computeKpiDay(env, day) {
  // 1. AI の利用者の依頼で読みに来た数（cr/）
  const ai = { total: 0, byBot: {}, unverifiedChatGPT: 0, viaButton: 0, pages: {} };
  for (const o of await listAll(env, `cr/${day}/`)) {
    const m = o.customMetadata || {};
    if (!AI_USER_BOTS.has(m.b) || AI_READ_SKIP.has(m.p)) continue;
    // ChatGPT-User は Cloudflare が本物かを確かめられる。確かめられなかったもの（偽物が混ざりうる）は数に入れず、別に数える
    if (m.b === 'ChatGPT-User' && m.ok === '0') { ai.unverifiedChatGPT++; continue; }
    ai.total++; inc(ai.byBot, m.b); inc(ai.pages, m.p);
    if (m.v) ai.viaButton++; // 記事の「AIで読む」ボタンから（エイイチ・Claude の試しが混ざりうる）
  }
  // 2. AI のサービスのリンクから来た人（ev/。同じ表示を送り直したものは n が最大の1件）
  const views = new Map();
  for (const o of await listAll(env, `ev/${day}/`)) {
    const m = o.customMetadata || {};
    if (!m.v) continue;
    const prev = views.get(m.v);
    if (!prev || Number(m.n) >= Number(prev.n)) views.set(m.v, m);
  }
  const ref = { total: 0, bySource: {}, landing: {}, notHuman: 0 };
  let humanViews = 0, humanEntries = 0;
  for (const m of views.values()) {
    const isHuman = m.k === 'h';
    if (isHuman) humanViews++;
    if (isHuman && !m.f) humanEntries++;
    const src = aiSourceOf(m.r || '', m.us || '');
    if (!src) continue;
    if (!isHuman) { ref.notHuman++; continue; } // 機械・操作なしの表示は数に入れない（別に数える）
    ref.total++; inc(ref.bySource, src); inc(ref.landing, m.p);
  }
  return {
    version: KPI_VERSION, day, week: weekOf(day), madeAt: new Date().toISOString(),
    aiReads: { total: ai.total, byBot: ai.byBot, viaButton: ai.viaButton, unverifiedChatGPTExcluded: ai.unverifiedChatGPT, topPages: topOf(ai.pages, 20) },
    aiReferrals: { total: ref.total, bySource: ref.bySource, landing: topOf(ref.landing, 20), notHumanExcluded: ref.notHuman },
    context: { humanViews, humanEntriesFromOutside: humanEntries },
  };
}

// 残してある1日分を読む（なければ作って残す。今日の分は途中なので残さない）
export async function getKpiDay(env, day, { create = true } = {}) {
  const today = todayJst();
  if (day >= today) return { ...(await computeKpiDay(env, day)), partial: true };
  const obj = await env.MESSAGES.get(`kpi/${day}.json`);
  if (obj) {
    try { const d = await obj.json(); if (d.version === KPI_VERSION) return d; } catch { /* 壊れていたら作り直す */ }
  }
  if (!create) return null;
  const d = await computeKpiDay(env, day);
  await env.MESSAGES.put(`kpi/${day}.json`, JSON.stringify(d), { httpMetadata: { contentType: 'application/json' } });
  return d;
}

// 3. サイト名で検索して来た人（Search Console の数。週ごとに kpi-sc/{週の月曜}.json に記録したもの）
export async function getNameSearch(env, since, until) {
  const out = [];
  const r = await env.MESSAGES.list({ prefix: 'kpi-sc/', limit: 1000 });
  for (const o of r.objects) {
    const w = o.key.slice('kpi-sc/'.length, -'.json'.length);
    if (w < weekOf(since) || w > until) continue;
    const obj = await env.MESSAGES.get(o.key);
    if (obj) { try { out.push(await obj.json()); } catch { /* 読めないものは飛ばす */ } }
  }
  return out.sort((a, b) => a.week.localeCompare(b.week));
}

// 期間の数をまとめる。足りない日は作る（1回で作るのは maxCreate 日まで。残りは次に読んだときに作る）
export async function kpiRange(env, since, until, { maxCreate = 10 } = {}) {
  since = since < KPI_FIRST_DAY ? KPI_FIRST_DAY : since;
  const today = todayJst();
  if (until > today) until = today;
  const days = [];
  let created = 0, pending = [];
  for (let d = since; d <= until; d = addDays(d, 1)) {
    let k = await getKpiDay(env, d, { create: false });
    if (!k && created < maxCreate) { k = await getKpiDay(env, d); created++; }
    if (k) days.push(k); else pending.push(d);
  }
  const weeks = new Map();
  for (const k of days) {
    const w = weeks.get(k.week) || { week: k.week, days: 0, aiReads: 0, aiReadsViaButton: 0, aiReferrals: 0, humanViews: 0, aiReadsByBot: {}, aiReferralsBySource: {} };
    w.days++; w.aiReads += k.aiReads.total; w.aiReadsViaButton += k.aiReads.viaButton; w.aiReferrals += k.aiReferrals.total; w.humanViews += k.context.humanViews;
    for (const [b, n] of Object.entries(k.aiReads.byBot)) inc(w.aiReadsByBot, b, n);
    for (const [s, n] of Object.entries(k.aiReferrals.bySource)) inc(w.aiReferralsBySource, s, n);
    weeks.set(k.week, w);
  }
  const nameSearch = await getNameSearch(env, since, until);
  return {
    since, until, pendingDays: pending,
    byWeek: [...weeks.values()].map((w) => ({ ...w, nameSearch: (nameSearch.find((x) => x.week === w.week) || {}).clicks ?? null })),
    byDay: days.map((k) => ({ day: k.day, partial: !!k.partial, aiReads: k.aiReads.total, aiReadsViaButton: k.aiReads.viaButton, aiReferrals: k.aiReferrals.total, humanViews: k.context.humanViews, aiReadsByBot: k.aiReads.byBot, aiReferralsBySource: k.aiReferrals.bySource })),
    detailDays: days,
    nameSearch,
  };
}

// 5分ごとの scheduled から：前の日の分がまだなければ作る
export async function kpiCron(env) {
  const y = addDays(todayJst(), -1);
  if (await env.MESSAGES.head(`kpi/${y}.json`)) return 'kpi: ok';
  await getKpiDay(env, y);
  return `kpi: made ${y}`;
}
