import type {
  Condition,
  EffectPatch,
  PreparedWorldDefinition,
  ValueDefinition,
  WorldDefinition,
} from './types';
import { clone } from './utils';

function assert(condition: unknown, message: string): asserts condition {
  if (!condition) {
    throw new Error(`Invalid world definition: ${message}`);
  }
}

function assertUniqueIds(scope: string, entries: Array<{ id: string }>): void {
  const seen = new Set<string>();

  for (const entry of entries) {
    assert(entry.id.trim().length > 0, `${scope} id is required`);
    assert(!seen.has(entry.id), `duplicate ${scope} id "${entry.id}"`);
    seen.add(entry.id);
  }
}

function validateValueDefinitions(scope: string, definitions: ValueDefinition[]): void {
  assertUniqueIds(scope, definitions);

  for (const definition of definitions) {
    assert(definition.label.trim().length > 0, `${scope} "${definition.id}" label is required`);

    if (definition.min !== undefined && definition.max !== undefined) {
      assert(
        definition.min <= definition.max,
        `${scope} "${definition.id}" has min greater than max`,
      );
    }

    if (definition.initial !== undefined && definition.min !== undefined) {
      assert(
        definition.initial >= definition.min,
        `${scope} "${definition.id}" initial is below min`,
      );
    }

    if (definition.initial !== undefined && definition.max !== undefined) {
      assert(
        definition.initial <= definition.max,
        `${scope} "${definition.id}" initial is above max`,
      );
    }
  }
}

function assertKnownKeys(
  scope: string,
  keys: string[],
  knownKeys: Set<string>,
  keyType: string,
): void {
  for (const key of keys) {
    assert(knownKeys.has(key), `${scope} references unknown ${keyType} "${key}"`);
  }
}

function validateEffects(
  effects: EffectPatch | undefined,
  references: {
    stats: Set<string>;
    resources: Set<string>;
    inventory: Set<string>;
  },
  scope: string,
): void {
  if (!effects) {
    return;
  }

  assertKnownKeys(`${scope}.stats`, Object.keys(effects.stats ?? {}), references.stats, 'stat');
  assertKnownKeys(
    `${scope}.resources`,
    Object.keys(effects.resources ?? {}),
    references.resources,
    'resource',
  );
  assertKnownKeys(
    `${scope}.inventory`,
    Object.keys(effects.inventory ?? {}),
    references.inventory,
    'inventory value',
  );
}

function validateCondition(
  condition: Condition | undefined,
  references: {
    phases: Set<string>;
    actions: Set<string>;
    stats: Set<string>;
    resources: Set<string>;
    inventory: Set<string>;
  },
  scope: string,
): void {
  if (!condition) {
    return;
  }

  if (condition.phaseIn) {
    assertKnownKeys(`${scope}.phaseIn`, condition.phaseIn, references.phases, 'phase');
  }

  if (condition.actionIn) {
    assertKnownKeys(`${scope}.actionIn`, condition.actionIn, references.actions, 'action');
  }

  if (condition.stat) {
    assert(references.stats.has(condition.stat), `${scope} references unknown stat "${condition.stat}"`);
  }

  if (condition.resource) {
    assert(
      references.resources.has(condition.resource),
      `${scope} references unknown resource "${condition.resource}"`,
    );
  }

  if (condition.inventory) {
    assert(
      references.inventory.has(condition.inventory),
      `${scope} references unknown inventory value "${condition.inventory}"`,
    );
  }

  for (const [index, entry] of (condition.all ?? []).entries()) {
    validateCondition(entry, references, `${scope}.all[${index}]`);
  }

  for (const [index, entry] of (condition.any ?? []).entries()) {
    validateCondition(entry, references, `${scope}.any[${index}]`);
  }

  if (condition.not) {
    validateCondition(condition.not, references, `${scope}.not`);
  }
}

export function validateWorldDefinition(world: WorldDefinition): void {
  assert(world.id.trim().length > 0, 'world id is required');
  assert(world.name.trim().length > 0, 'world name is required');
  assert(world.description.trim().length > 0, 'world description is required');
  assert(world.phases.length > 0, 'at least one phase is required');
  assert(world.actions.length > 0, 'at least one action is required');

  validateValueDefinitions('stat', world.stats);
  validateValueDefinitions('resource', world.resources ?? []);
  validateValueDefinitions('inventory', world.inventory ?? []);
  assertUniqueIds('phase', world.phases);
  assertUniqueIds('action', world.actions);
  assertUniqueIds('event', world.events);

  if (world.calendar?.daysPerAge !== undefined) {
    assert(
      Number.isInteger(world.calendar.daysPerAge) && world.calendar.daysPerAge > 0,
      'calendar.daysPerAge must be a positive integer',
    );
  }

  if (world.calendar?.maxDays !== undefined) {
    assert(
      Number.isInteger(world.calendar.maxDays) && world.calendar.maxDays > 0,
      'calendar.maxDays must be a positive integer',
    );
  }

  const references = {
    phases: new Set(world.phases.map((phase) => phase.id)),
    actions: new Set(world.actions.map((action) => action.id)),
    stats: new Set(world.stats.map((stat) => stat.id)),
    resources: new Set((world.resources ?? []).map((resource) => resource.id)),
    inventory: new Set((world.inventory ?? []).map((item) => item.id)),
  };

  if (world.start.phaseId) {
    assert(
      references.phases.has(world.start.phaseId),
      `start phase "${world.start.phaseId}" does not exist`,
    );
  }

  assertKnownKeys(
    'world.start.stats',
    Object.keys(world.start.stats ?? {}),
    references.stats,
    'stat',
  );
  assertKnownKeys(
    'world.start.resources',
    Object.keys(world.start.resources ?? {}),
    references.resources,
    'resource',
  );
  assertKnownKeys(
    'world.start.inventory',
    Object.keys(world.start.inventory ?? {}),
    references.inventory,
    'inventory value',
  );

  for (const action of world.actions) {
    assert(action.name.trim().length > 0, `action "${action.id}" name is required`);
    assert(action.description.trim().length > 0, `action "${action.id}" description is required`);

    if (action.advancePhases !== undefined) {
      assert(
        Number.isInteger(action.advancePhases) && action.advancePhases > 0,
        `action "${action.id}" advancePhases must be a positive integer`,
      );
    }

    validateCondition(action.availability, references, `action "${action.id}" availability`);
    validateEffects(action.effects, references, `action "${action.id}" effects`);

    for (const [phaseId, modifier] of Object.entries(action.timing ?? {})) {
      assert(
        references.phases.has(phaseId),
        `action "${action.id}" timing references unknown phase "${phaseId}"`,
      );
      assert(
        modifier.label.trim().length > 0,
        `action "${action.id}" timing "${phaseId}" label is required`,
      );
      validateCondition(modifier.when, references, `action "${action.id}" timing "${phaseId}"`);
      validateEffects(modifier.effects, references, `action "${action.id}" timing "${phaseId}"`);
    }
  }

  for (const event of world.events) {
    assert(event.name.trim().length > 0, `event "${event.id}" name is required`);
    assert(event.description.trim().length > 0, `event "${event.id}" description is required`);

    if (event.weight !== undefined) {
      assert(event.weight > 0, `event "${event.id}" weight must be greater than 0`);
    }

    validateCondition(event.when, references, `event "${event.id}" condition`);
    validateEffects(event.effects, references, `event "${event.id}" effects`);
  }
}

export function prepareWorldDefinition(world: WorldDefinition): PreparedWorldDefinition {
  validateWorldDefinition(world);

  return {
    ...clone(world),
    resources: clone(world.resources ?? []),
    inventory: clone(world.inventory ?? []),
  };
}

export function createWorldRegistry<TWorldMap extends Record<string, WorldDefinition>>(
  definitions: TWorldMap,
): {
  byId: { [K in keyof TWorldMap]: PreparedWorldDefinition };
  list: PreparedWorldDefinition[];
  get<K extends keyof TWorldMap>(worldId: K): PreparedWorldDefinition;
} {
  const preparedEntries = Object.entries(definitions).map(([key, world]) => {
    const prepared = prepareWorldDefinition(world);

    assert(
      prepared.id === key,
      `registry key "${key}" must match world id "${prepared.id}"`,
    );

    return [key, prepared] as const;
  });

  const byId = Object.fromEntries(preparedEntries) as {
    [K in keyof TWorldMap]: PreparedWorldDefinition;
  };

  return {
    byId,
    list: preparedEntries.map(([, world]) => world),
    get(worldId) {
      return byId[worldId];
    },
  };
}
