import type { Plan } from "./plan.ts";
import type { Point2 } from "./types.ts";

export type RobotJobKind = "patrol" | "deliver" | "inspect" | "charge";
export type RobotActivityMode = RobotJobKind | "idle" | "blocked-recovery";

export type RobotStationAnchor =
  | { kind: "prop"; propId: string; standoffM?: number; side?: 1 | -1 }
  | { kind: "seat"; seatId: string; standoffM?: number }
  | { kind: "room"; roomId: string }
  | { kind: "point"; levelId: string; position: Point2; facing?: Point2 };

export interface RobotStationDefinition {
  id: string;
  label: string;
  role: "patrol" | "pickup" | "dropoff" | "inspect" | "charge";
  anchor: RobotStationAnchor;
}

export interface RobotJobDefinition {
  id: string;
  label: string;
  kind: RobotJobKind;
  stationIds: readonly string[];
  /** Fixed simulation ticks spent performing work at each station. */
  dwellTicks: number;
}

export interface RobotSpawnDefinition {
  id: string;
  label: string;
  levelId: string;
  spawnStationId: string;
  schedule: readonly string[];
  initialBattery: number;
}

/**
 * Authored simulated operations for one office. This is demonstration data,
 * never presence data and never a representation of real people or real work.
 */
export interface RobotOperationsDefinition {
  id: string;
  version: number;
  officeId: string;
  disclosure: string;
  stations: readonly RobotStationDefinition[];
  jobs: readonly RobotJobDefinition[];
  robots: readonly RobotSpawnDefinition[];
}

export interface ResolvedRobotStation extends RobotStationDefinition {
  levelId: string;
  position: Point2;
  facing: Point2;
}

export interface ResolvedRobotOperations {
  definition: RobotOperationsDefinition;
  stations: ReadonlyMap<string, ResolvedRobotStation>;
  jobs: ReadonlyMap<string, RobotJobDefinition>;
  robots: ReadonlyMap<string, RobotSpawnDefinition>;
}

const ROBOT_RADIUS_M = 0.28;
const DEFAULT_PROP_STANDOFF_M = 0.9;
const DEFAULT_SEAT_STANDOFF_M = 0.65;

/** Resolve authored addresses and reject ambiguous, cross-floor, or obstructed work. */
export function resolveRobotOperations(
  plan: Plan,
  definition: RobotOperationsDefinition,
): ResolvedRobotOperations {
  if (definition.officeId !== plan.office.id) {
    throw new Error(`robot operations ${definition.id} target ${definition.officeId}, not ${plan.office.id}`);
  }
  if (!Number.isSafeInteger(definition.version) || definition.version < 1) {
    throw new Error("robot operations version must be a positive integer");
  }
  if (!definition.disclosure.toLowerCase().includes("simulat")) {
    throw new Error("robot operations disclosure must identify the activity as simulated");
  }

  const stations = new Map<string, ResolvedRobotStation>();
  for (const station of definition.stations) {
    unique(stations, station.id, "station");
    const resolved = resolveStation(plan, station);
    if (!plan.level(resolved.levelId)) {
      throw new Error(`robot station ${station.id} names unknown level ${resolved.levelId}`);
    }
    if (!plan.roomAt(resolved.levelId, resolved.position)) {
      throw new Error(`robot station ${station.id} is not on a resolved room floor`);
    }
    if (plan.blocked(resolved.levelId, resolved.position, resolved.position, ROBOT_RADIUS_M)) {
      throw new Error(`robot station ${station.id} has no robot clearance`);
    }
    stations.set(station.id, resolved);
  }

  const jobs = new Map<string, RobotJobDefinition>();
  for (const job of definition.jobs) {
    unique(jobs, job.id, "job");
    if (!Number.isSafeInteger(job.dwellTicks) || job.dwellTicks < 1) {
      throw new Error(`robot job ${job.id} dwellTicks must be a positive integer`);
    }
    const expected = job.kind === "deliver" ? 2 : job.kind === "patrol" ? 1 : 1;
    if (job.stationIds.length < expected || (job.kind !== "patrol" && job.stationIds.length !== expected)) {
      throw new Error(`robot job ${job.id} has the wrong number of stations for ${job.kind}`);
    }
    const levels = new Set<string>();
    for (const stationId of job.stationIds) {
      const station = stations.get(stationId);
      if (!station) throw new Error(`robot job ${job.id} names unknown station ${stationId}`);
      levels.add(station.levelId);
    }
    if (levels.size !== 1) throw new Error(`robot job ${job.id} crosses levels without an authored connector`);
    jobs.set(job.id, Object.freeze({ ...job, stationIds: Object.freeze([...job.stationIds]) }));
  }

  const robots = new Map<string, RobotSpawnDefinition>();
  for (const robot of definition.robots) {
    unique(robots, robot.id, "robot");
    if (!(robot.initialBattery > 0 && robot.initialBattery <= 1)) {
      throw new Error(`robot ${robot.id} initialBattery must be in (0, 1]`);
    }
    const spawn = stations.get(robot.spawnStationId);
    if (!spawn || spawn.levelId !== robot.levelId) {
      throw new Error(`robot ${robot.id} spawn is missing or on a different level`);
    }
    if (robot.schedule.length === 0) throw new Error(`robot ${robot.id} has an empty schedule`);
    for (const jobId of robot.schedule) {
      const job = jobs.get(jobId);
      if (!job) throw new Error(`robot ${robot.id} names unknown job ${jobId}`);
      const station = stations.get(job.stationIds[0]!);
      if (station?.levelId !== robot.levelId) {
        throw new Error(`robot ${robot.id} schedule crosses levels at job ${jobId}`);
      }
    }
    robots.set(robot.id, Object.freeze({ ...robot, schedule: Object.freeze([...robot.schedule]) }));
  }

  return { definition, stations, jobs, robots };
}

function resolveStation(plan: Plan, station: RobotStationDefinition): ResolvedRobotStation {
  const anchor = station.anchor;
  if (anchor.kind === "point") {
    return {
      ...station,
      levelId: anchor.levelId,
      position: copy(anchor.position),
      facing: normalized(anchor.facing ?? { x: 0, z: -1 }),
    };
  }
  if (anchor.kind === "room") {
    const room = plan.levels.flatMap((level) => level.rooms).find((candidate) => candidate.id === anchor.roomId);
    if (!room) throw new Error(`robot station ${station.id} names unknown room ${anchor.roomId}`);
    return { ...station, levelId: room.levelId, position: copy(room.centroid), facing: { x: 0, z: -1 } };
  }
  if (anchor.kind === "seat") {
    const seat = plan.seat(anchor.seatId);
    if (!seat) throw new Error(`robot station ${station.id} names unknown seat ${anchor.seatId}`);
    const forward = yawForward(seat.facing);
    const standoff = anchor.standoffM ?? DEFAULT_SEAT_STANDOFF_M;
    return {
      ...station,
      levelId: seat.levelId,
      position: { x: seat.position.x - forward.x * standoff, z: seat.position.z - forward.z * standoff },
      facing: forward,
    };
  }
  const prop = plan.prop(anchor.propId);
  if (!prop) throw new Error(`robot station ${station.id} names unknown prop ${anchor.propId}`);
  const outward = yawForward(prop.rotation);
  const side = anchor.side ?? 1;
  const standoff = anchor.standoffM ?? DEFAULT_PROP_STANDOFF_M;
  const position = {
    x: prop.position.x + outward.x * side * standoff,
    z: prop.position.z + outward.z * side * standoff,
  };
  return {
    ...station,
    levelId: prop.levelId,
    position,
    facing: normalized({ x: prop.position.x - position.x, z: prop.position.z - position.z }),
  };
}

function yawForward(yaw: number): Point2 {
  return { x: -Math.sin(yaw), z: -Math.cos(yaw) };
}

function normalized(point: Point2): Point2 {
  const length = Math.hypot(point.x, point.z);
  return length > 1e-9 ? { x: point.x / length, z: point.z / length } : { x: 0, z: -1 };
}

function unique<T>(map: ReadonlyMap<string, T>, id: string, kind: string): void {
  if (!id || map.has(id)) throw new Error(`robot ${kind} id ${JSON.stringify(id)} is empty or duplicated`);
}

function copy(point: Point2): Point2 {
  return { x: point.x, z: point.z };
}
