<?php

namespace Drupal\visitors\Service;

use Drupal\Core\Database\Connection;
use Drupal\visitors\VisitorsTrackerInterface;

/**
 * Tracker for web analytics.
 */
class TrackerService implements VisitorsTrackerInterface {

  /**
   * The database connection.
   *
   * @var \Drupal\Core\Database\Connection
   */
  protected $database;

  /**
   * Tracks visits and events.
   *
   * @param \Drupal\Core\Database\Connection $database
   *   The database connection.
   */
  public function __construct(Connection $database) {
    $this->database = $database;
  }

  /**
   * {@inheritdoc}
   */
  public function getVisitId(array $fields, int $request_time): int {

    $fields['config_id'] = $this->generateConfigId($fields);

    $ago = $request_time - VisitorsTrackerInterface::SESSION_TIMEOUT;

    $id = $this->getCurrentSession($fields, $ago);
    if ($id) {
      return $id;
    }

    $this->doReturningVisit($fields);

    $id = $this->database->insert('visitors_visit')
      ->fields($fields)
      ->execute();

    return $id;
  }

  /**
   * Get the current session id.
   *
   * @param array $fields
   *   The fields to get the session id from.
   * @param int $ago
   *   The time to check for the session id.
   */
  protected function getCurrentSession(array $fields, int $ago): ?int {

    if (empty($fields['visitor_id'])) {
      return $this->getCurrentSessionByConfigId($fields, $ago);
    }

    $current_session = $this->database->select('visitors_visit', 'vv');
    $current_session->fields('vv', ['id']);
    $current_session->condition('vv.visitor_id', $fields['visitor_id']);
    $current_session->condition('vv.exit_time', $ago, '>');

    $id = $current_session->execute()->fetchField();

    return $id;
  }

  /**
   * Get the current session id by config id.
   *
   * @param array $fields
   *   The fields to get the session id from.
   * @param int $ago
   *   The time to check for the session id.
   */
  protected function getCurrentSessionByConfigId(array $fields, int $ago): ?int {
    if (empty($fields['config_id'])) {
      return NULL;
    }

    $current_session = $this->database->select('visitors_visit', 'vv');
    $current_session->fields('vv', ['id']);
    $current_session->condition('vv.config_id', $fields['config_id']);
    $current_session->condition('vv.location_ip', $fields['location_ip']);
    $current_session->condition('vv.exit_time', $ago, '>');

    $id = $current_session->execute()->fetchField();

    return $id;
  }

  /**
   * Check if the visitor has visited before.
   *
   * If the visitor has visited before, update the visit count and set the
   * returning flag.
   *
   * @param array $fields
   *   The fields to check for the visitor.
   */
  protected function doReturningVisit(&$fields): void {
    if (empty($fields['visitor_id'])) {
      return;
    }

    $returning_visitor = $this->database->select('visitors_visit', 'vv2');
    $returning_visitor->fields('vv2', ['total_visits', 'entry_time', 'exit_time', 'time_since_first'])
      ->condition('vv2.visitor_id', $fields['visitor_id'])
      ->orderBy('vv2.exit_time', 'DESC')
      ->range(0, 1);

    $result = $returning_visitor->execute()->fetchAssoc();

    if ($result !== FALSE) {
      $fields['total_visits'] = $result['total_visits'] + 1;
      $fields['returning'] = 1;

      // Calculate time since last visit (seconds)
      $fields['time_since_last'] = $fields['entry_time'] - $result['exit_time'];

      // Calculate time since first visit by adding the time between visits
      // to the previous visit's time_since_first value.
      $fields['time_since_first'] = $result['time_since_first'] + ($fields['entry_time'] - $result['entry_time']);
    }
    else {
      // New visitor - no previous visits.
      $fields['total_visits'] = 1;
      $fields['returning'] = 0;
      $fields['time_since_first'] = 0;
      $fields['time_since_last'] = 0;
    }
  }

  /**
   * {@inheritdoc}
   */
  public function writeEvent(array $fields): int {
    $id = $this->database->insert('visitors_event')
      ->fields($fields)
      ->execute();

    return $id;
  }

  /**
   * {@inheritdoc}
   */
  public function updateVisit(int $visit_id, int $event_id, int $exit_time, ?int $uid) {

    $update = $this->database->update('visitors_visit');
    $update->fields([
      'exit_time' => $exit_time,
      'exit' => $event_id,
    ])
      ->expression('total_time', ':now - entry_time', [':now' => $exit_time])
      ->expression(
        'total_page_views',
        '(SELECT COUNT(DISTINCT page_view) FROM {visitors_event} ve WHERE ve.visit_id = :id)',
        [':id' => $visit_id]
      )
      ->expression(
        'total_events',
        '(SELECT COUNT(id) FROM {visitors_event} ve WHERE ve.visit_id = :id)',
        [':id' => $visit_id],
        )
      ->expression('entry', 'COALESCE(entry, :event_id)', [':event_id' => $event_id])
      ->expression('uid', 'CASE WHEN uid > 0 THEN uid ELSE :uid END', [':uid' => $uid])
      ->condition('id', $visit_id)
      ->execute();

  }

  /**
   * Generate a config id.
   *
   * @param array $fields
   *   The fields to generate the config id from.
   *
   * @return string
   *   The generated config id 16 characters.
   */
  protected function generateConfigId($fields) {
    $os = $fields['bot'] ? 'bot' : $fields['config_os'];
    $config_string =
      $os
      . $fields['config_browser_name']
      . $fields['config_browser_version']
      . $fields['config_flash']
      . $fields['config_java']
      . $fields['config_quicktime']
      . $fields['config_realplayer']
      . $fields['config_pdf']
      . $fields['config_windowsmedia']
      . $fields['config_silverlight']
      . $fields['config_cookie']
      . $fields['config_resolution']
      . $fields['location_browser_lang'];

    $sha = hash('sha256', $config_string);
    $config_id = substr($sha, 0, 16);

    return $config_id;
  }

}
