<?php
namespace Zeedhi\ZhuLogScripts;

/**
 * Como usar:
 *
 * 1 - Adicione os dados de conexão do seu banco de dados no array $dbParams.
 * 2 - Adicione no array $params os filtros SQL os quais você pretende
 *     utilizar caso não queira migrar todos os dados.
 * 3 - Execute o comando "php Migrate.php" no diretório deste arquivo.
 * 4 - Espere a impressão do texto "Fim." no seu console.
 * 5 - Confira no banco se tudo ocorreu corretamente.
 *
 * That'a all folks!
 */

require_once __DIR__."/../vendor/autoload.php";

use Doctrine\DBAL\Connection;
use Doctrine\DBAL\DriverManager;
use Doctrine\DBAL\Query\QueryBuilder;

class Migrate{

    /** @var $_conn Connection O objeto da conexão. */
    private $_conn = null;
    /** @var $_rows array A 'paginação' atual de resultados. */
    private $_rows = [];
    /** @var $_pageSize int O tamanho da página. */
    private $_pageSize = 500;
    /** @var $_lastOfPage int A 'paginação' atual de resultados. */
    private $_lastOfPage = 0;


    /**
     * @param array $dbParams Os parâmetro do banco de dados.
     */
    public function __construct(array $dbParams){
        $this->_conn = DriverManager::getConnection($dbParams);
    }

    /**
     *  Responsável por executar todos os processos necessários para migrar a tabela.
     *
     * @param array $params Vetor associativo de filtros a serem adicionados na cláusula
     * 'where' do texto SQL a ser executado para buscar dados da tabela ZHU_LOG.
     *
     * @return boolean $migrated Estado da migração, verdadeiro se concluída com sucesso
     * e falso se não foi concluída com sucesso porém não lançou exceções.
     *
     * @throws \Exception Lança uma exceção com a mensagem do erro encontrado.
     */
    public function run($params){
        try {
            $this->_pageSize = array_shift($params);
            $totalRows = $this->getTotalRows($params);

            while($totalRows > 0){
                echo "Faltam: ".$totalRows.PHP_EOL;

                // Processo de seleção.
                if(!$this->executeSelect($params))
                    return false;

                // Processo de inserção.
                if(!$this->executeInsert($this->_rows))
                    return false;

                // Reseta as tuplas na memória para liberar espaço.
                $this->_rows = [];

                // Mantém registro da posição da última tupla da página.
                $this->_lastOfPage += $this->_pageSize;


                $totalRows-=$this->_pageSize;
            }

            // Encerra conexão com banco.
            $this->_conn->close();

            // Tudo ocorreu como o esperado.
            return true;
        } catch (\Exception $e) {
            throw $e;
        }
    }

    /**
     * Retorna o total de dados.
     *
     * @return int
     */
    private function getTotalRows($params){
        $query = $this->_conn->createQueryBuilder()
            ->select('COUNT(*)')
            ->from('ZHU_LOG');

        $query = $this->prepareWhereClause($query, $params);

        $fetch = $query->execute()
            ->fetch();

        return reset($fetch);
    }

    /**
     * Executa a busca dos dados.
     *
     * @param array $params Os parâmetro a serem adicionados no texto SQL.
     *
     * @return boolean Retorna verdadeiro se tudo ocorrer como devia.
     */
    private function executeSelect($params){
        $findQuery = $this->buildFindQuery($params);
        $findStmt = null;

        if(!$findStmt = $findQuery->execute())
            throw new \Exception("Couldn't select data from ZHU_LOG!");

        $this->_rows = $findStmt->fetchAllAssociative();

        echo "Retornadas: ".count($this->_rows).PHP_EOL;

        return !empty($this->_rows);
    }

    /**
     * Executa a inserção dos dados.
     *
     * @param array $rows As tuplas sobre a qual o programa está iterando no momento.
     *
     * @return boolean Retorna verdadeiro se tudo ocorrer como devia.
     */
    private function executeInsert($rows){
        $migrated = 0;
        foreach($rows as $row){
            $setQuery = $this->buildSetQuery($this->prepareColumnsToInsert($row));
            $setStmt = null;

            if(!$setQuery){
                echo "Requisição {$row['REQ_ID']} não migrada pois existem dados nulos  ZHU_LOG.USER_DATA.".PHP_EOL;
                continue;
            }

            if(!$setStmt = $setQuery->execute())
                throw new \Exception("Couldn't insert data from ZH_LOG!");

            $migrated++;
        }

        echo "Migradas: $migrated".PHP_EOL;

        return true;
    }

    /**
     * Prepara a cláusula "or" do objeto de filtro SQL.
     *
     * @param String       $column  A coluna do filtro do SQL.
     * @param String       $param   A subcoluna do filtro do SQL.
     * @param array        $values  Os parâmetros de filtro do SQL.
     *
     * @return QueryBuilder Retorna o objeto de filtro do SQL.
     */
    private function buildOrClause($column, $param, $values){
        $innerQuery = '';

        if(empty($values)) return $innerQuery;

        reset($values);

        $value = current($values);
        $innerQuery .= " ($column LIKE '%\"$param\":\"$value\"%') ";

        while($value = next($values)){
            $innerQuery .= " OR ($column LIKE '%\"$param\":\"$value\"%') ";
        }

        return $innerQuery;
    }

    /**
     * Prepara a cláusula "where" do objeto de filtro SQL.
     *
     * @param QueryBuilder $query   O objeto de filtro do SQL.
     * @param array        $params  Os parâmetros de filtro do SQL.
     *
     * @return QueryBuilder Retorna o objeto de filtro do SQL.
     */
    private function prepareWhereClause(QueryBuilder $query, array $params) {
        if(!empty($params['start']))
            $query = $query->andWhere("TIMESTAMP >= TO_DATE('${params['start']}', '${params['format']}')");

        if(!empty($params['end']))
            $query = $query->andWhere("TIMESTAMP <= TO_DATE('${params['end']}', '${params['format']}')");

        if(!empty($params['type']))
            $query = $query->andWhere("TYPE = '${params['type']}'");

        if(!empty($params['code']))
            $query = $query->andWhere("HTTP_RESPONSE_CODE = ${params['code']}");

        if(!empty($params['route']))
            $query = $query->andWhere("ROUTE = '${params['route']}'");

        if(!empty($params['ORGANIZATION_ID']))
            $query = $query->andWhere($this->buildOrClause('USER_DATA', 'ORGANIZATION_ID', $params['ORGANIZATION_ID']));

        if(!empty($params['USER_ID']))
            $query = $query->andWhere($this->buildOrClause('USER_DATA', 'USER_ID', $params['USER_ID']));

        if(!empty($params['widgetName']))
            $query = $query->andWhere($this->buildOrClause('CONTEXT_DATA', 'widgetName', $params['widgetName']));

        if(!empty($params['containerName']))
            $query = $query->andWhere($this->buildOrClause('CONTEXT_DATA', 'containerName', $params['containerName']));

        return $query->andWhere("REQUEST_TYPE <> 'FilterData'");
    }

    /**
     * Constrói o texto SQL a ser executado para buscar os dados da tabela
     * ZHU_LOG_OLD em forma de um objeto.
     *
     * @param array $params Os parâmetro a serem adicionados no texto SQL.
     *
     * @return QueryBuilder O objeto montado sob o texto SQL que será executado.
     */
    private function buildFindQuery(array $params) {
        $query = $this->_conn->createQueryBuilder()
            ->select('*')
            ->from('ZHU_LOG');

        $query = $this->prepareWhereClause($query, $params);

        if(in_array('oci', $this->_conn->getParams())) {
            $paginationString = '';
            $paginationString = "OFFSET {$this->_lastOfPage} ROWS FETCH NEXT {$this->_pageSize} ROWS ONLY";
            $query = $this->_conn->createQueryBuilder()
                ->select('*')
                ->from("(".$query->getSQL().") ".$paginationString);
        } else {
            $query = $query->setFirstResult($this->_lastOfPage);
            $query = $query->setMaxResults($this->_pageSize);
        }

        return $query;
    }

    /**
     * Constrói o texto SQL a ser executado para inserir os dados na tabela
     * ZHU_LOG em forma de um objeto.
     *
     * @param array $values A tupla que será inserida nessa iteração.
     *
     * @return QueryBuilder O objeto montado sob o texto SQL que será executado.
     */
    private function buildSetQuery($values) {
        if(is_null($values["USER_ID"]) || is_null($values["ORGANIZATION_ID"]))
            return false;

        return $this->_conn->createQueryBuilder()
            ->insert('ZH_LOG')
            ->values($values);
    }

    /**
     * Constrói o vetor de dados a serem inseridos em cada coluna da tabela ZH_LOG.
     *
     * @param array $row A tupla resultado da tabela ZHU_LOG.
     *
     * @return array $newRow A tupla a ser inserida na nova tabela ZH_LOG.
     */
    private function prepareColumnsToInsert(array $row) {
        $newRow = [];

        foreach($row as $key=>$value) {
            if($key === "USER_DATA"){
                $momentData = json_decode($value, true);
                $newRow["ORGANIZATION_ID"] = isset($momentData["NRORG"]) ? "'${momentData['NRORG']}'" : null;
                $newRow["USER_ID"] = isset($momentData["CDOPERADOR"]) ? "'${momentData['CDOPERADOR']}'" : null;
            } elseif($key === "CONTEXT_DATA") {
                $momentData = json_decode($value, true);
                $newRow["WIDGET_NAME"] = isset($momentData["widgetName"]) ? "'${momentData['widgetName']}'" : 'null';
                $newRow["CONTAINER_NAME"] = isset($momentData["containerName"]) ? "'${momentData['containerName']}'" : 'null';
            } elseif(is_string($value)) {
                $newRow[$key] = "'$value'";
            } else {
                $newRow[$key] = $value;
            }
        }

        if(!empty($newRow)) $newRow["PRODUCT_ID"] = 'null';

        return $newRow;
    }
}

/** @var array $dbParams Os parâmetros do banco de dados. */
$dbParams = [
    'driverClass' => '',            // Classe que será utilizada para o driver.
    'driver'      => '',            // Driver a ser utilizado (tipo do banco).
    'host'        => '',            // Endereço do servidor de banco.
    'port'        => '',            // Porta.
    'user'        => '',            // Usuário.
    'password'    => '',            // Senha.
    'dbname'      => '',            // Nome do banco.
    'service'     => true,          // Define que o que for feito será realmente executado no banco.
];

/** @var Migrate $migration O objeto de migração de tabela. */
$migration = new Migrate($dbParams);

/** @var array $params Os filtros a serem adicionados na cláusula 'where' do texto SQL a ser executado na busca de dados da tabela ZHU_LOG. */
$params = [
    'pageSize'        => null,        // Tamanho da página.
    'type'            => '',          // Tipo do log. ('REQUEST' ou 'RESPONSE').
    'code'            => null,        // HTTP_RESPONSE_CODE (status http).
    'start'           => '',          // Data inicial.
    'end'             => '',          // Data final.
    'format'          => '',          // Formato da data.
    'route'           => '',          // Uma rota específica.
    'ORGANIZATION_ID' => [],          // Números das organizações.
    'USER_ID'         => [],          // Códigos dos usuários.
    'widgetName'      => [],          // Nomes das widgets.
    'containerName'   => [],          // Nomes dos containers.
];

/** @var float $startTime A hora atual em segundos com precisão em microsegundos. */
$startTime = microtime(true);

/**
 * Retorna "Fim." se a migração for um sucesso.
 */
if($migration->run($params))
    echo "\033[32m Fim. \033".PHP_EOL;

/**
 * Retorna o texto abaixo se a migração não for um sucesso.
 */
else
    echo "\033[31m Migração não concluída com sucesso apesar de não apresentar erros de execução.\033".PHP_EOL;

/**
 * Imprime o tempo gasto e finaliza a execução do programa.
 */
die("\033[37m  Tempo gasto: \033".(microtime(true) - $startTime).PHP_EOL);