<?php

namespace App\Services;

use Symfony\Component\ExpressionLanguage\ExpressionLanguage;

class CalculatorService {
  protected ExpressionLanguage $language;

  /**
   * Gender mapping for calculations
   * 0 = Unknown, 1 = Female, 2 = Male
   */
  protected const GENDER_MAP = [
    'unknown' => 0,
    'female' => 1,
    'male' => 2,
    '0' => 0,
    '1' => 1,
    '2' => 2,
  ];

  public function __construct() {
    $this->language = new ExpressionLanguage();
  }

  /**
   * Calculate formula with variables
   *
   * @param string $formula Formula with placeholders like {result}, {factor}, {gender}
   * @param array $variables Array of variable values
   * @return float|null Calculated result or null on error
   * @throws \Exception
   */
  public function calculate(string $formula, array $variables = []): ?float {
    try {
      $normalizedFormula = $this->normalizeFormulaVariables($formula, $variables);
      [$expression, $context] = $this->prepareExpression($normalizedFormula, $variables);

      $result = $this->language->evaluate($expression, $context);

      return (float) $result;
    } catch (\Throwable $e) {
      log_message('error', 'Calculator error: ' . $e->getMessage() . ' | Formula: ' . $formula);
      throw new \Exception('Invalid formula: ' . $e->getMessage());
    }
  }

  /**
   * Validate formula syntax
   *
   * @param string $formula Formula to validate
   * @return array ['valid' => bool, 'error' => string|null]
   */
  public function validate(string $formula): array {
    $formula = $this->normalizeFormulaVariables($formula, []);
    $variables = array_fill_keys($this->extractVariables($formula), 1);

    try {
      [$expression, $context] = $this->prepareExpression($formula, $variables);
      $this->language->evaluate($expression, $context);

      return ['valid' => true, 'error' => null];
    } catch (\Throwable $e) {
      return ['valid' => false, 'error' => $e->getMessage()];
    }
  }

  /**
   * Extract variable names from formula
   *
   * @param string $formula Formula with placeholders
   * @return array List of variable names
   */
  public function extractVariables(string $formula): array {
    preg_match_all('/\{([^}]+)\}/', $formula, $matches);
    return array_unique($matches[1]);
  }

  /**
   * Prepare expression by replacing placeholders with values
   */
  protected function prepareExpression(string $formula, array $variables): array {
    $expression = $formula;
    $context = [];
    $usedNames = [];

    foreach ($variables as $key => $value) {
      $placeholder = '{' . $key . '}';

      if ($key === 'gender') {
        $value = $this->normalizeGender($value);
      }

      if (!is_numeric($value)) {
        throw new \Exception("Variable '{$key}' must be numeric, got: " . var_export($value, true));
      }

      $variableName = $this->getSafeVariableName($key, $usedNames);
      $usedNames[] = $variableName;
      $context[$variableName] = (float) $value;

      $expression = str_replace($placeholder, $variableName, $expression);
    }

    if (preg_match('/\{([^}]+)\}/', $expression, $unreplaced)) {
      throw new \Exception("Missing variable value for: {$unreplaced[1]}");
    }

    return [$expression, $context];
  }

  protected function getSafeVariableName(string $key, array $usedNames): string {
    $base = preg_replace('/[^A-Za-z0-9_]/', '_', (string) $key);
    if ($base === '' || ctype_digit($base[0])) {
      $base = 'v_' . $base;
    }

    $name = $base;
    $suffix = 1;
    while (in_array($name, $usedNames, true)) {
      $name = $base . '_' . $suffix++;
    }

    return $name;
  }

  /**
   * Normalize formulas that reference raw variable names instead of placeholders.
   */
  protected function normalizeFormulaVariables(string $formula, array $variables): string
  {
    if (str_contains($formula, '{')) {
      return $formula;
    }

    if (empty($variables)) {
      return $formula;
    }

    $keys = array_keys($variables);
    usort($keys, fn($a, $b) => mb_strlen($b) <=> mb_strlen($a));

    foreach ($keys as $key) {
      $escaped = preg_quote($key, '/');
      $formula = preg_replace_callback('/\b' . $escaped . '\b/i', function () use ($key) {
        return '{' . $key . '}';
      }, $formula);
    }

    return $formula;
  }

  /**
   * Normalize gender value to numeric (0, 1, or 2)
   */
  protected function normalizeGender($gender): int {
    if (is_numeric($gender)) {
      $num = (int) $gender;
      return in_array($num, [0, 1, 2], true) ? $num : 0;
    }
    
    $genderLower = strtolower((string) $gender);
    return self::GENDER_MAP[$genderLower] ?? 0;
  }

  /**
   * Calculate from TestDefCal record
   *
   * @param array $calcDef Test calculation definition
   * @param array $testValues Test result values
   * @return float|null
   */
  public function calculateFromDefinition(array $calcDef, array $testValues): ?float {
    $formula = $calcDef['FormulaCode'] ?? '';
    
    if (empty($formula)) {
      throw new \Exception('No formula defined');
    }

    // Build variables array
    $variables = [
      'result' => $testValues['result'] ?? 0,
      'factor' => $calcDef['Factor'] ?? 1,
    ];

    // Add optional variables
    if (isset($testValues['gender'])) {
      $variables['gender'] = $testValues['gender'];
    }
    if (isset($testValues['age'])) {
      $variables['age'] = $testValues['age'];
    }
    if (isset($testValues['ref_low'])) {
      $variables['ref_low'] = $testValues['ref_low'];
    }
    if (isset($testValues['ref_high'])) {
      $variables['ref_high'] = $testValues['ref_high'];
    }

    // Merge any additional test values
    $variables = array_merge($variables, $testValues);

    return $this->calculate($formula, $variables);
  }
}
