/** PDCAサイクルを回すゲーム本体。フェーズ遷移・日報生成・改善策の購入を担当する。 */
import {
  BUILDS,
  CATWALK_COST,
  GRID_H,
  HANDLER_EQUIPMENT,
  MAX_TIERS,
  TIER_COST,
  GRID_W,
  BREAK_PLANS,
  HAZARD_KINDS,
  PAPER_BACKLOG_LIMIT,
  PAPER_FINE,
  PAPER_FINE_CAP,
  SHIFT_TICKS,
  START_MONEY,
  ALL_ACTIONS,
  COUNTERS,
  machineDef,
  GENERATORS,
  GRID_PRICE,
  WAGE_PER_CAT,
  type CounterDef,
} from '../core/config';
import { createGrid, idx, inBounds, makeTile, tx, ty } from '../core/grid';
import { Rng } from '../core/rng';
import {
  CatState,
  Dir,
  Phase,
  type Cat,
  type DailyReport,
  type HandlerEquipment,
  type HazardKind,
  type TileKind,
  type YardMode,
} from '../core/types';
import { createCat, startCatDay } from './cat';
import { tickCats } from './cat';
import { flushTrucks, tickFactory } from './factory';
import { syncForklifts, tickForklifts } from './forklift';
import { computePower, placeStarterPole, refreshPower, rollWeather } from './power';
import {
  MATERIALS,
  STARTER_BUILDS,
  TECHS,
  TECH_BY_ID,
  type TechId,
} from '../core/research';
import { raiseHazard, recalcMitigation, resetHazardCooldowns } from './incidents';
import {
  activeCats,
  breakLength,
  milestoneProgress,
  nextMilestone,
  pushLog,
  type GameState,
} from './state';

function emptyHazardRecord(): Record<HazardKind, number> {
  const r = {} as Record<HazardKind, number>;
  for (const k of HAZARD_KINDS) r[k] = 0;
  return r;
}

export function createGame(seed = Date.now() % 100000): GameState {
  const s: GameState = {
    tiles: createGrid(),
    cats: [],
    rng: new Rng(seed),
    day: 1,
    tick: 0,
    phase: Phase.Plan,
    speed: 1,
    money: START_MONEY,
    trust: 80,
    itemSeq: 1,
    catSeq: 0,
    forkliftSeq: 0,
    forklifts: [],
    researchPoints: 0,
    researchToday: 0,
    materialCostToday: 0,
    techs: [],
    materialGrade: 0,
    solarFactor: 1,
    powerCoverage: new Uint8Array(0),
    powerRatio: 1,
    powerDemand: 0,
    powerSupply: 0,
    palletsHauledToday: 0,
    basketsHandledToday: 0,
    containersHauledToday: 0,
    truckDeparturesToday: 0,
    routes: [],
    injuriesToday: { injury: 0, serious: 0, fatal: 0 },
    compensationToday: 0,
    safeDays: 0,
    injuriesTotal: { injury: 0, serious: 0, fatal: 0 },
    paperwork: [],
    paperworkDoneToday: 0,
    breakPlan: 'short',
    breakRoomTiles: 0,
    breakRoomBest: 0,
    rank: 0,
    totalShipped: 0,
    safeStreak: 0,
    won: false,
    shippedToday: 0,
    defectsToday: 0,
    revenueToday: 0,
    damagesToday: 0,
    incidentsToday: [],
    risk: emptyHazardRecord(),
    mitigation: emptyHazardRecord(),
    upgrades: {},
    log: [],
    reports: [],
    lastReport: null,
    gameOverReason: null,
  };

  // 初期リスクは低いが、放置すればすぐ育つ
  for (const k of HAZARD_KINDS) s.risk[k] = 8;

  for (let i = 0; i < 3; i++) {
    s.cats.push(createCat(s, 3 + i * 2, 12));
  }

  placeStarterPole(s);
  refreshPower(s);
  pushLog(s, 'デッドキャットファクトリー 操業開始。まずはラインを敷いてください。', 'info');
  pushLog(s, '開発室で研究を進めると、扱える機械と素材が増えます', 'info');
  return s;
}

// ---------------------------------------------------------------- 建設フェーズ

export function buildCost(s: GameState, kind: TileKind, index: number): number {
  const def = BUILDS.find((b) => b.kind === kind);
  if (!def) return 0;
  const cur = s.tiles[index];
  if (kind === 'floor') {
    const prev = BUILDS.find((b) => b.kind === cur.kind);
    return prev ? -Math.round(prev.cost * 0.3) : 0;
  }
  // 同じ向き付きタイルの再クリックは向き変更のみ (無料)
  if (isDirectional(kind) && cur.kind === kind) return 0;
  return def.cost;
}

/** 向きを持つタイル (再クリックで回転する) */
export function isDirectional(kind: TileKind): boolean {
  return (
    kind === 'conveyor' || kind === 'stocker' || kind === 'basket' || kind === 'splitter' || kind === 'merger'
  );
}

export interface BuildResult {
  ok: boolean;
  message?: string;
}

/** タイルを1マス設置する。PLANフェーズ限定。 */
export function placeTile(s: GameState, index: number, kind: TileKind): BuildResult {
  if (s.phase !== Phase.Plan) return { ok: false, message: '稼働中は設計を変更できません' };
  const cur = s.tiles[index];

  if (cur.kind === 'intake' || cur.kind === 'output') {
    return { ok: false, message: '搬入口・出荷口は変更できません' };
  }
  if (cur.kind === 'boiler') {
    return { ok: false, message: 'ボイラー本体は撤去できません' };
  }
  // 外周の壁は工場の外郭なので触らせない
  if (cur.kind === 'wall' && isOuterWall(index)) {
    return { ok: false, message: '外壁は変更できません' };
  }

  // 向き付きタイルの再クリックは向きを回す
  if (isDirectional(kind) && cur.kind === kind) {
    cur.dir = ((cur.dir + 1) % 4) as typeof cur.dir;
    return { ok: true };
  }
  if (kind === cur.kind && kind !== 'floor') return { ok: false };
  if (kind === 'floor' && cur.kind === 'floor' && !cur.catwalk) return { ok: false };

  if (!isBuildUnlocked(s, kind)) {
    return { ok: false, message: 'まだ開発されていません（開発ツリーを進めてください）' };
  }

  const cost = buildCost(s, kind, index);
  if (cost > s.money) return { ok: false, message: '資金が足りません' };

  // 撤去: 積荷や割り当てを解除する
  releaseTile(s, index);
  const next = makeTile(kind);
  next.catwalk = kind === 'floor' ? false : cur.catwalk;
  if (isDirectional(kind)) next.dir = cur.dir;
  s.tiles[index] = next;
  s.money -= cost;
  // 送電範囲と需給はその場で反映して、建てながら確認できるようにする
  refreshPower(s);
  return { ok: true };
}

/**
 * 段積みの段数を上げる (1→2→3→1)。ストッカーとパレット置き場が対象。
 * 段を重ねるほど積めるが、二段目以降は崩落するようになる。
 */
export function cycleTiers(s: GameState, index: number): BuildResult {
  if (s.phase !== Phase.Plan) return { ok: false, message: '稼働中は設計を変更できません' };
  const t = s.tiles[index];
  if (!isActionUnlocked(s, 'stack')) {
    return { ok: false, message: '段積みはまだ開発されていません' };
  }
  if (t.kind !== 'stocker' && t.kind !== 'pallet' && t.kind !== 'basket') {
    return { ok: false, message: '段積みできるのはストッカー・パレット・カゴだけです' };
  }

  const maxTier = t.kind === 'basket' ? 2 : MAX_TIERS;
  if (t.tiers >= maxTier) {
    // 一段に戻す (増設分を払い戻す)
    s.money += TIER_COST * (t.tiers - 1);
    t.tiers = 1;
    return { ok: true };
  }
  if (s.money < TIER_COST) return { ok: false, message: '資金が足りません' };
  s.money -= TIER_COST;
  t.tiers++;
  return { ok: true };
}

/** 置き場（ストッカー・パレット・カゴ・コンテナ）の受入モードを循環切り替えする。 */
export function cycleYardMode(s: GameState, index: number): BuildResult {
  if (s.phase !== Phase.Plan) return { ok: false, message: '稼働中は設定を変更できません' };
  const t = s.tiles[index];
  const isYard = t.kind === 'stocker' || t.kind === 'pallet' || t.kind === 'basket' || t.kind === 'container';
  if (!isYard) return { ok: false, message: '受入モードを変更できるのは置き場・容器のみです' };

  const modes: YardMode[] = ['all', 'raw', 'wip', 'done'];
  const nextIdx = (modes.indexOf(t.yardMode) + 1) % modes.length;
  t.yardMode = modes[nextIdx];
  const labels: Record<YardMode, string> = {
    all: 'すべて',
    raw: '材料のみ',
    wip: '仕掛品のみ',
    done: '完成品のみ',
  };
  pushLog(s, `置き場の受入モードを「${labels[t.yardMode]}」に変更しました`, 'info');
  return { ok: true };
}

export interface RouteResult {
  ok: boolean;
  count: number;
  cost: number;
  message?: string;
}

/** 経路に沿ってコンベアを一括敷設・配線する */
export function applyConveyorRoute(s: GameState, path: number[]): RouteResult {
  if (s.phase !== Phase.Plan) return { ok: false, count: 0, cost: 0, message: '稼働中は設計を変更できません' };
  if (!path || path.length < 2) return { ok: false, count: 0, cost: 0, message: '経路が無効です' };

  const CONVEYOR_COST = 320;
  let neededNewConveyors = 0;

  // 設置が必要なマス数と費用を計算
  for (let k = 0; k < path.length - 1; k++) {
    const index = path[k];
    const t = s.tiles[index];
    if (t.kind === 'floor') {
      neededNewConveyors++;
    }
  }

  const totalCost = neededNewConveyors * CONVEYOR_COST;
  if (s.money < totalCost) {
    return { ok: false, count: 0, cost: totalCost, message: `資金が足りません (必要: ¥${totalCost.toLocaleString()})` };
  }

  let placedCount = 0;

  for (let k = 0; k < path.length - 1; k++) {
    const from = path[k];
    const to = path[k + 1];
    const dx = tx(to) - tx(from);
    const dy = ty(to) - ty(from);
    const dir = dx === 1 ? Dir.East : dy === 1 ? Dir.South : dx === -1 ? Dir.West : Dir.North;

    const t = s.tiles[from];

    if (t.kind === 'floor') {
      releaseTile(s, from);
      const next = makeTile('conveyor');
      next.dir = dir;
      s.tiles[from] = next;
      placedCount++;
    } else if (t.kind === 'conveyor') {
      t.dir = dir;
      placedCount++;
    } else if (t.kind === 'basket' || t.kind === 'stocker') {
      // 始点がカゴやストッカーの場合、配線方向を向かせる
      t.dir = dir;
    }
  }

  // 終点のコンベア向き調整
  const last = path[path.length - 1];
  const lastTile = s.tiles[last];
  if (lastTile.kind === 'conveyor' && path.length >= 2) {
    const prev = path[path.length - 2];
    const dx = tx(last) - tx(prev);
    const dy = ty(last) - ty(prev);
    lastTile.dir = dx === 1 ? Dir.East : dy === 1 ? Dir.South : dx === -1 ? Dir.West : Dir.North;
  }

  s.money -= totalCost;
  refreshPower(s);
  return { ok: true, count: path.length - 1, cost: totalCost };
}

/** 手作業運搬ラインの追加 (左クリック操作) */
export function addLogisticsRoute(
  s: GameState,
  from: number,
  to: number,
): { ok: boolean; message?: string } {
  if (s.phase !== Phase.Plan) {
    return { ok: false, message: '稼働中は設計を変更できません' };
  }
  if (from < 0 || to < 0 || from === to) {
    return { ok: false, message: '始点と終点が同じか無効です' };
  }

  if (!s.routes) s.routes = [];

  const existing = s.routes.some((r) => r.from === from && r.to === to);
  if (existing) {
    return { ok: false, message: '既に設定されている運搬ラインです' };
  }

  s.routes.push({ from, to });
  pushLog(s, `手作業運搬ライン (${tx(from)}, ${ty(from)}) ➜ (${tx(to)}, ${ty(to)}) を設定しました`, 'good');
  return { ok: true, message: '手作業運搬ラインを設定しました (荷役・手すき猫が搬送)' };
}

/** 手作業運搬ラインの削除 (右クリック操作: 2点間またはタイル関連) */
export function removeLogisticsRoute(
  s: GameState,
  from: number,
  to?: number,
): { ok: boolean; count: number; message?: string } {
  if (s.phase !== Phase.Plan) {
    return { ok: false, count: 0, message: '稼働中は設計を変更できません' };
  }
  if (!s.routes || s.routes.length === 0) {
    return { ok: false, count: 0, message: '削除できる運搬ラインがありません' };
  }

  if (to !== undefined && to >= 0 && to !== from) {
    const idx = s.routes.findIndex((r) => r.from === from && r.to === to);
    if (idx >= 0) {
      s.routes.splice(idx, 1);
      pushLog(s, `手作業運搬ライン (${tx(from)}, ${ty(from)}) ➜ (${tx(to)}, ${ty(to)}) を削除しました`, 'info');
      return { ok: true, count: 1, message: '手作業運搬ラインを削除しました' };
    }
    return { ok: false, count: 0, message: '指定された運搬ラインは見つかりませんでした' };
  }

  // toが指定されていない場合、fromに関連する全ルートを削除
  const beforeLen = s.routes.length;
  s.routes = s.routes.filter((r) => r.from !== from && r.to !== from);
  const deleted = beforeLen - s.routes.length;
  if (deleted > 0) {
    pushLog(s, `マス (${tx(from)}, ${ty(from)}) に関連する運搬ラインを ${deleted} 本削除しました`, 'info');
    return { ok: true, count: deleted, message: `運搬ラインを ${deleted} 本削除しました` };
  }
  return { ok: false, count: 0, message: 'このマスに関連する運搬ラインはありません' };
}

/** 手作業運搬ライン（荷役・手すきの猫がピストン搬送するルート）の追加・解除トグル */
export function toggleLogisticsRoute(
  s: GameState,
  from: number,
  to: number,
): { ok: boolean; action: 'added' | 'removed'; message?: string } {
  if (s.phase !== Phase.Plan) {
    return { ok: false, action: 'added', message: '稼働中は設計を変更できません' };
  }
  if (from < 0 || to < 0 || from === to) {
    return { ok: false, action: 'added', message: '始点と終点が同じか無効です' };
  }

  if (!s.routes) s.routes = [];

  const existingIdx = s.routes.findIndex((r) => r.from === from && r.to === to);
  if (existingIdx >= 0) {
    s.routes.splice(existingIdx, 1);
    pushLog(s, `手作業運搬ライン (${tx(from)}, ${ty(from)}) ➜ (${tx(to)}, ${ty(to)}) を解除しました`, 'info');
    return { ok: true, action: 'removed', message: '手作業運搬ラインを解除しました' };
  }

  s.routes.push({ from, to });
  pushLog(s, `手作業運搬ライン (${tx(from)}, ${ty(from)}) ➜ (${tx(to)}, ${ty(to)}) を設定しました`, 'good');
  return { ok: true, action: 'added', message: '手作業運搬ラインを設定しました (荷役・手すき猫が搬送)' };
}

// ================================================================ 従業員管理

/** 雇用費を計算する。 */
export function hireCost(s: GameState): number {
  return 3000 + s.cats.length * 1500;
}

/** 新しい猫を雇う。Plan フェーズのみ。 */
export function hireCat(s: GameState): BuildResult {
  if (s.phase !== Phase.Plan) return { ok: false, message: '稼働中は雇用できません' };
  const cost = hireCost(s);
  if (s.money < cost) return { ok: false, message: `資金が足りません（必要: ¥${cost.toLocaleString()}）` };
  s.money -= cost;

  // 搬入口の近くにスポーン
  const intake = s.tiles.findIndex((t) => t.kind === 'intake');
  const sx = intake >= 0 ? tx(intake) + 1 : 3;
  const sy = intake >= 0 ? ty(intake) : 12;
  const cat = createCat(s, sx, sy);
  s.cats.push(cat);
  pushLog(s, `${cat.name} を雇用しました（¥${cost.toLocaleString()}）`, 'good');
  return { ok: true };
}

/** 猫を解雇する。Plan フェーズのみ。 */
export function fireCat(s: GameState, catId: number): BuildResult {
  if (s.phase !== Phase.Plan) return { ok: false, message: '稼働中は解雇できません' };
  const cat = s.cats.find((c) => c.id === catId);
  if (!cat) return { ok: false, message: '従業員が見つかりません' };
  if (cat.carrying.length > 0) return { ok: false, message: '荷物を持っている従業員は解雇できません' };
  if (cat.offDutyDays > 0) return { ok: false, message: '休職中の従業員は解雇できません' };
  if (s.cats.length <= 1) return { ok: false, message: '最後の従業員は解雇できません' };

  const severance = 1000;
  s.money -= severance;

  // 持ち場を解放
  if (cat.stationIndex !== null) {
    s.tiles[cat.stationIndex].assigned = null;
    cat.stationIndex = null;
  }

  // 手作業運搬ラインも考慮不要（ルートはタイル依存）
  const catIndex = s.cats.findIndex((c) => c.id === catId);
  if (catIndex >= 0) s.cats.splice(catIndex, 1);

  pushLog(s, `${cat.name} を解雇しました（退職金 ¥${severance.toLocaleString()}）`, 'info');
  return { ok: true };
}

/** 荷役機材を変更する。Plan フェーズのみ。差額を精算する。 */
export function setEquipment(
  s: GameState,
  catId: number,
  eq: HandlerEquipment,
): BuildResult {
  if (s.phase !== Phase.Plan) return { ok: false, message: '稼働中は機材変更できません' };
  const cat = s.cats.find((c) => c.id === catId);
  if (!cat) return { ok: false, message: '従業員が見つかりません' };

  const oldCost = HANDLER_EQUIPMENT[cat.equipment]?.cost ?? 0;
  const newCost = HANDLER_EQUIPMENT[eq]?.cost ?? 0;
  const diff = newCost - oldCost;

  if (diff > 0 && s.money < diff) {
    return { ok: false, message: `資金が足りません（差額: ¥${diff.toLocaleString()}）` };
  }

  s.money -= diff;
  cat.equipment = eq;
  const label = HANDLER_EQUIPMENT[eq]?.label ?? eq;
  if (diff > 0) {
    pushLog(s, `${cat.name} に${label}を支給しました（¥${diff.toLocaleString()}）`, 'good');
  } else if (diff < 0) {
    pushLog(s, `${cat.name} の機材を${label}に変更しました（¥${(-diff).toLocaleString()} 返却）`, 'info');
  }
  return { ok: true };
}

/** キャットウォークの敷設・撤去をトグルする。 */
export function toggleCatwalk(s: GameState, index: number): BuildResult {
  if (s.phase !== Phase.Plan) return { ok: false, message: '稼働中は設計を変更できません' };
  const t = s.tiles[index];
  if (t.kind === 'wall' || t.kind === 'intake' || t.kind === 'output') {
    return { ok: false, message: 'ここには敷設できません' };
  }
  if (t.catwalk) {
    t.catwalk = false;
    s.money += Math.round(CATWALK_COST * 0.3);
    return { ok: true };
  }
  if (s.money < CATWALK_COST) return { ok: false, message: '資金が足りません' };
  t.catwalk = true;
  s.money -= CATWALK_COST;
  return { ok: true };
}

function isOuterWall(index: number): boolean {
  const x = index % GRID_W;
  const y = Math.floor(index / GRID_W);
  return x === 0 || y === 0 || x === GRID_W - 1 || y === GRID_H - 1;
}

function releaseTile(s: GameState, index: number): void {
  const t = s.tiles[index];
  if (t.assigned !== null) {
    const cat = s.cats.find((c) => c.id === t.assigned);
    if (cat) cat.stationIndex = null;
  }
  for (const cat of s.cats) {
    if (cat.stationIndex === index) cat.stationIndex = null;
  }
}

/** 猫を割り当てられる持ち場か (作業台・事務机・開発室・詰所・バース・荷役ステーション) */
export function isStation(t: { kind: TileKind }): boolean {
  return (
    machineDef(t.kind)?.needsOperator === true ||
    t.kind === 'office' ||
    t.kind === 'lab' ||
    t.kind === 'handler_post' ||
    t.kind === 'forkdepot' ||
    t.kind === 'dock'
  );
}

/** 持ち場に猫を割り当てる (既に他の持ち場に付いていれば移す)。 */
export function assignCat(s: GameState, catId: number, stationIndex: number | null): void {
  const cat = s.cats.find((c) => c.id === catId);
  if (!cat) return;
  if (cat.stationIndex !== null) s.tiles[cat.stationIndex].assigned = null;
  if (stationIndex === null) {
    cat.stationIndex = null;
    return;
  }
  const station = s.tiles[stationIndex];
  if (!isStation(station)) return;
  if (station.assigned !== null) {
    const prev = s.cats.find((c) => c.id === station.assigned);
    if (prev) prev.stationIndex = null;
  }
  station.assigned = catId;
  cat.stationIndex = stationIndex;
}

/** 休職中・稼働不能な猫が握ったままの作業台を解放する。 */
export function releaseUnavailableAssignments(s: GameState): void {
  for (const cat of s.cats) {
    if (cat.offDutyDays <= 0 && cat.lives > 0) continue;
    if (cat.stationIndex === null) continue;
    const station = s.tiles[cat.stationIndex];
    if (isStation(station) && station.assigned === cat.id) station.assigned = null;
    cat.stationIndex = null;
  }
}

/**
 * 未割り当ての猫を空いている持ち場へ自動で配置する。
 * 生産が止まらないよう作業台を先に埋め、余った猫を事務机に回す。
 */
export function autoAssign(s: GameState): void {
  const machines = s.tiles
    .map((t, i) => ({ t, i }))
    .filter(({ t }) => machineDef(t.kind)?.needsOperator && t.assigned === null)
    .map(({ i }) => i);
  const pickKind = (kind: TileKind): number[] =>
    s.tiles
      .map((t, i) => ({ t, i }))
      .filter(({ t }) => t.kind === kind && t.assigned === null)
      .map(({ i }) => i);

  // 出荷が止まると全部止まるので、バース → 詰所 → 荷役ステーション → 事務机 の順に埋める
  const free = [
    ...machines,
    ...pickKind('dock'),
    ...pickKind('forkdepot'),
    ...pickKind('handler_post'),
    ...pickKind('office'),
    ...pickKind('lab'),
  ];
  for (const cat of activeCats(s)) {
    if (cat.stationIndex !== null) continue;
    const next = free.shift();
    if (next === undefined) break;
    assignCat(s, cat.id, next);
  }
}

/** 担当者がいない機械のタイルindex。ここでラインが詰まる。 */
export function unmannedBenches(s: GameState): number[] {
  return s.tiles
    .map((t, i) => ({ t, i }))
    .filter(({ t }) => machineDef(t.kind)?.needsOperator && t.assigned === null)
    .map(({ i }) => i);
}

// ---------------------------------------------------------------- フェーズ遷移

export function startShift(s: GameState): BuildResult {
  if (s.phase !== Phase.Plan) return { ok: false };
  releaseUnavailableAssignments(s);
  autoAssign(s);
  resetHazardCooldowns();
  clearLine(s);

  rollWeather(s);
  refreshPower(s);
  s.researchToday = 0;
  s.materialCostToday = 0;

  const power = computePower(s);
  if (power.unpowered > 0) {
    pushLog(s, `配電盤の範囲外に機械が ${power.unpowered} 台あります。動きません`, 'warn');
  }
  if (power.ratio < 1) {
    pushLog(
      s,
      `電力が不足しています (需要 ${power.demand.toFixed(1)} / 供給 ${power.supply.toFixed(1)})。全機械が ${Math.round(power.ratio * 100)}% の速度になります`,
      'warn',
    );
  }

  const rooms = measureBreakRooms(s);
  s.breakRoomTiles = rooms.total;
  s.breakRoomBest = rooms.best;
  s.paperworkDoneToday = 0;

  // 事務代行サービスが前夜のうちに一部を処理してくれる
  const outsourced = Math.min(s.paperwork.length, (s.upgrades['outsource'] ?? 0) * 3);
  if (outsourced > 0) {
    s.paperwork.splice(0, outsourced);
    pushLog(s, `事務代行が書類 ${outsourced} 件を処理しました`, 'good');
  }
  s.phase = Phase.Do;
  s.tick = 0;
  s.shippedToday = 0;
  s.defectsToday = 0;
  s.revenueToday = 0;
  s.damagesToday = 0;
  s.incidentsToday = [];
  s.injuriesToday = { injury: 0, serious: 0, fatal: 0 };
  s.compensationToday = 0;
  s.palletsHauledToday = 0;
  s.basketsHandledToday = 0;
  s.containersHauledToday = 0;
  s.truckDeparturesToday = 0;
  syncForklifts(s);
  for (const f of s.forklifts) {
    f.x = tx(f.depot);
    f.y = ty(f.depot);
    f.path = [];
    f.load = [];
    f.state = 'parked';
    f.busy = 0;
    f.target = null;
  }
  for (const cat of s.cats) startCatDay(s, cat);
  pushLog(s, `--- ${s.day}日目 稼働開始 ---`, 'info');

  if (activeCats(s).length === 0) {
    pushLog(
      s,
      '本日は全員が休業中です。生産はできませんが、1日経てば復帰が近づきます',
      'warn',
    );
  }

  if (breakLength(s) > 0 && s.breakRoomTiles < activeCats(s).length) {
    pushLog(
      s,
      `休憩室が ${s.breakRoomTiles} マスしかありません（従業員 ${activeCats(s).length} 匹）。入れない猫が出ます`,
      'warn',
    );
  }
  if (s.paperwork.length > 0) {
    pushLog(s, `未処理の書類が ${s.paperwork.length} 件あります`, s.paperwork.length > PAPER_BACKLOG_LIMIT ? 'warn' : 'info');
  }

  const unmanned = unmannedBenches(s);
  if (unmanned.length > 0) {
    pushLog(
      s,
      `担当者のいない作業台が ${unmanned.length} 台あります。そこでラインが詰まります`,
      'warn',
    );
  }
  return { ok: true };
}

/**
 * 休憩室の総マス数と、最も広く繋がった部屋のマス数を数える。
 * バラバラに置くより1つの部屋にまとめたほうが回復量が上がる。
 */
export function measureBreakRooms(s: GameState): { total: number; best: number } {
  const seen = new Uint8Array(s.tiles.length);
  let total = 0;
  let best = 0;

  for (let i = 0; i < s.tiles.length; i++) {
    if (s.tiles[i].kind !== 'breakroom') continue;
    total++;
    if (seen[i]) continue;

    let size = 0;
    const stack = [i];
    seen[i] = 1;
    while (stack.length) {
      const cur = stack.pop()!;
      size++;
      const cx = tx(cur);
      const cy = ty(cur);
      for (const [dx, dy] of [[1, 0], [-1, 0], [0, 1], [0, -1]] as const) {
        const nx = cx + dx;
        const ny = cy + dy;
        if (!inBounds(nx, ny)) continue;
        const n = idx(nx, ny);
        if (seen[n] || s.tiles[n].kind !== 'breakroom') continue;
        seen[n] = 1;
        stack.push(n);
      }
    }
    best = Math.max(best, size);
  }
  return { total, best };
}

/** 休憩プランを設定する (PLANフェーズのみ)。 */
export function setBreakPlan(s: GameState, id: GameState['breakPlan']): void {
  if (s.phase !== Phase.Plan) return;
  if (!BREAK_PLANS.some((p) => p.id === id)) return;
  s.breakPlan = id;
}

/** 始業時にラインを空にする。前日の滞留を持ち越さないための措置。 */
function clearLine(s: GameState): void {
  for (const t of s.tiles) {
    t.cargo = null;
    t.queue.length = 0;
    t.outSlot = null;
    t.progress = 0;
    t.fullFor = 0;
    t.brokenFor = 0;
    t.cooldown = 0;
    t.uses = 0;
    t.truck = null;
    t.truckTimer = 0;
  }
}

/** DOフェーズを1ティック進める。 */
export function tickShift(s: GameState): void {
  if (s.phase !== Phase.Do) return;
  s.tick++;
  tickFactory(s);
  tickCats(s);
  tickForklifts(s);
  if (s.tick >= SHIFT_TICKS) endShift(s);
}

function endShift(s: GameState): void {
  // 荷台に残った荷は終業時に送り出す
  flushTrucks(s);

  const workers = activeCats(s).length;
  const power = computePower(s);
  const wages = workers * WAGE_PER_CAT;
  // 自家発電でまかなえない分だけ系統から買う
  const fromGrid = Math.max(0, Math.min(power.demand, power.grid));
  const utilities = Math.round(fromGrid * GRID_PRICE);
  const fuel = s.tiles.reduce((a, t) => a + (GENERATORS[t.kind]?.fuel ?? 0), 0);
  s.money -= fuel;
  s.money -= wages + utilities;

  // 書類が滞留したままだと是正費用がかかる。
  // 立て直せなくならないよう、1日に請求される件数には上限を設けている。
  const billable = Math.min(s.paperwork.length, PAPER_FINE_CAP);
  const fines = billable * PAPER_FINE;
  if (fines > 0) {
    s.money -= fines;
    pushLog(
      s,
      `未処理書類 ${s.paperwork.length} 件（うち ${billable} 件）の是正費用 ¥${fines.toLocaleString()} を支払いました`,
      'danger',
    );
  }
  if (s.paperwork.length > PAPER_BACKLOG_LIMIT) {
    raiseHazard(s, 'paperwork', null, null);
  }

  const hazardCounts = emptyHazardRecord();
  for (const inc of s.incidentsToday) hazardCounts[inc.kind]++;

  // 労災が1件でもあれば無災害記録は途切れる
  const injuryCount =
    s.injuriesToday.injury + s.injuriesToday.serious + s.injuriesToday.fatal;
  s.safeDays = injuryCount > 0 ? 0 : s.safeDays + 1;
  s.injuriesTotal.injury += s.injuriesToday.injury;
  s.injuriesTotal.serious += s.injuriesToday.serious;
  s.injuriesTotal.fatal += s.injuriesToday.fatal;

  if (injuryCount > 0) {
    s.trust = Math.max(0, s.trust - injuryCount * 2);
  } else {
    s.trust = Math.min(100, s.trust + 3);
  }

  const report: DailyReport = {
    day: s.day,
    rank: s.rank,
    milestoneAchieved: null,
    shipped: s.shippedToday,
    defects: s.defectsToday,
    revenue: s.revenueToday,
    wages,
    utilities,
    damages: s.damagesToday,
    fines,
    net:
      s.revenueToday - wages - utilities - fuel - s.materialCostToday - s.damagesToday - fines,
    paperworkDone: s.paperworkDoneToday,
    paperworkLeft: s.paperwork.length,
    breakTicks: breakLength(s),
    truckDepartures: s.truckDeparturesToday,
    palletsHauled: s.palletsHauledToday,
    basketsHandled: s.basketsHandledToday,
    containersHauled: s.containersHauledToday,
    research: Math.round(s.researchToday),
    power: { demand: s.powerDemand, supply: s.powerSupply },
    fuel,
    materialCost: s.materialCostToday,
    injuries: { ...s.injuriesToday },
    compensation: s.compensationToday,
    safeDays: s.safeDays,
    incidents: [...s.incidentsToday],
    hazardCounts,
    catSummaries: s.cats.map((c) => ({
      id: c.id,
      name: c.name,
      produced: c.producedToday,
      mood: Math.round(c.mood),
      hp: Math.round(c.hp),
      lives: c.lives,
      offDuty: c.offDutyDays > 0,
      injury: c.injury,
      clerical: c.stationIndex !== null && s.tiles[c.stationIndex]?.kind === 'office',
    })),
  };

  s.totalShipped += report.shipped;
  s.safeStreak = s.safeDays;
  report.rank = s.rank;
  report.milestoneAchieved = checkMilestone(s);

  s.reports.push(report);
  s.lastReport = report;
  if (s.phase !== Phase.GameOver) s.phase = Phase.Check;

  // 対策されていないリスクは翌日以降も少しずつ育つ / 何も起きなかった項目は自然に鎮まる
  for (const k of HAZARD_KINDS) {
    if (hazardCounts[k] === 0) s.risk[k] = Math.max(0, s.risk[k] - 3);
  }

  pushLog(
    s,
    `${s.day}日目 終了: 出荷${report.shipped} / 不良${report.defects} / 収支 ${report.net >= 0 ? '+' : ''}¥${report.net.toLocaleString()}`,
    report.net >= 0 ? 'good' : 'danger',
  );

  checkGameOver(s);
}

/**
 * マイルストーンを達成していればランクを上げる。
 * @returns 達成したマイルストーンのID (無ければ null)
 */
function checkMilestone(s: GameState): string | null {
  const next = nextMilestone(s);
  if (!next) return null;
  if (!next.check(milestoneProgress(s))) return null;

  s.rank++;
  s.money += next.reward;
  pushLog(s, `【昇格】工場ランクが「${next.label}」になりました`, 'good');
  if (next.reward > 0) {
    pushLog(s, `達成報奨金 ¥${next.reward.toLocaleString()} を受け取りました`, 'good');
  }

  if (nextMilestone(s) === null) {
    s.won = true;
    s.phase = Phase.GameOver;
    s.gameOverReason = '宇宙船の部品を作る巨大工場まで登りつめました。猫たちの勝利です。';
  }
  return next.id;
}

/** CHECK → ACTION */
export function goToAction(s: GameState): void {
  if (s.phase === Phase.Check) s.phase = Phase.Action;
}

/** ACTION → 翌日のPLAN */
export function goToNextDay(s: GameState): void {
  if (s.phase !== Phase.Action && s.phase !== Phase.Check) return;
  s.day++;
  s.phase = Phase.Plan;
  s.tick = 0;
  checkGameOver(s);
}

export function buyUpgrade(s: GameState, id: string): BuildResult {
  const def = ALL_ACTIONS.find((u) => u.id === id);
  if (!def) return { ok: false };
  if (def.once && (s.upgrades[id] ?? 0) > 0) return { ok: false, message: '導入済みです' };
  if (s.money < def.cost) return { ok: false, message: '資金が足りません' };

  s.money -= def.cost;
  s.upgrades[id] = (s.upgrades[id] ?? 0) + 1;
  recalcMitigation(s);

  if (id === 'hire') {
    const cat = createCat(s, 3, 12);
    s.cats.push(cat);
    pushLog(s, `${cat.name} を採用しました`, 'good');
  } else {
    // 対策を打った分だけ、対応するリスクスコアが即座に下がる
    for (const kind of def.mitigates) {
      s.risk[kind] = Math.max(0, s.risk[kind] * (1 - def.reduction));
    }
    pushLog(s, `改善策「${def.label}」を実施しました`, 'good');
  }
  return { ok: true };
}

function checkGameOver(s: GameState): void {
  if (s.money < 0) {
    s.phase = Phase.GameOver;
    s.gameOverReason = '資金がショートしました。工場は倒産です。';
    return;
  }
  // 労災の休業は一時的なので、全員が「9つの命を使い切った」時だけ労働力の喪失とみなす
  if (s.cats.length > 0 && s.cats.every((c) => c.lives <= 0)) {
    s.phase = Phase.GameOver;
    s.gameOverReason =
      '全ての従業員が9つの命を使い切りました。労働力を確保できず、工場は閉鎖です。';
  }
}

// ---------------------------------------------------------------- 技術開発

/** その建設物が開発済みで置けるか */
export function isBuildUnlocked(s: GameState, kind: TileKind): boolean {
  if (STARTER_BUILDS.includes(kind)) return true;
  return TECHS.some(
    (t) => t.unlocksBuild.includes(kind) && s.techs.includes(t.id),
  );
}

/** 段積みなどの操作が解禁されているか */
export function isActionUnlocked(s: GameState, action: 'stack'): boolean {
  return TECHS.some((t) => t.unlocksAction === action && s.techs.includes(t.id));
}

/** 調達できる素材グレードの上限 */
export function maxMaterialGrade(s: GameState): number {
  let max = 0;
  for (const t of TECHS) {
    if (t.unlocksMaterial === undefined) continue;
    if (s.techs.includes(t.id)) max = Math.max(max, t.unlocksMaterial);
  }
  return max;
}

/** 搬入口が調達する素材を切り替える。 */
export function setMaterialGrade(s: GameState, grade: number): BuildResult {
  if (s.phase !== Phase.Plan) return { ok: false, message: '稼働中は変更できません' };
  if (grade > maxMaterialGrade(s)) return { ok: false, message: 'その素材はまだ開発されていません' };
  if (grade < 0 || grade >= MATERIALS.length) return { ok: false };
  s.materialGrade = grade;
  return { ok: true };
}

/** 前提条件を満たしていて、いま開発に着手できるか */
export function canResearch(s: GameState, id: TechId): boolean {
  if (s.techs.includes(id)) return false;
  const node = TECH_BY_ID[id];
  return node.requires.every((r) => s.techs.includes(r));
}

/** 技術を開発する。研究ポイントを消費する。 */
export function research(s: GameState, id: TechId): BuildResult {
  const node = TECH_BY_ID[id];
  if (!node) return { ok: false };
  if (s.techs.includes(id)) return { ok: false, message: '開発済みです' };
  if (!canResearch(s, id)) return { ok: false, message: '前提となる開発が終わっていません' };
  if (s.researchPoints < node.cost) return { ok: false, message: '研究ポイントが足りません' };

  s.researchPoints -= node.cost;
  s.techs.push(id);
  pushLog(s, `【開発完了】${node.label}`, 'good');
  if (node.unlocksBuild.length > 0) {
    pushLog(s, `${node.desc}`, 'info');
  }
  return { ok: true };
}

/** 継続対策の対象にする残存リスクのしきい値 */
const LINGERING_RISK = 12;

export interface ActionTopic {
  kind: HazardKind;
  /** 本日の発生件数 */
  count: number;
  /** 対策後の実効リスク */
  risk: number;
  /** その事案に打てる対策 */
  counters: CounterDef[];
  /** 本日発生したのではなく、前日までの火種として残っているもの */
  lingering: boolean;
}

/**
 * PDCAの Action は「Check で挙がった事案」に対してだけ打てる。
 * 本日発生した事案と、まだリスクが残っている継続案件だけを議題に載せる。
 */
export function actionTopics(s: GameState, report: DailyReport | null): ActionTopic[] {
  const topics: ActionTopic[] = [];

  for (const kind of HAZARD_KINDS) {
    const count = report?.hazardCounts[kind] ?? 0;
    const risk = s.risk[kind] * (1 - s.mitigation[kind]);
    // 書類は「溜まっていること自体」が議題になる
    const backlog = kind === 'paperwork' && s.paperwork.length >= 5;

    if (count === 0 && risk < LINGERING_RISK && !backlog) continue;

    const counters = COUNTERS.filter(
      (c) => c.target === kind && !(c.once && (s.upgrades[c.id] ?? 0) > 0),
    );
    if (counters.length === 0) continue;

    topics.push({ kind, count, risk, counters, lingering: count === 0 });
  }

  // 今日の件数が多い順、次にリスクの高い順
  return topics.sort((a, b) => b.count - a.count || b.risk - a.risk);
}

/** 日報の「提言」を組み立てる。発生数の多いハザード順。 */
export function buildAdvice(report: DailyReport): Array<{ kind: HazardKind; count: number }> {
  return HAZARD_KINDS.map((k) => ({ kind: k, count: report.hazardCounts[k] }))
    .filter((e) => e.count > 0)
    .sort((a, b) => b.count - a.count);
}

/** 現在プレイヤーが操作可能な猫（作業台への割り当て候補） */
export function assignableCats(s: GameState): Cat[] {
  return s.cats.filter((c) => c.lives > 0 && c.state !== CatState.OffDuty);
}

export { idx };
