/**
 * Helper Utilities for BaoLife
 * Ported from Python utils/helpers.py
 */

import { config } from '../config.js';
import type { Player, GameLocation } from '../models/Player.js';

// ============================================================================
// String Utilities
// ============================================================================

/**
 * Remove text before the first colon in a string
 * Used to clean up AI responses that start with "Character: message"
 */
export function removeTextBeforeFirstColon(text: string): string {
  const colonIndex = text.indexOf(':');
  if (colonIndex !== -1 && colonIndex < 30) {
    // Only remove if colon is within first 30 chars (likely a name prefix)
    return text.substring(colonIndex + 1).trim();
  }
  return text;
}

/**
 * Get upcoming Saturday from a date string
 */
export function upcomingSaturday(dateStr: string): string {
  const [month, day] = dateStr.split('-').map((n) => parseInt(n, 10));
  const date = new Date();
  date.setMonth(month - 1);
  date.setDate(day);

  const dayOfWeek = date.getDay();
  const daysUntilSaturday = (6 - dayOfWeek + 7) % 7 || 7;
  date.setDate(date.getDate() + daysUntilSaturday);

  const resultMonth = String(date.getMonth() + 1).padStart(2, '0');
  const resultDay = String(date.getDate()).padStart(2, '0');
  return `${resultMonth}-${resultDay}`;
}

/**
 * Random integer between min and max (inclusive)
 */
export function randomInt(min: number, max: number): number {
  return Math.floor(Math.random() * (max - min + 1)) + min;
}

/**
 * Pick a random item from an array
 */
export function randomChoice<T>(arr: T[]): T {
  return arr[Math.floor(Math.random() * arr.length)];
}

/**
 * Format money amount
 */
export function formatMoney(amount: number): string {
  return `$${amount.toLocaleString()}`;
}

/**
 * Capitalize first letter
 */
export function capitalize(str: string): string {
  return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
}

// ============================================================================
// Game Speed Utilities
// Ported from Python ws/utils/game_speed.py
// ============================================================================

/** Default speed values */
export const SPEED_MIN = 1;
export const SPEED_MAX = 10000;

/**
 * Validate and clamp game speed to acceptable range.
 * Ported from Python validate_game_speed().
 *
 * @param speed - The speed value to validate (can be number or string)
 * @param allowQuestionPause - If true, allows SPEED_QUESTION_PAUSE value
 * @returns A valid game speed within acceptable bounds
 *
 * Note: Higher speed values = slower gameplay (more ticks between updates)
 */
export function validateGameSpeed(
  speed: unknown,
  allowQuestionPause = false
): number {
  let speedNum: number;

  try {
    speedNum = typeof speed === 'string' ? parseInt(speed, 10) : Number(speed);

    if (isNaN(speedNum)) {
      console.warn(`Invalid game speed value: ${speed}, using default`);
      return config.SPEED_DEFAULT;
    }

    // Allow question pause value if specified
    if (allowQuestionPause && speedNum === config.SPEED_QUESTION_PAUSE) {
      return speedNum;
    }

    // Clamp to valid range
    const validatedSpeed = Math.max(SPEED_MIN, Math.min(speedNum, SPEED_MAX));

    if (validatedSpeed !== speedNum) {
      console.warn(`Game speed ${speedNum} out of bounds, clamped to ${validatedSpeed}`);
    }

    return validatedSpeed;
  } catch {
    console.error(`Invalid game speed value: ${speed}, using default`);
    return config.SPEED_DEFAULT;
  }
}

/**
 * Get the appropriate speed button values based on config.
 * Ported from Python get_speed_button_values().
 */
export function getSpeedButtonValues(): readonly number[] {
  return config.SPEED_BUTTON_VALUES;
}

/**
 * Log speed changes for debugging and monitoring.
 * Ported from Python log_speed_change().
 *
 * @param userId - User/player identifier
 * @param oldSpeed - Previous speed value
 * @param newSpeed - New speed value
 * @param source - Source of the change (manual, button, reset, question, etc.)
 */
export function logSpeedChange(
  userId: string,
  oldSpeed: number,
  newSpeed: number,
  source = 'manual'
): void {
  // Calculate approximate updates per second
  // Based on 1000 ticks/sec (TICK_INTERVAL = 1ms)
  const ticksPerSec = 1000 / config.TICK_INTERVAL;
  const oldUps = oldSpeed > 0 ? ticksPerSec / oldSpeed : 0;
  const newUps = newSpeed > 0 ? ticksPerSec / newSpeed : 0;

  console.log(
    `Speed change [user=${userId}] [${source}]: ` +
    `${oldSpeed} (${oldUps.toFixed(1)} ups) -> ${newSpeed} (${newUps.toFixed(1)} ups)`
  );
}

// ============================================================================
// Array/Object Search Utilities
// Ported from Python ws/utils/helpers.py
// ============================================================================

/**
 * Object with a type property
 */
export interface TypedObject {
  type: string;
  [key: string]: unknown;
}

/**
 * Object with an id property
 */
export interface Identifiable {
  id: string | number;
  [key: string]: unknown;
}

/**
 * Filter array by object type attribute.
 * Ported from Python getFromArray().
 *
 * @param array - List of objects with 'type' attribute
 * @param type - Value to match against object.type
 * @param returnFirst - If true, return first match only; otherwise return all matches
 * @returns Array of matching objects, or single object if returnFirst is true, or undefined if none found
 */
export function getByType<T extends TypedObject>(
  array: T[],
  type: string,
  returnFirst?: false
): T[];
export function getByType<T extends TypedObject>(
  array: T[],
  type: string,
  returnFirst: true
): T | undefined;
export function getByType<T extends TypedObject>(
  array: T[],
  type: string,
  returnFirst = false
): T[] | T | undefined {
  const result = array.filter((item) => item.type === type);

  if (returnFirst) {
    return result.length > 0 ? result[0] : undefined;
  }

  return result;
}

/**
 * Find an object in array by its id attribute.
 * Ported from Python find_by_id().
 *
 * @param array - Collection of objects with 'id' attribute
 * @param id - ID value to search for
 * @returns First matching object or undefined if not found
 */
export function findById<T extends Identifiable>(
  array: T[],
  id: string | number
): T | undefined {
  return array.find((item) => item.id === id);
}

/**
 * Find first object where all key-value pairs match.
 * Ported from Python find_where().
 *
 * @param array - Collection of objects to search
 * @param criteria - Object with attribute names and values to match
 * @returns First matching object or undefined if not found
 */
export function findWhere<T extends Record<string, unknown>>(
  array: T[],
  criteria: Partial<T>
): T | undefined {
  return array.find((item) =>
    Object.entries(criteria).every(([key, value]) => item[key] === value)
  );
}

/**
 * Filter objects where all key-value pairs match.
 *
 * @param array - Collection of objects to search
 * @param criteria - Object with attribute names and values to match
 * @returns Array of matching objects
 */
export function filterWhere<T extends Record<string, unknown>>(
  array: T[],
  criteria: Partial<T>
): T[] {
  return array.filter((item) =>
    Object.entries(criteria).every(([key, value]) => item[key] === value)
  );
}

/**
 * Comparison operator for advanced search
 */
type ComparisonOperator = '__gt' | '__lt' | '__gte' | '__lte';

/**
 * Parse key with potential comparison operator suffix.
 */
function parseKeyWithOperator(key: string): { key: string; operator: ComparisonOperator | null } {
  for (const op of ['__gte', '__lte', '__gt', '__lt'] as ComparisonOperator[]) {
    if (key.endsWith(op)) {
      return { key: key.slice(0, -op.length), operator: op };
    }
  }
  return { key, operator: null };
}

/**
 * Check if a value matches a comparison.
 */
function compareValue(itemValue: unknown, criteriaValue: unknown, operator: ComparisonOperator | null): boolean {
  if (operator === null) {
    return itemValue === criteriaValue;
  }

  const numItem = Number(itemValue);
  const numCriteria = Number(criteriaValue);

  if (isNaN(numItem) || isNaN(numCriteria)) {
    return false;
  }

  switch (operator) {
    case '__gt':
      return numItem > numCriteria;
    case '__lt':
      return numItem < numCriteria;
    case '__gte':
      return numItem >= numCriteria;
    case '__lte':
      return numItem <= numCriteria;
    default:
      return false;
  }
}

/**
 * Find objects with advanced comparison operators.
 * Supports __gt (greater than), __lt (less than), __gte (>=), __lte (<=) suffixes in keys.
 * Ported from Python find_where_test().
 *
 * @param array - Collection of objects to search
 * @param criteria - Object with keys like 'age__gt': 23 or 'score__lt': 100
 * @returns Generator yielding matching objects
 *
 * @example
 * // Find all people over 23 named John
 * const results = [...findWhereAdvanced(people, { age__gt: 23, name: 'John' })];
 */
export function* findWhereAdvanced<T extends Record<string, unknown>>(
  array: T[],
  criteria: Record<string, unknown>
): Generator<T, void, undefined> {
  for (const item of array) {
    let match = true;

    for (const [criteriaKey, criteriaValue] of Object.entries(criteria)) {
      const { key, operator } = parseKeyWithOperator(criteriaKey);
      const itemValue = item[key];

      if (!compareValue(itemValue, criteriaValue, operator)) {
        match = false;
        break;
      }
    }

    if (match) {
      yield item;
    }
  }
}

/**
 * Find first object with advanced comparison operators.
 * Convenience wrapper around findWhereAdvanced().
 *
 * @param array - Collection of objects to search
 * @param criteria - Object with keys like 'age__gt': 23 or 'score__lt': 100
 * @returns First matching object or undefined
 */
export function findFirstWhereAdvanced<T extends Record<string, unknown>>(
  array: T[],
  criteria: Record<string, unknown>
): T | undefined {
  for (const item of findWhereAdvanced(array, criteria)) {
    return item;
  }
  return undefined;
}

/**
 * Get all objects matching advanced criteria.
 * Convenience wrapper around findWhereAdvanced().
 *
 * @param array - Collection of objects to search
 * @param criteria - Object with keys like 'age__gt': 23 or 'score__lt': 100
 * @returns Array of matching objects
 */
export function filterWhereAdvanced<T extends Record<string, unknown>>(
  array: T[],
  criteria: Record<string, unknown>
): T[] {
  return [...findWhereAdvanced(array, criteria)];
}

// ============================================================================
// Location Utilities
// Ported from Python parseLocations()
// ============================================================================

/**
 * Update location objects with current people present.
 * Increases familiarity between characters at same location.
 * Ported from Python parseLocations().
 *
 * @param player - Player object with c (character), r (relationships), l (locations)
 * @returns Updated player object
 */
export function parseLocations(player: Player): Player {
  // Skip if no locations
  if (!player.l || player.l.length === 0) {
    return player;
  }

  // Go through all locations and update people arrays
  for (const location of player.l) {
    location.people = [];
    let playerPresent = false;

    // Check if player character is at this location
    if (player.c.location === location.id) {
      location.people.push(player.c.id);
      playerPresent = true;
    }

    // Check all relationships
    for (const person of player.r) {
      if (person.location === location.id) {
        // Increase familiarity if player is present
        if (playerPresent) {
          person.familiarity = (person.familiarity ?? 0) + 10;
        }
        location.people.push(person.id);
      }
    }
  }

  return player;
}

/**
 * Find a location by ID.
 *
 * @param player - Player object with locations
 * @param locationId - Location ID to find
 * @returns Location object or undefined
 */
export function findLocation(player: Player, locationId: string): GameLocation | undefined {
  return player.l?.find((loc) => loc.id === locationId);
}

/**
 * Get all people at a specific location.
 *
 * @param player - Player object
 * @param locationId - Location ID
 * @returns Array of person IDs at the location
 */
export function getPeopleAtLocation(player: Player, locationId: string): string[] {
  const location = findLocation(player, locationId);
  return location?.people ?? [];
}
