import type { TojuPluginManifest } from '../../../../shared-kernel';
import type {
  PluginLoadBlocker,
  PluginLoadCandidate,
  PluginLoadOrderResult
} from '../models/plugin-runtime.models';

const PRIORITY_WEIGHT: Record<string, number> = {
  bootstrap: 0,
  high: 1,
  default: 2,
  low: 3
};

interface PluginLoadGraph {
  edges: Map<string, Set<string>>;
  inboundCounts: Map<string, number>;
}

function priorityWeight(manifest: TojuPluginManifest): number {
  return PRIORITY_WEIGHT[manifest.load?.priority ?? 'default'] ?? PRIORITY_WEIGHT['default'];
}

function sortManifests(firstManifest: TojuPluginManifest, secondManifest: TojuPluginManifest): number {
  const firstPriority = priorityWeight(firstManifest);
  const secondPriority = priorityWeight(secondManifest);

  if (firstPriority !== secondPriority) {
    return firstPriority - secondPriority;
  }

  return firstManifest.id.localeCompare(secondManifest.id);
}

function addEdge(edges: Map<string, Set<string>>, fromPluginId: string, toPluginId: string): void {
  const targets = edges.get(fromPluginId) ?? new Set<string>();

  targets.add(toPluginId);
  edges.set(fromPluginId, targets);
}

function addBlocker(blocked: PluginLoadBlocker[], pluginId: string, reason: PluginLoadBlocker['reason'], message: string): void {
  blocked.push({ pluginId, reason, message });
}

function collectManifests(
  candidates: readonly PluginLoadCandidate[],
  blocked: PluginLoadBlocker[]
): Map<string, TojuPluginManifest> {
  const manifestsById = new Map<string, TojuPluginManifest>();

  for (const candidate of candidates) {
    if (candidate.enabled === false) {
      addBlocker(blocked, candidate.manifest.id, 'disabled', 'Plugin is disabled');
      continue;
    }

    if (manifestsById.has(candidate.manifest.id)) {
      addBlocker(blocked, candidate.manifest.id, 'duplicate', 'Duplicate plugin id');
      continue;
    }

    manifestsById.set(candidate.manifest.id, candidate.manifest);
  }

  return manifestsById;
}

function createLoadGraph(manifestsById: Map<string, TojuPluginManifest>): PluginLoadGraph {
  const graph: PluginLoadGraph = {
    edges: new Map<string, Set<string>>(),
    inboundCounts: new Map<string, number>()
  };

  for (const pluginId of manifestsById.keys()) {
    graph.edges.set(pluginId, new Set<string>());
    graph.inboundCounts.set(pluginId, 0);
  }

  return graph;
}

function addRequiredEdges(
  manifest: TojuPluginManifest,
  manifestsById: Map<string, TojuPluginManifest>,
  edges: Map<string, Set<string>>,
  blocked: PluginLoadBlocker[]
): void {
  for (const required of manifest.relationships?.requires ?? []) {
    if (!manifestsById.has(required.id)) {
      addBlocker(blocked, manifest.id, 'missingDependency', `Missing required plugin ${required.id}`);
      continue;
    }

    addEdge(edges, required.id, manifest.id);
  }
}

function addOrderingEdges(
  manifest: TojuPluginManifest,
  manifestsById: Map<string, TojuPluginManifest>,
  edges: Map<string, Set<string>>
): void {
  for (const afterPluginId of manifest.relationships?.after ?? []) {
    if (manifestsById.has(afterPluginId)) {
      addEdge(edges, afterPluginId, manifest.id);
    }
  }

  for (const beforePluginId of manifest.relationships?.before ?? []) {
    if (manifestsById.has(beforePluginId)) {
      addEdge(edges, manifest.id, beforePluginId);
    }
  }
}

function addConflictBlockers(
  manifest: TojuPluginManifest,
  manifestsById: Map<string, TojuPluginManifest>,
  blocked: PluginLoadBlocker[]
): void {
  for (const conflictPluginId of manifest.relationships?.conflicts ?? []) {
    if (manifestsById.has(conflictPluginId)) {
      addBlocker(blocked, manifest.id, 'conflict', `Conflicts with plugin ${conflictPluginId}`);
    }
  }
}

function applyRelationships(
  manifestsById: Map<string, TojuPluginManifest>,
  edges: Map<string, Set<string>>,
  blocked: PluginLoadBlocker[]
): void {
  for (const manifest of manifestsById.values()) {
    addRequiredEdges(manifest, manifestsById, edges, blocked);
    addOrderingEdges(manifest, manifestsById, edges);
    addConflictBlockers(manifest, manifestsById, blocked);
  }
}

function countInboundEdges(graph: PluginLoadGraph, blockedIds: Set<string>): void {
  for (const [fromPluginId, targets] of graph.edges.entries()) {
    if (blockedIds.has(fromPluginId)) {
      continue;
    }

    for (const targetPluginId of targets) {
      if (!blockedIds.has(targetPluginId)) {
        graph.inboundCounts.set(targetPluginId, (graph.inboundCounts.get(targetPluginId) ?? 0) + 1);
      }
    }
  }
}

function getInitialReadyManifests(
  manifestsById: Map<string, TojuPluginManifest>,
  inboundCounts: Map<string, number>,
  blockedIds: Set<string>
): TojuPluginManifest[] {
  return Array.from(manifestsById.values())
    .filter((manifest) => !blockedIds.has(manifest.id) && (inboundCounts.get(manifest.id) ?? 0) === 0)
    .sort(sortManifests);
}

function pushReadyManifest(
  ready: TojuPluginManifest[],
  manifestsById: Map<string, TojuPluginManifest>,
  pluginId: string
): void {
  const targetManifest = manifestsById.get(pluginId);

  if (targetManifest) {
    ready.push(targetManifest);
    ready.sort(sortManifests);
  }
}

function consumeReadyManifest(
  manifest: TojuPluginManifest,
  graph: PluginLoadGraph,
  manifestsById: Map<string, TojuPluginManifest>,
  ready: TojuPluginManifest[],
  blockedIds: Set<string>
): void {
  for (const targetPluginId of graph.edges.get(manifest.id) ?? []) {
    if (blockedIds.has(targetPluginId)) {
      continue;
    }

    const nextInboundCount = Math.max(0, (graph.inboundCounts.get(targetPluginId) ?? 0) - 1);

    graph.inboundCounts.set(targetPluginId, nextInboundCount);

    if (nextInboundCount === 0) {
      pushReadyManifest(ready, manifestsById, targetPluginId);
    }
  }
}

function buildOrderedManifests(
  graph: PluginLoadGraph,
  manifestsById: Map<string, TojuPluginManifest>,
  blockedIds: Set<string>
): TojuPluginManifest[] {
  const ready = getInitialReadyManifests(manifestsById, graph.inboundCounts, blockedIds);
  const ordered: TojuPluginManifest[] = [];

  while (ready.length > 0) {
    const nextManifest = ready.shift();

    if (!nextManifest) {
      break;
    }

    ordered.push(nextManifest);
    consumeReadyManifest(nextManifest, graph, manifestsById, ready, blockedIds);
  }

  return ordered;
}

function addCycleBlockers(
  manifestsById: Map<string, TojuPluginManifest>,
  ordered: TojuPluginManifest[],
  blockedIds: Set<string>,
  blocked: PluginLoadBlocker[]
): void {
  const orderedIds = new Set(ordered.map((manifest) => manifest.id));

  for (const manifest of manifestsById.values()) {
    if (!blockedIds.has(manifest.id) && !orderedIds.has(manifest.id)) {
      addBlocker(blocked, manifest.id, 'cycle', 'Plugin load order contains a cycle');
    }
  }
}

export function resolvePluginLoadOrder(candidates: readonly PluginLoadCandidate[]): PluginLoadOrderResult {
  const blocked: PluginLoadBlocker[] = [];
  const manifestsById = collectManifests(candidates, blocked);
  const graph = createLoadGraph(manifestsById);

  applyRelationships(manifestsById, graph.edges, blocked);
  const blockedIds = new Set(blocked.map((blocker) => blocker.pluginId));

  countInboundEdges(graph, blockedIds);
  const ordered = buildOrderedManifests(graph, manifestsById, blockedIds);

  addCycleBlockers(manifestsById, ordered, blockedIds, blocked);

  return { blocked, ordered };
}
