<?php
/*************************************************************************************/
/*      This file is part of the Thelia package.                                     */
/*                                                                                   */
/*      Copyright (c) OpenStudio                                                     */
/*      email : dev@thelia.net                                                       */
/*      web : http://www.thelia.net                                                  */
/*                                                                                   */
/*      For the full copyright and license information, please view the LICENSE.txt  */
/*      file that was distributed with this source code.                             */
/*************************************************************************************/

namespace Thelia\Install;

use Michelf\Markdown;
use PDO;
use PDOException;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Finder\Finder;
use Symfony\Component\Translation\Translator;
use Symfony\Component\Yaml\Exception\ParseException;
use Symfony\Component\Yaml\Yaml;
use Thelia\Config\DatabaseConfigurationSource;
use Thelia\Install\Exception\UpdateException;
use Thelia\Install\Exception\UpToDateException;
use Thelia\Log\Tlog;
use Thelia\Tools\Version\Version;

/**
 * Class Update
 * @package Thelia\Install
 * @author Manuel Raynaud <manu@raynaud.io>
 */
class Update
{
    const SQL_DIR = 'update/sql/';

    const PHP_DIR = 'update/php/';

    const INSTRUCTION_DIR = 'update/instruction/';

    protected $version = null;

    /** @var bool */
    protected $usePropel = null;

    /** @var null|Tlog */
    protected $logger = null;

    /** @var array log messages */
    protected $logs = [];

    /** @var array post instructions */
    protected $postInstructions = [];

    /** @var array */
    protected $updatedVersions = [];

    /** @var PDO  */
    protected $connection = null;

    /** @var string|null  */
    protected $backupFile = null;

    /** @var string */
    protected $backupDir = 'local/backup/';

    /** @var array */
    protected $messages = [];

    /** @var Translator */
    protected $translator;

    public function __construct($usePropel = true)
    {
        $this->usePropel = $usePropel;

        if ($this->usePropel) {
            $this->logger = Tlog::getInstance();
            $this->logger->setLevel(Tlog::DEBUG);
        } else {
            $this->logs = [];
        }

        $dbConfig = null;

        try {
            $this->connection = $this->getDatabasePDO();
        } catch (ParseException $ex) {
            throw new UpdateException("database.yml is not a valid file : " . $ex->getMessage());
        } catch (\PDOException $ex) {
            throw new UpdateException('Wrong connection information' . $ex->getMessage());
        }

        $this->version = $this->getVersionList();
    }

    /**
     * retrieve the database connection
     *
     * @return \PDO
     * @throws ParseException
     * @throws \PDOException
     */
    protected function getDatabasePDO()
    {
        $configPath = THELIA_CONF_DIR . "database.yml";

        if (!file_exists($configPath)) {
            throw new UpdateException("Thelia is not installed yet");
        }

        $definePropel = new DatabaseConfigurationSource(
            Yaml::parse(file_get_contents($configPath)),
            $this->getEnvParameters()
        );

        return $definePropel->getTheliaConnectionPDO();
    }

    /**
     * Gets the environment parameters.
     *
     * Only the parameters starting with "SYMFONY__" are considered.
     *
     * @return array An array of parameters
     */
    protected function getEnvParameters()
    {
        $parameters = array();
        foreach ($_SERVER as $key => $value) {
            if (0 === strpos($key, 'SYMFONY__')) {
                $parameters[strtolower(str_replace('__', '.', substr($key, 9)))] = $value;
            }
        }

        return $parameters;
    }

    public function isLatestVersion($version = null)
    {
        if (null === $version) {
            $version = $this->getCurrentVersion();
        }
        $lastEntry = end($this->version);

        return $lastEntry == $version;
    }

    public function process()
    {
        $this->updatedVersions = array();

        $currentVersion = $this->getCurrentVersion();
        $this->log('debug', "start update process");

        if (true === $this->isLatestVersion($currentVersion)) {
            $this->log('debug', "You already have the latest version. No update available");
            throw new UpToDateException('You already have the latest version. No update available');
        }

        $index = array_search($currentVersion, $this->version);

        $this->connection->beginTransaction();

        $database = new Database($this->connection);
        $version = null;

        try {
            $size = \count($this->version);

            for ($i = ++$index; $i < $size; $i++) {
                $version = $this->version[$i];
                $this->updateToVersion($version, $database);
                $this->updatedVersions[] = $version;
            }

            $currentVersion = Version::parse();
            $this->log('debug', sprintf('setting database configuration to %s', $currentVersion['version']));
            $updateConfigVersion = [
                'thelia_version'         => $currentVersion['version'],
                'thelia_major_version'   => $currentVersion['major'],
                'thelia_minus_version'   => $currentVersion['minus'],
                'thelia_release_version' => $currentVersion['release'],
                'thelia_extra_version'    => $currentVersion['extra'],
            ];

            foreach ($updateConfigVersion as $name => $value) {
                $stmt = $this->connection->prepare('SELECT * FROM `config` WHERE `name` = ?');
                $stmt->execute([$name]);

                if ($stmt->rowCount()) {
                    $stmt = $this->connection->prepare('UPDATE `config` SET `value` = ? WHERE `name` = ?');
                    $stmt->execute([$version, $value]);
                } else {
                    $stmt = $this->connection->prepare('INSERT INTO `config` (?) VALUES (?)');
                    $stmt->execute([$version, $value]);
                }
            }

            $this->connection->commit();
            $this->log('debug', 'update successfully');
        } catch (\Exception $e) {
            $this->connection->rollBack();

            $this->log('error', sprintf('error during update process with message : %s', $e->getMessage()));

            $ex = new UpdateException($e->getMessage(), $e->getCode(), $e->getPrevious());
            $ex->setVersion($version);
            throw $ex;
        }
        $this->log('debug', 'end of update processing');

        return $this->updatedVersions;
    }

    /**
     * Backup current DB to file local/backup/update.sql
     * @return bool if it succeeds, false otherwise
     * @throws \Exception
     */
    public function backupDb()
    {
        $database = new Database($this->connection);

        if (! $this->checkBackupIsPossible()) {
            $message = 'Your database is too big for an automatic backup';

            $this->log('error', $message);

            throw new UpdateException($message);
        }

        $this->backupFile = THELIA_ROOT . $this->backupDir . 'update.sql';
        $backupDir = THELIA_ROOT . $this->backupDir;

        $fs = new Filesystem();

        try {
            $this->log('debug', sprintf('Backup database to file : %s', $this->backupFile));

            // test if backup dir exists
            if (!$fs->exists($backupDir)) {
                $fs->mkdir($backupDir);
            }

            if (!is_writable($backupDir)) {
                throw new \RuntimeException(sprintf('impossible to write in directory : %s', $backupDir));
            }

            // test if backup file already exists
            if ($fs->exists($this->backupFile)) {
                // remove file
                $fs->remove($this->backupFile);
            }

            $database->backupDb($this->backupFile);
        } catch (\Exception $ex) {
            $this->log('error', sprintf('error during backup process with message : %s', $ex->getMessage()));
            throw $ex;
        }
    }

    /**
     * Restores file local/backup/update.sql to current DB
     *
     * @return bool if it succeeds, false otherwise
     */
    public function restoreDb()
    {
        $database = new Database($this->connection);

        try {
            $this->log('debug', sprintf('Restore database with file : %s', $this->backupFile));

            if (!file_exists($this->backupFile)) {
                return false;
            }

            $database->restoreDb($this->backupFile);
        } catch (\Exception $ex) {
            $this->log('error', sprintf('error during restore process with message : %s', $ex->getMessage()));
            print $ex->getMessage();
            return false;
        }

        return true;
    }

    /**
     * @return null|string
     */
    public function getBackupFile()
    {
        return $this->backupFile;
    }

    public function getLogs()
    {
        return $this->logs;
    }

    protected function log($level, $message)
    {
        if ($this->usePropel) {
            switch ($level) {
                case 'debug':
                    $this->logger->debug($message);
                    break;
                case 'info':
                    $this->logger->info($message);
                    break;
                case 'notice':
                    $this->logger->notice($message);
                    break;
                case 'warning':
                    $this->logger->warning($message);
                    break;
                case 'error':
                    $this->logger->error($message);
                    break;
                case 'critical':
                    $this->logger->critical($message);
                    break;
            }
        } else {
            $this->logs[] = [$level, $message];
        }
    }

    protected function updateToVersion($version, Database $database)
    {
        // sql update
        $filename = sprintf(
            "%s%s%s",
            THELIA_SETUP_DIRECTORY,
            str_replace('/', DS, self::SQL_DIR),
            $version . '.sql'
        );

        if (file_exists($filename)) {
            $this->log('debug', sprintf('inserting file %s', $version . '.sql'));
            $database->insertSql(null, [$filename]);
            $this->log('debug', sprintf('end inserting file %s', $version . '.sql'));
        }

        // php update
        $filename = sprintf(
            "%s%s%s",
            THELIA_SETUP_DIRECTORY,
            str_replace('/', DS, self::PHP_DIR),
            $version . '.php'
        );

        if (file_exists($filename)) {
            $this->log('debug', sprintf('executing file %s', $version . '.php'));
            include_once($filename);
            $this->log('debug', sprintf('end executing file %s', $version . '.php'));
        }

        // instructions
        $filename = sprintf(
            "%s%s%s",
            THELIA_SETUP_DIRECTORY,
            str_replace('/', DS, self::INSTRUCTION_DIR),
            $version . '.md'
        );

        if (file_exists($filename)) {
            $this->addPostInstructions($version, file_get_contents($filename));
        }

        $this->setCurrentVersion($version);
    }

    public function getCurrentVersion()
    {
        $stmt = $this->connection->query("SELECT `value` FROM `config` WHERE name='thelia_version'");

        return $stmt->fetchColumn();
    }

    public function setCurrentVersion($version)
    {
        $currentVersion = null;

        if (null !== $this->connection) {
            try {
                $stmt = $this->connection->prepare('UPDATE config set value = ? where name = ?');
                $stmt->execute([$version, 'thelia_version']);
            } catch (PDOException $e) {
                $this->log('error', sprintf('Error setting current version : %s', $e->getMessage()));

                throw $e;
            }
        }
    }

    /**
     * Returns the database size in Mo
     * @return float
     * @throws \Exception
     */
    public function getDataBaseSize()
    {
        $stmt = $this->connection->query(
            "SELECT sum(data_length) / 1024 / 1024 'size' FROM information_schema.TABLES WHERE table_schema = DATABASE() GROUP BY table_schema"
        );

        if ($stmt->rowCount()) {
            return \floatval($stmt->fetch(PDO::FETCH_OBJ)->size);
        }

        throw new \Exception('Impossible to calculate the database size');
    }


    /**
     * Checks whether it is possible to make a data base backup
     *
     * @return bool
     */
    public function checkBackupIsPossible()
    {
        $size = 0;
        if (preg_match('/^(\d+)(.)$/', ini_get('memory_limit'), $matches)) {
            switch (strtolower($matches[2])) {
                case 'k':
                    $size = $matches[1] / 1024;
                    break;
                case 'm':
                    $size = $matches[1];
                    break;
                case 'g':
                    $size = $matches[1] * 1024;
                    break;
            }
        }

        if ($this->getDataBaseSize() > ($size - 64) / 8) {
            return false;
        }

        return true;
    }

    public function getLatestVersion()
    {
        return end($this->version);
    }

    public function getVersions()
    {
        return $this->version;
    }

    /**
     * @return array
     */
    public function getUpdatedVersions()
    {
        return $this->updatedVersions;
    }

    /**
     * @param array $updatedVersions
     */
    public function setUpdatedVersions($updatedVersions)
    {
        $this->updatedVersions = $updatedVersions;
    }

    /**
     * Add a new post update instruction
     *
     * @param string $instructions content of the instruction un markdown format
     */
    protected function addPostInstructions($version, $instructions)
    {
        if (!isset($this->postInstructions[$version])) {
            $this->postInstructions[$version] = [];
        }

        $this->postInstructions[$version][] = $instructions;
    }

    /**
     * Return the content of all instructions
     *
     * @param string $format the format of the export : plain (default) or html
     * @return string the instructions in plain text or html
     */
    public function getPostInstructions($format = 'plain')
    {
        $content = [];

        if (\count($this->postInstructions) == 0) {
            return null;
        }

        ksort($this->postInstructions);

        foreach ($this->postInstructions as $version => $instructions) {
            $content[] = sprintf("## %s", $version);
            foreach ($instructions as $instruction) {
                $content[] = sprintf("%s", $instruction);
            }
        }

        $content = implode("\n\n", $content);

        if ($format === 'html') {
            $content = Markdown::defaultTransform($content);
        }

        return $content;
    }

    public function hasPostInstructions()
    {
        return (\count($this->postInstructions) !== 0);
    }

    public function getVersionList()
    {
        $list = [];
        $finder = new Finder();
        $path = sprintf("%s%s", THELIA_SETUP_DIRECTORY, str_replace('/', DS, self::SQL_DIR));
        $sort = function (\SplFileInfo $a, \SplFileInfo $b) {
            $a = strtolower(substr($a->getRelativePathname(), 0, -4));
            $b = strtolower(substr($b->getRelativePathname(), 0, -4));
            return version_compare($a, $b);
        };

        $files = $finder->name('*.sql')->in($path)->sort($sort);
        foreach ($files as $file) {
            $list[] = substr($file->getRelativePathname(), 0, -4);
        }
        return $list;
    }

    /**
     * @param string $message
     * @param string $type
     * @return $this
     */
    public function setMessage($message, $type = 'info')
    {
        $this->messages[] = [$message, $type];

        return $this;
    }

    /**
     * @return array
     */
    public function getMessages()
    {
        return $this->messages;
    }

    /**
     * @param string $string
     * @return string
     */
    public function trans($string)
    {
        return $this->translator ? $this->translator->trans($string) : $string;
    }

    /**
     * @param Translator $translator
     * @return $this
     */
    public function setTranslator(Translator $translator)
    {
        $this->translator = $translator;

        return $this;
    }

    public function getWebVersion()
    {
        $url = "http://thelia.net/version.php";
        $curl = curl_init($url);
        curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($curl, CURLOPT_HEADER, false);
        curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, 5);
        curl_setopt($curl, CURLOPT_TIMEOUT, 5);
        $res = curl_exec($curl);

        try {
            if (Version::parse($res)) {
                return trim($res);
            }
        } catch (\Exception $e) {
            return null;
        }
    }
}
