import type { MotionDiagnostic, PathValidationResult, TrajectoryResult } from "../kdl/types.js";
import type { OlpCalibrationRecord, OlpProjectModel } from "../olp/index.js";

export interface ValidationReportSection {
  name: "reachability" | "cycle_time" | "io_wait" | "post" | "import" | "collision" | "calibration";
  status: "pass" | "warn" | "fail";
  summary: Record<string, unknown>;
  diagnostics: MotionDiagnostic[];
}

export interface ValidationReport {
  schemaVersion: "validation-report/0.1";
  id: string;
  projectId: string;
  generatedAt: string;
  status: "pass" | "warn" | "fail";
  sections: ValidationReportSection[];
  sourceMap: Array<{
    kind: string;
    id: string;
    file?: string;
    line?: number;
    column?: number;
  }>;
}

export interface ValidationReportInput {
  id?: string;
  project: OlpProjectModel;
  generatedAt?: string;
  pathValidation?: PathValidationResult;
  trajectories?: TrajectoryResult[];
  diagnostics?: MotionDiagnostic[];
  importDiagnostics?: MotionDiagnostic[];
  postDiagnostics?: MotionDiagnostic[];
  ioWaitDiagnostics?: MotionDiagnostic[];
  collisionDiagnostics?: MotionDiagnostic[];
}

export interface DeliveryPackageFile {
  path: string;
  content: string;
  mediaType?: string;
}

export interface DeliveryPackage {
  format: "customer-delivery/0.1";
  projectId: string;
  files: DeliveryPackageFile[];
  manifest: {
    sourcePrograms: string[];
    brandPrograms: string[];
    reports: string[];
    calibrationFiles: string[];
    traceFiles: string[];
    ioMaps?: string[];
  };
  checksum?: string;
}

export function createValidationReport(input: ValidationReportInput): ValidationReport {
  const sections = [
    reachabilitySection(input.pathValidation, input.diagnostics ?? []),
    cycleTimeSection(input.pathValidation, input.trajectories ?? []),
    diagnosticsSection("io_wait", input.ioWaitDiagnostics ?? []),
    diagnosticsSection("post", input.postDiagnostics ?? []),
    diagnosticsSection("import", input.importDiagnostics ?? []),
    diagnosticsSection("collision", input.collisionDiagnostics ?? []),
    calibrationSection(input.project)
  ];
  return {
    schemaVersion: "validation-report/0.1",
    id: input.id ?? `${input.project.project.id}-validation`,
    projectId: input.project.project.id,
    generatedAt: input.generatedAt ?? new Date().toISOString(),
    status: combineStatus(sections.map((section) => section.status)),
    sections,
    sourceMap: [
      ...(input.diagnostics ?? []),
      ...(input.importDiagnostics ?? []),
      ...(input.postDiagnostics ?? []),
      ...(input.ioWaitDiagnostics ?? []),
      ...(input.collisionDiagnostics ?? [])
    ]
      .filter((diagnostic) => diagnostic.sourceMap)
      .map((diagnostic, index) => ({
        kind: diagnostic.code,
        id: `${diagnostic.code}-${index}`,
        ...(diagnostic.sourceMap?.file ? { file: diagnostic.sourceMap.file } : {}),
        ...(diagnostic.sourceMap?.line !== undefined ? { line: diagnostic.sourceMap.line } : {}),
        ...(diagnostic.sourceMap?.column !== undefined ? { column: diagnostic.sourceMap.column } : {})
      }))
  };
}

export function renderValidationReportHtml(report: ValidationReport): string {
  const rows = report.sections.map((section) =>
    `<tr><td>${escapeHtml(section.name)}</td><td>${section.status}</td><td>${escapeHtml(JSON.stringify(section.summary))}</td></tr>`
  ).join("");
  const diagnostics = report.sections.flatMap((section) => section.diagnostics.map((diagnostic) =>
    `<li>${diagnostic.severity} ${escapeHtml(diagnostic.code)}: ${escapeHtml(diagnostic.message)}</li>`
  )).join("");
  return `<!doctype html>
<html>
<head><meta charset="utf-8"><title>${escapeHtml(report.id)}</title></head>
<body>
<h1>${escapeHtml(report.id)}</h1>
<p>Status: ${report.status}</p>
<table><thead><tr><th>Section</th><th>Status</th><th>Summary</th></tr></thead><tbody>${rows}</tbody></table>
<h2>Source Map</h2>
<pre>${escapeHtml(JSON.stringify(report.sourceMap, null, 2))}</pre>
<h2>Diagnostics</h2>
<ul>${diagnostics}</ul>
<script type="application/json" id="validation-report-json">${escapeHtml(stableStringify(report))}</script>
</body>
</html>`;
}

export function createCustomerDeliveryPackage(input: {
  project: OlpProjectModel;
  report: ValidationReport;
  reportHtml?: string;
  brandPrograms?: Record<string, string>;
  ioMap?: Record<string, unknown>;
  trace?: unknown;
}): DeliveryPackage {
  const files: DeliveryPackageFile[] = [
    { path: "project/project.json", content: `${stableStringify(input.project)}\n`, mediaType: "application/json" },
    { path: `reports/${input.report.id}.json`, content: `${stableStringify(input.report)}\n`, mediaType: "application/json" },
    { path: `reports/${input.report.id}.html`, content: input.reportHtml ?? renderValidationReportHtml(input.report), mediaType: "text/html" },
    { path: "calibration/calibrations.json", content: `${stableStringify(input.project.calibrations)}\n`, mediaType: "application/json" }
  ];
  for (const program of input.project.programs) {
    files.push({
      path: `source/${program.id}.${program.language === "grl" ? "grl" : "txt"}`,
      content: program.source.text,
      mediaType: "text/plain"
    });
  }
  for (const [filename, text] of Object.entries(input.brandPrograms ?? {})) {
    files.push({ path: `post/${filename}`, content: text, mediaType: "text/plain" });
  }
  if (input.ioMap) {
    files.push({ path: "io/io_map.json", content: `${stableStringify(input.ioMap)}\n`, mediaType: "application/json" });
  }
  if (input.trace) {
    files.push({ path: "trace/trace.json", content: `${stableStringify(input.trace)}\n`, mediaType: "application/json" });
  }

  const sortedFiles = files.sort((left, right) => left.path.localeCompare(right.path));
  return {
    format: "customer-delivery/0.1",
    projectId: input.project.project.id,
    files: sortedFiles,
    manifest: {
      sourcePrograms: sortedFiles.filter((file) => file.path.startsWith("source/")).map((file) => file.path),
      brandPrograms: sortedFiles.filter((file) => file.path.startsWith("post/")).map((file) => file.path),
      reports: sortedFiles.filter((file) => file.path.startsWith("reports/")).map((file) => file.path),
      calibrationFiles: sortedFiles.filter((file) => file.path.startsWith("calibration/")).map((file) => file.path),
      traceFiles: sortedFiles.filter((file) => file.path.startsWith("trace/")).map((file) => file.path),
      ioMaps: sortedFiles.filter((file) => file.path.startsWith("io/")).map((file) => file.path)
    },
    checksum: checksumDeliveryFiles(sortedFiles)
  };
}

export function validateReportSchema(report: ValidationReport): { ok: boolean; issues: string[] } {
  const issues: string[] = [];
  if (report.schemaVersion !== "validation-report/0.1") issues.push("schemaVersion");
  if (!["pass", "warn", "fail"].includes(report.status)) issues.push("status");
  for (const section of report.sections) {
    if (!section.name || !["pass", "warn", "fail"].includes(section.status)) {
      issues.push(`sections.${section.name || "unknown"}`);
    }
  }
  return {
    ok: issues.length === 0,
    issues
  };
}

export const exportValidationReportHtml = renderValidationReportHtml;
export const exportCustomerDeliveryPackage = createCustomerDeliveryPackage;

export function serializeDeliveryPackage(pkg: DeliveryPackage): string {
  return `${stableStringify(pkg)}\n`;
}

export function calibrationSummary(calibrations: OlpCalibrationRecord[]): Record<string, number> {
  return calibrations.reduce<Record<string, number>>((summary, calibration) => {
    summary[calibration.kind] = (summary[calibration.kind] ?? 0) + 1;
    return summary;
  }, {});
}

function reachabilitySection(pathValidation: PathValidationResult | undefined, diagnostics: MotionDiagnostic[]): ValidationReportSection {
  return {
    name: "reachability",
    status: pathValidation ? (pathValidation.ok && pathValidation.reachable ? "pass" : "fail") : statusFromDiagnostics(diagnostics),
    summary: {
      reachable: pathValidation?.reachable ?? diagnostics.every((diagnostic) => diagnostic.severity !== "error"),
      segments: pathValidation?.segmentReports.length ?? 0
    },
    diagnostics: [...(pathValidation?.diagnostics ?? []), ...diagnostics]
  };
}

function cycleTimeSection(pathValidation: PathValidationResult | undefined, trajectories: TrajectoryResult[]): ValidationReportSection {
  const cycleTime = pathValidation?.cycleTime ?? trajectories.reduce((sum, trajectory) => sum + trajectory.duration, 0);
  return {
    name: "cycle_time",
    status: cycleTime > 0 ? "pass" : "warn",
    summary: { cycleTime, trajectories: trajectories.length },
    diagnostics: []
  };
}

function diagnosticsSection(name: ValidationReportSection["name"], diagnostics: MotionDiagnostic[]): ValidationReportSection {
  return {
    name,
    status: statusFromDiagnostics(diagnostics),
    summary: { diagnostics: diagnostics.length },
    diagnostics
  };
}

function calibrationSection(project: OlpProjectModel): ValidationReportSection {
  return {
    name: "calibration",
    status: project.calibrations.length > 0 ? "pass" : "warn",
    summary: { records: project.calibrations.length },
    diagnostics: []
  };
}

function statusFromDiagnostics(diagnostics: MotionDiagnostic[]): ValidationReportSection["status"] {
  if (diagnostics.some((diagnostic) => diagnostic.severity === "error")) {
    return "fail";
  }
  if (diagnostics.some((diagnostic) => diagnostic.severity === "warning")) {
    return "warn";
  }
  return "pass";
}

function combineStatus(statuses: ValidationReportSection["status"][]): ValidationReport["status"] {
  if (statuses.includes("fail")) {
    return "fail";
  }
  if (statuses.includes("warn")) {
    return "warn";
  }
  return "pass";
}

function escapeHtml(value: string): string {
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
}

function checksumDeliveryFiles(files: DeliveryPackageFile[]): string {
  let hash = 0x811c9dc5;
  for (const file of files) {
    const content = `${file.path}\0${file.mediaType ?? ""}\0${file.content}\0`;
    for (let index = 0; index < content.length; index += 1) {
      hash ^= content.charCodeAt(index);
      hash = Math.imul(hash, 0x01000193) >>> 0;
    }
  }
  return `fnv1a32:${hash.toString(16).padStart(8, "0")}`;
}

function stableStringify(value: unknown): string {
  return JSON.stringify(sortForStableStringify(value), null, 2);
}

function sortForStableStringify(value: unknown): unknown {
  if (Array.isArray(value)) {
    return value.map(sortForStableStringify);
  }
  if (typeof value === "object" && value !== null) {
    return Object.fromEntries(
      Object.entries(value as Record<string, unknown>)
        .filter(([, item]) => item !== undefined)
        .sort(([left], [right]) => left.localeCompare(right))
        .map(([key, item]) => [key, sortForStableStringify(item)])
    );
  }
  return value;
}
