export type OlpSchemaVersion = "olp/0.1";
export type OlpId = string;
export type OlpBrand = "abb" | "fanuc" | "kuka" | "generic";

export type JsonPrimitive = string | number | boolean | null;
export type JsonValue = JsonPrimitive | JsonValue[] | { [key: string]: JsonValue };
export type JsonObject = { [key: string]: JsonValue };

export type Pose6D = [number, number, number, number, number, number];
export type Vec3 = [number, number, number];

export interface OlpNamedEntity {
  id: OlpId;
  name: string;
  metadata?: JsonObject;
}

export interface OlpProjectInfo extends OlpNamedEntity {
  customer?: string;
  revision?: string;
}

export interface OlpStation extends OlpNamedEntity {
  resourceIds: OlpId[];
  activeRobotIds: OlpId[];
  defaultFrameId?: OlpId;
}

export interface OlpJointLimit {
  name: string;
  lower: number;
  upper: number;
  velocity: number;
  acceleration?: number;
}

export interface OlpRobotResource extends OlpNamedEntity {
  kind: "robot";
  brand: OlpBrand;
  model: string;
  dof: number;
  jointNames: string[];
  limits?: OlpJointLimit[];
  baseFrameId?: OlpId;
  controller?: {
    family: string;
    version?: string;
  };
}

export interface OlpTool extends OlpNamedEntity {
  kind: "tool";
  tcp: Pose6D;
  robotId?: OlpId;
  massKg?: number;
}

export interface OlpFrame extends OlpNamedEntity {
  kind: "frame";
  pose: Pose6D;
  parentFrameId?: OlpId;
  robotId?: OlpId;
}

export interface OlpGeometryAsset extends OlpNamedEntity {
  kind: "point" | "edge" | "curve" | "box" | "mesh_ref";
  data: JsonObject;
}

export interface OlpCollisionObject extends OlpNamedEntity {
  kind: "collision_object";
  shape:
    | { kind: "box"; size: Vec3 }
    | { kind: "sphere"; radius: number }
    | { kind: "capsule"; radius: number; height: number };
  pose: Pose6D;
  attachedToId?: OlpId;
}

export interface OlpResourceLibrary {
  robots: OlpRobotResource[];
  tools: OlpTool[];
  frames: OlpFrame[];
  geometryAssets: OlpGeometryAsset[];
  collisionObjects: OlpCollisionObject[];
}

export interface OlpSpeed extends OlpNamedEntity {
  kind: "joint_percent" | "linear_mm_s";
  value: number;
}

export interface OlpZone extends OlpNamedEntity {
  kind: "fine" | "distance_mm" | "cnt";
  value?: number;
}

export interface OlpTarget extends OlpNamedEntity {
  kind: "joint" | "pose";
  robotId?: OlpId;
  pose?: Pose6D;
  joints?: number[];
  toolId?: OlpId;
  frameId?: OlpId;
  externalAxes?: Record<OlpId, number>;
}

export type OlpMotionKind = "movej" | "movel" | "movec";

export interface OlpPathPoint extends OlpNamedEntity {
  motion: OlpMotionKind;
  targetId: OlpId;
  viaTargetId?: OlpId;
  speedId?: OlpId;
  zoneId?: OlpId;
  toolId?: OlpId;
  frameId?: OlpId;
}

export interface OlpPath extends OlpNamedEntity {
  defaults?: {
    speedId?: OlpId;
    zoneId?: OlpId;
    toolId?: OlpId;
    frameId?: OlpId;
  };
  source?: Record<string, string | number | boolean>;
  points: OlpPathPoint[];
}

export interface OlpOperation extends OlpNamedEntity {
  kind: string;
  pathId: OlpId;
  robotId?: OlpId;
  process?: JsonObject;
  startAction?: string;
  endAction?: string;
}

export interface OlpProgram extends OlpNamedEntity {
  language: "grl" | "abb_rapid" | "kuka_krl" | "fanuc_ls" | "text";
  entryOperationIds: OlpId[];
  source: {
    kind: "generated" | "imported" | "authored";
    text: string;
    generatedFrom?: {
      operationId: OlpId;
      pathId: OlpId;
    };
  };
}

export interface OlpPostProfile extends OlpNamedEntity {
  brand: Exclude<OlpBrand, "generic">;
  controllerFamily: string;
  fileExtension: string;
  options?: JsonObject;
}

export interface OlpReportRef extends OlpNamedEntity {
  reportKind: "validation" | "import" | "delivery" | "controller_check";
  status: "pass" | "warn" | "fail";
  uri?: string;
  summary?: JsonObject;
}

export interface OlpCalibrationRecord extends OlpNamedEntity {
  kind: "tcp" | "frame" | "base" | "external_axis";
  targetResourceId: OlpId;
  poseDelta?: Pose6D;
  axisOffset?: number;
  measuredAt?: string;
  evidence?: JsonObject;
}

export interface OlpExternalAxis extends OlpNamedEntity {
  axisKind: "linear" | "rotary";
  jointName: string;
  limits: {
    lower: number;
    upper: number;
    velocity: number;
  };
  attachedRobotIds?: OlpId[];
}

export interface OlpMotionGroup extends OlpNamedEntity {
  robotIds: OlpId[];
  externalAxisIds: OlpId[];
  coordination: "independent" | "synchronized" | "master_slave";
}

export interface OlpProcessTemplate extends OlpNamedEntity {
  operationKind: string;
  defaults: JsonObject;
  deliveryTags: string[];
}

export interface OlpProjectModel {
  schemaVersion: OlpSchemaVersion;
  project: OlpProjectInfo;
  station: OlpStation;
  resources: OlpResourceLibrary;
  speeds: OlpSpeed[];
  zones: OlpZone[];
  targets: OlpTarget[];
  paths: OlpPath[];
  operations: OlpOperation[];
  programs: OlpProgram[];
  postProfiles: OlpPostProfile[];
  reports: OlpReportRef[];
  calibrations: OlpCalibrationRecord[];
  externalAxes: OlpExternalAxis[];
  motionGroups: OlpMotionGroup[];
  processTemplates: OlpProcessTemplate[];
  metadata?: JsonObject;
}

export interface OlpValidationIssue {
  severity: "info" | "warning" | "error";
  code: string;
  message: string;
  path?: string;
}

export interface OlpValidationReport {
  ok: boolean;
  issues: OlpValidationIssue[];
}

export interface CreateOlpProjectOptions {
  id: OlpId;
  name: string;
  customer?: string;
  revision?: string;
}

export function createOlpProject(options: CreateOlpProjectOptions): OlpProjectModel {
  const project: OlpProjectInfo = {
    id: options.id,
    name: options.name,
    ...(options.customer ? { customer: options.customer } : {}),
    ...(options.revision ? { revision: options.revision } : {})
  };
  return {
    schemaVersion: "olp/0.1",
    project,
    station: {
      id: `${options.id}_station`,
      name: `${options.name} Station`,
      resourceIds: [],
      activeRobotIds: []
    },
    resources: {
      robots: [],
      tools: [],
      frames: [],
      geometryAssets: [],
      collisionObjects: []
    },
    speeds: [],
    zones: [],
    targets: [],
    paths: [],
    operations: [],
    programs: [],
    postProfiles: [],
    reports: [],
    calibrations: [],
    externalAxes: [],
    motionGroups: [],
    processTemplates: []
  };
}

export function sampleOlpProject(): OlpProjectModel {
  return {
    ...createOlpProject({ id: "demo_cell", name: "DemoCell", customer: "KDL" }),
    station: {
      id: "station_demo",
      name: "Demo Station",
      resourceIds: ["robot_1", "tool_gripper", "frame_world"],
      activeRobotIds: ["robot_1"],
      defaultFrameId: "frame_world"
    },
    resources: {
      robots: [
        {
          id: "robot_1",
          name: "IRB120",
          kind: "robot",
          brand: "abb",
          model: "IRB120",
          dof: 6,
          jointNames: ["J1", "J2", "J3", "J4", "J5", "J6"],
          baseFrameId: "frame_world"
        }
      ],
      tools: [
        {
          id: "tool_gripper",
          name: "gripper",
          kind: "tool",
          tcp: [0, 0, 100, 0, 0, 0],
          robotId: "robot_1",
          massKg: 2.5
        }
      ],
      frames: [
        {
          id: "frame_world",
          name: "world",
          kind: "frame",
          pose: [0, 0, 0, 0, 0, 0]
        },
        {
          id: "frame_fixture",
          name: "fixture",
          kind: "frame",
          pose: [800, 0, 0, 0, 0, 0],
          parentFrameId: "frame_world"
        }
      ],
      geometryAssets: [
        {
          id: "edge_pick_line",
          name: "pick_line",
          kind: "edge",
          data: {
            points: [
              [500, 0, 0],
              [600, 0, 0]
            ]
          }
        }
      ],
      collisionObjects: [
        {
          id: "fixture_box",
          name: "fixture_box",
          kind: "collision_object",
          shape: { kind: "box", size: [200, 200, 100] },
          pose: [550, 0, -60, 0, 0, 0]
        }
      ]
    },
    speeds: [
      { id: "v_joint", name: "v_joint", kind: "joint_percent", value: 50 },
      { id: "v_linear", name: "v_linear", kind: "linear_mm_s", value: 200 }
    ],
    zones: [
      { id: "zf", name: "zf", kind: "fine" },
      { id: "z10", name: "z10", kind: "distance_mm", value: 10 }
    ],
    targets: [
      {
        id: "home",
        name: "home",
        kind: "joint",
        robotId: "robot_1",
        joints: [0, 0, 0, 0, 0, 0]
      },
      {
        id: "pick",
        name: "pick",
        kind: "pose",
        robotId: "robot_1",
        pose: [500, 0, 0, 0, 0, 0],
        toolId: "tool_gripper",
        frameId: "frame_fixture"
      },
      {
        id: "place",
        name: "place",
        kind: "pose",
        robotId: "robot_1",
        pose: [600, 0, 0, 0, 0, 0],
        toolId: "tool_gripper",
        frameId: "frame_fixture"
      }
    ],
    paths: [
      {
        id: "pick_path",
        name: "pick_path",
        source: {
          type: "cad_curve",
          id: "edge_pick_line",
          sample_distance: 5
        },
        defaults: {
          speedId: "v_linear",
          zoneId: "z10",
          toolId: "tool_gripper",
          frameId: "frame_fixture"
        },
        points: [
          {
            id: "p00",
            name: "p00",
            motion: "movej",
            targetId: "home",
            speedId: "v_joint",
            zoneId: "zf"
          },
          {
            id: "p01",
            name: "p01",
            motion: "movel",
            targetId: "pick"
          },
          {
            id: "p02",
            name: "p02",
            motion: "movel",
            targetId: "place",
            zoneId: "zf"
          }
        ]
      }
    ],
    operations: [
      {
        id: "pick_op",
        name: "pick_op",
        kind: "handling",
        pathId: "pick_path",
        robotId: "robot_1",
        process: { gripper: "vacuum" },
        startAction: "io.do[1] = true",
        endAction: "io.do[1] = false"
      }
    ],
    postProfiles: [
      {
        id: "abb_default",
        name: "ABB default",
        brand: "abb",
        controllerFamily: "IRC5",
        fileExtension: ".mod"
      }
    ],
    calibrations: [
      {
        id: "tcp_cal_1",
        name: "gripper_tcp_cal",
        kind: "tcp",
        targetResourceId: "tool_gripper",
        poseDelta: [0.5, -0.2, 1.1, 0, 0, 0]
      }
    ],
    externalAxes: [
      {
        id: "track_1",
        name: "track_1",
        axisKind: "linear",
        jointName: "E1",
        limits: { lower: -1000, upper: 1000, velocity: 250 },
        attachedRobotIds: ["robot_1"]
      }
    ],
    motionGroups: [
      {
        id: "group_1",
        name: "robot_with_track",
        robotIds: ["robot_1"],
        externalAxisIds: ["track_1"],
        coordination: "synchronized"
      }
    ],
    processTemplates: [
      {
        id: "handling_default",
        name: "Handling Default",
        operationKind: "handling",
        defaults: { speedId: "v_linear", zoneId: "z10" },
        deliveryTags: ["source", "post", "report"]
      }
    ]
  };
}

export function validateOlpProject(model: OlpProjectModel): OlpValidationReport {
  const issues: OlpValidationIssue[] = [];
  if (model.schemaVersion !== "olp/0.1") {
    issues.push(error("OLP_SCHEMA_VERSION", `Unsupported OLP schema ${String(model.schemaVersion)}`, "/schemaVersion"));
  }
  try {
    JSON.stringify(model);
  } catch (cause) {
    issues.push(error("OLP_NOT_SERIALIZABLE", `Project is not JSON serializable: ${String(cause)}`));
  }

  const robots = idMap(model.resources.robots, "/resources/robots", issues);
  const tools = idMap(model.resources.tools, "/resources/tools", issues);
  const frames = idMap(model.resources.frames, "/resources/frames", issues);
  const targets = idMap(model.targets, "/targets", issues);
  const paths = idMap(model.paths, "/paths", issues);
  const operations = idMap(model.operations, "/operations", issues);
  const speeds = idMap(model.speeds, "/speeds", issues);
  const zones = idMap(model.zones, "/zones", issues);
  const externalAxes = idMap(model.externalAxes, "/externalAxes", issues);
  idMap(model.programs, "/programs", issues);
  idMap(model.postProfiles, "/postProfiles", issues);
  idMap(model.reports, "/reports", issues);
  idMap(model.calibrations, "/calibrations", issues);
  idMap(model.motionGroups, "/motionGroups", issues);
  idMap(model.processTemplates, "/processTemplates", issues);
  idMap(model.resources.geometryAssets, "/resources/geometryAssets", issues);
  idMap(model.resources.collisionObjects, "/resources/collisionObjects", issues);

  for (const robot of model.resources.robots) {
    if (robot.dof !== robot.jointNames.length) {
      issues.push(error("OLP_ROBOT_DOF", `Robot ${robot.id} dof does not match jointNames`, `/resources/robots/${robot.id}/dof`));
    }
    if (robot.baseFrameId && !frames.has(robot.baseFrameId)) {
      issues.push(error("OLP_REF_FRAME_MISSING", `Robot ${robot.id} references missing frame ${robot.baseFrameId}`, `/resources/robots/${robot.id}/baseFrameId`));
    }
  }

  for (const tool of model.resources.tools) {
    if (tool.robotId && !robots.has(tool.robotId)) {
      issues.push(error("OLP_REF_ROBOT_MISSING", `Tool ${tool.id} references missing robot ${tool.robotId}`, `/resources/tools/${tool.id}/robotId`));
    }
    validatePose(tool.tcp, `/resources/tools/${tool.id}/tcp`, issues);
  }

  for (const frame of model.resources.frames) {
    validatePose(frame.pose, `/resources/frames/${frame.id}/pose`, issues);
    if (frame.parentFrameId && !frames.has(frame.parentFrameId)) {
      issues.push(error("OLP_REF_FRAME_MISSING", `Frame ${frame.id} references missing parent ${frame.parentFrameId}`, `/resources/frames/${frame.id}/parentFrameId`));
    }
  }

  for (const target of model.targets) {
    if (!isIdentifier(target.name)) {
      issues.push(error("OLP_GRL_IDENTIFIER", `Target ${target.id} name is not a GRL identifier`, `/targets/${target.id}/name`));
    }
    if (target.robotId && !robots.has(target.robotId)) {
      issues.push(error("OLP_REF_ROBOT_MISSING", `Target ${target.id} references missing robot ${target.robotId}`, `/targets/${target.id}/robotId`));
    }
    if (target.toolId && !tools.has(target.toolId)) {
      issues.push(error("OLP_REF_TOOL_MISSING", `Target ${target.id} references missing tool ${target.toolId}`, `/targets/${target.id}/toolId`));
    }
    if (target.frameId && !frames.has(target.frameId)) {
      issues.push(error("OLP_REF_FRAME_MISSING", `Target ${target.id} references missing frame ${target.frameId}`, `/targets/${target.id}/frameId`));
    }
    if (target.kind === "pose") {
      if (!target.pose) {
        issues.push(error("OLP_TARGET_POSE_MISSING", `Pose target ${target.id} has no pose`, `/targets/${target.id}/pose`));
      } else {
        validatePose(target.pose, `/targets/${target.id}/pose`, issues);
      }
    }
    if (target.kind === "joint" && !target.joints) {
      issues.push(error("OLP_TARGET_JOINTS_MISSING", `Joint target ${target.id} has no joints`, `/targets/${target.id}/joints`));
    }
  }

  for (const path of model.paths) {
    if (!isIdentifier(path.name)) {
      issues.push(error("OLP_GRL_IDENTIFIER", `Path ${path.id} name is not a GRL identifier`, `/paths/${path.id}/name`));
    }
    if (path.defaults?.speedId && !speeds.has(path.defaults.speedId)) {
      issues.push(error("OLP_REF_SPEED_MISSING", `Path ${path.id} references missing speed ${path.defaults.speedId}`, `/paths/${path.id}/defaults/speedId`));
    }
    if (path.defaults?.zoneId && !zones.has(path.defaults.zoneId)) {
      issues.push(error("OLP_REF_ZONE_MISSING", `Path ${path.id} references missing zone ${path.defaults.zoneId}`, `/paths/${path.id}/defaults/zoneId`));
    }
    const pointNames = new Set<string>();
    for (const point of path.points) {
      if (pointNames.has(point.name)) {
        issues.push(error("OLP_PATH_POINT_DUPLICATE", `Path ${path.id} contains duplicate point ${point.name}`, `/paths/${path.id}/points/${point.id}`));
      }
      pointNames.add(point.name);
      if (!targets.has(point.targetId)) {
        issues.push(error("OLP_REF_TARGET_MISSING", `Point ${point.id} references missing target ${point.targetId}`, `/paths/${path.id}/points/${point.id}/targetId`));
      }
      if (point.motion === "movec" && (!point.viaTargetId || !targets.has(point.viaTargetId))) {
        issues.push(error("OLP_REF_VIA_TARGET_MISSING", `MoveC point ${point.id} requires an existing via target`, `/paths/${path.id}/points/${point.id}/viaTargetId`));
      }
      if (point.speedId && !speeds.has(point.speedId)) {
        issues.push(error("OLP_REF_SPEED_MISSING", `Point ${point.id} references missing speed ${point.speedId}`, `/paths/${path.id}/points/${point.id}/speedId`));
      }
      if (point.zoneId && !zones.has(point.zoneId)) {
        issues.push(error("OLP_REF_ZONE_MISSING", `Point ${point.id} references missing zone ${point.zoneId}`, `/paths/${path.id}/points/${point.id}/zoneId`));
      }
    }
  }

  for (const operation of model.operations) {
    if (!isIdentifier(operation.name)) {
      issues.push(error("OLP_GRL_IDENTIFIER", `Operation ${operation.id} name is not a GRL identifier`, `/operations/${operation.id}/name`));
    }
    if (!paths.has(operation.pathId)) {
      issues.push(error("OLP_REF_PATH_MISSING", `Operation ${operation.id} references missing path ${operation.pathId}`, `/operations/${operation.id}/pathId`));
    }
    if (operation.robotId && !robots.has(operation.robotId)) {
      issues.push(error("OLP_REF_ROBOT_MISSING", `Operation ${operation.id} references missing robot ${operation.robotId}`, `/operations/${operation.id}/robotId`));
    }
  }

  for (const program of model.programs) {
    for (const operationId of program.entryOperationIds) {
      if (!operations.has(operationId)) {
        issues.push(error("OLP_REF_OPERATION_MISSING", `Program ${program.id} references missing operation ${operationId}`, `/programs/${program.id}/entryOperationIds`));
      }
    }
  }

  for (const calibration of model.calibrations) {
    const exists = tools.has(calibration.targetResourceId) || frames.has(calibration.targetResourceId) || robots.has(calibration.targetResourceId) || externalAxes.has(calibration.targetResourceId);
    if (!exists) {
      issues.push(error("OLP_REF_CALIBRATION_TARGET_MISSING", `Calibration ${calibration.id} references missing resource ${calibration.targetResourceId}`, `/calibrations/${calibration.id}/targetResourceId`));
    }
  }

  for (const group of model.motionGroups) {
    for (const robotId of group.robotIds) {
      if (!robots.has(robotId)) {
        issues.push(error("OLP_REF_ROBOT_MISSING", `Motion group ${group.id} references missing robot ${robotId}`, `/motionGroups/${group.id}/robotIds`));
      }
    }
    for (const axisId of group.externalAxisIds) {
      if (!externalAxes.has(axisId)) {
        issues.push(error("OLP_REF_EXTERNAL_AXIS_MISSING", `Motion group ${group.id} references missing external axis ${axisId}`, `/motionGroups/${group.id}/externalAxisIds`));
      }
    }
  }

  return {
    ok: !issues.some((issue) => issue.severity === "error"),
    issues
  };
}

function idMap<T extends OlpNamedEntity>(items: T[], path: string, issues: OlpValidationIssue[]): Map<OlpId, T> {
  const map = new Map<OlpId, T>();
  for (const item of items) {
    if (!item.id) {
      issues.push(error("OLP_ID_MISSING", `Item at ${path} has no id`, path));
      continue;
    }
    if (map.has(item.id)) {
      issues.push(error("OLP_ID_DUPLICATE", `Duplicate id ${item.id}`, `${path}/${item.id}`));
      continue;
    }
    map.set(item.id, item);
  }
  return map;
}

function validatePose(pose: Pose6D, path: string, issues: OlpValidationIssue[]): void {
  if (pose.length !== 6 || pose.some((value) => !Number.isFinite(value))) {
    issues.push(error("OLP_POSE_INVALID", `Pose at ${path} must contain six finite numbers`, path));
  }
}

function isIdentifier(value: string): boolean {
  return /^[A-Za-z_][A-Za-z0-9_]*$/.test(value);
}

function error(code: string, message: string, path?: string): OlpValidationIssue {
  return {
    severity: "error",
    code,
    message,
    ...(path ? { path } : {})
  };
}
