/** 生産ラインのシミュレーション: 搬入口・コンベア・作業台・出荷口。 */
import {
  BENCH_QUEUE_CAP,
  CONVEYOR_TICKS,
  DEFECT_PENALTY,
  INTAKE_INTERVAL,
  MERGER_CAP,
  OVERFLOW_GRACE_TICKS,
  LOAD_TICKS,
  PACKED_PRICE_BONUS,
  PALLET_TIER,
  BASKET_TIER,
  BASKET_INTERVAL,
  CONTAINER_CAP,
  TRUCK_CAP,
  TRUCK_PRICE_BONUS,
  TRUCK_SWAP_TICKS,
  collapseRate,
  PAPER_RISK_RELIEF,
  PAPER_WORK_TICKS,
  machineDef,
  STOCKER_CAP,
  STOCKER_INTERVAL,
} from '../core/config';
import { idx, inBounds, tx, ty } from '../core/grid';
import { MATERIALS, RESEARCH_RATE } from '../core/research';
import { CatState, DIR_VEC, type Cat, type Item, type Tile, type YardMode } from '../core/types';
import { powerFactor } from './power';
import { WORK_FATIGUE } from './cat';
import { raiseHazard } from './incidents';
import { catById, intakeMultiplier, priceMultiplier, pushLog, type GameState } from './state';

function neighborIndex(index: number, dir: number): number {
  const [dx, dy] = DIR_VEC[dir];
  const nx = tx(index) + dx;
  const ny = ty(index) + dy;
  return inBounds(nx, ny) ? idx(nx, ny) : -1;
}

/** 搬送物を載せられるタイルか */
function isTransport(t: Tile): boolean {
  return (
    t.kind === 'conveyor' ||
    t.kind === 'stocker' ||
    t.kind === 'basket' ||
    t.kind === 'container' ||
    t.kind === 'splitter' ||
    t.kind === 'merger' ||
    t.kind === 'pallet' ||
    t.kind === 'dock'
  );
}

/** 分岐機の払い出し方向 (正面・左・右)。背面は入力専用。 */
function splitterOutDirs(t: Tile): [number, number, number] {
  return [t.dir, (t.dir + 3) % 4, (t.dir + 1) % 4];
}

/** n が from からの搬送物を受け入れられる向き・構造か */
export function acceptsFrom(tiles: Tile[], n: number, from: number): boolean {
  const t = tiles[n];
  switch (t.kind) {
    case 'conveyor':
    case 'stocker':
    case 'basket':
      return neighborIndex(n, (t.dir + 2) % 4) === from;
    case 'merger':
      return neighborIndex(n, t.dir) !== from;
    case 'pallet':
    case 'container':
    case 'dock':
      return true;
    case 'splitter':
      return neighborIndex(n, (t.dir + 2) % 4) === from;
    case 'output':
      return true;
    default:
      return machineDef(t.kind) !== null;
  }
}

/** タイルが前方に接続先（コンベア・機械・出荷口など）を持っているか */
export function hasForwardConnection(tiles: Tile[], index: number): boolean {
  const t = tiles[index];
  if (t.kind !== 'basket' && t.kind !== 'stocker' && t.kind !== 'pallet' && t.kind !== 'container') return false;
  const n = neighborIndex(index, t.dir);
  if (n < 0) return false;
  return acceptsFrom(tiles, n, index);
}

/** 置き場モードによる受け入れ可否判定 */
export function matchesYardMode(item: Item, mode: YardMode): boolean {
  if (!mode || mode === 'all') return true;
  if (mode === 'raw') return item.stage === 0;
  if (mode === 'done') return isFinished(item);
  if (mode === 'wip') return item.stage > 0 && !isFinished(item);
  return true;
}

/** index から搬送物を渡せる隣接タイルを列挙する (= 出力先候補) */
function outboundTargets(tiles: Tile[], index: number): number[] {
  const out: number[] = [];
  for (let d = 0; d < 4; d++) {
    const n = neighborIndex(index, d);
    if (n < 0) continue;
    const t = tiles[n];
    if (t.kind !== 'output' && !isTransport(t)) continue;
    if (acceptsFrom(tiles, n, index)) out.push(n);
  }
  return out;
}

/** 出力先タイルが from から今すぐ1個受け取れるか */
function canAccept(tiles: Tile[], n: number, item: Item, from: number): boolean {
  const dest = tiles[n];
  if (dest.brokenFor > 0) return false;
  if (!acceptsFrom(tiles, n, from)) return false;
  switch (dest.kind) {
    case 'conveyor':
    case 'splitter':
      return dest.cargo === null;
    case 'merger':
      return dest.queue.length < MERGER_CAP;
    case 'stocker':
      if (!matchesYardMode(item, dest.yardMode)) return false;
      return dest.queue.length < STOCKER_CAP * dest.tiers;
    case 'pallet':
      if (!matchesYardMode(item, dest.yardMode)) return false;
      return dest.queue.length < PALLET_TIER * dest.tiers;
    case 'basket':
      if (!matchesYardMode(item, dest.yardMode)) return false;
      return dest.queue.length < BASKET_TIER * dest.tiers;
    case 'container':
      if (!matchesYardMode(item, dest.yardMode)) return false;
      return dest.queue.length < CONTAINER_CAP;
    case 'dock':
      return dest.queue.length < TRUCK_CAP * 2;
    case 'output':
      return true;
    default: {
      const def = machineDef(dest.kind);
      if (!def) return false;
      if (dest.queue.length >= BENCH_QUEUE_CAP) return false;
      // 加工機は未完成品だけを受け取る。
      // 梱包機は例外で、完成済みでも未梱包なら受け取って封をする。
      return !isFinished(item) || (def.packs && !item.packed);
    }
  }
}

/** 出力先タイルへ1個渡す。canAccept が true の時だけ呼ぶこと。 */
function handOver(s: GameState, n: number, item: Item): void {
  const dest = s.tiles[n];
  item.slide = 0;
  switch (dest.kind) {
    case 'conveyor':
    case 'splitter':
      dest.cargo = item;
      break;
    case 'merger':
    case 'stocker':
    case 'pallet':
    case 'basket':
    case 'container':
    case 'dock':
      dest.queue.push(item);
      break;
    case 'output':
      if (dest.truck !== null && dest.truck.length < TRUCK_CAP) {
        dest.truck.push(item);
        dest.uses++;
        if (dest.truck.length >= TRUCK_CAP) departTruck(s, dest);
      } else {
        dest.queue.push(item);
      }
      break;
    default:
      if (machineDef(dest.kind)) dest.queue.push(item);
      break;
  }
}

export function newItem(s: GameState): Item {
  return {
    id: s.itemSeq++,
    grade: s.materialGrade,
    stage: 0,
    precision: 0,
    defective: false,
    packed: false,
    slide: 0,
  };
}

/** 空き判定にだけ使う原材料のダミー。実体は払い出し時に生成する。 */
const RAW_PROBE: Item = {
  id: -1, grade: 0, stage: 0, precision: 0, defective: false, packed: false, slide: 0,
};

/** 猫のコンディションから作業速度倍率を求める。 */
function workRate(cat: Cat): number {
  const focus = 0.45 + (cat.focus / 100) * 0.75;
  const stamina = 0.5 + (cat.hp / 100) * 0.6;
  const mood = 0.55 + (cat.mood / 100) * 0.6;
  return focus * stamina * mood;
}

export function tickFactory(s: GameState): void {
  tickBlackout(s);
  tickOffices(s);
  tickLabs(s);
  tickStacks(s);
  tickDocks(s);
  tickOutputs(s);
  tickMachines(s);
  tickSplitters(s);
  tickMergers(s);
  tickStockers(s);
  tickBaskets(s);
  tickConveyors(s);
  tickIntakes(s);
}

/**
 * 事務室: 割り当てられた猫が書類を1件ずつ処理する。
 * 処理が終わると、その事案のリスクスコアが下がり信頼度が少し戻る。
 */
function tickOffices(s: GameState): void {
  for (let i = 0; i < s.tiles.length; i++) {
    const t = s.tiles[i];
    if (t.kind !== 'office') continue;

    const cat = catById(s, t.assigned);
    const present =
      cat !== null &&
      cat.offDutyDays <= 0 &&
      cat.state === CatState.Clerical &&
      Math.round(cat.x) === tx(i) &&
      Math.round(cat.y) === ty(i);

    if (!present || !cat) {
      t.progress = 0;
      continue;
    }
    if (s.paperwork.length === 0) {
      t.progress = 0;
      continue;
    }

    const doc = s.paperwork[0];
    const need = PAPER_WORK_TICKS[doc.severity];
    // 事務作業は集中力と記憶力がものを言う
    const rate = (0.4 + (cat.focus / 100) * 0.7) * (0.6 + (cat.memory / 100) * 0.6);
    t.progress += rate / need;

    if (t.progress >= 1) {
      t.progress = 0;
      s.paperwork.shift();
      s.paperworkDoneToday++;
      t.uses++;
      cat.producedToday++;
      cat.hp = Math.max(0, cat.hp - 0.5);
      cat.boredom = Math.min(100, cat.boredom + 3);
      s.risk[doc.kind] = Math.max(0, s.risk[doc.kind] - PAPER_RISK_RELIEF[doc.severity]);
      s.trust = Math.min(100, s.trust + 0.4);
    }
  }
}

/**
 * 加工機械 (作業台・旋盤・プレス機・アームロボット・梱包機) を進める。
 * どれも「入力バッファから1個取り、加工段階を1つ上げる」点は共通で、
 * 速度・不良率・仕上がり・固有の労災だけが機械ごとに違う。
 */
function tickMachines(s: GameState): void {
  for (let i = 0; i < s.tiles.length; i++) {
    const t = s.tiles[i];
    const def = machineDef(t.kind);
    if (!def) continue;

    if (t.brokenFor > 0) {
      t.brokenFor--;
      if (t.brokenFor === 0) {
        pushLog(s, `${def.label} (${tx(i)},${ty(i)}) の復旧が完了しました`, 'good');
      }
      continue;
    }

    // 完成品の払い出しを先に試みる
    if (t.outSlot) deliverFromMachine(s, i);

    const cat = catById(s, t.assigned);
    const catPresent =
      cat !== null &&
      cat.offDutyDays <= 0 &&
      Math.round(cat.x) === tx(i) &&
      Math.round(cat.y) === ty(i);
    const operating = def.needsOperator
      ? catPresent && cat !== null && cat.state === CatState.Working
      : true;

    // オーバーフロー: 担当が席を外している間に部品が積み上がると、
    // 戻ってきた猫が「殺到」に威嚇して作業を放棄する。
    // 猫が付いていて捌けているだけの満杯は健全な飽和なので数えない。
    if (def.needsOperator) {
      const unattended = t.queue.length >= BENCH_QUEUE_CAP && !operating;
      t.fullFor = unattended ? t.fullFor + 1 : Math.max(0, t.fullFor - 2);
      if (t.fullFor > OVERFLOW_GRACE_TICKS && cat && catPresent) {
        raiseHazard(s, 'overflow', cat, i);
        t.fullFor = 0;
      }
    }

    if (!operating) continue;
    if (t.outSlot !== null) continue;
    if (t.queue.length === 0) continue;

    // 機械固有の労災。稼働している間ずっと危険が続く。
    if (def.hazard) {
      const victim = def.needsOperator ? cat : nearbyCat(s, i);
      if (victim && s.rng.chance(def.hazardRate)) {
        raiseHazard(s, def.hazard, victim, i);
        continue;
      }
    }

    const rate = (def.needsOperator && cat ? workRate(cat) : 1) * powerFactor(s, i);
    if (rate <= 0) continue;
    t.progress += rate / def.workTicks;

    if (t.progress >= 1) {
      t.progress = 0;
      const item = t.queue.shift()!;
      item.stage += 1;
      item.precision += def.precision;
      item.slide = 0;
      if (def.packs) item.packed = true;
      // 集中力が切れていると加工ミスで不良品になる (機械の精度で増減する)
      const missChance = cat ? Math.max(0, (60 - cat.focus) / 400) : 0.01;
      if (s.rng.chance(missChance * def.defectFactor)) item.defective = true;
      t.outSlot = item;
      t.uses++;
      if (cat) {
        cat.producedToday++;
        cat.hp = Math.max(0, cat.hp - WORK_FATIGUE);
        cat.boredom = Math.max(0, cat.boredom - 12);
      }
    }
  }
}

/** アームロボットの可動範囲 (隣接マス) にいる猫を1匹返す。 */
function nearbyCat(s: GameState, index: number): Cat | null {
  const cx = tx(index);
  const cy = ty(index);
  for (const cat of s.cats) {
    if (cat.offDutyDays > 0 || cat.lives <= 0) continue;
    if (Math.abs(cat.x - cx) <= 1.2 && Math.abs(cat.y - cy) <= 1.2) return cat;
  }
  return null;
}

/** 機械の完成品を、離れる向きのコンベアか隣接する出荷口へ渡す。 */
function deliverFromMachine(s: GameState, stationIndex: number): boolean {
  const t = s.tiles[stationIndex];
  if (!t.outSlot) return false;

  for (let d = 0; d < 4; d++) {
    const n = neighborIndex(stationIndex, d);
    if (n < 0) continue;
    if (s.tiles[n].kind === 'output') {
      shipItem(s, t.outSlot);
      t.outSlot = null;
      return true;
    }
  }

  for (const n of outboundTargets(s.tiles, stationIndex)) {
    if (canAccept(s.tiles, n, t.outSlot, stationIndex)) {
      handOver(s, n, t.outSlot);
      t.outSlot = null;
      return true;
    }
  }
  return false;
}

function tickConveyors(s: GameState): void {
  const conveyors: number[] = [];
  for (let i = 0; i < s.tiles.length; i++) {
    if (s.tiles[i].kind === 'conveyor') conveyors.push(i);
  }

  // 進捗を進める。電気が来ていないコンベアは止まる。
  for (const i of conveyors) {
    const t = s.tiles[i];
    if (t.brokenFor > 0) {
      t.brokenFor--;
      continue;
    }
    const p = powerFactor(s, i);
    if (p <= 0) continue;
    if (t.cargo) t.cargo.slide = Math.min(1, t.cargo.slide + p / CONVEYOR_TICKS);
  }

  // 下流から順に空くので、変化が無くなるまで繰り返して転送する
  let changed = true;
  let guard = 0;
  while (changed && guard++ < 8) {
    changed = false;
    for (const i of conveyors) {
      const t = s.tiles[i];
      if (t.brokenFor > 0 || !t.cargo || t.cargo.slide < 1) continue;
      if (powerFactor(s, i) <= 0) continue;
      if (transferFrom(s, i)) changed = true;
    }
  }

  // 詰まっている荷物は手前で止めて、見た目の重なりを防ぐ
  for (const i of conveyors) {
    const t = s.tiles[i];
    if (t.cargo && t.cargo.slide >= 1) t.cargo.slide = 0.5;
  }
}

function transferFrom(s: GameState, from: number): boolean {
  const t = s.tiles[from];
  const item = t.cargo!;
  const n = neighborIndex(from, t.dir);
  if (n < 0) return false;
  if (!canAccept(s.tiles, n, item, from)) return false;
  handOver(s, n, item);
  t.cargo = null;
  return true;
}

/** 分岐機: 抱えた1個を正面・左・右へラウンドロビンで振り分ける。 */
function tickSplitters(s: GameState): void {
  for (let i = 0; i < s.tiles.length; i++) {
    const t = s.tiles[i];
    if (t.kind !== 'splitter') continue;
    if (powerFactor(s, i) <= 0) continue;
    if (t.brokenFor > 0) {
      t.brokenFor--;
      continue;
    }
    if (!t.cargo) continue;

    const dirs = splitterOutDirs(t);
    for (let k = 0; k < dirs.length; k++) {
      const d = dirs[(t.rr + k) % dirs.length];
      const n = neighborIndex(i, d);
      if (n < 0) continue;
      if (!canAccept(s.tiles, n, t.cargo, i)) continue;
      handOver(s, n, t.cargo);
      t.cargo = null;
      // 次は「今使った出口の次」から探し始める
      t.rr = (t.rr + k + 1) % dirs.length;
      break;
    }
  }
}

/** 合流機: 各方向から受け取った荷を待避スロットに溜め、正面へ順に流す。 */
function tickMergers(s: GameState): void {
  for (let i = 0; i < s.tiles.length; i++) {
    const t = s.tiles[i];
    if (t.kind !== 'merger') continue;
    if (powerFactor(s, i) <= 0) continue;
    if (t.brokenFor > 0) {
      t.brokenFor--;
      continue;
    }
    if (t.queue.length === 0) continue;
    const n = neighborIndex(i, t.dir);
    if (n < 0) continue;
    const head = t.queue[0];
    if (!canAccept(s.tiles, n, head, i)) continue;
    t.queue.shift();
    handOver(s, n, head);
  }
}

/** 電力不足が続くと停電事故になる。 */
function tickBlackout(s: GameState): void {
  if (s.powerRatio >= 1 || s.powerDemand <= 0) return;
  // 不足が大きいほど頻発する
  const severity = 1 - s.powerRatio;
  if (!s.rng.chance(severity * 0.0012)) return;
  const victim = s.cats.find((c) => c.offDutyDays <= 0 && c.lives > 0) ?? null;
  raiseHazard(s, 'blackout', victim, null);
}

/**
 * 開発室: 割り当てられた猫が研究ポイントを稼ぐ。
 * 集中力と記憶力がそのまま開発速度になる。
 */
function tickLabs(s: GameState): void {
  for (let i = 0; i < s.tiles.length; i++) {
    const t = s.tiles[i];
    if (t.kind !== 'lab') continue;

    const cat = catById(s, t.assigned);
    const present =
      cat !== null &&
      cat.offDutyDays <= 0 &&
      cat.state === CatState.Research &&
      Math.round(cat.x) === tx(i) &&
      Math.round(cat.y) === ty(i);
    if (!present || !cat) {
      t.progress = 0;
      continue;
    }

    const rate =
      RESEARCH_RATE * (0.45 + (cat.focus / 100) * 0.8) * (0.5 + (cat.memory / 100) * 0.8);
    s.researchPoints += rate;
    s.researchToday += rate;
    t.progress = (t.progress + rate * 0.6) % 1;
    t.uses++;
    cat.hp = Math.max(0, cat.hp - 0.006);
    cat.boredom = Math.min(100, cat.boredom + 0.02);
  }
}

/**
 * 段積みしたストッカー・パレット置き場の崩落判定。
 * 二段目以降は、満載に近いほど崩れやすい。
 */
function tickStacks(s: GameState): void {
  for (let i = 0; i < s.tiles.length; i++) {
    const t = s.tiles[i];
    if (t.kind !== 'stocker' && t.kind !== 'pallet' && t.kind !== 'basket') continue;
    if (t.tiers <= 1 || t.queue.length === 0) continue;

    const baseCap = t.kind === 'stocker' ? STOCKER_CAP : t.kind === 'pallet' ? PALLET_TIER : BASKET_TIER;
    const cap = baseCap * t.tiers;
    const rate = collapseRate(t.tiers, t.queue.length / cap);
    if (rate <= 0 || !s.rng.chance(rate)) continue;

    // 近くにいる猫が巻き添えになる
    const victim = nearbyCat(s, i);
    const sev = raiseHazard(s, 'stack_collapse', victim, i);
    if (sev !== null && sev !== 'nearmiss') {
      // 崩れた荷は不良品になって床に散る
      const lost = t.queue.splice(0, Math.ceil(t.queue.length / 2));
      for (const item of lost) {
        item.defective = true;
        s.money -= DEFECT_PENALTY;
        s.damagesToday += DEFECT_PENALTY;
        s.defectsToday++;
      }
    }
  }
}

/**
 * トラックバース: 割り当てられた猫が1個ずつ荷台へ積み込む。
 * 満載になるとトラックが出発し、まとめて出荷される (単価+25%)。
 */
function tickDocks(s: GameState): void {
  for (let i = 0; i < s.tiles.length; i++) {
    const t = s.tiles[i];
    if (t.kind !== 'dock') continue;

    if (t.truckTimer > 0) {
      t.truckTimer--;
      if (t.truckTimer === 0) t.truck = [];
      continue;
    }
    if (t.truck === null) t.truck = [];

    const cat = catById(s, t.assigned);
    const present =
      cat !== null &&
      cat.offDutyDays <= 0 &&
      cat.state === CatState.Loading &&
      Math.round(cat.x) === tx(i) &&
      Math.round(cat.y) === ty(i);

    if (!present || !cat || t.queue.length === 0) {
      t.progress = 0;
      continue;
    }

    // 積み込みは体力仕事。疲れた猫が続けると荷台から落ちる。
    if (s.rng.chance(cat.hp < 35 ? 0.0009 : 0.00025)) {
      raiseHazard(s, 'truck_load', cat, i);
      continue;
    }

    t.progress += (0.5 + (cat.hp / 100) * 0.7) / LOAD_TICKS;
    if (t.progress >= 1) {
      t.progress = 0;
      const item = t.queue.shift()!;
      t.truck.push(item);
      t.uses++;
      cat.producedToday++;
      cat.hp = Math.max(0, cat.hp - 0.6);

      if (t.truck.length >= TRUCK_CAP) departTruck(s, t);
    }
  }
}

/** 満載のトラックを送り出す。 */
function departTruck(s: GameState, t: Tile): void {
  if (!t.truck) return;
  for (const item of t.truck) shipItem(s, item, TRUCK_PRICE_BONUS);
  pushLog(s, `🚚 トラックが ${t.truck.length} 個を積んで出発しました (単価+25%)`, 'good');
  s.truckDeparturesToday++;
  t.truck = null;
  t.truckTimer = TRUCK_SWAP_TICKS;
}

/** 出荷口 (output): 定期的にトラックが着車し、コンベアや荷役から届いた完成品を積み込んで出発する。 */
function tickOutputs(s: GameState): void {
  for (let i = 0; i < s.tiles.length; i++) {
    const t = s.tiles[i];
    if (t.kind !== 'output') continue;

    // トラックの入れ替えタイマー
    if (t.truckTimer > 0) {
      t.truckTimer--;
      if (t.truckTimer === 0) {
        t.truck = [];
        pushLog(s, '🚚 出荷場にトラックが着車しました', 'info');
      }
      continue;
    }
    if (t.truck === null) t.truck = [];

    // 待機キューに荷物があればトラック荷台へ順次積み込み
    while (t.queue.length > 0 && t.truck.length < TRUCK_CAP) {
      const item = t.queue.shift()!;
      t.truck.push(item);
      t.uses++;
    }

    if (t.truck.length >= TRUCK_CAP) {
      departTruck(s, t);
    }
  }
}

/** ストッカー: 溜めた在庫を一定間隔で下流へ払い出す。 */
function tickStockers(s: GameState): void {
  for (let i = 0; i < s.tiles.length; i++) {
    const t = s.tiles[i];
    if (t.kind !== 'stocker') continue;
    if (powerFactor(s, i) <= 0) continue;
    if (t.brokenFor > 0) {
      t.brokenFor--;
      continue;
    }
    if (t.cooldown > 0) {
      t.cooldown--;
      continue;
    }
    if (t.queue.length === 0) continue;
    const n = neighborIndex(i, t.dir);
    if (n < 0) continue;
    const head = t.queue[0];
    if (!canAccept(s.tiles, n, head, i)) continue;
    t.queue.shift();
    handOver(s, n, head);
    t.cooldown = STOCKER_INTERVAL;
  }
}

/** カゴ: 溜めた在庫を一定間隔で下流へ払い出す（手作業運搬も可）。 */
function tickBaskets(s: GameState): void {
  for (let i = 0; i < s.tiles.length; i++) {
    const t = s.tiles[i];
    if (t.kind !== 'basket') continue;
    if (t.brokenFor > 0) {
      t.brokenFor--;
      continue;
    }
    if (t.cooldown > 0) {
      t.cooldown--;
      continue;
    }
    if (t.queue.length === 0) continue;
    const n = neighborIndex(i, t.dir);
    if (n < 0) continue;
    const head = t.queue[0];
    if (!canAccept(s.tiles, n, head, i)) continue;
    t.queue.shift();
    handOver(s, n, head);
    t.cooldown = BASKET_INTERVAL;
  }
}

/** その素材で完成品と言えるところまで加工されているか */
export function isFinished(item: Item): boolean {
  const mat = MATERIALS[item.grade] ?? MATERIALS[0];
  return item.stage >= mat.stages;
}

export function shipItem(s: GameState, item: Item, bonus = 1): void {
  const mat = MATERIALS[item.grade] ?? MATERIALS[0];
  // 工程が足りない、精度が足りない、汚れている、のいずれかで不良品
  const finished = isFinished(item) && item.precision >= mat.precision && !item.defective;
  if (finished) {
    const price = Math.round(
      mat.price *
        priceMultiplier(s) *
        (0.6 + (s.trust / 100) * 0.5) *
        (item.packed ? PACKED_PRICE_BONUS : 1) *
        bonus,
    );
    s.money += price;
    s.revenueToday += price;
    s.shippedToday++;
  } else {
    s.money -= DEFECT_PENALTY;
    s.damagesToday += DEFECT_PENALTY;
    s.defectsToday++;
  }
}

function tickIntakes(s: GameState): void {
  for (let i = 0; i < s.tiles.length; i++) {
    const t = s.tiles[i];
    if (t.kind !== 'intake') continue;
    if (t.cooldown > 0) {
      t.cooldown--;
      continue;
    }
    for (const n of outboundTargets(s.tiles, i)) {
      if (canAccept(s.tiles, n, RAW_PROBE, i)) {
        const mat = MATERIALS[s.materialGrade] ?? MATERIALS[0];
        s.money -= mat.cost;
        s.materialCostToday += mat.cost;
        handOver(s, n, newItem(s));
        t.cooldown = Math.max(6, Math.round(INTAKE_INTERVAL / intakeMultiplier(s)));
        break;
      }
    }
  }
}

/** 終業時、荷台に残っている荷を送り出す。 */
export function flushTrucks(s: GameState): void {
  for (const t of s.tiles) {
    if ((t.kind !== 'dock' && t.kind !== 'output') || !t.truck || t.truck.length === 0) continue;
    departTruck(s, t);
  }
}

/** ライン全体の物流ネットワーク診断結果 */
export interface LineFlowAnalysis {
  /** 搬入口から有効に繋がっているタイル */
  connectedTiles: Set<number>;
  /** 搬出先がない・行き止まりになっているタイル */
  deadEndTiles: Set<number>;
  /** 荷役作業員が稼働中か */
  hasHandler: boolean;
  /** 荷役作業員が回収可能な供給元 (置き場・機械) */
  handlerPickups: number[];
  /** 荷役作業員が投入可能な先 (機械・置き場・出荷口) */
  handlerDropoffs: number[];
}

/** 工場全体のライン接続・物流ネットワークを詳細診断する */
export function analyzeLineFlow(s: GameState): LineFlowAnalysis {
  const connectedTiles = new Set<number>();
  const deadEndTiles = new Set<number>();
  const stack: number[] = [];

  const hasHandler = s.tiles.some(
    (t) => t.kind === 'handler_post' && t.assigned !== null,
  );

  const handlerPickups: number[] = [];
  const handlerDropoffs: number[] = [];

  for (let i = 0; i < s.tiles.length; i++) {
    const t = s.tiles[i];
    if (t.kind === 'intake') {
      connectedTiles.add(i);
      for (const n of outboundTargets(s.tiles, i)) stack.push(n);
      if (s.routes) {
        for (const r of s.routes) {
          if (r.from === i) stack.push(r.to);
        }
      }
    }
    if (hasHandler) {
      if (t.kind === 'basket' || t.kind === 'stocker' || t.kind === 'pallet' || t.kind === 'container' || machineDef(t.kind)) {
        handlerPickups.push(i);
      }
      if (t.kind === 'output' || t.kind === 'dock' || machineDef(t.kind) || t.kind === 'basket' || t.kind === 'stocker') {
        handlerDropoffs.push(i);
      }
    }
  }

  while (stack.length) {
    const cur = stack.pop()!;
    if (connectedTiles.has(cur)) continue;
    connectedTiles.add(cur);
    const t = s.tiles[cur];

    if (t.kind === 'output' || t.kind === 'dock') continue;

    // 手作業運搬ライン (s.routes) の接続先を下流へ伝播
    if (s.routes) {
      for (const r of s.routes) {
        if (r.from === cur && !connectedTiles.has(r.to)) {
          stack.push(r.to);
        }
      }
    }

    if (t.kind === 'pallet' || t.kind === 'container' || t.kind === 'basket' || t.kind === 'stocker') {
      if (hasHandler) {
        // 荷役作業員がいれば、次工程の機械や出荷口へ運ばれる
        for (const drop of handlerDropoffs) {
          if (!connectedTiles.has(drop)) stack.push(drop);
        }
      }
      // コンベア等の前向き接続があればそれも追跡
      const n = neighborIndex(cur, t.dir);
      if (n >= 0 && acceptsFrom(s.tiles, n, cur)) stack.push(n);
      continue;
    }

    if (t.kind === 'splitter') {
      let outCount = 0;
      for (const d of splitterOutDirs(t)) {
        const n = neighborIndex(cur, d);
        if (n >= 0 && acceptsFrom(s.tiles, n, cur)) {
          stack.push(n);
          outCount++;
        }
      }
      if (outCount === 0) deadEndTiles.add(cur);
    } else if (t.kind === 'conveyor' || t.kind === 'merger') {
      const n = neighborIndex(cur, t.dir);
      if (n >= 0 && acceptsFrom(s.tiles, n, cur)) {
        stack.push(n);
      } else {
        deadEndTiles.add(cur);
      }
    } else if (machineDef(t.kind)) {
      const targets = outboundTargets(s.tiles, cur);
      for (const n of targets) stack.push(n);
      if (hasHandler) {
        // 荷役作業員がいれば、機械の出口から次工程へ運ばれる
        for (const drop of handlerDropoffs) {
          if (!connectedTiles.has(drop)) stack.push(drop);
        }
      } else if (targets.length === 0) {
        deadEndTiles.add(cur);
      }
    }
  }

  return {
    connectedTiles,
    deadEndTiles,
    hasHandler,
    handlerPickups,
    handlerDropoffs,
  };
}

/** レイアウト診断: 出荷口まで到達できる経路が通っているか大まかに判定する。 */
export function lineIsConnected(s: GameState): boolean {
  const analysis = analyzeLineFlow(s);
  for (let i = 0; i < s.tiles.length; i++) {
    const t = s.tiles[i];
    if ((t.kind === 'output' || t.kind === 'dock') && analysis.connectedTiles.has(i)) {
      return true;
    }
  }
  // 荷役作業員がいて、搬入口から何らかの加工機・置き場に繋がっていれば到達可能
  if (analysis.hasHandler && analysis.connectedTiles.size > 2) {
    return true;
  }
  return false;
}

export { outboundTargets, neighborIndex };
