/**
 * YAML World Model Parser
 * Loads and validates world definitions from YAML files
 */

import * as fs from 'fs/promises';
import * as yaml from 'js-yaml';
import { WorldModel } from '../interfaces/world-model';

export class YamlWorldParser {
  /**
   * Load a world model from a YAML file
   */
  public static async loadFromFile(filePath: string): Promise<WorldModel> {
    try {
      const fileContents = await fs.readFile(filePath, 'utf8');
      const worldData = yaml.load(fileContents) as unknown;
      
      return this.validateAndTransform(worldData);
    } catch (error) {
      console.error(`Error loading world from ${filePath}:`, error);
      throw new Error(`Failed to load world from ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
    }
  }

  /**
   * Validate the loaded YAML data and transform it into a WorldModel
   */
  private static validateAndTransform(data: unknown): WorldModel {
    if (!data || typeof data !== 'object') {
      throw new Error('Invalid world data: must be an object');
    }

    const worldData = data as Record<string, unknown>;
    
    // Validate required top-level fields
    this.validateRequiredFields(worldData, ['title', 'author', 'version', 'introduction', 'rooms', 'initialState']);
    
    // Transform and validate the world model
    const worldModel: WorldModel = {
      title: this.validateString(worldData.title, 'title'),
      author: this.validateString(worldData.author, 'author'),
      version: this.validateString(worldData.version, 'version'),
      introduction: this.validateString(worldData.introduction, 'introduction'),
      rooms: this.validateRooms(worldData.rooms),
      objects: this.validateObjects(worldData.objects),
      characters: this.validateCharacters(worldData.characters),
      actions: this.validateActions(worldData.actions),
      initialState: this.validateInitialState(worldData.initialState)
    };

    // Validate references between entities
    this.validateReferences(worldModel);

    return worldModel;
  }

  /**
   * Validate that an object has all required fields
   */
  private static validateRequiredFields(data: Record<string, unknown>, requiredFields: string[]): void {
    for (const field of requiredFields) {
      if (!(field in data)) {
        throw new Error(`Missing required field: ${field}`);
      }
    }
  }

  /**
   * Validate that a value is a string
   */
  private static validateString(value: unknown, fieldName: string): string {
    if (typeof value !== 'string') {
      throw new Error(`Field ${fieldName} must be a string`);
    }
    return value;
  }

  /**
   * Validate room definitions
   */
  private static validateRooms(rooms: unknown): WorldModel['rooms'] {
    if (!rooms || typeof rooms !== 'object') {
      throw new Error('Rooms must be an object mapping room IDs to room definitions');
    }

    const roomsData = rooms as Record<string, unknown>;
    const validatedRooms: WorldModel['rooms'] = {};

    for (const [roomId, roomData] of Object.entries(roomsData)) {
      if (!roomData || typeof roomData !== 'object') {
        throw new Error(`Room ${roomId} must be an object`);
      }

      const room = roomData as Record<string, unknown>;
      this.validateRequiredFields(room, ['name', 'description', 'exits']);

      validatedRooms[roomId] = {
        id: roomId,
        name: this.validateString(room.name, `rooms.${roomId}.name`),
        description: this.validateString(room.description, `rooms.${roomId}.description`),
        exits: this.validateExits(room.exits, roomId),
        objects: this.validateStringArray(room.objects || [], `rooms.${roomId}.objects`),
        characters: this.validateStringArray(room.characters || [], `rooms.${roomId}.characters`)
      };
    }

    return validatedRooms;
  }

  /**
   * Validate exit definitions
   */
  private static validateExits(exits: unknown, roomId: string): WorldModel['rooms'][string]['exits'] {
    if (!Array.isArray(exits)) {
      throw new Error(`Exits for room ${roomId} must be an array`);
    }

    return exits.map((exit, index) => {
      if (!exit || typeof exit !== 'object') {
        throw new Error(`Exit ${index} in room ${roomId} must be an object`);
      }

      const exitData = exit as Record<string, unknown>;
      this.validateRequiredFields(exitData, ['direction', 'targetRoomId']);

      return {
        direction: this.validateString(exitData.direction, `rooms.${roomId}.exits[${index}].direction`),
        targetRoomId: this.validateString(exitData.targetRoomId, `rooms.${roomId}.exits[${index}].targetRoomId`),
        description: exitData.description ? this.validateString(exitData.description, `rooms.${roomId}.exits[${index}].description`) : undefined,
        isLocked: typeof exitData.isLocked === 'boolean' ? exitData.isLocked : false,
        keyId: exitData.keyId ? this.validateString(exitData.keyId, `rooms.${roomId}.exits[${index}].keyId`) : undefined
      };
    });
  }

  /**
   * Validate object definitions
   */
  private static validateObjects(objects: unknown): WorldModel['objects'] {
    if (!objects) return {}; // Objects are optional
    
    if (typeof objects !== 'object') {
      throw new Error('Objects must be an object mapping object IDs to object definitions');
    }

    const objectsData = objects as Record<string, unknown>;
    const validatedObjects: WorldModel['objects'] = {};

    for (const [objectId, objectData] of Object.entries(objectsData)) {
      if (!objectData || typeof objectData !== 'object') {
        throw new Error(`Object ${objectId} must be an object`);
      }

      const obj = objectData as Record<string, unknown>;
      this.validateRequiredFields(obj, ['name', 'description', 'traits', 'allowedActions']);

      validatedObjects[objectId] = {
        id: objectId,
        name: this.validateString(obj.name, `objects.${objectId}.name`),
        description: this.validateString(obj.description, `objects.${objectId}.description`),
        traits: this.validateStringArray(obj.traits, `objects.${objectId}.traits`),
        states: this.validateObjectStates(obj.states, objectId),
        allowedActions: this.validateStringArray(obj.allowedActions, `objects.${objectId}.allowedActions`),
        containedObjects: obj.containedObjects ? this.validateStringArray(obj.containedObjects, `objects.${objectId}.containedObjects`) : []
      };
    }

    return validatedObjects;
  }

  /**
   * Validate character definitions
   */
  private static validateCharacters(characters: unknown): WorldModel['characters'] {
    if (!characters) return {}; // Characters are optional
    
    if (typeof characters !== 'object') {
      throw new Error('Characters must be an object mapping character IDs to character definitions');
    }

    const charactersData = characters as Record<string, unknown>;
    const validatedCharacters: WorldModel['characters'] = {};

    for (const [characterId, characterData] of Object.entries(charactersData)) {
      if (!characterData || typeof characterData !== 'object') {
        throw new Error(`Character ${characterId} must be an object`);
      }

      const character = characterData as Record<string, unknown>;
      this.validateRequiredFields(character, ['name', 'description', 'dialogue', 'defaultResponse']);

      validatedCharacters[characterId] = {
        id: characterId,
        name: this.validateString(character.name, `characters.${characterId}.name`),
        description: this.validateString(character.description, `characters.${characterId}.description`),
        dialogue: this.validateDialogue(character.dialogue, characterId),
        inventory: this.validateStringArray(character.inventory || [], `characters.${characterId}.inventory`),
        defaultResponse: this.validateString(character.defaultResponse, `characters.${characterId}.defaultResponse`),
        mood: character.mood ? this.validateString(character.mood, `characters.${characterId}.mood`) : undefined
      };
    }

    return validatedCharacters;
  }

  /**
   * Validate action definitions
   */
  private static validateActions(actions: unknown): WorldModel['actions'] {
    if (!actions) return {}; // Actions are optional
    
    if (typeof actions !== 'object') {
      throw new Error('Actions must be an object mapping action names to action definitions');
    }

    const actionsData = actions as Record<string, unknown>;
    const validatedActions: WorldModel['actions'] = {};

    for (const [actionName, actionData] of Object.entries(actionsData)) {
      if (!actionData || typeof actionData !== 'object') {
        throw new Error(`Action ${actionName} must be an object`);
      }

      const action = actionData as Record<string, unknown>;
      this.validateRequiredFields(action, ['patterns', 'handler']);

      validatedActions[actionName] = {
        name: actionName,
        patterns: this.validateStringArray(action.patterns, `actions.${actionName}.patterns`),
        requiresObject: typeof action.requiresObject === 'boolean' ? action.requiresObject : false,
        requiresTarget: typeof action.requiresTarget === 'boolean' ? action.requiresTarget : false,
        handler: this.validateString(action.handler, `actions.${actionName}.handler`)
      };
    }

    return validatedActions;
  }

  /**
   * Validate initial game state
   */
  private static validateInitialState(initialState: unknown): WorldModel['initialState'] {
    if (!initialState || typeof initialState !== 'object') {
      throw new Error('Initial state must be an object');
    }

    const stateData = initialState as Record<string, unknown>;
    this.validateRequiredFields(stateData, ['currentRoomId']);

    return {
      currentRoomId: this.validateString(stateData.currentRoomId, 'initialState.currentRoomId'),
      inventory: this.validateStringArray(stateData.inventory || [], 'initialState.inventory'),
      visitedRooms: this.validateStringArray(stateData.visitedRooms || [], 'initialState.visitedRooms'),
      flags: this.validateFlags(stateData.flags),
      counters: this.validateCounters(stateData.counters)
    };
  }

  /**
   * Validate object states (record of boolean values)
   */
  private static validateObjectStates(states: unknown, objectId: string): Record<string, boolean> {
    if (!states) return {};
    
    if (typeof states !== 'object') {
      throw new Error(`States for object ${objectId} must be an object`);
    }

    const statesData = states as Record<string, unknown>;
    const validatedStates: Record<string, boolean> = {};

    for (const [stateName, stateValue] of Object.entries(statesData)) {
      if (typeof stateValue !== 'boolean') {
        throw new Error(`State ${stateName} for object ${objectId} must be a boolean value`);
      }
      validatedStates[stateName] = stateValue;
    }

    return validatedStates;
  }

  /**
   * Validate dialogue (record of string values)
   */
  private static validateDialogue(dialogue: unknown, characterId: string): Record<string, string> {
    if (!dialogue || typeof dialogue !== 'object') {
      throw new Error(`Dialogue for character ${characterId} must be an object`);
    }

    const dialogueData = dialogue as Record<string, unknown>;
    const validatedDialogue: Record<string, string> = {};

    for (const [topic, response] of Object.entries(dialogueData)) {
      validatedDialogue[topic] = this.validateString(response, `characters.${characterId}.dialogue.${topic}`);
    }

    return validatedDialogue;
  }

  /**
   * Validate flags (record of boolean values)
   */
  private static validateFlags(flags: unknown): Record<string, boolean> {
    if (!flags) return {};
    
    if (typeof flags !== 'object') {
      throw new Error('Flags must be an object');
    }

    const flagsData = flags as Record<string, unknown>;
    const validatedFlags: Record<string, boolean> = {};

    for (const [flagName, flagValue] of Object.entries(flagsData)) {
      if (typeof flagValue !== 'boolean') {
        throw new Error(`Flag ${flagName} must be a boolean value`);
      }
      validatedFlags[flagName] = flagValue;
    }

    return validatedFlags;
  }

  /**
   * Validate counters (record of number values)
   */
  private static validateCounters(counters: unknown): Record<string, number> {
    if (!counters) return {};
    
    if (typeof counters !== 'object') {
      throw new Error('Counters must be an object');
    }

    const countersData = counters as Record<string, unknown>;
    const validatedCounters: Record<string, number> = {};

    for (const [counterName, counterValue] of Object.entries(countersData)) {
      if (typeof counterValue !== 'number') {
        throw new Error(`Counter ${counterName} must be a numeric value`);
      }
      validatedCounters[counterName] = counterValue;
    }

    return validatedCounters;
  }

  /**
   * Validate that an array of strings is valid
   */
  private static validateStringArray(arr: unknown, fieldName: string): string[] {
    if (!arr) return [];
    
    if (!Array.isArray(arr)) {
      throw new Error(`Field ${fieldName} must be an array`);
    }

    return arr.map((item, index) => {
      if (typeof item !== 'string') {
        throw new Error(`Item at index ${index} in ${fieldName} must be a string`);
      }
      return item;
    });
  }

  /**
   * Validate references between entities
   */
  private static validateReferences(worldModel: WorldModel): void {
    const { rooms, objects, characters, initialState } = worldModel;
    
    // Check that the initial room exists
    if (!rooms[initialState.currentRoomId]) {
      throw new Error(`Initial room ${initialState.currentRoomId} does not exist`);
    }
    
    // Check room exits
    for (const [roomId, room] of Object.entries(rooms)) {
      for (const exit of room.exits) {
        if (!rooms[exit.targetRoomId]) {
          throw new Error(`Room ${roomId} has an exit to non-existent room ${exit.targetRoomId}`);
        }
        if (exit.keyId && !objects[exit.keyId]) {
          throw new Error(`Room ${roomId} has an exit requiring non-existent key ${exit.keyId}`);
        }
      }
      
      // Check room objects
      for (const objectId of room.objects) {
        if (!objects[objectId]) {
          throw new Error(`Room ${roomId} contains non-existent object ${objectId}`);
        }
      }
      
      // Check room characters
      for (const characterId of room.characters) {
        if (!characters[characterId]) {
          throw new Error(`Room ${roomId} contains non-existent character ${characterId}`);
        }
      }
    }
    
    // Check object containment
    for (const [objectId, object] of Object.entries(objects)) {
      if (object.containedObjects) {
        for (const containedId of object.containedObjects) {
          if (!objects[containedId]) {
            throw new Error(`Object ${objectId} contains non-existent object ${containedId}`);
          }
        }
      }
    }
    
    // Check character inventory
    for (const [characterId, character] of Object.entries(characters)) {
      for (const objectId of character.inventory) {
        if (!objects[objectId]) {
          throw new Error(`Character ${characterId} has non-existent object ${objectId} in inventory`);
        }
      }
    }
    
    // Check player inventory
    for (const objectId of initialState.inventory) {
      if (!objects[objectId]) {
        throw new Error(`Initial inventory contains non-existent object ${objectId}`);
      }
    }
  }
}