import { createHash } from "node:crypto";
import { diagnose, parse, ParseError } from "@wrnexus/syntax";
import { generate } from "./codegen.ts";
import type { CompileResult } from "./index.ts";

function compileSource(source: string, filePath: string): CompileResult {
  const richDiagnostics = diagnose(source, { file: filePath, accessibility: true });
  const errors = richDiagnostics.filter((diagnostic) => diagnostic.severity === "error");
  if (errors.length > 0) {
    throw new ParseError(
      errors.map((diagnostic) => diagnostic.message).join("\n"),
      errors[0]!.code,
    );
  }

  const ast = parse(source);
  return {
    code: `// compiled from .wrn\n${generate(ast)}`,
    ast,
    diagnostics: richDiagnostics.map((diagnostic) => `${diagnostic.code}: ${diagnostic.message}`),
    richDiagnostics,
  };
}

export interface CompilationCacheEntry extends CompileResult {
  key: string;
  file: string;
  sourceHash: string;
  createdAt: number;
}

export interface CompilationCacheOptions {
  maxEntries?: number;
  now?: () => number;
}

export interface CompilationCache {
  compile(source: string, file?: string, salt?: string): CompilationCacheEntry;
  get(key: string): CompilationCacheEntry | undefined;
  invalidate(file?: string): number;
  clear(): void;
  size(): number;
  stats(): { hits: number; misses: number; entries: number };
}

export function compilationKey(source: string, file = "<inline .wrn>", salt = ""): string {
  return createHash("sha256")
    .update(file)
    .update("\0")
    .update(salt)
    .update("\0")
    .update(source)
    .digest("hex");
}

export function createCompilationCache(options: CompilationCacheOptions = {}): CompilationCache {
  const maxEntries = options.maxEntries ?? 500;
  if (!Number.isInteger(maxEntries) || maxEntries < 1)
    throw new RangeError("maxEntries must be positive");
  const now = options.now ?? Date.now;
  const entries = new Map<string, CompilationCacheEntry>();
  let hits = 0;
  let misses = 0;

  function touch(key: string, value: CompilationCacheEntry): void {
    entries.delete(key);
    entries.set(key, value);
    while (entries.size > maxEntries) entries.delete(entries.keys().next().value!);
  }

  return {
    compile(source, file = "<inline .wrn>", salt = "") {
      const key = compilationKey(source, file, salt);
      const existing = entries.get(key);
      if (existing) {
        hits++;
        touch(key, existing);
        return existing;
      }
      misses++;
      const result = compileSource(source, file);
      const entry: CompilationCacheEntry = {
        ...result,
        key,
        file,
        sourceHash: createHash("sha256").update(source).digest("hex"),
        createdAt: now(),
      };
      touch(key, entry);
      return entry;
    },
    get(key) {
      const entry = entries.get(key);
      if (entry) touch(key, entry);
      return entry;
    },
    invalidate(file) {
      let removed = 0;
      for (const [key, entry] of entries) {
        if (!file || entry.file === file) {
          entries.delete(key);
          removed++;
        }
      }
      return removed;
    },
    clear() {
      entries.clear();
    },
    size: () => entries.size,
    stats: () => ({ hits, misses, entries: entries.size }),
  };
}

export class DependencyGraph {
  readonly #dependencies = new Map<string, Set<string>>();
  readonly #dependents = new Map<string, Set<string>>();

  set(file: string, dependencies: Iterable<string>): void {
    this.remove(file);
    const values = new Set(dependencies);
    this.#dependencies.set(file, values);
    for (const dependency of values) {
      const set = this.#dependents.get(dependency) ?? new Set<string>();
      set.add(file);
      this.#dependents.set(dependency, set);
    }
  }

  remove(file: string): void {
    for (const dependency of this.#dependencies.get(file) ?? []) {
      const set = this.#dependents.get(dependency);
      set?.delete(file);
      if (set?.size === 0) this.#dependents.delete(dependency);
    }
    this.#dependencies.delete(file);
  }

  dependencies(file: string): string[] {
    return [...(this.#dependencies.get(file) ?? [])].sort();
  }
  dependents(file: string): string[] {
    return [...(this.#dependents.get(file) ?? [])].sort();
  }

  affected(file: string): string[] {
    const found = new Set<string>();
    const queue = [file];
    while (queue.length) {
      const current = queue.shift()!;
      for (const dependent of this.#dependents.get(current) ?? []) {
        if (found.has(dependent)) continue;
        found.add(dependent);
        queue.push(dependent);
      }
    }
    return [...found].sort();
  }
}
