<?php

namespace Drupal\visitors\Controller;

use Drupal\Component\Datetime\TimeInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Controller\ControllerBase;
use Drupal\maxmind_geoip\MaxMindGeoIpInterface;
use Drupal\visitors\Helper\VisitorsUrl;
use Drupal\visitors\VisitorsAiAssistantsInterface;
use Drupal\visitors\VisitorsCampaignInterface;
use Drupal\visitors\VisitorsCookieInterface;
use Drupal\visitors\VisitorsCounterInterface;
use Drupal\visitors\VisitorsDeviceInterface;
use Drupal\visitors\VisitorsLocationInterface;
use Drupal\visitors\VisitorsSearchEngineInterface;
use Drupal\visitors\VisitorsSocialNetworksInterface;
use Drupal\visitors\VisitorsSpamInterface;
use Drupal\visitors\VisitorsTrackerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\ServerBag;

/**
 * Visitors tracking controller.
 */
final class VisitorsController extends ControllerBase {

  /**
   * The time service.
   *
   * @var \Drupal\Component\Datetime\TimeInterface
   */
  protected $time;

  /**
   * The visitors settings.
   *
   * @var \Drupal\Core\Config\Config
   */
  protected $settings;

  /**
   * The logger service.
   *
   * @var \Psr\Log\LoggerInterface
   */
  protected $logger;

  /**
   * The counter service.
   *
   * @var \Drupal\visitors\VisitorsCounterInterface
   */
  protected $counter;

  /**
   * The cookie service.
   *
   * @var \Drupal\visitors\VisitorsCookieInterface
   */
  protected $cookie;

  /**
   * The device service.
   *
   * @var \Drupal\visitors\VisitorsDeviceInterface
   */
  protected $device;

  /**
   * The location service.
   *
   * @var \Drupal\visitors\VisitorsLocationInterface
   */
  protected $location;

  /**
   * The tracker service.
   *
   * @var \Drupal\visitors\VisitorsTrackerInterface
   */
  protected $tracker;

  /**
   * The search engine service.
   *
   * @var \Drupal\visitors\VisitorsSearchEngineInterface
   */
  protected $searchEngine;

  /**
   * The spam service.
   *
   * @var \Drupal\visitors\VisitorsSpamInterface
   */
  protected $spamService;

  /**
   * The social networks service.
   *
   * @var \Drupal\visitors\VisitorsSocialNetworksInterface
   */
  protected $socialNetworksService;

  /**
   * The AI assistants service.
   *
   * @var \Drupal\visitors\VisitorsAiAssistantsInterface
   */
  protected $aiAssistantsService;

  /**
   * The campaign service.
   *
   * @var \Drupal\visitors\VisitorsCampaignInterface
   */
  protected $campaignService;

  /**
   * The geoip service.
   *
   * @var \Drupal\maxmind_geoip\MaxMindGeoIpInterface|null
   */
  protected $geoip;

  /**
   * Visitor tracker.
   *
   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
   *   The config factory service.
   * @param \Drupal\Component\Datetime\TimeInterface $time
   *   The time service.
   * @param \Psr\Log\LoggerInterface $logger
   *   The logger service.
   * @param \Drupal\visitors\VisitorsCounterInterface $counter
   *   The counter service.
   * @param \Drupal\visitors\VisitorsCookieInterface $cookie
   *   The cookie service.
   * @param \Drupal\visitors\VisitorsDeviceInterface $device
   *   The device service.
   * @param \Drupal\visitors\VisitorsLocationInterface $location
   *   The location service.
   * @param \Drupal\visitors\VisitorsTrackerInterface $tracker
   *   The date service.
   * @param \Drupal\visitors\VisitorsSearchEngineInterface $search_engine
   *   The search engine service.
   * @param \Drupal\visitors\VisitorsSpamInterface $spam_service
   *   The spam service.
   * @param \Drupal\visitors\VisitorsSocialNetworksInterface $social_networks_service
   *   The social networks service.
   * @param \Drupal\visitors\VisitorsAiAssistantsInterface $ai_assistants_service
   *   The AI assistants service.
   * @param \Drupal\visitors\VisitorsCampaignInterface $campaign_service
   *   The campaign service.
   * @param \Drupal\maxmind_geoip\MaxMindGeoIpInterface|null $geoip
   *   The geoip service.
   */
  public function __construct(
    ConfigFactoryInterface $config_factory,
    TimeInterface $time,
    LoggerInterface $logger,
    VisitorsCounterInterface $counter,
    VisitorsCookieInterface $cookie,
    VisitorsDeviceInterface $device,
    VisitorsLocationInterface $location,
    VisitorsTrackerInterface $tracker,
    VisitorsSearchEngineInterface $search_engine,
    VisitorsSpamInterface $spam_service,
    VisitorsSocialNetworksInterface $social_networks_service,
    VisitorsAiAssistantsInterface $ai_assistants_service,
    VisitorsCampaignInterface $campaign_service,
    ?MaxMindGeoIpInterface $geoip = NULL,
  ) {

    $this->settings = $config_factory->get('visitors.settings');

    $this->time = $time;
    $this->logger = $logger;
    $this->counter = $counter;
    $this->cookie = $cookie;
    $this->device = $device;
    $this->location = $location;
    $this->tracker = $tracker;
    $this->searchEngine = $search_engine;
    $this->spamService = $spam_service;
    $this->socialNetworksService = $social_networks_service;
    $this->aiAssistantsService = $ai_assistants_service;
    $this->campaignService = $campaign_service;
    $this->geoip = $geoip;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container): VisitorsController {
    return new self(
      $container->get('config.factory'),
      $container->get('datetime.time'),
      $container->get('logger.channel.visitors'),
      $container->get('visitors.counter'),
      $container->get('visitors.cookie'),
      $container->get('visitors.device'),
      $container->get('visitors.location'),
      $container->get('visitors.tracker'),
      $container->get('visitors.search_engine'),
      $container->get('visitors.spam'),
      $container->get('visitors.social_networks'),
      $container->get('visitors.ai_assistants'),
      $container->get('visitors.campaign'),
      $container->get('maxmind_geoip.lookup', ContainerInterface::NULL_ON_INVALID_REFERENCE),
    );

  }

  /**
   * Tracks visits.
   */
  public function track(Request $request): Response {

    $server = $request->server;
    $query = $request->query->all();

    $response = $this->getResponse($query['send_image'] ?? FALSE);

    $request_time = $this->time->getRequestTime();

    $visit = $this->getDefaultFields();
    $log = [];

    $ip = $request->getClientIp();
    $visit['location_ip'] = $ip;
    $log['uid'] = $query['uid'] ?? 0;
    $visit['uid'] = $log['uid'] == 0 ? NULL : $log['uid'];
    $visit['entry_time'] = $request_time;
    $visit['exit_time'] = $request_time;
    $visit['total_time'] = 0;
    $visit['total_page_views'] = 0;
    $visit['total_events'] = 0;
    $visit['returning'] = 0;
    $visit['total_visits'] = 1;

    $log['title'] = $query['action_name'] ?? '';
    $log['page_view'] = $query['pv_id'] ?? NULL;
    $log['created'] = $request_time;

    $bot_retention_log = $this->settings->get('bot_retention_log');
    $discard_bot = ($bot_retention_log == -1);
    $this->doDeviceDetect($visit, $server);
    if ($discard_bot && $visit['bot']) {
      return $response;
    }

    $this->doVisitorId($visit, $query);
    $this->doUrl($log, $query);
    $this->doReferrer($log, $query);

    $custom_page_var = $query['cvar'] ?? NULL;
    $this->doCustom($log, $custom_page_var);

    $custom_event_var = $query['e_cvar'] ?? NULL;
    $this->doEvent($log, $custom_event_var);

    $this->doCounter($custom_page_var);

    $this->doConfig($visit, $query);
    $this->doPerformance($log, $query);

    $this->doLocalTime($visit, $query);

    $languages = $request->getLanguages() ?? [];
    $this->doLanguage($visit, $languages);
    $this->doLocation($visit, $ip, $languages);

    $this->doRefererFields($visit, $query);

    $visit_id = $this->tracker->getVisitId($visit, $request_time);
    $log['visit_id'] = $visit_id;

    // Write fields to database.
    $event_id = $this->tracker->writeEvent($log);

    $uid = $visit['uid'] ?? NULL;
    $this->tracker->updateVisit($visit_id, $event_id, $request_time, $uid);

    return $response;
  }

  /**
   * Get the response.
   *
   * @param bool $send_image
   *   Whether to send the image.
   *
   * @return \Symfony\Component\HttpFoundation\Response
   *   The response.
   */
  protected function getResponse(bool $send_image): Response {
    $headers = [
      'Cache-Control' => 'no-cache, no-store, must-revalidate',
      'Pragma' => 'no-cache',
      'Expires' => '0',
    ];
    $content = '';
    if ($send_image) {
      $content = $this->getImageContent();
      $headers['Content-Type'] = 'image/gif';
      $headers['Content-Length'] = strlen($content);
    }

    $response = new Response(
      $content,
      ($send_image) ? Response::HTTP_OK : Response::HTTP_NO_CONTENT,
      $headers,
    );

    return $response;
  }

  /**
   * Get the image content.
   *
   * @return string
   *   The image content.
   */
  protected function getImageContent(): string {
    return hex2bin('47494638396101000100800000000000FFFFFF21F9040100000000002C00000000010001000002024401003B');
  }

  /**
   * Get the default fields.
   *
   * @return array
   *   The default fields.
   */
  protected function getDefaultFields(): array {
    $fields = [
      'bot' => 0,
    ];

    return $fields;
  }

  /**
   * Detects the visitor url.
   *
   * @param string[] $fields
   *   The fields array.
   * @param string[] $query
   *   The query array.
   */
  protected function doUrl(array &$fields, array $query) {
    $url = $query['url'] ?? '';

    $visitors_url = new VisitorsUrl($url);

    $fields['url'] = $visitors_url->getUrl();
    $fields['url_prefix'] = $visitors_url->getPrefix();
  }

  /**
   * Detects the visitor id.
   *
   * @param string[] $fields
   *   The fields array.
   * @param string[] $query
   *   The query array.
   */
  protected function doVisitorId(array &$fields, array $query) {
    $visitor_id = $query['_id'] ?? $this->cookie->getId();
    $fields['visitor_id'] = $visitor_id;
  }

  /**
   * Detects the referrer.
   *
   * @param string[] $fields
   *   The fields array.
   * @param string[] $query
   *   The query array.
   */
  protected function doReferrer(array &$fields, array $query) {
    $referrer = $query['urlref'] ?? '';
    $fields['referrer_url'] = $referrer;
  }

  /**
   * Detects the device.
   *
   * @param string[] $fields
   *   The fields array.
   * @param \Symfony\Component\HttpFoundation\ServerBag $server
   *   The server array.
   */
  protected function doDeviceDetect(array &$fields, ServerBag $server) {
    if (!$this->device->hasLibrary()) {
      return NULL;
    }

    $user_agent = $server->get('HTTP_USER_AGENT', '');
    $this->device->doDeviceFields($fields, $user_agent, $server->all());

  }

  /**
   * Set the fields with data in the custom variable.
   */
  protected function doCustom(array &$fields, $cvar = NULL) {
    if (!is_null($cvar)) {
      $custom = json_decode($cvar);
      foreach ($custom as $c) {
        switch ($c[0]) {
          case 'path':
            $fields['path'] = $c[1];
            break;

          case 'route':
            $fields['route'] = $c[1];
            break;

          case 'server':
            $fields['server'] = $c[1];
            break;

        }
      }
    }
  }

  /**
   * Set the fields with data in the custom variable.
   */
  protected function doEvent(array &$fields, $cvar = NULL) {
    if (!is_null($cvar)) {
      $custom = json_decode($cvar);
      foreach ($custom as $c) {
        switch ($c[0]) {
          case 'plugin':
            $fields['plugin'] = $c[1];
            break;

          case 'event':
            $fields['event'] = $c[1];
            break;

          case 'plugin_int_1':
            $fields['plugin_int_1'] = $c[1];
            break;

          case 'plugin_int_2':
            $fields['plugin_int_2'] = $c[1];
            break;

          case 'plugin_var_1':
            $fields['plugin_var_1'] = $c[1];
            break;

          case 'plugin_var_2':
            $fields['plugin_var_2'] = $c[1];
            break;

          case 'plugin_var_3':
            $fields['plugin_var_3'] = $c[1];
            break;

          case 'plugin_var_4':
            $fields['plugin_var_4'] = $c[1];
            break;
        }
      }
    }
  }

  /**
   * Record the view of the entity.
   */
  protected function doCounter($cvar = NULL) {

    $viewed = NULL;
    if (!is_null($cvar)) {
      $custom = json_decode($cvar);
      foreach ($custom as $c) {
        if ($c[0] == 'viewed') {
          $viewed = $c[1];
        }
      }
    }

    if (!is_null($viewed)) {
      [$type, $id] = explode(':', $viewed);
      $this->counter->recordView($type, $id);
    }
  }

  /**
   * Set the configuration fields.
   */
  protected function doConfig(array &$fields, array $query) {

    $fields['config_resolution']   = $query['res'] ?? NULL;
    $fields['config_pdf']          = $query['pdf'] ?? NULL;
    $fields['config_flash']        = $query['fla'] ?? NULL;
    $fields['config_java']         = $query['java'] ?? NULL;
    $fields['config_quicktime']    = $query['qt'] ?? NULL;
    $fields['config_realplayer']   = $query['realp'] ?? NULL;
    $fields['config_windowsmedia'] = $query['wma'] ?? NULL;
    $fields['config_silverlight']  = $query['ag'] ?? NULL;
    $fields['config_cookie']       = $query['cookie'] ?? NULL;
  }

  /**
   * Set the performance fields.
   */
  protected function doPerformance(array &$fields, array $query) {
    $fields['pf_network']        = $query['pf_net'] ?? NULL;
    $fields['pf_server']         = $query['pf_srv'] ?? NULL;
    $fields['pf_transfer']       = $query['pf_tfr'] ?? NULL;
    $fields['pf_dom_processing'] = $query['pf_dm1'] ?? NULL;
    $fields['pf_dom_complete']   = $query['pf_dm2'] ?? NULL;
    $fields['pf_on_load']        = $query['pf_onl'] ?? NULL;

    $fields['pf_total'] = ($fields['pf_network'] ?? 0)
    + ($fields['pf_server'] ?? 0)
    + ($fields['pf_transfer'] ?? 0)
    + ($fields['pf_dom_processing'] ?? 0)
    + ($fields['pf_dom_complete'] ?? 0)
    + ($fields['pf_on_load'] ?? 0);
  }

  /**
   * Set the visitor's local time field.
   */
  protected function doLocalTime(array &$fields, array $query) {
    $hours = $query['h'] ?? NULL;
    $minutes = $query['m'] ?? NULL;
    $seconds = $query['s'] ?? NULL;

    $has_null = is_null($hours) || is_null($minutes) || is_null($seconds);
    if ($has_null) {
      return NULL;
    }

    $time = $hours * 3600 + $minutes * 60 + $seconds;

    $fields['localtime'] = $time;
  }

  /**
   * Set the language fields.
   */
  protected function doLanguage(array &$fields, array $languages) {
    if (empty($languages)) {
      return NULL;
    }

    $language = $languages[0] ?? '';
    $lang = explode('_', $language);
    $fields['location_browser_lang'] = $lang[0];

    // $fields['location_language'] = $language;
  }

  /**
   * Set the location fields.
   */
  protected function doLocation(array &$fields, $ip_address, $languages) {
    if (!empty($languages)) {
      $language = $languages[0] ?? '';
      $lang = explode('_', $language);
      $country_code = strtoupper($lang[1] ?? '');
      if ($this->location->isValidCountryCode($country_code)) {
        $fields['location_country'] = $country_code;
        $fields['location_continent'] = $this->location->getContinent($country_code);
      }
    }

    if (!$this->geoip) {
      return NULL;
    }

    /** @var \GeoIp2\Model\City|null $location */
    $location = $this->geoip->city($ip_address);
    if (!$location) {
      return NULL;
    }

    $fields['location_continent'] = $location->continent->code;
    $fields['location_country']   = $location->country->isoCode;
    $fields['location_region']    = $location->subdivisions[0]->isoCode;
    $fields['location_city']      = $location->city->names['en'];
    $fields['location_latitude']  = $location->location->latitude;
    $fields['location_longitude'] = $location->location->longitude;
  }

  /**
   * Set the referer fields.
   */
  protected function doRefererFields(array &$fields, array $query) {

    $referer = [];
    $fields['referer_url'] = $query['urlref'] ?? '';
    $url_ref_parts = parse_url($fields['referer_url']);
    $fields['referer_name'] = $url_ref_parts['host'] ?? '';
    $url_parts = parse_url($query['url'] ?? '');
    $same_host = ($url_parts['host'] ?? '') == $fields['referer_name'];

    if ($this->spamService->match($fields['referer_name'])) {
      $referer['referer_type'] = 'spam';
    }
    elseif ($data = $this->campaignService->parse($fields['referer_url'])) {
      $referer['referer_type'] = 'campaign';
      $referer['referer_name'] = $data['campaign'] ?? '';
      $referer['referer_keyword'] = $data['keyword'] ?? '';
    }
    elseif (empty($fields['referer_url'])) {
      $referer['referer_type'] = 'direct';
    }
    elseif ($same_host) {
      $referer['referer_type'] = 'internal';
    }
    elseif ($referer_name = $this->socialNetworksService->match($fields['referer_name'])) {
      $referer['referer_type'] = 'social_network';
      $referer['referer_name'] = $referer_name;
    }
    elseif ($referer_name = $this->aiAssistantsService->match($fields['referer_name'])) {
      $referer['referer_type'] = 'ai_assistant';
      $referer['referer_name'] = $referer_name;
    }
    elseif ($data = $this->searchEngine->match($fields['referer_url'])) {
      $referer['referer_type'] = 'search_engine';
      $referer['referer_name'] = $data['name'];
      $referer['referer_keyword'] = $data['keyword'];
    }
    else {
      $referer['referer_type'] = 'website';
    }

    $fields = array_merge($fields, $referer);

  }

}
