import {
  generateGrlProgram,
  type GeneratedGrlProgram,
  type GeneratedPathPointSpec,
  type GrlGeneratorStyle,
  type GrlProgramGenerationSpec
} from "../grl/generator/index.js";
import type {
  OlpOperation,
  OlpPath,
  OlpPathPoint,
  OlpProgram,
  OlpProjectModel,
  OlpSpeed,
  OlpTarget,
  OlpZone
} from "./model.js";
import { validateOlpProject } from "./model.js";

export interface PathOperationGrlOptions {
  moduleName?: string;
  programId?: string;
  style?: GrlGeneratorStyle;
}

export interface PathOperationGrlResult extends GeneratedGrlProgram {
  spec: GrlProgramGenerationSpec;
  program: OlpProgram;
}

export function pathOperationToGrl(
  model: OlpProjectModel,
  operationId: string,
  options: PathOperationGrlOptions = {}
): PathOperationGrlResult {
  const validation = validateOlpProject(model);
  const errors = validation.issues.filter((issue) => issue.severity === "error");
  if (errors.length > 0) {
    throw new Error(`Cannot generate GRL from invalid OLP model: ${errors.map((issue) => issue.code).join(", ")}`);
  }

  const operation = required(model.operations.find((item) => item.id === operationId), `Operation ${operationId} not found`);
  const path = required(model.paths.find((item) => item.id === operation.pathId), `Path ${operation.pathId} not found`);
  const targets = collectTargetsForPath(model.targets, path);
  const spec = toGenerationSpec(model, operation, path, targets, options.moduleName);
  const generated = generateGrlProgram(spec, options.style ?? "expanded");
  const program: OlpProgram = {
    id: options.programId ?? `${operation.id}_grl`,
    name: spec.moduleName,
    language: "grl",
    entryOperationIds: [operation.id],
    source: {
      kind: "generated",
      text: generated.text,
      generatedFrom: {
        operationId: operation.id,
        pathId: path.id
      }
    }
  };

  return {
    ...generated,
    spec,
    program
  };
}

function toGenerationSpec(
  model: OlpProjectModel,
  operation: OlpOperation,
  path: OlpPath,
  targets: OlpTarget[],
  moduleName?: string
): GrlProgramGenerationSpec {
  return {
    moduleName: moduleName ?? sanitizeIdentifier(model.project.name),
    speeds: Object.fromEntries(model.speeds.map((speed) => [speed.name, renderSpeed(speed)])),
    zones: Object.fromEntries(model.zones.map((zone) => [zone.name, renderZone(zone)])),
    targets: targets.map((target) => {
      if (target.kind === "joint") {
        return {
          name: target.name,
          kind: "joint" as const,
          values: target.joints ?? []
        };
      }
      return {
        name: target.name,
        kind: "pose" as const,
        values: target.pose ?? [0, 0, 0, 0, 0, 0]
      };
    }),
    path: {
      name: path.name,
      ...(path.source ? { source: path.source } : {}),
      defaults: {
        speed: resolveSpeedName(model, path.defaults?.speedId) ?? "v_linear",
        zone: resolveZoneName(model, path.defaults?.zoneId) ?? "z10"
      },
      points: path.points.map((point) => toGeneratedPoint(model, point))
    },
    operation: {
      name: operation.name,
      kind: operation.kind,
      path: path.name,
      ...(operation.startAction ? { startAction: operation.startAction } : {}),
      ...(operation.endAction ? { endAction: operation.endAction } : {})
    }
  };
}

function collectTargetsForPath(allTargets: OlpTarget[], path: OlpPath): OlpTarget[] {
  const targetIds = new Set(path.points.flatMap((point) => [point.targetId, point.viaTargetId].filter((id): id is string => Boolean(id))));
  return allTargets.filter((target) => targetIds.has(target.id));
}

function toGeneratedPoint(model: OlpProjectModel, point: OlpPathPoint): GeneratedPathPointSpec {
  const target = required(model.targets.find((item) => item.id === point.targetId), `Target ${point.targetId} not found`);
  const via = point.viaTargetId
    ? required(model.targets.find((item) => item.id === point.viaTargetId), `Via target ${point.viaTargetId} not found`)
    : undefined;
  return {
    id: point.name,
    motion: point.motion,
    target: target.name,
    ...(via ? { via: via.name } : {}),
    ...(point.speedId ? { speed: required(resolveSpeedName(model, point.speedId), `Speed ${point.speedId} not found`) } : {}),
    ...(point.zoneId ? { zone: required(resolveZoneName(model, point.zoneId), `Zone ${point.zoneId} not found`) } : {})
  };
}

function renderSpeed(speed: OlpSpeed): string {
  if (speed.kind === "joint_percent") {
    return `joint(${speed.value} %)`;
  }
  return `linear(${speed.value} mm/s)`;
}

function renderZone(zone: OlpZone): string {
  if (zone.kind === "fine") {
    return "fine";
  }
  if (zone.kind === "cnt") {
    return `z(${zone.value ?? 10})`;
  }
  return `z(${zone.value ?? 10} mm)`;
}

function resolveSpeedName(model: OlpProjectModel, id?: string): string | undefined {
  if (!id) return undefined;
  return model.speeds.find((speed) => speed.id === id)?.name;
}

function resolveZoneName(model: OlpProjectModel, id?: string): string | undefined {
  if (!id) return undefined;
  return model.zones.find((zone) => zone.id === id)?.name;
}

function sanitizeIdentifier(value: string): string {
  const sanitized = value.replace(/[^A-Za-z0-9_]/g, "_");
  if (/^[A-Za-z_]/.test(sanitized)) {
    return sanitized;
  }
  return `Project_${sanitized}`;
}

function required<T>(value: T | undefined, message: string): T {
  if (value === undefined) {
    throw new Error(message);
  }
  return value;
}
