<?php

/**
 * Tirreno ~ Open source user analytics
 * Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
 *
 * Licensed under GNU Affero General Public License version 3 of the or any later version.
 * For full copyright and license information, please see the LICENSE
 * Redistributions of files must retain the above copyright notice.
 *
 * @copyright     Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
 * @license       https://opensource.org/licenses/AGPL-3.0 AGPL License
 * @link          https://www.tirreno.com Tirreno(tm)
 */

namespace Models\Context;

class Data {
    private \Models\Context\User $userModel;
    private \Models\Context\Ip $ipModel;
    private \Models\Context\Device $deviceModel;
    private \Models\Context\Email $emailModel;
    private \Models\Context\Phone $phoneModel;
    private \Models\Context\Event $eventModel;
    private \Models\Context\Session $sessionModel;
    private \Models\ApiKeys $keyModel;

    private array $suspiciousWordsUrl;
    private array $suspiciousWordsUserAgent;
    private array $suspiciousWordsEmail;

    public function __construct() {
        $this->userModel    = new User();
        $this->ipModel      = new Ip();
        $this->deviceModel  = new Device();
        $this->emailModel   = new Email();
        $this->phoneModel   = new Phone();
        $this->eventModel   = new Event();
        $this->sessionModel = new Session();
        $this->keyModel     = new \Models\ApiKeys();

        $this->suspiciousWordsUrl       = \Utils\WordsLists\Url::getWords();
        $this->suspiciousWordsUserAgent = \Utils\WordsLists\UserAgent::getWords();
        $this->suspiciousWordsEmail     = \Utils\WordsLists\Email::getWords();
    }

    public function getContext(array $accountIds, int $apiKey): array {
        $userDetails        = $this->userModel->getContext($accountIds, $apiKey);
        $ipDetails          = $this->ipModel->getContext($accountIds, $apiKey);
        $deviceDetails      = $this->deviceModel->getContext($accountIds, $apiKey);
        $emailDetails       = $this->emailModel->getContext($accountIds, $apiKey);
        $phoneDetails       = $this->phoneModel->getContext($accountIds, $apiKey);
        $eventDetails       = $this->eventModel->getContext($accountIds, $apiKey);
        //$domainDetails    = $this->domainModel->getContext($accountIds, $apiKey);

        $timezoneName       = $this->keyModel->getTimezoneByKeyId($apiKey);
        $utcTime            = new \DateTime('now', new \DateTimeZone('UTC'));
        $timezone           = new \DateTimeZone($timezoneName);
        $offsetInSeconds    = $timezone->getOffset($utcTime);

        // get only suspicious sessions
        $sessionDetails     = $this->sessionModel->getContext($accountIds, $apiKey, $offsetInSeconds);

        //Extend user details context
        foreach ($userDetails as $userId => $user) {
            $user['le_exists']      = ($user['le_email'] ?? null) !== null;
            $user['le_email']       = $user['le_email'] ?? '';
            $user['le_local_part']  = explode('@', $user['le_email'])[0] ?? '';
            $user['le_domain_part'] = explode('@', $user['le_email'])[1] ?? '';

            $userId     = $user['ea_id'];
            $ip         = $ipDetails[$userId] ?? [];
            $device     = $deviceDetails[$userId] ?? [];
            $email      = $emailDetails[$userId] ?? [];
            $phone      = $phoneDetails[$userId] ?? [];
            $events     = $eventDetails[$userId] ?? [];
            $session    = $sessionDetails[$userId] ?? [];

            $user['eip_ip_id']              = $ip['eip_ip_id'] ?? [];
            $user['eip_ip']                 = $ip['eip_ip'] ?? [];
            $user['eip_cidr']               = $ip['eip_cidr'] ?? [];
            $user['eip_country_id']         = $ip['eip_country_id'] ?? [];
            $user['eip_data_center']        = $ip['eip_data_center'] ?? [];
            $user['eip_tor']                = $ip['eip_tor'] ?? [];
            $user['eip_vpn']                = $ip['eip_vpn'] ?? [];
            $user['eip_relay']              = $ip['eip_relay'] ?? [];
            $user['eip_starlink']           = $ip['eip_starlink'] ?? [];
            $user['eip_total_visit']        = $ip['eip_total_visit'] ?? [];
            $user['eip_blocklist']          = $ip['eip_blocklist'] ?? [];
            $user['eip_shared']             = $ip['eip_shared'] ?? [];
            //$user['eip_domains']          = $ip['eip_domains'] ?? [];
            $user['eip_country_id']         = $ip['eip_country_id'] ?? [];
            $user['eip_fraud_detected']     = $ip['eip_fraud_detected'] ?? [];
            $user['eip_alert_list']         = $ip['eip_alert_list'] ?? [];
            $user['eip_domains_count_len']  = $ip['eip_domains_count_len'] ?? [];

            $user['eup_device']             = $device['eup_device'] ?? [];
            $user['eup_device_id']          = $device['eup_device_id'] ?? [];
            $user['eup_browser_name']       = $device['eup_browser_name'] ?? [];
            $user['eup_browser_version']    = $device['eup_browser_version'] ?? [];
            $user['eup_os_name']            = $device['eup_os_name'] ?? [];
            $user['eup_lang']               = $device['eup_lang'] ?? [];
            $user['eup_ua']                 = $device['eup_ua'] ?? [];
            // $user['eup_lastseen']        = $device['eup_lastseen'] ?? [];
            // $user['eup_created']         = $device['eup_created'] ?? [];

            $user['ee_email']               = $email['ee_email'] ?? [];
            $user['ee_earliest_breach']     = $email['ee_earliest_breach'] ?? [];

            $user['ep_phone_number']        = $phone['ep_phone_number'] ?? [];
            $user['ep_shared']              = $phone['ep_shared'] ?? [];
            $user['ep_type']                = $phone['ep_type'] ?? [];

            $user['event_ip']               = $events['event_ip'] ?? [];
            $user['event_url_string']       = $events['event_url_string'] ?? [];
            $user['event_empty_referer']    = $events['event_empty_referer'] ?? [];
            $user['event_device']           = $events['event_device'] ?? [];
            $user['event_type']             = $events['event_type'] ?? [];
            $user['event_http_method']      = $events['event_http_method'] ?? [];
            $user['event_http_code']        = $events['event_http_code'] ?? [];
            $user['event_device_created']   = $events['event_device_created'] ?? [];
            $user['event_device_lastseen']  = $events['event_device_lastseen'] ?? [];

            $user['event_session_multiple_country'] = $session[0]['event_session_multiple_country'] ?? false;
            $user['event_session_multiple_ip']      = $session[0]['event_session_multiple_ip'] ?? false;
            $user['event_session_multiple_device']  = $session[0]['event_session_multiple_device'] ?? false;
            $user['event_session_night_time']       = $session[0]['event_session_night_time'] ?? false;

            //Extra params for rules
            $user = $this->extendParams($user);

            $userDetails[$userId] = $this->extendEventParams($user);
        }

        return $userDetails;
    }

    private function extendParams(array $record): array {
        //$record['timezone']

        $localPartLen   = strlen($record['le_local_part']);
        $domainPartLen  = strlen($record['le_domain_part']);
        $fullName       = $this->getUserFullName($record);

        $record['le_local_part_len']                = $localPartLen;
        $record['ea_fullname_has_numbers']          = preg_match('~[0-9]+~', $fullName) > 0;
        $record['ea_fullname_has_spaces_hyphens']   = preg_match('~[\-\s]~', $fullName) > 0;
        $record['ea_days_since_account_creation']   = $this->getDaysSinceAccountCreation($record);
        $record['ea_days_since_last_visit']         = $this->getDaysSinceLastVisit($record);

        //$record['le_has_no_profiles']               = $record['le_profiles'] === 0;
        $record['le_has_no_data_breaches']          = $record['le_data_breach'] === false;
        $record['le_has_suspicious_str']            = $this->checkEmailForSuspiciousString($record);
        $record['le_has_numeric_only_local_part']   = preg_match('/^[0-9]+$/', $record['le_local_part']) > 0;
        $record['le_email_has_consec_s_chars']      = preg_match('/[^a-zA-Z0-9]{2,}/', $record['le_local_part']) > 0;
        $record['le_email_has_consec_nums']         = preg_match('/\d{2}/', $record['le_local_part']) > 0;
        $record['le_email_has_no_digits']           = !preg_match('/\d/', $record['le_local_part']);
        $record['le_email_has_vowels']              = preg_match('/[aeoui]/i', $record['le_local_part']) > 0;
        $record['le_email_has_consonants']          = preg_match('/[bcdfghjklmnpqrstvwxyz]/i', $record['le_local_part']) > 0;

        $record['le_with_long_local_part_length']   = $localPartLen > \Utils\Constants::get('RULE_EMAIL_MAXIMUM_LOCAL_PART_LENGTH');
        $record['le_with_long_domain_length']       = $domainPartLen > \Utils\Constants::get('RULE_EMAIL_MAXIMUM_DOMAIN_LENGTH');
        $record['le_email_in_blockemails']          = $record['le_blockemails'] ?? false;
        $record['le_is_invalid']                    = $record['le_exists'] && filter_var($record['le_email'], FILTER_VALIDATE_EMAIL) === false;

        $record['le_appears_on_alert_list']         = $record['le_alert_list'] ?? false;

        $record['ld_is_disposable']                 = $record['ld_disposable_domains'] ?? false;
        $record['ld_days_since_domain_creation']    = $this->getDaysSinceDomainCreation($record);
        $record['ld_domain_free_email_provider']    = $record['ld_free_email_provider'] ?? false;
        $record['ld_from_blockdomains']             = $record['ld_blockdomains'] ?? false;
        $record['ld_domain_without_mx_record']      = $record['ld_mx_record'] === false;
        $record['ld_website_is_disabled']           = $record['ld_disabled'] ?? false;
        $record['ld_tranco_rank']                   = $record['ld_tranco_rank'] ?? -1;

        $record['lp_invalid_phone'] = $record['lp_invalid'] === true;
        $record['ep_shared_phone']  = (bool) count(array_filter($record['ep_shared'], static function ($item) {
            return $item !== null && $item > 1;
        }));

        $daysSinceBreaches = array_map(function ($item) {
            return $this->getDaysTillToday($item);
        }, $record['ee_earliest_breach']);

        $record['ee_days_since_first_breach'] = count($daysSinceBreaches) ? max($daysSinceBreaches) : -1;

        $onlyNonResidentialParams = !(bool) count(array_filter(array_merge(
            $record['eip_fraud_detected'],
            $record['eip_blocklist'],
            $record['eip_tor'],
            $record['eip_starlink'],
            $record['eip_relay'],
            $record['eip_vpn'],
            $record['eip_data_center'],
        ), static function ($value): bool {
            return $value === true;
        }));
        $record['eip_only_residential'] = $onlyNonResidentialParams && !in_array(0, $record['eip_country_id']);
        $record['eip_has_fraud']        = in_array(true, $record['eip_fraud_detected']);
        $record['eip_unique_cidrs']     = count(array_unique($record['eip_cidr']));
        $record['lp_fraud_detected']    = $record['lp_fraud_detected'] ?? false;
        $record['le_fraud_detected']    = $record['le_fraud_detected'] ?? false;

        $record['eup_has_rare_browser'] = (bool) count(array_diff($record['eup_browser_name'], array_keys(\Utils\Constants::get('RULE_REGULAR_BROWSER_NAMES'))));
        $record['eup_has_rare_os']      = (bool) count(array_diff($record['eup_os_name'], \Utils\Constants::get('RULE_REGULAR_OS_NAMES')));
        $record['eup_device_count']     = count($record['eup_device']);

        $record['eup_vulnerable_ua']    = false;

        if (count($this->suspiciousWordsUserAgent)) {
            foreach ($record['eup_ua'] as $url) {
                foreach ($this->suspiciousWordsUserAgent as $sub) {
                    if (stripos($url, $sub) !== false) {
                        $record['eup_vulnerable_ua'] = true;
                        break 2;
                    }
                }
            }
        }

        return $record;
    }

    private function extendEventParams(array $record): array {
        // Remove null values specifically
        $eventTypeFiltered                  = $this->filterStringNum($record['event_type']);
        $eventHttpCodeFiltered              = $this->filterStringNum($record['event_http_code']);

        $eventTypeCount                     = array_count_values($eventTypeFiltered);

        //$accountLoginFailId = \Utils\Constants::get('ACCOUNT_LOGIN_FAIL_EVENT_TYPE_ID');
        $accountEmailChangeId               = \Utils\Constants::get('ACCOUNT_EMAIL_CHANGE_EVENT_TYPE_ID');
        $accountPwdChangeId                 = \Utils\Constants::get('ACCOUNT_PASSWORD_CHANGE_EVENT_TYPE_ID');

        //$record['event_failed_login_attempts'] = $eventTypeCount[$accountLoginFailId] ?? 0;
        $record['event_email_changed']      = array_key_exists($accountEmailChangeId, $eventTypeCount);
        $record['event_password_changed']   = array_key_exists($accountPwdChangeId, $eventTypeCount);

        $record['event_http_method_head']   = in_array(\Utils\Constants::get('EVENT_REQUEST_TYPE_HEAD'), $record['event_http_method']);

        $record['event_empty_referer']      = in_array(true, $record['event_empty_referer'], true);

        $clientErrors   = 0;
        $serverErrors   = 0;
        $successEvents  = 0;
        foreach ($eventHttpCodeFiltered as $code) {
            if (is_int($code) && $code >= 400 && $code < 500) {
                ++$clientErrors;
            } elseif (is_int($code) && $code >= 500 && $code < 600) {
                ++$serverErrors;
            } elseif (is_int($code) && $code >= 200 && $code < 300) {
                ++$successEvents;
            }
        }

        $record['event_multiple_5xx_http']  = $serverErrors;
        $record['event_multiple_4xx_http']  = $clientErrors;

        $record['event_2xx_http']           = (bool) $successEvents;

        $record['event_vulnerable_url']     = false;

        if (count($this->suspiciousWordsUrl)) {
            foreach ($record['event_url_string'] as $url) {
                foreach ($this->suspiciousWordsUrl as $sub) {
                    if (stripos($url, $sub) !== false) {
                        $record['event_vulnerable_url'] = true;
                        break 2;
                    }
                }
            }
        }

        return $record;
    }

    private function getDaysSinceDomainCreation(array $params): int {
        $dt1 = date('Y-m-d');
        $dt2 = $params['ld_creation_date'];

        return $this->getDaysDiff($dt1, $dt2);
    }

    private function getDaysSinceAccountCreation(array $params): int {
        $dt1 = date('Y-m-d');
        $dt2 = $params['ea_created'] ?? null;

        return $this->getDaysDiff($dt1, $dt2);
    }

    private function getDaysSinceLastVisit(array $params): int {
        $dt1 = date('Y-m-d');
        $dt2 = $params['ea_lastseen'] ?? null;

        return $this->getDaysDiff($dt1, $dt2);
    }

    private function getDaysTillToday(?string $dt2): int {
        $diff = -1;

        if ($dt2 !== null) {
            $dt1 = date('Y-m-d');
            $dt1 = new \DateTime($dt1);
            $dt2 = new \DateTime($dt2);
            $diff = $dt1->diff($dt2)->format('%a');
        }

        return $diff;
    }

    private function getDaysDiff(?string $dt1, ?string $dt2): int {
        $diff = -1;

        if ($dt2) {
            $dt1 = new \DateTime($dt1);
            $dt2 = new \DateTime($dt2);
            $diff = $dt1->diff($dt2)->format('%a');
        }

        return $diff;
    }

    private function getUserFullName(array $record): string {
        $name = [];
        $fName = $record['ea_firstname'] ?? '';
        if ($fName) {
            $name[] = $fName;
        }

        $lName = $record['ea_lastname'] ?? '';
        if ($lName) {
            $name[] = $lName;
        }

        return trim(join(' ', $name));
    }

    private function checkEmailForSuspiciousString(array $record): bool {
        foreach ($this->suspiciousWordsEmail as $sub) {
            if (stripos($record['le_email'], $sub) !== false) {
                return true;
            }
        }

        return false;
    }

    private function filterStringNum(array $record): array {
        return array_filter($record, static function ($value): bool {
            return is_string($value) || is_int($value);
        });
    }
}
