#!/usr/bin/env node
import { readdir, readFile, stat } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";

const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const defaultFixtureDir = path.join(repoRoot, "fixtures/mvp/topology-constraints");

const PATCH_PANEL_SERVICE_ID = "hwlab-patch-panel";
const PATCH_PANEL_ONLY = "patch-panel-only";
const ACTIVE = "active";

function isObject(value) {
  return typeof value === "object" && value !== null && !Array.isArray(value);
}

function hasOwn(value, key) {
  return Object.prototype.hasOwnProperty.call(value, key);
}

function addViolation(violations, code, message, jsonPath = "$") {
  violations.push({ code, message, path: jsonPath });
}

function asArray(value) {
  return Array.isArray(value) ? value : [];
}

function uniqueSorted(values) {
  return [...new Set(values)].sort();
}

function sameStringSet(left, right) {
  const leftSorted = uniqueSorted(left);
  const rightSorted = uniqueSorted(right);
  return (
    leftSorted.length === rightSorted.length &&
    leftSorted.every((value, index) => value === rightSorted[index])
  );
}

async function collectJsonFiles(target) {
  const targetStat = await stat(target);
  if (targetStat.isFile()) {
    return target.endsWith(".json") ? [target] : [];
  }

  const entries = await readdir(target, { withFileTypes: true });
  const files = [];
  for (const entry of entries) {
    const entryPath = path.join(target, entry.name);
    if (entry.isDirectory()) {
      files.push(...(await collectJsonFiles(entryPath)));
    } else if (entry.isFile() && entry.name.endsWith(".json")) {
      files.push(entryPath);
    }
  }
  return files.sort();
}

function getFixtureTargets() {
  const args = process.argv.slice(2);
  if (args.length === 0) {
    return [defaultFixtureDir];
  }
  return args.map((arg) => path.resolve(process.cwd(), arg));
}

function getCaseId(filePath, fixture) {
  return fixture.caseId ?? path.basename(filePath, ".json");
}

function getTopology(fixture) {
  return fixture.topology ?? fixture;
}

function getExpectedValidation(fixture) {
  return (
    fixture.expectedValidation ?? {
      valid: true,
      violationCodes: []
    }
  );
}

function getWiringConfigs(topology, violations) {
  if (Array.isArray(topology.wiringConfigs)) {
    return topology.wiringConfigs;
  }
  if (isObject(topology.wiringConfig)) {
    return [topology.wiringConfig];
  }

  addViolation(
    violations,
    "topology_shape",
    "topology requires wiringConfigs[] or wiringConfig",
    "$.topology.wiringConfigs"
  );
  return [];
}

function indexBoxes(topology, violations) {
  const boxes = topology.services?.boxes;
  if (!Array.isArray(boxes) || boxes.length === 0) {
    addViolation(violations, "topology_shape", "topology requires at least one box simulator", "$.topology.services.boxes");
    return new Map();
  }

  const resources = new Map();
  boxes.forEach((box, index) => {
    const jsonPath = `$.topology.services.boxes[${index}]`;
    const resource = box.resource;
    const resourceId = resource?.resourceId;

    if (box.serviceId !== "hwlab-box-simu") {
      addViolation(violations, "topology_shape", "box serviceId must be hwlab-box-simu", `${jsonPath}.serviceId`);
    }
    if (box.projectId !== topology.projectId || resource?.projectId !== topology.projectId) {
      addViolation(violations, "topology_shape", "box projectId must match topology projectId", jsonPath);
    }
    if (!resourceId) {
      addViolation(violations, "topology_shape", "box resource.resourceId is required", `${jsonPath}.resource.resourceId`);
      return;
    }
    if (resources.has(resourceId)) {
      addViolation(violations, "topology_shape", `duplicate resourceId ${resourceId}`, `${jsonPath}.resource.resourceId`);
    }
    if (box.constraints?.crossDevicePropagation !== PATCH_PANEL_ONLY) {
      addViolation(
        violations,
        "patch_panel_only",
        "box cross-device propagation must be patch-panel-only",
        `${jsonPath}.constraints.crossDevicePropagation`
      );
    }
    if (box.constraints?.localLoopbackEnabled !== false) {
      addViolation(
        violations,
        "patch_panel_only",
        "box local loopback cannot substitute for cross-device propagation",
        `${jsonPath}.constraints.localLoopbackEnabled`
      );
    }
    if (resource.metadata?.propagation !== PATCH_PANEL_ONLY) {
      addViolation(
        violations,
        "patch_panel_only",
        "box resource metadata propagation must be patch-panel-only",
        `${jsonPath}.resource.metadata.propagation`
      );
    }

    resources.set(resourceId, {
      box,
      jsonPath,
      ports: isObject(box.ports) ? box.ports : {},
      declaredPorts: new Set(asArray(resource.metadata?.ports))
    });
  });

  return resources;
}

function validateGateways(topology, resources, violations) {
  const gateways = topology.services?.gateways;
  if (!Array.isArray(gateways) || gateways.length === 0) {
    addViolation(violations, "topology_shape", "topology requires gateway simulators", "$.topology.services.gateways");
    return new Set();
  }

  const sessions = new Set();
  gateways.forEach((gateway, index) => {
    const jsonPath = `$.topology.services.gateways[${index}]`;
    if (gateway.serviceId !== "hwlab-gateway-simu") {
      addViolation(violations, "topology_shape", "gateway serviceId must be hwlab-gateway-simu", `${jsonPath}.serviceId`);
    }
    if (gateway.projectId !== topology.projectId || gateway.session?.projectId !== topology.projectId) {
      addViolation(violations, "topology_shape", "gateway projectId must match topology projectId", jsonPath);
    }
    if (gateway.session?.status !== "connected") {
      addViolation(violations, "topology_shape", "gateway simulator session must be connected", `${jsonPath}.session.status`);
    }
    if (gateway.session?.gatewaySessionId) {
      sessions.add(gateway.session.gatewaySessionId);
    }

    asArray(gateway.boxes).forEach((resourceId, boxIndex) => {
      if (!resources.has(resourceId)) {
        addViolation(
          violations,
          "endpoint_reference",
          `gateway references unknown box resource ${resourceId}`,
          `${jsonPath}.boxes[${boxIndex}]`
        );
      }
    });
  });

  return sessions;
}

function validatePatchPanel(topology, sessions, violations) {
  const patchPanel = topology.services?.patchPanel;
  if (!isObject(patchPanel)) {
    addViolation(violations, "topology_shape", "topology requires one patch panel status object", "$.topology.services.patchPanel");
    return {};
  }

  if (patchPanel.serviceId !== PATCH_PANEL_SERVICE_ID) {
    addViolation(
      violations,
      "patch_panel_override_invalid",
      "patch panel override must be reported by hwlab-patch-panel",
      "$.topology.services.patchPanel.serviceId"
    );
  }
  if (patchPanel.state !== ACTIVE) {
    addViolation(violations, "topology_shape", "patch panel status must be active", "$.topology.services.patchPanel.state");
  }
  if (patchPanel.projectId !== topology.projectId) {
    addViolation(violations, "topology_shape", "patch panel projectId must match topology projectId", "$.topology.services.patchPanel.projectId");
  }
  if (patchPanel.metadata?.propagation !== PATCH_PANEL_ONLY) {
    addViolation(
      violations,
      "patch_panel_only",
      "patch panel propagation metadata must be patch-panel-only",
      "$.topology.services.patchPanel.metadata.propagation"
    );
  }
  if (patchPanel.gatewaySessionId && sessions.size > 0 && !sessions.has(patchPanel.gatewaySessionId)) {
    addViolation(
      violations,
      "endpoint_reference",
      "patch panel gatewaySessionId must belong to a known gateway simulator",
      "$.topology.services.patchPanel.gatewaySessionId"
    );
  }

  return patchPanel;
}

function portExists(resourceEntry, port) {
  return hasOwn(resourceEntry.ports, port) || resourceEntry.declaredPorts.has(port);
}

function validateEndpoint(endpoint, resources, violations, jsonPath) {
  if (!isObject(endpoint)) {
    addViolation(violations, "topology_shape", "connection endpoint must be an object", jsonPath);
    return;
  }

  const resource = resources.get(endpoint.resourceId);
  if (!resource) {
    addViolation(violations, "endpoint_reference", `unknown resource ${endpoint.resourceId}`, `${jsonPath}.resourceId`);
    return;
  }
  if (!portExists(resource, endpoint.port)) {
    addViolation(
      violations,
      "endpoint_reference",
      `unknown port ${endpoint.resourceId}:${endpoint.port}`,
      `${jsonPath}.port`
    );
  }
}

function validateWiringConfig(config, index, topology, resources, sessions, violations) {
  const jsonPath = `$.topology.wiringConfigs[${index}]`;
  if (config.projectId !== topology.projectId) {
    addViolation(violations, "topology_shape", "wiring config projectId must match topology projectId", `${jsonPath}.projectId`);
  }
  if (config.gatewaySessionId && sessions.size > 0 && !sessions.has(config.gatewaySessionId)) {
    addViolation(
      violations,
      "endpoint_reference",
      "wiring config gatewaySessionId must belong to a known gateway simulator",
      `${jsonPath}.gatewaySessionId`
    );
  }
  if (!Array.isArray(config.connections) || config.connections.length === 0) {
    addViolation(violations, "topology_shape", "wiring config requires at least one connection", `${jsonPath}.connections`);
    return;
  }
  if (config.constraints?.propagation !== PATCH_PANEL_ONLY) {
    addViolation(
      violations,
      "patch_panel_only",
      "wiring config propagation must be patch-panel-only",
      `${jsonPath}.constraints.propagation`
    );
  }
  if (config.constraints?.localLoopbackSubstituteAllowed !== false) {
    addViolation(
      violations,
      "patch_panel_only",
      "wiring config cannot allow local loopback as a substitute",
      `${jsonPath}.constraints.localLoopbackSubstituteAllowed`
    );
  }

  config.connections.forEach((connection, connectionIndex) => {
    const connectionPath = `${jsonPath}.connections[${connectionIndex}]`;
    validateEndpoint(connection.from, resources, violations, `${connectionPath}.from`);
    validateEndpoint(connection.to, resources, violations, `${connectionPath}.to`);
  });
}

function getActiveConnectionKey(connection) {
  return [
    connection.fromResourceId,
    connection.fromPort,
    connection.toResourceId,
    connection.toPort
  ].join("->");
}

function connectionToPatchPanelConnection(connection) {
  return {
    fromResourceId: connection.from.resourceId,
    fromPort: connection.from.port,
    toResourceId: connection.to.resourceId,
    toPort: connection.to.port
  };
}

function validatePatchPanelMatchesSelectedConfig(patchPanel, wiringConfigs, violations) {
  if (!patchPanel.wiringConfigId) {
    addViolation(
      violations,
      "patch_panel_config_mismatch",
      "active patch panel status must name the selected wiringConfigId",
      "$.topology.services.patchPanel.wiringConfigId"
    );
    return;
  }

  const selectedConfig = wiringConfigs.find((config) => config.wiringConfigId === patchPanel.wiringConfigId);
  if (!selectedConfig) {
    addViolation(
      violations,
      "patch_panel_config_mismatch",
      `patch panel references unknown wiring config ${patchPanel.wiringConfigId}`,
      "$.topology.services.patchPanel.wiringConfigId"
    );
    return;
  }

  const expectedConnections = selectedConfig.connections.map(connectionToPatchPanelConnection).map(getActiveConnectionKey);
  const actualConnections = asArray(patchPanel.activeConnections).map(getActiveConnectionKey);
  if (!sameStringSet(expectedConnections, actualConnections)) {
    addViolation(
      violations,
      "patch_panel_config_mismatch",
      "patch panel activeConnections must match the selected wiring config",
      "$.topology.services.patchPanel.activeConnections"
    );
  }
}

function detectDirectedCycle(connections) {
  const graph = new Map();

  for (const connection of connections) {
    const from = connection.from?.resourceId;
    const to = connection.to?.resourceId;
    if (!from || !to) {
      continue;
    }
    if (!graph.has(from)) {
      graph.set(from, new Set());
    }
    if (!graph.has(to)) {
      graph.set(to, new Set());
    }
    graph.get(from).add(to);
  }

  const visiting = new Set();
  const visited = new Set();

  function visit(node) {
    if (visiting.has(node)) {
      return true;
    }
    if (visited.has(node)) {
      return false;
    }

    visiting.add(node);
    for (const next of graph.get(node) ?? []) {
      if (visit(next)) {
        return true;
      }
    }
    visiting.delete(node);
    visited.add(node);
    return false;
  }

  return [...graph.keys()].some((node) => visit(node));
}

function validateCycles(wiringConfigs, violations) {
  wiringConfigs.forEach((config, index) => {
    if (config.status !== ACTIVE) {
      return;
    }
    if (detectDirectedCycle(config.connections ?? [])) {
      addViolation(
        violations,
        "wiring_cycle",
        `active wiring config ${config.wiringConfigId} contains a directed resource cycle`,
        `$.topology.wiringConfigs[${index}].connections`
      );
    }
  });
}

function getResourceGroupId(config) {
  if (typeof config.constraints?.resourceGroupId === "string" && config.constraints.resourceGroupId.length > 0) {
    return config.constraints.resourceGroupId;
  }

  const resourceIds = [];
  for (const connection of config.connections ?? []) {
    if (connection.from?.resourceId) {
      resourceIds.push(connection.from.resourceId);
    }
    if (connection.to?.resourceId) {
      resourceIds.push(connection.to.resourceId);
    }
  }
  return uniqueSorted(resourceIds).join("+");
}

function validateOverrideForActiveGroup(override, groupId, activeConfigs, patchPanel) {
  if (!isObject(override)) {
    return "patch panel override must be an object";
  }

  const activeIds = activeConfigs.map((config) => config.wiringConfigId);
  const overrideIds = asArray(override.activeWiringConfigIds);
  if (override.resourceGroupId !== groupId) {
    return `override resourceGroupId must be ${groupId}`;
  }
  if (!sameStringSet(activeIds, overrideIds)) {
    return "override activeWiringConfigIds must exactly list the active configs in the resource group";
  }
  if (!activeIds.includes(override.selectedWiringConfigId)) {
    return "override selectedWiringConfigId must be one of the active configs";
  }
  if (override.selectedWiringConfigId !== patchPanel.wiringConfigId) {
    return "override selectedWiringConfigId must match patch panel wiringConfigId";
  }
  if (typeof override.reason !== "string" || override.reason.trim().length === 0) {
    return "override reason is required";
  }
  if (patchPanel.serviceId !== PATCH_PANEL_SERVICE_ID || patchPanel.state !== ACTIVE) {
    return "override must be reported by an active hwlab-patch-panel status object";
  }
  if (patchPanel.metadata?.propagation !== PATCH_PANEL_ONLY) {
    return "override must preserve patch-panel-only propagation";
  }

  return null;
}

function validateActiveConfigExclusivity(wiringConfigs, patchPanel, violations) {
  const groups = new Map();
  wiringConfigs.forEach((config) => {
    if (config.status !== ACTIVE) {
      return;
    }

    const groupId = getResourceGroupId(config);
    const key = `${config.projectId}:${groupId}`;
    if (!groups.has(key)) {
      groups.set(key, { groupId, configs: [] });
    }
    groups.get(key).configs.push(config);
  });

  for (const { groupId, configs } of groups.values()) {
    if (configs.length <= 1) {
      continue;
    }

    const override = patchPanel.metadata?.activeConfigOverride;
    if (!override) {
      addViolation(
        violations,
        "active_config_exclusivity",
        `resource group ${groupId} has ${configs.length} active wiring configs without a patch-panel override`,
        "$.topology.wiringConfigs"
      );
      continue;
    }

    const overrideError = validateOverrideForActiveGroup(override, groupId, configs, patchPanel);
    if (overrideError) {
      addViolation(
        violations,
        "patch_panel_override_invalid",
        overrideError,
        "$.topology.services.patchPanel.metadata.activeConfigOverride"
      );
    }
  }
}

function validatePatchPanelOnly(topology, violations) {
  if (topology.environment !== "dev") {
    addViolation(violations, "topology_shape", "topology validator fixtures must be DEV topologies", "$.topology.environment");
  }
  if (topology.constraints?.patchPanelIsOnlyCrossDeviceChannel !== true) {
    addViolation(
      violations,
      "patch_panel_only",
      "topology must declare patchPanelIsOnlyCrossDeviceChannel",
      "$.topology.constraints.patchPanelIsOnlyCrossDeviceChannel"
    );
  }
  if (topology.constraints?.boxSimuLocalLoopbackForCrossDeviceSync !== false) {
    addViolation(
      violations,
      "patch_panel_only",
      "topology must reject box simulator local loopback as cross-device sync",
      "$.topology.constraints.boxSimuLocalLoopbackForCrossDeviceSync"
    );
  }
}

function validateTopology(topology) {
  const violations = [];
  if (!isObject(topology)) {
    addViolation(violations, "topology_shape", "fixture topology must be an object");
    return violations;
  }

  validatePatchPanelOnly(topology, violations);
  const resources = indexBoxes(topology, violations);
  const sessions = validateGateways(topology, resources, violations);
  const patchPanel = validatePatchPanel(topology, sessions, violations);
  const wiringConfigs = getWiringConfigs(topology, violations);

  wiringConfigs.forEach((config, index) => {
    validateWiringConfig(config, index, topology, resources, sessions, violations);
  });

  validatePatchPanelMatchesSelectedConfig(patchPanel, wiringConfigs, violations);
  validateCycles(wiringConfigs, violations);
  validateActiveConfigExclusivity(wiringConfigs, patchPanel, violations);

  return violations;
}

function compareValidation(caseId, expected, violations) {
  const actualCodes = uniqueSorted(violations.map((violation) => violation.code));
  const expectedCodes = uniqueSorted(expected.violationCodes ?? []);

  if (expected.valid === true && actualCodes.length === 0) {
    return [];
  }
  if (expected.valid === false && sameStringSet(actualCodes, expectedCodes)) {
    return [];
  }

  const details = violations
    .map((violation) => `  - ${violation.code} at ${violation.path}: ${violation.message}`)
    .join("\n");
  const expectedSummary = expected.valid
    ? "valid"
    : `invalid with [${expectedCodes.join(", ")}]`;
  const actualSummary = actualCodes.length === 0 ? "valid" : `invalid with [${actualCodes.join(", ")}]`;

  return [
    `${caseId}: expected ${expectedSummary}, got ${actualSummary}`,
    details || "  - no violations"
  ];
}

async function loadFixture(filePath) {
  const raw = await readFile(filePath, "utf8");
  return JSON.parse(raw);
}

const files = (
  await Promise.all(getFixtureTargets().map((target) => collectJsonFiles(target)))
).flat();

if (files.length === 0) {
  console.error("No topology constraint fixtures found");
  process.exit(1);
}

const failures = [];
for (const filePath of files) {
  const fixture = await loadFixture(filePath);
  const caseId = getCaseId(filePath, fixture);
  const expected = getExpectedValidation(fixture);
  const violations = validateTopology(getTopology(fixture));
  const comparisonFailures = compareValidation(caseId, expected, violations);
  if (comparisonFailures.length > 0) {
    failures.push(...comparisonFailures);
    continue;
  }

  const label = expected.valid ? "valid" : `expected ${uniqueSorted(expected.violationCodes ?? []).join(", ")}`;
  console.log(`ok ${caseId}: ${label}`);
}

if (failures.length > 0) {
  console.error(failures.join("\n"));
  process.exit(1);
}

console.log(`Topology constraint validation passed (${files.length} fixtures)`);
