<?php

/**
 * Matomo - free/libre analytics platform
 *
 * @link    https://matomo.org
 * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
 */
namespace Piwik\Plugins\PrivacyManager\Model;

use Piwik\Common;
use Piwik\Date;
use Piwik\Db;
use Piwik\DbHelper;
use Piwik\Period\Factory as PeriodFactory;
use Piwik\Plugins\PrivacyManager\Dao\LogDataAnonymizer;
use Piwik\Site;
use Piwik\Validators\BaseValidator;
use Piwik\Validators\NotEmpty;
class LogDataAnonymizations
{
    /**
     * @var null|callable
     */
    private $onOutputCallback;
    /**
     * @var LogDataAnonymizer
     */
    private $logDataAnonymizer;
    private $tablePrefixed;
    public function __construct(LogDataAnonymizer $logDataAnonymizer)
    {
        $this->logDataAnonymizer = $logDataAnonymizer;
        $this->tablePrefixed = Common::prefixTable(self::getDbTableName());
    }
    public static function getDbTableName()
    {
        return 'privacy_logdata_anonymizations';
    }
    public function install()
    {
        DbHelper::createTable(self::getDbTableName(), "\n                  `idlogdata_anonymization` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,\n                  `idsites` TEXT NULL DEFAULT NULL,\n                  `date_start` DATETIME NOT NULL,\n                  `date_end` DATETIME NOT NULL,\n                  `anonymize_ip` TINYINT(1) UNSIGNED NOT NULL DEFAULT 0,\n                  `anonymize_location` TINYINT(1) UNSIGNED NOT NULL DEFAULT 0,\n                  `anonymize_userid` TINYINT(1) UNSIGNED NOT NULL DEFAULT 0,\n                  `unset_visit_columns` TEXT NOT NULL DEFAULT '',\n                  `unset_link_visit_action_columns` TEXT NOT NULL DEFAULT '',\n                  `output` MEDIUMTEXT NULL DEFAULT NULL,\n                  `scheduled_date` DATETIME NULL,\n                  `job_start_date` DATETIME NULL,\n                  `job_finish_date` DATETIME NULL,\n                  `requester` VARCHAR(100) NOT NULL DEFAULT '',\n                  PRIMARY KEY(`idlogdata_anonymization`), KEY(`job_start_date`)");
    }
    public function uninstall()
    {
        Db::query(sprintf('DROP TABLE IF EXISTS `%s`', $this->tablePrefixed));
    }
    public function getAllEntries()
    {
        $entries = Db::fetchAll(sprintf('SELECT * FROM `%s`', $this->tablePrefixed));
        return $this->enrichEntries($entries);
    }
    public function getEntry($idLogData)
    {
        $scheduled = Db::fetchRow(sprintf('SELECT * FROM `%s` WHERE idlogdata_anonymization = ?', $this->tablePrefixed), [$idLogData]);
        return $this->enrichEntry($scheduled);
    }
    public function getNextScheduledAnonymizationId()
    {
        $scheduled = Db::fetchOne(sprintf('SELECT idlogdata_anonymization FROM `%s` WHERE job_start_date is null ORDER BY idlogdata_anonymization asc LIMIT 1', $this->tablePrefixed));
        if (!empty($scheduled)) {
            return (int) $scheduled;
        }
        return $scheduled;
    }
    private function enrichEntries($entries)
    {
        if (empty($entries)) {
            return array();
        }
        foreach ($entries as $index => $entry) {
            $entries[$index] = $this->enrichEntry($entry);
        }
        return $entries;
    }
    private function enrichEntry($entry)
    {
        if (empty($entry)) {
            return $entry;
        }
        $entry['sites'] = array();
        if (!empty($entry['idsites'])) {
            $entry['idsites'] = json_decode($entry['idsites'], \true);
            foreach ($entry['idsites'] as $idSite) {
                try {
                    $entry['sites'][] = Site::getNameFor($idSite);
                } catch (\Exception $e) {
                    // site might be deleted
                    $entry['sites'][] = 'Site ID: ' . $idSite;
                }
            }
        } else {
            $entry['idsites'] = null;
            $entry['sites'][] = 'All Websites';
        }
        if (!empty($entry['unset_visit_columns'])) {
            $entry['unset_visit_columns'] = json_decode($entry['unset_visit_columns'], \true);
        } else {
            $entry['unset_visit_columns'] = array();
        }
        if (!empty($entry['unset_link_visit_action_columns'])) {
            $entry['unset_link_visit_action_columns'] = json_decode($entry['unset_link_visit_action_columns'], \true);
        } else {
            $entry['unset_link_visit_action_columns'] = array();
        }
        $entry['anonymize_ip'] = !empty($entry['anonymize_ip']);
        $entry['anonymize_location'] = !empty($entry['anonymize_location']);
        $entry['anonymize_userid'] = !empty($entry['anonymize_userid']);
        $entry['idlogdata_anonymization'] = (int) $entry['idlogdata_anonymization'];
        return $entry;
    }
    public function scheduleEntry($requester, $idSites, $dateString, $anonymizeIp, $anonymizeLocation, $anonymizeUserId, $unsetVisitColumns, $unsetLinkVisitActionColumns, $willBeStartedNow = \false)
    {
        BaseValidator::check('date', $dateString, [new NotEmpty()]);
        [$startDate, $endDate] = $this->getStartAndEndDate($dateString);
        // make sure valid date
        if (!empty($unsetVisitColumns)) {
            $this->logDataAnonymizer->checkAllVisitColumns($unsetVisitColumns);
        } else {
            $unsetVisitColumns = array();
        }
        if (!empty($unsetLinkVisitActionColumns)) {
            $this->logDataAnonymizer->checkAllLinkVisitActionColumns($unsetLinkVisitActionColumns);
        } else {
            $unsetLinkVisitActionColumns = array();
        }
        if (!empty($idSites) && $idSites !== 'all') {
            $idSites = array_map('intval', $idSites);
            $idSites = json_encode($idSites);
        } else {
            $idSites = null;
        }
        if (!$anonymizeIp && !$anonymizeLocation && !$anonymizeUserId && empty($unsetVisitColumns) && empty($unsetLinkVisitActionColumns)) {
            throw new \Exception('Nothing is selected to be anonymized');
        }
        $db = Db::get();
        $now = Date::now()->getDatetime();
        $values = array(
            'idsites' => $idSites,
            'date_start' => $startDate,
            'date_end' => $endDate,
            'anonymize_ip' => !empty($anonymizeIp) ? 1 : 0,
            'anonymize_location' => !empty($anonymizeLocation) ? 1 : 0,
            'anonymize_userid' => !empty($anonymizeUserId) ? 1 : 0,
            'unset_visit_columns' => json_encode($unsetVisitColumns),
            'unset_link_visit_action_columns' => json_encode($unsetLinkVisitActionColumns),
            'scheduled_date' => Date::now()->getDatetime(),
            'job_start_date' => $willBeStartedNow ? $now : null,
            // we set a start_date when executing from CLI to avoid a race condition to prevent a task from operating a scheduled entry
            'requester' => $requester,
        );
        $columns = implode('`,`', array_keys($values));
        $fields = Common::getSqlStringFieldsArray($values);
        $sql = sprintf('INSERT INTO `%s` (`%s`) VALUES(%s)', $this->tablePrefixed, $columns, $fields);
        $bind = array_values($values);
        $db->query($sql, $bind);
        $id = $db->lastInsertId();
        return (int) $id;
    }
    private function updateEntry($idLogDataAnonymization, $field, $value)
    {
        $query = sprintf('UPDATE `%s` SET %s = ? WHERE idlogdata_anonymization = ?', $this->tablePrefixed, $field);
        Db::query($query, array($value, $idLogDataAnonymization));
    }
    public function setCallbackOnOutput($callback)
    {
        $this->onOutputCallback = $callback;
    }
    private function appendToOutput($index, &$schedule, $message)
    {
        $schedule['output'] .= $message . "\n";
        $this->updateEntry($index, 'output', $schedule['output']);
        if ($this->onOutputCallback && is_callable($this->onOutputCallback)) {
            call_user_func($this->onOutputCallback, $message);
        }
    }
    public function getStartAndEndDate($date)
    {
        if (strpos($date, ',') === \false) {
            $period = PeriodFactory::build('day', $date);
        } else {
            $period = PeriodFactory::build('range', $date);
        }
        $startDate = $period->getDateTimeStart()->getDatetime();
        $endDate = $period->getDateTimeEnd()->getDatetime();
        return array($startDate, $endDate);
    }
    public function executeScheduledEntry($idLogData)
    {
        $schedule = $this->getEntry($idLogData);
        if (empty($schedule)) {
            throw new \Exception('Entry not found');
        }
        $this->updateEntry($idLogData, 'job_start_date', Date::now()->getDatetime());
        $idSites = $schedule['idsites'];
        $startDate = $schedule['date_start'];
        $endDate = $schedule['date_end'];
        if (empty($idSites)) {
            $idSites = null;
            $this->appendToOutput($idLogData, $schedule, "Running behaviour on all sites.");
        } else {
            $this->appendToOutput($idLogData, $schedule, 'Running behaviour on these sites: ' . implode(', ', $idSites));
        }
        $this->appendToOutput($idLogData, $schedule, sprintf("Applying this to visits between '%s' and '%s'.", $startDate, $endDate));
        if ($schedule['anonymize_ip'] || $schedule['anonymize_location'] || $schedule['anonymize_userid']) {
            $this->appendToOutput($idLogData, $schedule, 'Starting to anonymize visit information.');
            try {
                $numAnonymized = $this->logDataAnonymizer->anonymizeVisitInformation($idSites, $startDate, $endDate, $schedule['anonymize_ip'], $schedule['anonymize_location'], $schedule['anonymize_userid']);
                $this->appendToOutput($idLogData, $schedule, 'Number of anonymized IP and/or location and/or User ID: ' . $numAnonymized);
            } catch (\Exception $e) {
                $this->appendToOutput($idLogData, $schedule, 'Failed to anonymize IP and/or location and/or User ID:' . $e->getMessage());
            }
        }
        if (!empty($schedule['unset_visit_columns'])) {
            try {
                $this->appendToOutput($idLogData, $schedule, 'Starting to unset log_visit table entries.');
                $numColumnsUnset = $this->logDataAnonymizer->unsetLogVisitTableColumns($idSites, $startDate, $endDate, $schedule['unset_visit_columns']);
                $this->appendToOutput($idLogData, $schedule, 'Number of unset log_visit table entries: ' . $numColumnsUnset);
            } catch (\Exception $e) {
                $this->appendToOutput($idLogData, $schedule, 'Failed to unset log_visit table entries:' . $e->getMessage());
            }
            try {
                $this->appendToOutput($idLogData, $schedule, 'Starting to unset log_conversion table entries (if possible).');
                $numColumnsUnset = $this->logDataAnonymizer->unsetLogConversionTableColumns($idSites, $startDate, $endDate, $schedule['unset_visit_columns']);
                $this->appendToOutput($idLogData, $schedule, 'Number of unset log_conversion table entries: ' . $numColumnsUnset);
            } catch (\Exception $e) {
                $this->appendToOutput($idLogData, $schedule, 'Failed to unset log_conversion table entries:' . $e->getMessage());
            }
        }
        if (!empty($schedule['unset_link_visit_action_columns'])) {
            try {
                $this->appendToOutput($idLogData, $schedule, 'Starting to unset log_link_visit_action table entries.');
                $numColumnsUnset = $this->logDataAnonymizer->unsetLogLinkVisitActionColumns($idSites, $startDate, $endDate, $schedule['unset_link_visit_action_columns']);
                $this->appendToOutput($idLogData, $schedule, 'Number of unset log_link_visit_action table entries: ' . $numColumnsUnset);
            } catch (\Exception $e) {
                $this->appendToOutput($idLogData, $schedule, 'Failed to unset log_link_visit_action table entries:' . $e->getMessage());
            }
        }
        $this->updateEntry($idLogData, 'job_finish_date', Date::now()->getDatetime());
    }
}
