export type RelationKind =
  | "same-semantic"
  | "derived-value"
  | "completion-obligation"
  | "ownership-lifecycle"
  | "protocol-event"
  | "runtime-reference"
  | "perturbation-witness";

export type RelationNode = {
  id: string;
  representation: {
    source: string;
    kind: string;
    value: unknown;
    provenance: "observed" | "inferred" | "declared";
  };
};

export type Normalization = {
  name: string;
  apply(v: unknown): unknown;
};

export type Relation = {
  kind: RelationKind;
  nodes: string[];
  normalization?: Normalization;
  declaredBy: string;
};

export type RelationGraph = {
  nodes: Readonly<Record<string, RelationNode>>;
  relations: Readonly<Record<string, Relation>>;
};

export type RelationVerdict =
  | { status: "agrees" }
  | {
      status: "contradiction";
      facts: { nodeId: string; normalizedValue: unknown }[];
    }
  | { status: "unevaluable"; reason: string };

type NormalizedNode = {
  nodeId: string;
  normalizedValue: unknown;
};

function valuesEqual(left: unknown, right: unknown): boolean {
  return JSON.stringify(left) === JSON.stringify(right);
}

function normalizeNodeValue(
  node: RelationNode,
  normalization: Normalization | undefined,
): NormalizedNode {
  const rawValue = node.representation.value;
  const normalizedValue =
    normalization === undefined ? rawValue : normalization.apply(rawValue);
  return { nodeId: node.id, normalizedValue };
}

function collectNormalizedNodes(
  graph: RelationGraph,
  relation: Relation,
): NormalizedNode[] | RelationVerdict {
  const normalized: NormalizedNode[] = [];

  for (const nodeId of relation.nodes) {
    const node = graph.nodes[nodeId];
    if (node === undefined) {
      return {
        status: "unevaluable",
        reason: `relation node not found: ${nodeId}`,
      };
    }

    try {
      normalized.push(normalizeNodeValue(node, relation.normalization));
    } catch (error) {
      const message = error instanceof Error ? error.message : String(error);
      return {
        status: "unevaluable",
        reason: `normalization failed for node ${nodeId}: ${message}`,
      };
    }
  }

  return normalized;
}

function evaluateSameSemantic(normalized: NormalizedNode[]): RelationVerdict {
  if (normalized.length === 0) {
    return { status: "unevaluable", reason: "relation has no nodes" };
  }

  const [first, ...rest] = normalized;
  if (first === undefined) {
    return { status: "unevaluable", reason: "relation has no nodes" };
  }

  const disagrees = rest.some(
    (entry) => !valuesEqual(entry.normalizedValue, first.normalizedValue),
  );

  if (!disagrees) {
    return { status: "agrees" };
  }

  return {
    status: "contradiction",
    facts: normalized.map((entry) => ({
      nodeId: entry.nodeId,
      normalizedValue: entry.normalizedValue,
    })),
  };
}

function toFiniteNumber(value: unknown, nodeId: string): number | RelationVerdict {
  if (typeof value !== "number" || !Number.isFinite(value)) {
    return {
      status: "unevaluable",
      reason: `derived-value node ${nodeId} is not a finite number`,
    };
  }
  return value;
}

function evaluateDerivedClosure(
  relation: Relation,
  normalized: NormalizedNode[],
): RelationVerdict {
  const numericValues = new Map<string, number>();

  for (const entry of normalized) {
    const numeric = toFiniteNumber(entry.normalizedValue, entry.nodeId);
    if (typeof numeric !== "number") {
      return numeric;
    }
    numericValues.set(entry.nodeId, numeric);
  }

  if (relation.declaredBy === "used+remaining=capacity") {
    const used = numericValues.get("used");
    const remaining = numericValues.get("remaining");
    const capacity = numericValues.get("capacity");

    if (used === undefined || remaining === undefined || capacity === undefined) {
      return {
        status: "unevaluable",
        reason:
          "derived-value relation used+remaining=capacity requires used, remaining, and capacity nodes",
      };
    }

    if (used + remaining === capacity) {
      return { status: "agrees" };
    }

    return {
      status: "contradiction",
      facts: normalized.map((entry) => ({
        nodeId: entry.nodeId,
        normalizedValue: entry.normalizedValue,
      })),
    };
  }

  return {
    status: "unevaluable",
    reason: `unsupported derived-value closure: ${relation.declaredBy}`,
  };
}

export function evaluateRelation(
  graph: RelationGraph,
  relationId: string,
): RelationVerdict {
  const relation = graph.relations[relationId];
  if (relation === undefined) {
    return {
      status: "unevaluable",
      reason: `relation not found: ${relationId}`,
    };
  }

  const normalized = collectNormalizedNodes(graph, relation);
  if ("status" in normalized) {
    return normalized;
  }

  switch (relation.kind) {
    case "same-semantic":
      return evaluateSameSemantic(normalized);
    case "derived-value":
      return evaluateDerivedClosure(relation, normalized);
    case "completion-obligation":
    case "ownership-lifecycle":
    case "protocol-event":
    case "runtime-reference":
    case "perturbation-witness":
      return {
        status: "unevaluable",
        reason: `relation kind not evaluable in graph core: ${relation.kind}`,
      };
  }
}
