import { parseGrl } from "../grl/parser/index.js";
import { compileSemanticProgram } from "../grl/semantic/index.js";
import type { ExecutableInstruction, SemanticProgramIr } from "../grl/ir/index.js";
import type { MotionDiagnostic } from "../kdl/types.js";
import {
  applyPatch,
  createOlpProject,
  pathOperationToGrl,
  type JsonPatchOperation,
  type OlpOperation,
  type OlpPath,
  type OlpPathPoint,
  type OlpProgram,
  type OlpProjectModel,
  type OlpSpeed,
  type OlpTarget,
  type OlpZone,
  type Pose6D
} from "../olp/index.js";
import {
  createValidationReport,
  type ValidationReport
} from "../reports/index.js";

export type ImportBrand = "abb" | "kuka" | "fanuc";

export interface BrandProgramFile {
  name: string;
  text: string;
}

export interface BrandImportOptions {
  projectId?: string;
  projectName?: string;
  operationKind?: string;
  generatedAt?: string;
}

export interface BrandImportReportItem {
  code: string;
  severity: "info" | "warning" | "error";
  message: string;
  file?: string;
  line?: number;
}

export interface BrandImportResult {
  brand: ImportBrand;
  project: OlpProjectModel;
  patch: JsonPatchOperation[];
  grl: string;
  program: OlpProgram;
  ir: SemanticProgramIr;
  report: ValidationReport;
  importReport: BrandImportReportItem[];
  parsed: {
    targets: OlpTarget[];
    path: OlpPath;
    operation: OlpOperation;
  };
}

interface ParsedBrandProgram {
  targets: OlpTarget[];
  points: OlpPathPoint[];
  notes: BrandImportReportItem[];
}

const DEFAULT_SPEEDS: OlpSpeed[] = [
  { id: "import_vj", name: "import_vj", kind: "joint_percent", value: 50 },
  { id: "import_vl", name: "import_vl", kind: "linear_mm_s", value: 200 }
];

const DEFAULT_ZONES: OlpZone[] = [
  { id: "import_fine", name: "import_fine", kind: "fine" },
  { id: "import_z10", name: "import_z10", kind: "distance_mm", value: 10 }
];

export function importBrandProgram(
  brand: ImportBrand,
  files: BrandProgramFile[],
  options: BrandImportOptions = {}
): BrandImportResult {
  const parsed = parseByBrand(brand, files);
  const projectId = options.projectId ?? `${brand}_import`;
  const projectName = options.projectName ?? `${brand.toUpperCase()}Import`;
  const baseProject = createOlpProject({ id: projectId, name: projectName });
  const path: OlpPath = {
    id: `${brand}_path`,
    name: `${brand}_path`,
    source: {
      type: "brand_import",
      brand,
      files: files.map((file) => file.name).join(",")
    },
    defaults: {
      speedId: "import_vl",
      zoneId: "import_z10"
    },
    points: parsed.points
  };
  const operation: OlpOperation = {
    id: `${brand}_operation`,
    name: `${brand}_operation`,
    kind: options.operationKind ?? "imported_program",
    pathId: path.id,
    process: {
      import_brand: brand,
      source_files: files.map((file) => file.name)
    }
  };
  const patch = buildImportPatch(DEFAULT_SPEEDS, DEFAULT_ZONES, parsed.targets, path, operation);
  const project = applyPatch(baseProject, patch);
  const generated = pathOperationToGrl(project, operation.id, {
    moduleName: projectName.replace(/[^A-Za-z0-9_]/g, "_")
  });
  project.programs.push(generated.program);
  const compiledIr = compileSemanticProgram(parseGrl(generated.text), {
    startJoints: firstJointTarget(parsed.targets)?.joints ?? [0, 0, 0, 0, 0, 0],
    sampleTime: 0.004
  });
  const ir = expandRunInstructionsForPost(compiledIr);
  const report = createValidationReport({
    id: `${brand}_import_report`,
    project,
    ...(options.generatedAt ? { generatedAt: options.generatedAt } : {}),
    importDiagnostics: toImportDiagnostics(brand, parsed.targets, parsed.points, parsed.notes)
  });

  return {
    brand,
    project,
    patch,
    grl: generated.text,
    program: generated.program,
    ir,
    report,
    importReport: parsed.notes,
    parsed: {
      targets: parsed.targets,
      path,
      operation
    }
  };
}

export function parseAbbRapid(files: BrandProgramFile[]): ParsedBrandProgram {
  const source = joinFiles(files);
  const targets: OlpTarget[] = [];
  const targetByRaw = new Map<string, OlpTarget>();
  const notes: BrandImportReportItem[] = [];

  for (const match of source.text.matchAll(/\b(?:CONST|PERS|VAR)\s+robtarget\s+([A-Za-z_][A-Za-z0-9_]*)\s*:?=\s*\[\[\s*([-+0-9.Ee]+)\s*,\s*([-+0-9.Ee]+)\s*,\s*([-+0-9.Ee]+)\s*\]\s*,/g)) {
    const raw = match[1]!;
    const target = poseTarget(raw, [num(match[2]), num(match[3]), num(match[4]), 0, 0, 0]);
    addTarget(targets, targetByRaw, raw, target);
  }
  for (const match of source.text.matchAll(/\b(?:CONST|PERS|VAR)\s+jointtarget\s+([A-Za-z_][A-Za-z0-9_]*)\s*:?=\s*\[\[\s*([^\]]+)\]/g)) {
    const raw = match[1]!;
    const joints = match[2]!.split(",").slice(0, 6).map((value) => num(value));
    const target = jointTarget(raw, joints);
    addTarget(targets, targetByRaw, raw, target);
  }

  const points: OlpPathPoint[] = [];
  for (const line of source.lines) {
    const move = line.text.match(/\b(MoveJ|MoveL|MoveC)\s+([^,;]+)\s*,\s*([^,;]+)?/i);
    if (!move) continue;
    const motionRaw = move[1]!.toLowerCase();
    if (motionRaw === "movec") {
      const circular = line.text.match(/\bMoveC\s+([^,;]+)\s*,\s*([^,;]+)/i);
      if (!circular) continue;
      const via = resolveTarget(targets, targetByRaw, circular[1]!, notes, line);
      const target = resolveTarget(targets, targetByRaw, circular[2]!, notes, line);
      points.push(pathPoint(points.length, "movec", target.id, { viaTargetId: via.id, sourceLine: line.number }));
      continue;
    }
    const target = resolveTarget(targets, targetByRaw, move[2]!, notes, line);
    points.push(pathPoint(points.length, motionRaw === "movej" ? "movej" : "movel", target.id, { sourceLine: line.number }));
  }

  addFeatureNotes(notes, source, "abb");
  return { targets, points, notes };
}

export function parseKukaKrl(files: BrandProgramFile[]): ParsedBrandProgram {
  const source = joinFiles(files);
  const targets: OlpTarget[] = [];
  const targetByRaw = new Map<string, OlpTarget>();
  const notes: BrandImportReportItem[] = [];

  for (const match of source.text.matchAll(/\b(?:DECL\s+)?E6POS\s+([A-Za-z_][A-Za-z0-9_]*)\s*=\s*\{([^}]+)\}/gi)) {
    const raw = match[1]!;
    const body = match[2]!;
    const target = poseTarget(raw, [
      field(body, "X"),
      field(body, "Y"),
      field(body, "Z"),
      field(body, "A"),
      field(body, "B"),
      field(body, "C")
    ]);
    addTarget(targets, targetByRaw, raw, target);
  }
  for (const match of source.text.matchAll(/\b(?:DECL\s+)?E6AXIS\s+([A-Za-z_][A-Za-z0-9_]*)\s*=\s*\{([^}]+)\}/gi)) {
    const raw = match[1]!;
    const body = match[2]!;
    const target = jointTarget(raw, ["A1", "A2", "A3", "A4", "A5", "A6"].map((axis) => field(body, axis)));
    addTarget(targets, targetByRaw, raw, target);
  }

  const points: OlpPathPoint[] = [];
  for (const line of source.lines) {
    const move = line.text.match(/^\s*(PTP|LIN|CIRC)\s+([^ ;,]+)(?:\s*,\s*([^ ;,]+))?/i);
    if (!move) continue;
    const motion = move[1]!.toUpperCase();
    if (motion === "CIRC") {
      const via = resolveTarget(targets, targetByRaw, move[2]!, notes, line);
      const target = resolveTarget(targets, targetByRaw, move[3] ?? move[2]!, notes, line);
      points.push(pathPoint(points.length, "movec", target.id, { viaTargetId: via.id, sourceLine: line.number }));
      continue;
    }
    const target = resolveTarget(targets, targetByRaw, move[2]!, notes, line);
    points.push(pathPoint(points.length, motion === "PTP" ? "movej" : "movel", target.id, { sourceLine: line.number }));
  }

  addFeatureNotes(notes, source, "kuka");
  return { targets, points, notes };
}

export function parseFanucLs(files: BrandProgramFile[]): ParsedBrandProgram {
  const source = joinFiles(files);
  const targets: OlpTarget[] = [];
  const targetByRaw = new Map<string, OlpTarget>();
  const notes: BrandImportReportItem[] = [];

  for (const match of source.text.matchAll(/\bP\[(\d+)\]\s*\{([\s\S]*?)\};/g)) {
    const raw = `P[${match[1]!}]`;
    const body = match[2]!;
    const target = poseTarget(`P${match[1]!}`, [
      fanucField(body, "X"),
      fanucField(body, "Y"),
      fanucField(body, "Z"),
      fanucField(body, "W"),
      fanucField(body, "P"),
      fanucField(body, "R")
    ]);
    addTarget(targets, targetByRaw, raw, target);
  }
  for (const match of source.text.matchAll(/\bPR\[(\d+)\]\s*\{([\s\S]*?)\};/g)) {
    const raw = `PR[${match[1]!}]`;
    const body = match[2]!;
    const target = poseTarget(`PR${match[1]!}`, [
      fanucField(body, "X"),
      fanucField(body, "Y"),
      fanucField(body, "Z"),
      fanucField(body, "W"),
      fanucField(body, "P"),
      fanucField(body, "R")
    ]);
    addTarget(targets, targetByRaw, raw, target);
  }

  const points: OlpPathPoint[] = [];
  for (const line of source.lines) {
    const move = line.text.match(/^\s*\d+:\s*(J|L|C)\s+((?:P|PR)\[\d+\])(?:\s+((?:P|PR)\[\d+\]))?/i);
    if (!move) continue;
    const motion = move[1]!.toUpperCase();
    if (motion === "C") {
      const via = resolveTarget(targets, targetByRaw, move[2]!, notes, line);
      const target = resolveTarget(targets, targetByRaw, move[3] ?? move[2]!, notes, line);
      points.push(pathPoint(points.length, "movec", target.id, { viaTargetId: via.id, sourceLine: line.number }));
      continue;
    }
    const target = resolveTarget(targets, targetByRaw, move[2]!, notes, line);
    points.push(pathPoint(points.length, motion === "J" ? "movej" : "movel", target.id, { sourceLine: line.number }));
  }

  addFeatureNotes(notes, source, "fanuc");
  return { targets, points, notes };
}

function parseByBrand(brand: ImportBrand, files: BrandProgramFile[]): ParsedBrandProgram {
  const parsed = brand === "abb" ? parseAbbRapid(files) : brand === "kuka" ? parseKukaKrl(files) : parseFanucLs(files);
  if (parsed.points.length === 0) {
    parsed.notes.push({
      code: "IMPORT_NO_MOTION",
      severity: "error",
      message: `${brand} import did not find supported motion statements`
    });
    throw new Error(`${brand} import did not find supported motion statements`);
  }
  return parsed;
}

function buildImportPatch(
  speeds: OlpSpeed[],
  zones: OlpZone[],
  targets: OlpTarget[],
  path: OlpPath,
  operation: OlpOperation
): JsonPatchOperation[] {
  return [
    ...speeds.map((speed) => ({ op: "add" as const, path: "/speeds/-", value: speed })),
    ...zones.map((zone) => ({ op: "add" as const, path: "/zones/-", value: zone })),
    ...targets.map((target) => ({ op: "add" as const, path: "/targets/-", value: target })),
    { op: "add", path: "/paths/-", value: path },
    { op: "add", path: "/operations/-", value: operation }
  ];
}

function toImportDiagnostics(
  brand: ImportBrand,
  targets: OlpTarget[],
  points: OlpPathPoint[],
  notes: BrandImportReportItem[]
): MotionDiagnostic[] {
  const diagnostics: MotionDiagnostic[] = [
    {
      severity: targets.length > 0 ? "info" : "warning",
      code: "IMPORT_TARGETS",
      message: `${brand} import mapped ${targets.length} targets`
    },
    {
      severity: points.length > 0 ? "info" : "error",
      code: "IMPORT_MOTIONS",
      message: `${brand} import mapped ${points.length} motion points`
    }
  ];
  for (const note of notes) {
    diagnostics.push({
      severity: note.severity,
      code: note.code,
      message: note.message,
      ...(note.line || note.file ? { sourceMap: { ...(note.file ? { file: note.file } : {}), ...(note.line ? { line: note.line } : {}) } } : {})
    });
  }
  return diagnostics;
}

function expandRunInstructionsForPost(ir: SemanticProgramIr): SemanticProgramIr {
  return {
    ...ir,
    procedures: ir.procedures.map((procedure) => ({
      ...procedure,
      instructions: procedure.instructions.flatMap((instruction): ExecutableInstruction[] => {
        if (instruction.kind === "RUN_PATH") {
          return ir.paths.find((path) => path.pathId === instruction.pathId)?.motions ?? [instruction];
        }
        if (instruction.kind === "RUN_OPERATION") {
          const operation = ir.operations.find((item) => item.operationId === instruction.operationId);
          const motions = operation ? ir.paths.find((path) => path.pathId === operation.pathId)?.motions : undefined;
          return motions ?? [instruction];
        }
        return [instruction];
      })
    }))
  };
}

function pathPoint(
  index: number,
  motion: "movej" | "movel" | "movec",
  targetId: string,
  extra: { viaTargetId?: string; sourceLine?: number } = {}
): OlpPathPoint {
  return {
    id: `import_p${String(index).padStart(2, "0")}`,
    name: `import_p${String(index).padStart(2, "0")}`,
    motion,
    targetId,
    ...(extra.viaTargetId ? { viaTargetId: extra.viaTargetId } : {}),
    speedId: motion === "movej" ? "import_vj" : "import_vl",
    zoneId: "import_z10",
    ...(extra.sourceLine ? { metadata: { sourceLine: extra.sourceLine } } : {})
  };
}

function poseTarget(rawName: string, pose: Pose6D): OlpTarget {
  const name = sanitizeIdentifier(rawName);
  return {
    id: `target_${name}`,
    name,
    kind: "pose",
    pose
  };
}

function jointTarget(rawName: string, joints: number[]): OlpTarget {
  const name = sanitizeIdentifier(rawName);
  return {
    id: `target_${name}`,
    name,
    kind: "joint",
    joints
  };
}

function addTarget(targets: OlpTarget[], targetByRaw: Map<string, OlpTarget>, rawName: string, target: OlpTarget): void {
  if (!targets.some((item) => item.id === target.id)) {
    targets.push(target);
  }
  targetByRaw.set(rawName.trim().toUpperCase(), target);
  targetByRaw.set(target.name.toUpperCase(), target);
}

function resolveTarget(
  targets: OlpTarget[],
  targetByRaw: Map<string, OlpTarget>,
  rawToken: string,
  notes: BrandImportReportItem[],
  line: { file: string; number: number }
): OlpTarget {
  const raw = rawToken.trim().replace(/[;]/g, "");
  const existing = targetByRaw.get(raw.toUpperCase());
  if (existing) {
    return existing;
  }
  const target = poseTarget(raw, [0, 0, 0, 0, 0, 0]);
  addTarget(targets, targetByRaw, raw, target);
  notes.push({
    code: "IMPORT_TARGET_STUB",
    severity: "warning",
    message: `Target ${raw} was referenced by motion but had no parsed position; a zero pose stub was created`,
    file: line.file,
    line: line.number
  });
  return target;
}

function addFeatureNotes(notes: BrandImportReportItem[], source: JoinedFiles, brand: ImportBrand): void {
  const tool = brand === "abb" ? /\btooldata\b|\btool0\b/i : brand === "kuka" ? /\$TOOL\b/i : /\bUTOOL_NUM\b/i;
  const frame = brand === "abb" ? /\bwobjdata\b|\bwobj0\b/i : brand === "kuka" ? /\$BASE\b/i : /\bUFRAME_NUM\b/i;
  if (tool.test(source.text)) {
    notes.push({
      code: "IMPORT_TOOL_DETECTED",
      severity: "info",
      message: `${brand} tool selection detected and recorded as import metadata`
    });
  }
  if (frame.test(source.text)) {
    notes.push({
      code: "IMPORT_FRAME_DETECTED",
      severity: "info",
      message: `${brand} frame/base selection detected and recorded as import metadata`
    });
  }
}

function field(body: string, name: string): number {
  const match = body.match(new RegExp(`\\b${name}\\s+([-+0-9.Ee]+)`, "i"));
  return match ? num(match[1]) : 0;
}

function fanucField(body: string, name: string): number {
  const match = body.match(new RegExp(`\\b${name}\\s*=\\s*([-+0-9.Ee]+)`, "i"));
  return match ? num(match[1]) : 0;
}

function num(value: string | undefined): number {
  const parsed = Number(value?.trim());
  return Number.isFinite(parsed) ? parsed : 0;
}

function firstJointTarget(targets: OlpTarget[]): OlpTarget | undefined {
  return targets.find((target) => target.kind === "joint" && target.joints);
}

function sanitizeIdentifier(value: string): string {
  const normalized = value
    .trim()
    .replace(/\[(\d+)\]/g, "$1")
    .replace(/[^A-Za-z0-9_]/g, "_");
  if (/^[A-Za-z_]/.test(normalized)) {
    return normalized;
  }
  return `T_${normalized}`;
}

interface JoinedFiles {
  text: string;
  lines: Array<{ file: string; number: number; text: string }>;
}

function joinFiles(files: BrandProgramFile[]): JoinedFiles {
  const lines: JoinedFiles["lines"] = [];
  const textParts: string[] = [];
  for (const file of files) {
    textParts.push(file.text);
    file.text.split(/\r?\n/).forEach((text, index) => {
      lines.push({
        file: file.name,
        number: index + 1,
        text
      });
    });
  }
  return {
    text: textParts.join("\n"),
    lines
  };
}
