// Bot AI v3: 7-ray openness scoring with commitment, shrink-wall centering,
// edge-aware wandering, opportunistic cut-offs, boost/item management. Small
// mistake windows remain so humans keep fair odds.

// Original callsigns on purpose: the roster used to be the cast of a certain
// 1982 film, which is not something to hand a commercial portal's legal review.
const BOT_NAMES = [
  'VOLT-9', 'AXIOM', 'KESTREL', 'ZEPHYR', 'ONYX', 'FLUX', 'HALCYON', 'ANON-7',
  'VECTOR', 'CINDER', 'ECHO-3', 'DRIFT', 'PRISM', 'RAZOR', 'SOLSTICE', 'NOVA',
];
let nameIdx = Math.floor(Math.random() * BOT_NAMES.length);
export function nextBotName() { return BOT_NAMES[nameIdx++ % BOT_NAMES.length]; }

const THINK_EVERY = 0.07;      // seconds
// v4 (2026-07 feedback: "сделай ботов еще сильнее"): rarer and shorter lapses
const MISTAKE_CHANCE = 0.02;
const MISTAKE_BLIND = 0.16;    // seconds of late reaction
// wide flank rays spot pockets the old 5-ray fan drove straight past
const RAYS = [-1.6, -1.05, -0.55, 0, 0.55, 1.05, 1.6];
const COMMIT_TIME = 0.26;      // hold a chosen evasion so the bot doesn't dither

export class BotBrain {
  constructor(match, id, rng = Math.random) {
    this.match = match;
    this.id = id;
    this.rng = rng;
    this.acc = rng() * THINK_EVERY; // desync thinking across bots
    this.blindUntil = 0;
    this.wanderUntil = 0;
    this.wanderSteer = 0;
    this.commitUntil = 0;
    this.commitSteer = 0;
  }

  think(dt) {
    this.acc += dt;
    if (this.acc < THINK_EVERY) return;
    this.acc = 0;

    const m = this.match;
    const b = m.bikes.get(this.id);
    if (!b || !b.alive || b.ghost) return;

    const speed = b.boosting ? m.cfg.boostSpeed : m.cfg.speed;
    const look = speed * 1.8 + 10;
    const rays = RAYS.map((a) => m.raycast(b, a, look));
    const ahead = rays[3];

    // race: follow the map's racing line, corridor-aware avoidance
    if (m.race) { this.raceDrive(m, b, rays, look, ahead); return; }

    const danger = ahead < look * 0.85 || rays[2] < look * 0.35 || rays[4] < look * 0.35;

    if (danger) {
      if (m.time < this.blindUntil) return;
      if (this.rng() < MISTAKE_CHANCE && this.blindUntil < m.time - 0.5) {
        this.blindUntil = m.time + MISTAKE_BLIND;
        return;
      }
      // boxed in with an escape item: use it (phase through, blast a gap, or
      // drop the pursuers by snipping the tail)
      if (b.item && ahead < look * 0.35 && rays[1] < look * 0.55 && rays[5] < look * 0.55
        && (b.item === 'phase' || b.item === 'emp' || b.item === 'snip')) {
        m.useItem(this.id);
      }
      // stay committed to an evasion unless the wall is imminent
      if (m.time < this.commitUntil && ahead > speed * 0.35) {
        m.setSteer(this.id, this.commitSteer);
        return;
      }
      // pick the most open side, weighting the whole fan so we don't turn into
      // a pocket that is open only on the exact ray line
      const leftScore = rays[0] * 0.5 + rays[1] * 0.85 + rays[2];
      const rightScore = rays[6] * 0.5 + rays[5] * 0.85 + rays[4];
      let steer;
      if (Math.abs(leftScore - rightScore) < look * 0.07) {
        // near tie: break it toward the arena center so walls don't corner us
        const [minx, miny, maxx, maxy] = m.bounds();
        let toC = Math.atan2((miny + maxy) / 2 - b.y, (minx + maxx) / 2 - b.x) - b.heading;
        while (toC > Math.PI) toC -= Math.PI * 2;
        while (toC < -Math.PI) toC += Math.PI * 2;
        steer = toC < 0 ? -1 : 1;
      } else {
        steer = leftScore >= rightScore ? -1 : 1;
      }
      this.commitSteer = steer;
      this.commitUntil = m.time + COMMIT_TIME;
      m.setSteer(this.id, steer);
      // burst out of tight spots only when there is somewhere to go
      const escape = steer < 0 ? Math.max(rays[0], rays[1]) : Math.max(rays[5], rays[6]);
      m.setBoost(this.id, ahead < look * 0.3 && escape > look * 0.5 && b.energy > 30);
      return;
    }

    // offensive gear: a bolt for whoever crosses the sights, a mine for tails
    if (b.item === 'cannon') this.tryCannon(b);
    else if (b.item === 'trap') this.tryTrap(b);

    // shrinking walls: drift toward the safe center before they bite
    {
      const [minx, miny, maxx, maxy] = m.bounds();
      const margin = Math.min(b.x - minx, maxx - b.x, b.y - miny, maxy - b.y);
      const shrinking = minx > 0.5;
      if (shrinking && margin < 45) {
        let toC = Math.atan2((miny + maxy) / 2 - b.y, (minx + maxx) / 2 - b.x) - b.heading;
        while (toC > Math.PI) toC -= Math.PI * 2;
        while (toC < -Math.PI) toC += Math.PI * 2;
        if (Math.abs(toC) > 0.25) {
          const rayIdx = toC < -0.3 ? 2 : toC > 0.3 ? 4 : 3;
          if (rays[rayIdx] > look * 0.5) {
            m.setSteer(this.id, Math.max(-1, Math.min(1, toC * 1.8)));
            m.setBoost(this.id, margin < 22 && b.energy > 25);
            return;
          }
        }
      }
    }

    // open space: swing by a nearby pickup when empty-handed
    if (!b.item && m.pickups.size) {
      let target = null, td = Infinity;
      for (const p of m.pickups.values()) {
        const d = Math.hypot(p.x - b.x, p.y - b.y);
        if (d < td) { td = d; target = p; }
      }
      if (target && td < 120) {
        let want = Math.atan2(target.y - b.y, target.x - b.x) - b.heading;
        while (want > Math.PI) want -= Math.PI * 2;
        while (want < -Math.PI) want += Math.PI * 2;
        if (Math.abs(want) < 1.2) {
          const rayIdx = want < -0.3 ? 2 : want > 0.3 ? 4 : 3;
          if (rays[rayIdx] > Math.min(look * 0.8, td)) {
            m.setSteer(this.id, Math.max(-0.9, Math.min(0.9, want * 1.8)));
            m.setBoost(this.id, false);
            return;
          }
        }
      }
    }

    // open space: hunt. Find the nearest live enemy and try to cross ahead of
    // their nose — the classic light-cycle cut-off.
    let prey = null, preyDist = Infinity;
    for (const q of m.bikes.values()) {
      if (q === b || !q.alive) continue;
      const d = Math.hypot(q.x - b.x, q.y - b.y);
      if (d < preyDist) { prey = q; preyDist = d; }
    }
    if (prey && preyDist < 110 && ahead > look * 0.7) {
      // aim at a point ahead of the enemy
      const lead = Math.min(20, preyDist * 0.4);
      const tx = prey.x + Math.cos(prey.heading) * lead;
      const ty = prey.y + Math.sin(prey.heading) * lead;
      let want = Math.atan2(ty - b.y, tx - b.x) - b.heading;
      while (want > Math.PI) want -= Math.PI * 2;
      while (want < -Math.PI) want += Math.PI * 2;
      // gentle pursuit only — hard turns toward prey are how bots trade kills
      if (Math.abs(want) < 0.9) {
        const rayIdx = want < -0.3 ? 2 : want > 0.3 ? 4 : 3;
        if (rays[rayIdx] > Math.min(look * 0.9, preyDist)) {
          m.setSteer(this.id, Math.max(-0.75, Math.min(0.75, want * 1.5)));
          // boost to complete a cut-off when closing in roughly parallel
          m.setBoost(this.id, preyDist < 55 && Math.abs(want) < 0.35 && b.energy > 45);
          if (b.item === 'surge' && preyDist < 35 && Math.abs(want) < 0.25) m.useItem(this.id);
          return;
        }
      }
    }

    m.setBoost(this.id, false);
    // no target: mostly straight, lazy arcs biased toward the open side so the
    // bot doesn't cruise blindly at a distant wall
    if (m.time >= this.wanderUntil) {
      const roll = this.rng();
      if (rays[3] < look && Math.max(rays[1], rays[2]) > Math.max(rays[4], rays[5]) + look * 0.15) {
        this.wanderSteer = -1;
      } else if (rays[3] < look && Math.max(rays[4], rays[5]) > Math.max(rays[1], rays[2]) + look * 0.15) {
        this.wanderSteer = 1;
      } else {
        this.wanderSteer = roll < 0.15 ? -1 : roll < 0.3 ? 1 : 0;
      }
      this.wanderUntil = m.time + 0.5 + this.rng() * 1.2;
    }
    m.setSteer(this.id, this.wanderSteer);
  }

  // race driver: chase the next waypoint on the racing line; rays only nudge
  // away from walls or bail out of a genuine dead-ahead
  raceDrive(m, b, rays, look, ahead) {
    const wps = m.cfg.waypoints || [];
    if (!wps.length) return;
    if (this.wpIdx === undefined
      || Math.hypot(wps[this.wpIdx].x - b.x, wps[this.wpIdx].y - b.y) > 140) {
      // fresh brain, or somehow far off the line: re-acquire the nearest point
      let best = 0, bd = Infinity;
      wps.forEach((p, i) => {
        const d = Math.hypot(p.x - b.x, p.y - b.y);
        if (d < bd) { bd = d; best = i; }
      });
      this.wpIdx = (best + 1) % wps.length;
    }
    let w = wps[this.wpIdx];
    let guard = wps.length;
    while (Math.hypot(w.x - b.x, w.y - b.y) < 18 && guard-- > 0) {
      this.wpIdx = (this.wpIdx + 1) % wps.length;
      w = wps[this.wpIdx];
    }
    // blocked target (typical right after a crash-respawn near a chicane):
    // skip ahead to the first point the bike can actually see
    if (m.hitObstacle(b.x, b.y, w.x, w.y)) {
      let found = false;
      for (let k = 1; k <= 6; k++) {
        const cand = wps[(this.wpIdx + k) % wps.length];
        if (!m.hitObstacle(b.x, b.y, cand.x, cand.y)) {
          this.wpIdx = (this.wpIdx + k) % wps.length;
          w = cand;
          found = true;
          break;
        }
      }
      if (!found) {
        // pocketed behind a block: creep toward open space until the racing
        // line comes back into view
        const leftScore = rays[0] + rays[1] + rays[2];
        const rightScore = rays[6] + rays[5] + rays[4];
        m.setSteer(this.id, leftScore >= rightScore ? -1 : 1);
        m.setBoost(this.id, false);
        return;
      }
    }
    // blend the next point in when close, so turns start early and stay smooth
    const w2 = wps[(this.wpIdx + 1) % wps.length];
    const dw = Math.hypot(w.x - b.x, w.y - b.y);
    const blend = Math.max(0, Math.min(0.6, (34 - dw) / 34));
    const tx = w.x + (w2.x - w.x) * blend;
    const ty = w.y + (w2.y - w.y) * blend;
    let want = Math.atan2(ty - b.y, tx - b.x) - b.heading;
    while (want > Math.PI) want -= Math.PI * 2;
    while (want < -Math.PI) want += Math.PI * 2;
    let steer = Math.max(-1, Math.min(1, want * 2.2));
    const speed = b.boosting ? m.cfg.boostSpeed : m.cfg.speed;
    if (ahead < look * 0.4) {
      // something (trail, wreck wall) blocks the line: swerve to the open side
      const leftScore = rays[0] * 0.5 + rays[1] * 0.85 + rays[2];
      const rightScore = rays[6] * 0.5 + rays[5] * 0.85 + rays[4];
      steer = leftScore >= rightScore ? -1 : 1;
    } else {
      const near = Math.max(12, speed * 0.4);
      if (rays[2] < near) steer += 0.55;
      if (rays[4] < near) steer -= 0.55;
      steer = Math.max(-1, Math.min(1, steer));
    }
    m.setSteer(this.id, steer);
    m.setBoost(this.id, ahead > look * 0.85 && Math.abs(want) < 0.22
      && rays[2] > look * 0.4 && rays[4] > look * 0.4 && b.energy > 40);
    if (b.item === 'surge' && ahead > look * 0.95 && Math.abs(want) < 0.12) m.useItem(this.id);
    else if (b.item === 'phase' && ahead < look * 0.3) m.useItem(this.id);
    else if (b.item === 'cannon') this.tryCannon(b);
    else if (b.item === 'trap') this.tryTrap(b);
  }

  // fire when an enemy sits in the sights
  tryCannon(b) {
    const m = this.match;
    for (const q of m.bikes.values()) {
      if (q === b || !q.alive || q.ghost) continue;
      const d = Math.hypot(q.x - b.x, q.y - b.y);
      if (d > 75) continue;
      let rel = Math.atan2(q.y - b.y, q.x - b.x) - b.heading;
      while (rel > Math.PI) rel -= Math.PI * 2;
      while (rel < -Math.PI) rel += Math.PI * 2;
      if (Math.abs(rel) < 0.1) { m.useItem(this.id); return; }
    }
  }

  // drop a mine in the lap of whoever is tailing us
  tryTrap(b) {
    const m = this.match;
    for (const q of m.bikes.values()) {
      if (q === b || !q.alive || q.ghost) continue;
      const d = Math.hypot(q.x - b.x, q.y - b.y);
      if (d > 30) continue;
      let rel = Math.atan2(q.y - b.y, q.x - b.x) - b.heading;
      while (rel > Math.PI) rel -= Math.PI * 2;
      while (rel < -Math.PI) rel += Math.PI * 2;
      if (Math.abs(rel) > 2.2) { m.useItem(this.id); return; }
    }
  }
}
