// Level-document helpers: validation, migration entry point, queries.
import type { DocSpec, LevelDoc, LevelEntity } from './types';

export const cellKey = (x: number, y: number): string => `${x},${y}`;

export const cloneDoc = (doc: LevelDoc): LevelDoc => JSON.parse(JSON.stringify(doc)) as LevelDoc;

export const findEntities = (doc: LevelDoc, type: string): LevelEntity[] =>
  doc.entities.filter((e) => e.type === type);

export const entitiesAt = (doc: LevelDoc, x: number, y: number): LevelEntity[] =>
  doc.entities.filter((e) => e.x === x && e.y === y);

const isPlainObject = (v: unknown): v is Record<string, unknown> =>
  !!v && typeof v === 'object' && !Array.isArray(v);

/**
 * Parse + validate level JSON (string or already-parsed object) against a spec.
 * Non-v2 input is routed through spec.migrate (the v1 backward-compat path).
 * Throws with a human-readable message on bad data.
 */
export function parseDoc(raw: unknown, spec: DocSpec): LevelDoc {
  let data: unknown = typeof raw === 'string' ? JSON.parse(raw) : raw;
  if (!isPlainObject(data)) throw new Error('level: expected a JSON object');

  if ((data as Record<string, unknown>).version !== 2) {
    const migrated = spec.migrate?.(data);
    if (!migrated) throw new Error('level: unrecognized format (not v2, no migration matched)');
    data = migrated;
  }
  const o = data as Record<string, unknown>;
  if (o.gameType !== spec.gameType) {
    throw new Error(`level: gameType "${String(o.gameType)}" is not a ${spec.gameType} level`);
  }

  const sz = o.size;
  if (
    !Array.isArray(sz) || sz.length !== 2 ||
    typeof sz[0] !== 'number' || typeof sz[1] !== 'number' ||
    !Number.isFinite(sz[0]) || !Number.isFinite(sz[1])
  ) {
    throw new Error('level: bad "size"');
  }
  const [minW, minH] = spec.minSize ?? [1, 1];
  const w = Math.floor(sz[0]);
  const h = Math.floor(sz[1]);
  if (w < minW || h < minH || w > spec.maxSize[0] || h > spec.maxSize[1]) {
    throw new Error(`level: size must be between ${minW}x${minH} and ${spec.maxSize[0]}x${spec.maxSize[1]}`);
  }

  const known = new Map(spec.entities.map((d) => [d.type, d]));
  if (o.entities !== undefined && !Array.isArray(o.entities)) {
    throw new Error('level: "entities" must be an array');
  }
  const seen = new Set<string>();
  const singlesSeen = new Set<string>();
  const entities: LevelEntity[] = [];
  for (const item of (o.entities ?? []) as unknown[]) {
    if (!isPlainObject(item)) throw new Error('level: bad entity (expected an object)');
    const def = typeof item.type === 'string' ? known.get(item.type) : undefined;
    if (!def) continue; // unknown types are dropped, not fatal — forward compat
    if (typeof item.x !== 'number' || typeof item.y !== 'number' ||
        !Number.isFinite(item.x) || !Number.isFinite(item.y)) {
      throw new Error(`level: bad cell on a "${def.type}" entity`);
    }
    const x = Math.floor(item.x);
    const y = Math.floor(item.y);
    if (x < 0 || y < 0 || x >= w || y >= h) continue; // out of bounds: drop
    const k = `${def.type}:${cellKey(x, y)}`;
    if (seen.has(k)) continue; // dedupe same type on same cell
    if (!def.multiple) {
      if (singlesSeen.has(def.type)) continue; // singles: keep first
      singlesSeen.add(def.type);
    }
    seen.add(k);
    const ent: LevelEntity = { type: def.type, x, y };
    if (isPlainObject(item.props)) ent.props = JSON.parse(JSON.stringify(item.props)) as Record<string, unknown>;
    entities.push(ent);
  }

  const doc: LevelDoc = { version: 2, gameType: spec.gameType, size: [w, h], entities };
  if (isPlainObject(o.meta)) doc.meta = JSON.parse(JSON.stringify(o.meta)) as Record<string, unknown>;
  return doc;
}
