<?php

namespace Drupal\visitors\Plugin\views\field;

use Drupal\Core\Form\FormStateInterface;
use Drupal\views\Attribute\ViewsField;
use Drupal\views\Plugin\views\field\FieldPluginBase;
use Drupal\views\ResultRow;

/**
 * Field handler to display numeric values grouped into configured ranges.
 */
#[ViewsField("visitors_number_range")]
class NumberRange extends FieldPluginBase {

  /**
   * {@inheritdoc}
   */
  public function query() {
    $this->ensureMyTable();
    $suffix = '__range';
    $field = "$this->tableAlias.$this->realField";

    $ranges_text = $this->options['ranges'];
    $ranges = preg_split('/\r\n|\r|\n/', $ranges_text);
    $ranges = array_map('trim', $ranges);
    $ranges = array_filter($ranges, function ($range) {
      return trim($range) !== '';
    });

    if (empty($ranges)) {
      return parent::query();
    }

    $case_sql = [];
    foreach ($ranges as $i => $range) {

      $value = $i;
      $parsed_range = $this->parseTimeRange($range);

      // "X-Y" with or without time units.
      if (preg_match('/^(\d+(?:\.\d+)?)\-(\d+(?:\.\d+)?)$/', $range, $m)) {
        $start_seconds = $this->convertToSeconds($m[1], $parsed_range['start_unit']);
        $end_seconds = $this->convertToSeconds($m[2], $parsed_range['end_unit']);
        $case_sql[] = "WHEN $field BETWEEN $start_seconds AND $end_seconds THEN $value";
      }
      // "-X" format (0 to X).
      elseif (preg_match('/^\-(\d+(?:\.\d+)?)([mdwy]?)$/', $range, $m)) {
        $end_seconds = $this->convertToSeconds($m[1], $m[2] ?: NULL);
        $case_sql[] = "WHEN $field BETWEEN 0 AND $end_seconds THEN $value";
      }
      // "X+" with or without time units.
      elseif (preg_match('/^(\d+(?:\.\d+)?)\+$/', $range, $m)) {
        $seconds = $this->convertToSeconds($m[1], $parsed_range['start_unit']);
        $case_sql[] = "WHEN $field >= $seconds THEN $value";
      }
      // Exact number with or without time units.
      elseif (preg_match('/^(\d+(?:\.\d+)?)$/', $range, $m)) {
        $seconds = $this->convertToSeconds($m[1], $parsed_range['start_unit']);
        $case_sql[] = "WHEN $field = $seconds THEN $value";
      }

    }
    $max_value = count($ranges) + 1;
    $case_expression = "CASE " . implode(' ', $case_sql) . " ELSE $max_value END";

    // Tell Views to use this CASE expression as the field value.
    $this->field_alias = $this->query->addField(NULL, $case_expression, $this->tableAlias . '_' . $this->realField . $suffix);
  }

  /**
   * {@inheritdoc}
   */
  public function render(ResultRow $values) {
    $label = $this->getValue($values);

    // Get display name.
    $display_name = $this->options;

    // Always replace the index value with the range label.
    $label = $this->getRangeLabel($label);

    // If time format is enabled, convert the range to time format.
    if ($this->options['time_format']) {
      $time_unit = $this->options['time_unit'] ?? 'auto';
      if ($time_unit === 'auto') {
        $label = $this->formatTimeRange($label);
      }
      else {
        $label = $this->formatTimeRangeInUnit($label, $time_unit);
      }
    }
    elseif ($this->options['pluralize']) {
      // Example: pluralize with the word "item".
      return $this->formatPlural(
        $label,
        $this->options['singular_label'],
        $this->options['plural_label'],
      );
    }

    return $label;
  }

  /**
   * Formats a range value as time when the range represents seconds.
   *
   * @param string $range
   *   The range value (e.g., "60", "3600", "86400").
   *
   * @return string
   *   The formatted time range (e.g., "1m", "1h", "1d").
   */
  protected function formatTimeRange(string $range): string {
    // Handle ranges like "60-120" or "3600+".
    if (strpos($range, '-') !== FALSE) {
      $parts = explode('-', $range);
      $unit = NULL;
      $start = $this->formatSeconds((int) $parts[0], $unit);
      $end = $this->formatSeconds((int) $parts[1], $unit);
      // If start and end are less than 1 unit apart, return the integer with
      // the unit. Example: "1 day".
      if (is_numeric($start) && is_numeric($end)
          && abs($end - $start) < 1) {
        $integer = is_int($end) ? $end : (is_int($start) ? $start : $end);
        // Only return "0" without units if both start and end are exactly 0.
        if ($integer == 0 && $start == 0 && $end == 0) {
          return '0';
        }
        return $this->makePluralWith($integer, $unit);
      }
      if ($start == 0 && $end == 1) {
        return $this->makeSingle("$end", $unit);
      }
      elseif ($start == 0) {
        return $this->makeSingle("$start-$end", $unit);
      }

      return $this->makePlural(floor($start) . "-$end", $unit);
    }

    if (strpos($range, '+') !== FALSE) {
      $seconds = (int) rtrim($range, '+');
      $unit = NULL;
      $time = $this->formatSeconds($seconds, $unit);
      return $this->makePluralWith(floor($time), $unit ?? 'second', '+');
    }

    // Single value.
    $seconds = (int) $range;
    $unit = NULL;
    $time = $this->formatSeconds($seconds, $unit);
    return $this->makePluralWith($time, $unit ?? 'second');
  }

  /**
   * Formats a range value as time using a specific unit.
   *
   * @param string $range
   *   The range value (e.g., "60", "3600", "86400").
   * @param string $unit
   *   The time unit to use (second, minute, hour, day, week, year).
   *
   * @return string
   *   The formatted time range in the specified unit.
   */
  protected function formatTimeRangeInUnit(string $range, string $unit): string {
    // Handle "-X" format (0 to X).
    if (preg_match('/^\-(\d+)$/', $range, $m)) {
      $end = $this->formatSecondsInUnit((int) $m[1], $unit);
      if ($end == 0) {
        return '0';
      }
      return $this->makePluralWith($end, $unit);
    }

    // Handle ranges like "60-120" or "3600+".
    if (strpos($range, '-') !== FALSE) {
      $parts = explode('-', $range);
      $start = $this->formatSecondsInUnit((int) $parts[0], $unit);
      $end = $this->formatSecondsInUnit((int) $parts[1], $unit);

      // If both start and end are 0, return just "0".
      if ($start == 0 && $end == 0) {
        return '0';
      }

      // If start and end are less than 1 unit apart, return the integer with
      // the unit. Example: "1 day".
      if (is_numeric($start) && is_numeric($end)
          && abs($end - $start) < 1) {
        $integer = floor($start);
        // Only return "0" without units if both start and end are exactly 0.
        if ($integer == 0 && $start == 0 && $end == 0) {
          return '0';
        }
        return $this->makePluralWith($integer, $unit);
      }
      if ($start == 0 && $end == 1) {
        return $this->makeSingle("$end", $unit);
      }
      elseif ($start == 0) {
        return $this->makePlural($end, $unit);
      }

      return $this->makePlural(floor($start) . "-" . floor($end), $unit);
    }

    if (strpos($range, '+') !== FALSE) {
      $seconds = (int) rtrim($range, '+');
      $time = $this->formatSecondsInUnit($seconds, $unit);
      if ($time == 0) {
        return '0+';
      }
      return $this->makePluralWith(floor($time), $unit, '+');
    }

    // Single value.
    $seconds = (int) $range;
    $time = $this->formatSecondsInUnit($seconds, $unit);
    if ($time == 0) {
      return '0';
    }
    return $this->makePluralWith($time, $unit);
  }

  /**
   * Formats seconds into a specific time unit.
   *
   * @param int $seconds
   *   The number of seconds.
   * @param string $unit
   *   The unit of time to format the seconds into.
   *
   * @return float
   *   The formatted time value as a float.
   */
  protected function formatSecondsInUnit(int $seconds, string $unit): float {
    switch ($unit) {
      case 'second':
        return (float) $seconds;

      case 'minute':
        return $seconds / 60;

      case 'hour':
        return $seconds / 3600;

      case 'day':
        return $seconds / 86400;

      case 'week':
        return $seconds / 604800;

      case 'year':
        return $seconds / 31536000;

      default:
        return (float) $seconds;
    }
  }

  /**
   * Creates plural or singular form based on the time value.
   *
   * @param mixed $time
   *   The time value to format.
   * @param string $unit
   *   The time unit (second, minute, hour, etc.).
   * @param string $suffix
   *   Optional suffix to append to the time value.
   *
   * @return string
   *   The formatted time string with proper pluralization.
   */
  protected function makePluralWith($time, string $unit, $suffix = ''): string {
    if ($time == 1) {
      return $this->makeSingle($time, $unit, $suffix);
    }
    return $this->makePlural($time, $unit, $suffix);
  }

  /**
   * Creates plural form of time units.
   *
   * @param mixed $time
   *   The time value to format.
   * @param string $unit
   *   The time unit (second, minute, hour, etc.).
   * @param string $suffix
   *   Optional suffix to append to the time value.
   *
   * @return string
   *   The formatted time string in plural form.
   */
  protected function makePlural($time, string $unit, $suffix = ''): string {
    $plural = $this->t('Unknown unit');
    switch ($unit) {
      case 'second':
        $plural = $this->t('@time seconds', ['@time' => $time . $suffix]);
        break;

      case 'minute':
        $plural = $this->t('@time minutes', ['@time' => $time . $suffix]);
        break;

      case 'hour':
        $plural = $this->t('@time hours', ['@time' => $time . $suffix]);
        break;

      case 'day':
        $plural = $this->t('@time days', ['@time' => $time . $suffix]);
        break;

      case 'week':
        $plural = $this->t('@time weeks', ['@time' => $time . $suffix]);
        break;

      case 'month':
        $plural = $this->t('@time months', ['@time' => $time . $suffix]);
        break;

      case 'year':
        $plural = $this->t('@time years', ['@time' => $time . $suffix]);
        break;
    }

    return $plural;
  }

  /**
   * Creates singular form of time units.
   *
   * @param mixed $time
   *   The time value to format.
   * @param string $unit
   *   The time unit (second, minute, hour, etc.).
   * @param string $suffix
   *   Optional suffix to append to the time value.
   *
   * @return string
   *   The formatted time string in singular form.
   */
  protected function makeSingle($time, string $unit, $suffix = ''): string {
    $single = $this->t('Unknown unit');
    switch ($unit) {
      case 'second':
        $single = $this->t('@time second', ['@time' => $time . $suffix]);
        break;

      case 'minute':
        $single = $this->t('@time minute', ['@time' => $time . $suffix]);
        break;

      case 'hour':
        $single = $this->t('@time hour', ['@time' => $time . $suffix]);
        break;

      case 'day':
        $single = $this->t('@time day', ['@time' => $time . $suffix]);
        break;

      case 'week':
        $single = $this->t('@time week', ['@time' => $time . $suffix]);
        break;

      case 'month':
        $single = $this->t('@time month', ['@time' => $time . $suffix]);
        break;

      case 'year':
        $single = $this->t('@time year', ['@time' => $time . $suffix]);
        break;
    }
    return $single;
  }

  /**
   * Formats seconds into human-readable time units.
   *
   * @param int $seconds
   *   The number of seconds.
   * @param string|null $unit
   *   The unit of time used to format the seconds.
   *
   * @return float
   *   The formatted time value as a float.
   */
  protected function formatSeconds(int $seconds, &$unit = NULL): float {
    if ($seconds === 0) {
      return (float) $seconds;
    }

    // If unit is already specified and not empty, format seconds as that unit.
    if ($unit !== NULL && $unit !== '') {
      switch ($unit) {
        case 'second':
          return (float) $seconds;

        case 'minute':
          return $seconds / 60;

        case 'hour':
          return $seconds / 3600;

        case 'day':
          return $seconds / 86400;

        case 'week':
          return $seconds / 604800;

        case 'month':
          return $seconds / 2592000;

        case 'year':
          return $seconds / 31536000;

        default:
          // If unit is not recognized, fall through to automatic detection.
          break;
      }
    }

    // Automatic unit detection (existing logic)
    if ($seconds < 60) {
      $unit = 'second';
      return (float) $seconds;
    }

    if ($seconds < 3600) {
      $unit = 'minute';
      return $seconds / 60;
    }

    if ($seconds < 86400) {
      $unit = 'hour';
      return $seconds / 3600;
    }

    if ($seconds < 604800) {
      $unit = 'day';
      return $seconds / 86400;
    }

    if ($seconds < 2592000) {
      $unit = 'week';
      return $seconds / 604800;
    }

    if ($seconds < 31536000) {
      $unit = 'month';
      return $seconds / 2592000;
    }

    $unit = 'year';
    return $seconds / 31536000;
  }

  /**
   * Gets the range label for a given index value.
   *
   * @param string|int $index
   *   The index value (e.g., 1, 2, 3, 4, etc.).
   *
   * @return string
   *   The range label for the index.
   */
  protected function getRangeLabel($index): string {
    $ranges_text = $this->options['ranges'];
    $ranges = preg_split('/\r\n|\r|\n/', $ranges_text);

    $i = 0;
    foreach ($ranges as $range) {
      $range = trim($range);
      if ($range === '') {
        continue;
      }

      if ($i === (int) $index) {
        return $range;
      }

      $i += 1;
    }

    // If no match found, return the original index value.
    return (string) $index;
  }

  /**
   * Parses a time range string to extract time units.
   *
   * @param string $range
   *   The range string (e.g., "1m-2m", "5d+", "1w", "-60").
   *
   * @return array
   *   An array with 'start_unit' and 'end_unit' keys containing the time units.
   */
  protected function parseTimeRange(string $range): array {
    $result = ['start_unit' => NULL, 'end_unit' => NULL];

    // Handle range format "X-Y" with units.
    if (preg_match('/^(\d+(?:\.\d+)?)([mdwy]?)\-(\d+(?:\.\d+)?)([mdwy]?)$/', $range, $m)) {
      $result['start_unit'] = $m[2] ?: NULL;
      $result['end_unit'] = $m[4] ?: NULL;
    }
    // Handle "-X" format (0 to X) with units.
    elseif (preg_match('/^\-(\d+(?:\.\d+)?)([mdwy]?)$/', $range, $m)) {
      $result['start_unit'] = NULL;
      $result['end_unit'] = $m[2] ?: NULL;
    }
    // Handle single value or "X+" format with units.
    elseif (preg_match('/^(\d+(?:\.\d+)?)([mdwy]?)(\+?)$/', $range, $m)) {
      $result['start_unit'] = $m[2] ?: NULL;
      $result['end_unit'] = $m[2] ?: NULL;
    }

    return $result;
  }

  /**
   * Converts a number with time unit to seconds.
   *
   * @param string $value
   *   The numeric value.
   * @param string|null $unit
   *   The time unit (m, d, w, y) or NULL for no unit.
   *
   * @return int
   *   The value converted to seconds.
   */
  protected function convertToSeconds(string $value, ?string $unit): int {
    $num_value = (float) $value;

    if ($unit === NULL) {
      return (int) $num_value;
    }

    switch ($unit) {
      // Minutes.
      case 'm':
        return (int) ($num_value * 60);

      // Days.
      case 'd':
        return (int) ($num_value * 86400);

      // Weeks.
      case 'w':
        return (int) ($num_value * 604800);

      // Years.
      case 'y':
        return (int) ($num_value * 31536000);

      default:
        return (int) $num_value;
    }
  }

  /**
   * {@inheritdoc}
   */
  protected function defineOptions() {
    $options = parent::defineOptions();
    $options['pluralize'] = ['default' => FALSE];
    $options['singular_label'] = ['default' => '1 item'];
    $options['plural_label'] = ['default' => '@count items'];
    $options['ranges'] = ['default' => "0\n1\n2\n3\n4\n5\n6-7\n8-10\n11-14\n15-20\n21+"];
    $options['time_format'] = ['default' => FALSE];
    $options['time_unit'] = ['default' => 'auto'];
    return $options;
  }

  /**
   * {@inheritdoc}
   */
  public function buildOptionsForm(&$form, FormStateInterface $form_state) {
    parent::buildOptionsForm($form, $form_state);

    $form['pluralize'] = [
      '#type' => 'checkbox',
      '#title' => $this->t('Enable pluralization'),
      '#default_value' => $this->options['pluralize'],
    ];

    $form['singular_label'] = [
      '#type' => 'textfield',
      '#title' => $this->t('Singular label'),
      '#default_value' => $this->options['singular_label'],
      '#states' => [
        'visible' => [
          ':input[name="options[pluralize]"]' => ['checked' => TRUE],
        ],
      ],
    ];

    $form['plural_label'] = [
      '#type' => 'textfield',
      '#title' => $this->t('Plural label'),
      '#default_value' => $this->options['plural_label'],
      '#states' => [
        'visible' => [
          ':input[name="options[pluralize]"]' => ['checked' => TRUE],
        ],
      ],
    ];

    $form['ranges'] = [
      '#type' => 'textarea',
      '#title' => $this->t('Number ranges'),
      '#description' => $this->t("Enter one range per line. Examples: <code>0</code>, <code>6-7</code>, <code>21+</code>."),
      '#default_value' => $this->options['ranges'],
    ];

    $form['time_format'] = [
      '#type' => 'checkbox',
      '#title' => $this->t('Format ranges as time (e.g., 60s, 1h, 1d)'),
      '#default_value' => $this->options['time_format'],
    ];

    $form['time_unit'] = [
      '#type' => 'select',
      '#title' => $this->t('Time unit'),
      '#description' => $this->t('Choose the time unit for displaying all ranges. Auto will use the most appropriate unit for each range.'),
      '#default_value' => $this->options['time_unit'] ?? 'auto',
      '#options' => [
        'auto' => $this->t('Auto'),
        'second' => $this->t('Seconds'),
        'minute' => $this->t('Minutes'),
        'hour' => $this->t('Hours'),
        'day' => $this->t('Days'),
        'week' => $this->t('Weeks'),
        'year' => $this->t('Years'),
      ],
      '#states' => [
        'visible' => [
          ':input[name="options[time_format]"]' => ['checked' => TRUE],
        ],
      ],
    ];
  }

}
